A collectible attendee index for every face in the Doon Tech Community.
- Next.js 16 (App Router) + TypeScript + Tailwind CSS
- Appwrite TablesDB, Auth (email OTP), Storage
- Server-side rendering throughout — server components for reads, server actions for writes/auth/upload
- Dynamic OG images via
next/og
-
Install the Appwrite CLI and log in:
npm i -g appwrite-cli appwrite login
-
From the project root, push the schema defined in
appwrite.config.json:appwrite push tables appwrite push buckets
This provisions the
dtcdatabase, the six tables (attendees,tags,meetups,badges,attendee_badges,featured), and theavatarsstorage bucket. -
Create a server API key (Project → Overview → Integrations → API Keys) with scopes:
databases.read,databases.write— used by everyadminTables()row CRUD callusers.read—users.list/users.get(organizer-access lookup)users.write—users.updateLabels(granting theorganizerlabel) andusers.updateName(profile setup)files.read,files.write— avatar uploads viaadminStorage().createFilesessions.write—adminAccount().createSession(SSR receivessession.secretand sets the cookie); also implicitly required bycreateEmailTokenfor the OTP flow
-
Copy
.env.exampleto.env.localand fill in:NEXT_PUBLIC_APPWRITE_ENDPOINT=https://sgp.cloud.appwrite.io/v1 NEXT_PUBLIC_APPWRITE_PROJECT_ID=dtc-pokedex NEXT_PUBLIC_APPWRITE_DATABASE_ID=dtc NEXT_PUBLIC_APPWRITE_BUCKET_AVATARS=avatars APPWRITE_API_KEY=... NEXT_PUBLIC_SITE_URL=http://localhost:3000
-
Make yourself an organizer. Sign in once (see "Run locally" below), then in the Appwrite console open the user record and add the label
organizer(Users → your user → Labels →+ organizer). The app gates all admin actions on this label.
npm install
npm run devOpen http://localhost:3000.
- Visit
/login(or click Sign in on/admin) - Enter your email — Appwrite emails you a 6-digit code
- Enter the code; the session cookie is set server-side and you're in
- Signed-in users without a linked attendee card are sent to
/profile/setup - Completing setup creates an active public attendee profile linked to the user's Appwrite
$id - Users with the
organizerlabel see the full admin dashboard at/admin
A user can edit their own attendee profile when attendee.user_id matches their Appwrite $id:
- Organizer flow: edit any attendee → fill Linked user ID with the user's
$id(from the Appwrite console) - Self-claim flow:
claimAttendee(attendeeId)server action setsuser_idif the attendee is unclaimed (currently no UI button — wire as needed)
Attendee levels are derived from earned or hard-to-fake signals. Users and organizers do not manually set levels in the UI.
The source formula lives in lib/levels.ts:
xp = profile completeness XP + badge rarity XP + tenure XP
level = highest nonlinear threshold reached by xpProfile completeness is capped at 80 XP:
- 10 XP for avatar
- 10 XP for role/title
- 10 XP for company
- 10 XP for location
- 10 XP for a bio of at least 40 normalized characters
- 10 XP for at least one social/personal link
- 5 XP for one tag, 10 XP for two or more tags
- 10 XP for preferred stack or favorite topic
Badge XP uses rarity:
- common: 25 XP
- rare: 75 XP
- epic: 180 XP
- legendary: 400 XP
Tenure adds 5 XP per full month since the attendee row was created, capped at 60 XP.
attendees.level is a server-computed cache used for display. It is recalculated when profiles are created or updated, badges are awarded or removed, attendees are merged, and tags are removed from attendees.
npm run dev– start the dev servernpm run build– production buildnpm start– run the production buildnpm run typecheck– TypeScript check
app/
layout.tsx, page.tsx, globals.css
login/ (email OTP form)
dex/page.tsx (Pokédex grid)
attendees/[slug]/page.tsx (profile)
events/page.tsx (link list)
profile/page.tsx, profile/setup/page.tsx (self-service profile)
admin/page.tsx (organizer dashboard)
admin/attendees/new + [id]/edit (forms, with AttendeeBadgesPanel)
admin/events/page.tsx (event creation)
admin/tags/page.tsx (tag creation)
admin/badges/page.tsx (badge creation)
og/route.tsx (OG images)
sitemap.ts, robots.ts, not-found.tsx, icon.svg, apple-icon.tsx
components/
Nav, NavLinks, AttendeeCard, AttendeeForm, OrganizerAuthButton
Avatar, TagChip, BadgePill, FiltersBar
PokedexTilt, PowerButton, DeviceControls, SoundProvider
(admin: BadgesAdmin, EventsAdmin, TagsAdmin, AdminAttendeesTable)
lib/
appwrite.ts (server clients, IDs, helpers)
auth.ts (getCurrentUser, isOrganizer, requireOrganizer)
levels.ts (profile/badge/tenure XP and level thresholds)
queries.ts (Appwrite reads for pages)
actions.ts (server actions: OTP, CRUD, upload, merge, claim)
types.ts
appwrite.config.json (Appwrite CLI schema)
- All writes go through server actions in
lib/actions.ts. They authenticate the request via the session cookie, enforce theorganizerlabel (oruser_idmatch for self-edits), and use the admin API key to perform the mutation. - Organizers can add events from
/admin/events; new records appear on/eventsand update the dashboard count. - Organizers can add tags from
/admin/tags; new tags appear in attendee profile forms. The Pokedex filter row is driven by badges, not tags. - Organizers can create badges from
/admin/badgesand award them to attendees via the Badges panel on each attendee's edit page (/admin/attendees/[id]/edit). Awarded badges are stored inattendee_badgesand render asBadgePills on the profile page. - Avatars upload to the
avatarsbucket;avatar_file_idon the attendee references the file. The public view URL is built byavatarUrl(fileId)inlib/appwrite.ts. - The OTP flow uses Appwrite's
Account.createEmailToken+Account.createSessionthrough the server SDK admin client. The session secret is stored in an HTTP-onlya_session_<PROJECT_ID>cookie and passed back to Appwrite withsetSession().