尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证制作者的介绍仅有上面的一句话。 更多详情请阅读仓库 README。 仓库 README ↗
正在加载完整 README。
查看完整 README ↗The internet disagrees with you. Duelio is a small web experiment. Visitors see a low-stakes everyday dilemma with two sides, pick one, see where everyone landed, read or join the discussion, and share the question.
It tests one hypothesis: people will share a specific everyday disagreement, invite others to weigh in, and take part in a public discussion about it. See EXPERIMENT.md for how to judge that.
/d/<slug>: a straightforward prompt and voting panel, followed by the group-room tally when applicable, discussion, and report. After voting, the image card preview is open on the page rather than hidden in a dropdown./admin./submit: public dilemma submissions. Valid submissions are published immediately, appear in the searchable homepage feed, and get a shareable dilemma page./admin: editor desk (create, edit, publish, unpublish, remove, set closing date, review submissions, resolve reports, 7-day metrics)./guidelines: community rules.https://duelio.ink by default; set NEXT_PUBLIC_SITE_URL when attaching a different custom domain./admin/matchups.WikiRush (/wikirush) is "GeoGuessr for Wikipedia": you see a start page and a destination, then race there using only the links in the article.
/wikirush?c=SEED&t=90&r=5&s=SCORE&n=NAME) that replay a friend's exact rounds and show you how you compare.src/lib/wikirush/ and is unit tested; the same formula runs server-side in supabase/wikirush-v2.sql.🟩🟨🟧🟥), native share or copy, and a "Challenge a friend" link.submit_wikirush_game (run supabase/wikirush-v2.sql, see supabase/WIKIRUSH_SETUP.md); without it, scores and stats stay on the device.python3 scripts/make-wikirush-art.py regenerates public/wikirush/ (watermark mark, social card).Vote card. After you vote, you get a card with your pick ("I picked Split evenly. Do you agree?") and the live percentage. Pick a size, then share. On phones it opens the native share sheet with the image attached. On a computer it saves the PNG.
| Size | Pixels | Good for |
|---|---|---|
| Story 9:16 | 1080 x 1920 | Instagram, TikTok, Reels, WhatsApp status |
| Feed post 4:5 | 1080 x 1350 | Instagram and Facebook feed |
| Square 1:1 | 1080 x 1080 | iMessage, WhatsApp, any post |
| Link card 1.91:1 | 1200 x 630 | X, Facebook, LinkedIn (also the link preview image) |
Vote cards show the closing time when one is set. Every card (vote, group chat, take, and each dilemma's preview) uses the same look: a soft paper background with faint blue and pink glows, a hairline frame, a bold serif headline centered on the card, and small labels. Text is measured against the real font and sized to fit, so long questions shrink and wrap evenly instead of overflowing, and very long text ends in an ellipsis. Image URL pattern: /card/<slug>/a?fmt=story|portrait|square|wide (use b for side B, room?g=<code> for the group card).
Settle it in your group chat. One button creates a group link (/d/<slug>?g=<code>) and opens the share sheet, or copies it. Everyone who votes through that link is counted in the group. The link preview in iMessage, WhatsApp, Slack, and similar apps reads "Group chat verdict pending" with the question and how many votes are in. The page shows the group's own split live (it refreshes every 8 seconds). The split is shown only to people who voted in that group. Everyone else sees just how many have voted. Chat apps cache previews, so the vote count on a preview can lag.
Your take. After you vote on enough debates, you see how often you sided with the majority and can share it as a card. A debate counts once 3 people have voted and one side leads. You need 3 counted debates. No account is needed: it uses the same anonymous cookie as voting, so clearing cookies resets it.
Group codes are random labels, not secrets. Anyone with a link can vote in that group, and anyone can invent a code. A public voter nickname is optional; when supplied, it is displayed next to that person's side. It is not a verified identity, and voters can choose a different nickname on another dilemma.
Choices made on purpose: results are never locked behind sharing, and there are no email or push notifications, because both would pull in personal data and consent handling this experiment doesn't need. Group chats get a live tally instead.
The homepage illustration is a local SVG asset, so it does not depend on an external image-generation or image-hosting service. On Windows, postinstall also applies a narrow URL-resolution compatibility fix to Next.js's bundled @vercel/og renderer; without it, generated share cards fail to render because Node's Windows path joining corrupts the renderer's bundled font and WASM URLs.
Upgrading an existing Supabase project: run the full supabase/schema.sql again. Its create table if not exists and alter table ... add column if not exists statements add the daily schedule, public nickname and crowd-prediction fields, plus defense and endorsement tables, without removing existing data. The app requires those schema updates before deployment.
Next.js 14 (App Router) + TypeScript, Tailwind CSS, Supabase Postgres for production data, Threadline embed for discussion, Vercel for hosting. No web fonts and no animation libraries.
Requires Node 18.18 or newer.
npm install
npm run dev # http://localhost:3000
This zip already contains a .env.local with the Threadline values and a generated admin password. With no Supabase variables set, Duelio stores data in .data/duelio.json. That is for development and single-server hosting only.
Admin: open /admin, sign in with ADMIN_PASSWORD from .env.local. Admin is switched off if that variable is empty.
Copy .env.example to .env.local and fill it in.
| Variable | Needed | Purpose |
|---|---|---|
NEXT_PUBLIC_THREADLINE_SITE_ID | yes | Threadline site id. Public by design (it appears in the page HTML). |
NEXT_PUBLIC_THREADLINE_API_URL | yes | Threadline comments API URL. The redirecting threadline.ink/api alias is normalized to its direct API host. |
NEXT_PUBLIC_THREADLINE_EMBED_SRC | yes | Threadline embed script URL. |
NEXT_PUBLIC_SITE_URL | in production | Public URL without trailing slash, used for canonical and social URLs. |
ADMIN_PASSWORD | for /admin | Editor password. Choose a long one. |
ADMIN_SESSION_SECRET | optional | Signs the admin cookie. Defaults to ADMIN_PASSWORD. |
VOTE_SALT | recommended | Salt for hashing anonymous voter ids. Use a long random string. |
NEXT_PUBLIC_SUPABASE_URL | production | Supabase project URL. |
SUPABASE_SERVICE_ROLE_KEY | production | Server only. Never expose it to the browser. |
NEXT_PUBLIC_ANALYTICS_ID | optional | Reserved for connecting an analytics provider (see Analytics). |
NEXT_PUBLIC_* values are baked into the browser bundle at build time, so changing one means rebuilding.
src/components/ThreadlineComments.tsx loads the Threadline embed with next/script, reading the three NEXT_PUBLIC_THREADLINE_* variables. The API URL is normalized to avoid redirecting CORS preflights. If a variable is missing or the script fails to load, the page shows "The discussion is unavailable right now. Your vote still counts." and keeps working.
Threadline provides the discussion only (comments, replies, reactions, comment moderation). Voting, submissions, reports, and analytics are Duelio's own.
Two things the embed snippet does not document, so check them on your deployed site before you rely on them:
The "Be the first person to make the case" line is shown above the embed on every dilemma. Duelio cannot tell whether a thread has comments, so it does not hide that line.
Before enabling matchup voting in production, run supabase/matchups.sql in the Supabase SQL Editor. This adds the matchup, round, and vote tables plus server-only functions for atomic voting and round rollover. It does not delete or rewrite existing polls or votes. The feature uses the existing NEXT_PUBLIC_SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, VOTE_SALT, and Threadline environment variables; no new secrets are required. After the migration, deploy the branch and test creating the same pair in both orders, voting twice in one round, closing a round from /admin/matchups, and confirming that the share URL preview includes the chosen competitor.
supabase/schema.sql.npx vercel).NEXT_PUBLIC_SITE_URL.dilemmas table./admin on the live site, sign in, and check the storage line says "Supabase".Do not deploy to Vercel without Supabase. The JSON file store would live in temporary storage and lose votes.
Row level security is enabled on every table with no public policies, so the Supabase anon key can read nothing. The app only talks to Supabase from the server, using the service role key.
/admin; legacy submissions waiting for review can still be approved or rejected there.pending (hidden), published, rejected, removed (page shows a removal notice)./admin under "Open reports". Press Mark resolved when handled.Duelio records these events into your own database and shows 7-day counts in /admin: page views, dilemma views, vote started and completed, share clicked and completed, group chats created and joined, vote cards and takes shared, submission started and completed, report submitted, and discussion script loaded. Share completion is only measured for the native share sheet and for copying the link, because the browser cannot see where a link is pasted.
src/lib/analytics.ts is the adapter. To connect a provider, add its snippet so it creates window.dataLayer, and every event is mirrored there. Comment and reply activity inside Threadline is not visible to Duelio, so read it in Threadline's own dashboard.
npm test # 35 unit tests: results math, validation, store, rooms, crowd take, card text fitting
npm run typecheck
npm run build && npm run start
npm run smoke # 88 end-to-end checks, including every card size against a running server (set ADMIN_PASSWORD)
npm run smoke expects a fresh data store (delete .data/ first) because it casts votes.
src/assets/bg-<tone>-<size>.jpg. Edit the colours or glows in scripts/make-backgrounds.py and run python3 scripts/make-backgrounds.py (needs pip install pillow numpy).src/assets (see FONT-LICENSE.txt): Pagella Bold for headlines, Poppins Bold for small labels. The Pagella copy has its optional ligature table removed because the image renderer cannot read it.SPEC in src/lib/cards.tsx. Text fitting lives in src/lib/textFit.ts.The editorial dilemmas live in src/lib/seed.ts. They are inserted only into an empty database. Afterward, manage dilemmas in /admin.
src/app/ pages and API routes (/, /d/[slug], /submit, /guidelines, /admin, /api/*)
src/components/ VoteBox, ShareBar, ReportBox, SubmitForm, ThreadlineComments, DuelStrip
src/lib/ validation, results math, auth, rate limit, analytics, seed, card images and text fitting
src/lib/store/ Store interface, JSON file store, Supabase store
supabase/schema.sql database schema
scripts/ background image generator
tests/ unit tests and the end-to-end smoke script
/ and /wikirush (?play=1 starts a game, ?daily=1 starts the Daily, ?stream=1 turns on streamer mode).articleCache.ts), the API slims and caches Wikipedia HTML, and the first round warms up from the lobby.starters.ts)./wikirush/og, challenge links get their own social preview, plus /wikirush/how-to-play and /wikirush/streamers.gx-* classes in src/app/globals.css): top bar with your avatar, level and points; a bottom tab bar on phones.src/components/game/*): PLAY, Daily, mode cards for Quick, Hard, Wild, Duel and Dilemma, player card and top explorers./login is a full-screen sign-in / sign-up with avatar picker. Profile, Settings and Leaderboard use the same look.python3 scripts/make-game-art.py into public/game/.supabase/wikirush-v2.sql (avatar column) so avatars show on the public leaderboard./records shows the fastest finds on any page pair, per-pair record holders (with a "Beat it" button that loads that exact pair in the lobby via ?beat=<id>), best games, fastest clean sweeps and every Day N Challenge board./api/wikirush/records, which re-validates the route (starts and ends on the right pages, one hop per click, human pace) before writing to wikirush_records. Run the last block of supabase/wikirush-v2.sql to create the table.?pick=.