diff --git a/README.md b/README.md index c29f335..b0c77d4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # SquadSync -Balanced team formation for hackathons, workshops, and study groups. Organizers create an event and share a QR/link; attendees self-register; a **deterministic engine** builds balanced teams. Auth is **Nostr (NIP-98)** — no passwords, no email. +Balanced team formation for hackathons, workshops, and study groups. Organizers create an event and share a QR/link; attendees self-register; a **deterministic engine** builds balanced teams. **AI (Claude) reads each attendee's free-text "Other" strength and maps it to a standard category** so everyone is comparable before allocation — while the team-building itself stays fully deterministic and reproducible. Auth is **Nostr (NIP-98)** — no passwords, no email. ## What it does diff --git a/frontend/public/guide/02-create-event.png b/frontend/public/guide/02-create-event.png index d498d66..f43d8dd 100644 Binary files a/frontend/public/guide/02-create-event.png and b/frontend/public/guide/02-create-event.png differ diff --git a/frontend/public/guide/03-event-dashboard.png b/frontend/public/guide/03-event-dashboard.png index 1fc6829..e45374b 100644 Binary files a/frontend/public/guide/03-event-dashboard.png and b/frontend/public/guide/03-event-dashboard.png differ diff --git a/frontend/public/guide/04-attendees-qr.png b/frontend/public/guide/04-attendees-qr.png index af434f3..564bac5 100644 Binary files a/frontend/public/guide/04-attendees-qr.png and b/frontend/public/guide/04-attendees-qr.png differ diff --git a/frontend/public/guide/05-join-form.png b/frontend/public/guide/05-join-form.png index 24423e4..a3c8df3 100644 Binary files a/frontend/public/guide/05-join-form.png and b/frontend/public/guide/05-join-form.png differ diff --git a/frontend/public/guide/06-configure.png b/frontend/public/guide/06-configure.png index ff238ad..0e9b047 100644 Binary files a/frontend/public/guide/06-configure.png and b/frontend/public/guide/06-configure.png differ diff --git a/frontend/public/guide/07-engine-results.png b/frontend/public/guide/07-engine-results.png index 3889fb4..3065951 100644 Binary files a/frontend/public/guide/07-engine-results.png and b/frontend/public/guide/07-engine-results.png differ diff --git a/frontend/public/guide/08-published.png b/frontend/public/guide/08-published.png index 08ad2f4..43a923d 100644 Binary files a/frontend/public/guide/08-published.png and b/frontend/public/guide/08-published.png differ diff --git a/frontend/public/guide/09-ai-category.png b/frontend/public/guide/09-ai-category.png index a68fb84..9a6bfc8 100644 Binary files a/frontend/public/guide/09-ai-category.png and b/frontend/public/guide/09-ai-category.png differ diff --git a/scripts/capture-guide.mjs b/scripts/capture-guide.mjs index 0b98463..cade562 100644 --- a/scripts/capture-guide.mjs +++ b/scripts/capture-guide.mjs @@ -61,7 +61,9 @@ async function main() { await page.waitForSelector('[role="dialog"]', { timeout: 10000 }); await page.fill('input[id="title"]', "AI for Agriculture Hackathon"); await page.fill('#description', "Build AI + satellite tools to improve crop yields."); + await page.fill('input[id="event_at"]', "2026-07-15T09:00"); await page.fill('input[id="team_count"]', "3"); + await page.fill('input[id="participant_limit"]', "30"); await shot(page, "02-create-event"); await page.click('button:has-text("Create Event")'); await page.waitForURL(/\/dashboard\/events\/[^/]+$/, { timeout: 15000 }); @@ -94,9 +96,14 @@ async function main() { }); } - // 04 attendees + QR + // 04 attendees + QR — wait for the FULL QR card (its Download button) AND the + // populated table, so neither the QR nor the roster is still a skeleton. await page.goto(`${BASE}/dashboard/events/${eventId}/attendees`, { waitUntil: "domcontentloaded" }); await page.waitForSelector("text=Registration QR Code", { timeout: 45000 }); + await page.waitForSelector('button:has-text("Download PNG")', { timeout: 45000 }); + await page.waitForSelector("text=6 participants", { timeout: 45000 }); + await page.waitForSelector("text=Carol", { timeout: 45000 }); + await page.waitForTimeout(500); await shot(page, "04-attendees-qr"); // 05 public join form (NEW form) @@ -106,9 +113,12 @@ async function main() { await shot(join, "05-join-form"); await join.close(); - // 06 configure + // 06 configure — wait for BOTH cards (the form only renders past its loading + // skeleton once the config has loaded); "Role Constraints" is the last card. await page.goto(`${BASE}/dashboard/events/${eventId}/configure`, { waitUntil: "domcontentloaded" }); await page.waitForSelector("text=Balancing Weights", { timeout: 45000 }); + await page.waitForSelector("text=Role Constraints", { timeout: 45000 }); + await page.waitForTimeout(500); await shot(page, "06-configure"); // 07 engine results diff --git a/scripts/capture-join.mjs b/scripts/capture-join.mjs deleted file mode 100644 index bb795c7..0000000 --- a/scripts/capture-join.mjs +++ /dev/null @@ -1,54 +0,0 @@ -// Re-captures ONLY the join-form guide screenshot (05-join-form.png) after the -// registration form gained the optional npub field (B2b). Other guide shots are -// captured at 1280px where layout is unchanged, so they don't need regenerating. -// Prereqs: backend on :8000 and frontend on :3000 (see scripts/capture-guide.mjs header). -// node scripts/capture-join.mjs -import { chromium } from "playwright"; -import { generateSecretKey, getPublicKey, finalizeEvent } from "nostr-tools"; -import { mkdirSync } from "fs"; - -const BASE = process.env.GUIDE_BASE ?? "http://localhost:3000"; -const API = process.env.GUIDE_API ?? "http://localhost:8000"; -const OUT = "frontend/public/guide"; - -const SK = generateSecretKey(); -const PK = getPublicKey(SK); - -async function token() { - const event = finalizeEvent( - { kind: 27235, created_at: Math.floor(Date.now() / 1000), tags: [["u", `${API}/auth/nostr`], ["method", "POST"]], content: "" }, - SK - ); - const res = await fetch(`${API}/auth/nostr`, { - method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ pubkey: PK, event }), - }); - if (!res.ok) throw new Error(`/auth/nostr ${res.status}: ${await res.text()}`); - return (await res.json()).access_token; -} - -async function main() { - mkdirSync(OUT, { recursive: true }); - const t = await token(); - const ev = await (await fetch(`${API}/api/v1/events`, { - method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${t}` }, - body: JSON.stringify({ title: "AI for Agriculture Hackathon", team_count: 3 }), - })).json(); - await fetch(`${API}/api/v1/events/${ev.id}`, { - method: "PATCH", headers: { "Content-Type": "application/json", Authorization: `Bearer ${t}` }, - body: JSON.stringify({ status: "active" }), - }); - const slug = (await (await fetch(`${API}/api/v1/events/${ev.id}`, { headers: { Authorization: `Bearer ${t}` } })).json()).registration_slug; - - const browser = await chromium.launch({ headless: true }); - const ctx = await browser.newContext({ viewport: { width: 1280, height: 800 } }); - const join = await ctx.newPage(); - await join.goto(`${BASE}/join/${slug}`, { waitUntil: "domcontentloaded" }); - await join.waitForSelector("text=Primary Strength", { timeout: 90000 }); - await join.waitForSelector('input[id="npub"]', { timeout: 90000 }); // new B2b field must render - await join.screenshot({ path: `${OUT}/05-join-form.png`, fullPage: false }); - console.log("📸 05-join-form.png recaptured"); - await browser.close(); -} - -main().catch(e => { console.error("❌ capture failed:", e.message); process.exit(1); });