Easy-to-manage touch screens
무료 오픈소스 터치스크린으로 USB 설정 가이드, 드래그‑드롭 편집기, 로컬 제어를 제공하며 Home Assistant용으로 제작되었습니다
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Code for the secret santa picker app, a fun way to host a secret santa gift exchange.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗Live app: https://www.baseworkshop.app/secret-santa
A full-stack web application for running Secret Santa gift exchanges. Create a draw, assign participants to groups to prevent unwanted pairings, share a results link, and optionally send everyone an email revealing their match.
Built with Next.js, TypeScript, and Neon Postgres, deployed on Vercel.
More detail:
| Layer | Technology |
|---|---|
| Framework | Next.js (pages router), React 19, TypeScript 5 |
| Database | Neon Postgres via @neondatabase/serverless (HTTP driver) |
| Nodemailer + Gmail SMTP | |
| CAPTCHA | Cloudflare Turnstile |
| Deployment | Vercel |
| jsPDF + jspdf-autotable with Noto Sans (Latin, Greek, Cyrillic), loaded in the browser only when the organizer saves or shares a PDF | |
| Tests | Vitest, React Testing Library, PGlite (in-memory Postgres) |
DATABASE_URL is set for you)Copy .env.example to .env.local and fill in the values (or run vercel env pull .env.local to fetch them from the linked Vercel project). The file says what each variable is for and which ones are optional.
cp .env.example .env.local
Local development never uses the production database. Point DATABASE_URL in .env.local at the Neon dev branch (an empty copy of the schema, with no real participants); the production connection string lives only in Vercel. vercel env pull writes the production value, so swap DATABASE_URL back to the dev branch after running it.
The real Turnstile keys don't accept localhost. For npm run dev, put Cloudflare's published test keys in .env.development.local, which overrides .env.local only in development:
NEXT_PUBLIC_TURNSTILE_SITE_KEY=1x00000000000000000000AA
TURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AA
Run the schema script once to create all required tables (Draws, Participants, Matches, EmailSendLog, RateLimits, DailyStats):
psql "$DATABASE_URL" -f sql/setup.sql
You can also paste the script into the SQL Editor in the Neon console. See sql/setup.sql for the full schema.
setup.sql already includes every migration, so a new database needs nothing else. sql/migrations/ holds the one-off changes for databases created before them, each with a read-only _check.sql to run afterwards:
| Migration | Adds |
|---|---|
002_reveal_tokens.sql | Participants.reveal_token, for personal reveal links |
003_rate_limits.sql | The RateLimits table |
004_email_status.sql | Participants.email_sent_at, for per-person email tracking and resend |
005_email_claims.sql | Participants.email_claimed_at, so a send that dies midway leaves nobody wrongly marked as emailed |
006_match_constraints.sql | Draws.admin_key becomes NOT NULL; UNIQUE on each of Matches.giver_participant_id and receiver_participant_id |
007_email_send_log.sql | The EmailSendLog table, for the rolling 24-hour email cap (replaces DailyEmailLog, dropped by 010) |
008_short_links.sql | Participants.reveal_code, the 8-character personal link token (the uuid reveal_token is retired), and UNIQUE on Draws.admin_key, which get-draw now looks draws up by |
009_exchange_details.sql | Draws.organizer_note, budget_cents and exchange_date, the optional details shown in match emails and on personal link pages |
010_drop_daily_email_log.sql | Removes the unused DailyEmailLog table that 007 left behind, so older databases match setup.sql |
011_ops_stats.sql | Participants.revealed_at (when a personal link was first opened) and the DailyStats table the daily check job writes, for the usage stats |
012_v2_cleanup.sql | Removes every draw created before 17 October 2026 for good (a one-time purge), drops the retired Participants.reveal_token, makes reveal_code NOT NULL, and adds Draws.replaced_by so the usage stats leave out draws replaced by Edit & Redraw |
Numbering starts at 002 because 001 was the original Azure SQL schema, which was replaced by setup.sql when the app moved to Neon. How to apply one safely is in docs/OPERATIONS.md.
npm install
npm run dev # → http://localhost:3000/secret-santa
npm run typecheck # tsc --noEmit
npm test # Vitest tests in tests/
npm run build && npm start # production
CI (.github/workflows/ci.yml) runs the tests and the build, which type-checks every file including the tests, on every pull request, and blocks pull requests during the yearly holiday change freeze (see docs/OPERATIONS.md).
src/
lib/
db.ts # Neon serverless SQL client
tokens.ts # Organizer keys (14 characters) and personal link tokens (8): generation and shape checks
matching.ts # Matching algorithm (shuffle + feasibility-checked fallback)
email.ts # Nodemailer email service: the match, organizer link and ops emails
limits.ts # The email cap, the per-IP email budget and the send claim expiry
report.ts # reportError(), reportWarning(): log and send to Sentry
email-address.ts # isValidEmail(): the one address check, used by the forms and the API
sanitize.ts # HTML tag and web-address checks, escaping for emails
base-path.json # The /secret-santa basePath, read by next.config.js and site.ts
site.ts # BASE_PATH, and siteUrl(): public URLs with the basePath
turnstile.ts # verifyTurnstile(): server-side CAPTCHA check, 5s timeout, never throws
groups.ts # Home page form state: groups by id, Edit & Redraw hand-off, the as-you-type draft (parseFormDraft)
names.ts # duplicateNames(): names must differ ignoring case and spaces (form and API)
details.ts # The note, budget and date: checks, formatting and the lines the emails and lists show
email-draft.ts # The draw page's email card draft in session storage, kept per draw
match-list.ts # The organizer's list of matches: table order and CSV columns
match-pdf.ts # The matches as a PDF (jsPDF), made in the browser
ip-prefix.ts # rateLimitClient(): IPv4 address, or IPv6 /64, as one rate-limit client
rate-limit.ts # clientIp(), isWhitelisted(), rateLimitBucket(), addressBucket(): shared by the proxy and the email limits
pages/
api/
create-draw.ts # POST: validate, match, persist in a transaction
get-draw.ts # GET: the organizer's view of a draw, found by its admin key
reveal.ts # GET: one participant's own match, by reveal token
send-emails.ts # POST: CAPTCHA + 24-hour cap + per-IP budget + per-address limit + per-person claimed send / resend
save-details.ts # POST: save the note, budget and date before any send
delete-draw.ts # POST: soft-delete
health.ts # GET: liveness check for the uptime monitor; never touches the database
ops-check.ts # GET: daily check job + weekly stats email (Vercel Cron, CRON_SECRET)
_app.tsx # App wrapper: global styles, icons, and the fonts (next/font, self-hosted)
_document.tsx # Sets <html lang="en">
index.tsx # Home page — participant form
draw.tsx # Organizer page (/draw#<adminKey>) — organizer link, matches, personal links, email, admin actions
reveal/[token].tsx # Participant's private tap-to-reveal page
components/
draw/ # The results page's sections (organizer link, matches table, email, personal links, save and print, delete); state stays in the page.
# PrintSheet.tsx is what the page prints once the matches are shown
# TurnstileWidget.tsx loads the CAPTCHA script, only where the send/resend form renders
proxy.ts # Per-IP hourly rate limits (RateLimits table)
instrumentation.ts # Loads sentry.server.config.ts: server errors to Sentry when SENTRY_DSN is set
styles/ # globals.css design tokens + CSS Modules per page
types/
api.ts # JSON response types, shared by the API routes and the pages
turnstile.d.ts # Types for Cloudflare Turnstile's window.turnstile
tests/ # Vitest tests: lib, every API route, and the home form (React Testing Library, jsdom)
real-sql.test.ts # The routes' real SQL against sql/setup.sql in PGlite
helpers/ # callRoute() for route tests, and the PGlite stand-in for the database
public/ # Logo, favicons, app icons, link-preview image, and fonts/ (Noto Sans for the PDF; docs/OPERATIONS.md)
sql/
setup.sql # Database schema (Postgres)
migrations/ # One-off schema changes for existing databases, each with a _check.sql
docs/ # Architecture and operations notes
scripts/ # freeze.ts: the yearly holiday change freeze dates, checked by CI (check-freeze.ts)
All endpoints are under /secret-santa/api/. Errors always return { "error": "<message>" }: 405 (with an Allow header) for a wrong HTTP method, and the codes listed per route. Routes marked admin key required find the draw by that key alone. Routes marked rate limited return 429 when an IP goes over its hourly limit.
| Method | Path | Description |
|---|---|---|
POST | /api/create-draw | Rate limited (10/h). Validate 2–50 participants ({ name, email?, group? }; names may not contain HTML or a web address, since they go into the emails, and must differ ignoring case and spaces, so each person can tell who they drew), generate matches, persist in a transaction. Returns { adminKey }, retrying with fresh tokens in the rare case one repeats a saved one. 400 bad input, 422 no valid matching. Edit & Redraw also sends replacesKey, the old draw's admin key, to soft-delete the old draw in the same transaction; the new draw keeps its saved note, budget and date. 403 missing, malformed or unknown replacesKey, 409 the old draw's emails were sent, 410 it was already deleted. |
GET | /api/get-draw | The organizer's view of a draw, found by the header X-Admin-Key: <adminKey> (never the query string): its participants with emails, groups, reveal codes, per-person email status and when each first opened their link (revealed_at), matches, each as { giver, receiver } indexes into participants, organizer_name (set by the first send), and organizer_note, budget_cents and exchange_date (YYYY-MM-DD), each null when not set. 404 for a missing, malformed or unknown key, 410 for deleted draws. |
GET | /api/reveal?token=<token> | One participant's name, their match, the draw date, the organizer (once emails are sent), and the note, budget and exchange date (whenever set). 404 for unknown or malformed tokens, 410 for deleted draws. |
POST | /api/send-emails | Admin key required. Rate limited (5/h). Body { key, turnstileToken, organizerName, organizerEmail, organizerNote?, budget?, exchangeDate? } (budget is a dollar amount as typed, like "25.50"; the date is YYYY-MM-DD; the saved details are used on a resend), or { key, turnstileToken, retryFailed: true } to resend only to people whose email failed. A resend can add person (an index into get-draw's ) to resend to just that person, and to correct their address first; neither works for someone whose email already went out. Also emails the organizer their organizer link on the first send, or only that when no participant has an email (the draw then stays unsent). Each address gets at most 9 emails in any 24 hours across all draws (the organizer link included); anyone over it isn't emailed and is listed in . Returns . bad input (including an organizer name over 200 characters or containing HTML or a web address, or an email over 320; a note over 150 characters or containing HTML or a web address; a budget that isn't a dollar amount of $10,000 or less; a date that isn't a real day), missing, malformed or unknown key, or failed CAPTCHA (including when Cloudflare can't be reached), already sent (or nothing to resend, or that person already has their email or has no address), deleted, the sender's IP has used its budget of 100 emails in 24 hours, or a resend's addresses (or an organizer-only send's) are all at their limit, cap of 495 emails in 24 hours reached. |
Import the repository into Vercel and add all environment variables under Project Settings → Environment Variables.
Key configuration notes:
/secret-santa (basePath in next.config.js, read from src/lib/base-path.json) — do not remove this. To change the prefix, edit only that JSON fileDATABASE_URL is provided by the Neon integration on Vercelvercel.json gives each API route file its own entry (no globs, so no file matches two patterns): all run in the cle1 region with a 10s timeout, except send-emails, which gets 120s so a 50-person draw has time to send through a slow mail server, and ops-check, which gets 30s so one stalled SMTP stage (15s) can't stop the daily job before it reports. A new API route needs its own entry; tests/vercel-config.test.ts fails until it has oneDay-to-day operations (migrations, key rotation, rollback) are in docs/OPERATIONS.md.
Pull requests are welcome. A few conventions to follow:
anynpm run typecheck and npm test before opening a PR; CI runs the tests and the build, which type-checks toomain; use descriptive kebab-case branch namesMIT © 2026 Dan Basar
participantsemaillimitedNames{ success, totalSent, totalFailed, failedNames, limitedNames, organizerLinkSent }400403409410429503POST | /api/save-details | Admin key required. Body { key, organizerNote?, budget?, exchangeDate? }, checked like the first send; an empty value clears that detail. Saves them on the draw without sending any email, so they show on personal links. Returns { organizer_note, budget_cents, exchange_date } as saved. 400 bad input, 403 missing, malformed or unknown key, 409 emails already sent, 410 deleted. |
POST | /api/delete-draw | Admin key required. Body { key }. Soft-delete a draw. Idempotent: deleting again keeps the first deletion time. 403 missing, malformed or unknown key. |
GET | /api/health | Always { ok: true }, showing the site's functions run; any method works. It never touches the database, so the uptime monitor can poll it without keeping Neon awake. |
GET | /api/ops-check | Authorization: Bearer <CRON_SECRET> required; called by Vercel Cron. Daily: writes yesterday's DailyStats row and emails OPS_ALERT_EMAIL if people on sent draws were never emailed or the 24-hour email count nears the cap; with ?report=weekly, emails the last 7 days of stats. 403 wrong secret, 500 not configured or the job failed. |