diff --git a/README.md b/README.md index 7fc619e6..edaeb8e3 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ Two entities anchor the graph: `user_account_link`, and `stripe_payment.linked_user_id`. - **`hackathon`** — every event-scoped table cascades from it: teams, participants, projects, hackathon events, judges, judge assignments, judging - projects, judge queue, and maps. `member` is also scoped to a hackathon. + projects, judge queue, and maps. Nearly all foreign keys are `onDelete: "cascade"`, so deleting a user or a hackathon removes its dependent rows rather than orphaning them. @@ -69,14 +69,18 @@ Two aspects share the database and touch nowhere: - **Hackathon** — editions, registration, teams, project submission, judging. Everything here hangs off a `hackathon` row. -- **Club** — `initiative`, its applications, and the `project_leader` role. - Deliberately **not** scoped to a hackathon. A club project runs whenever - somebody leads one, and leading is a standing appointment rather than a - yearly re-grant. Nothing in this half is ever judged; judges only score - `hackathon_project`. - -`member` is the one crossing case: a paid year still hangs off an edition, so -membership resolves the current hackathon even though initiatives do not. +- **Club** — `member`, `membership_history`, `event`, `event_check_in`, + `initiative`, its applications, and the `project_leader` role. Deliberately + **not** scoped to a hackathon. A club project runs whenever somebody leads + one, and leading is a standing appointment rather than a yearly re-grant. + Nothing in this half is ever judged; judges only score `hackathon_project`. + +The two halves no longer cross. `member` used to be `unique(user_id, +hackathon_id)`, which welded a paid year to an edition: the day the next +hackathon opened, every paying member read as a non-member. It is now +`unique(user_id)` and a membership is defined entirely by its own dates, with +`membership_history` recording which years somebody held one. The club half +therefore works with no hackathon in the database at all. #### One-off step — only for a database that already has the edition-scoped tables diff --git a/packages/api/src/routers/initiative.ts b/packages/api/src/routers/initiative.ts index 57bb7ed2..430ee700 100644 --- a/packages/api/src/routers/initiative.ts +++ b/packages/api/src/routers/initiative.ts @@ -70,14 +70,12 @@ function canManage( /** * Applying is a member benefit, so it needs a membership that has not lapsed. * - * Initiatives are unscoped but membership is not — a paid year still hangs off - * an edition, so this resolves the current one. No edition means nobody has a - * live membership to check, which refuses rather than waving everyone through. + * Keyed on the person alone. This used to resolve a "current edition" and look + * the membership up against it, so between hackathons — when no edition + * qualified — it refused everybody, which is how the club half went dead + * outside event season. */ async function requireActiveMember(db: Reader, userId: string) { - // Initiatives were deliberately un-scoped from hackathons; membership now is - // too. This previously resolved a current edition and refused everyone when - // none existed, which is how the club half went dead outside event season. const member = await db.query.members.findFirst({ where: eq(members.userId, userId), columns: { isActive: true, membershipEndDate: true }, diff --git a/sites/mainweb/app/(portal)/admin/hackathons/[id]/page.tsx b/sites/mainweb/app/(portal)/admin/hackathons/[id]/page.tsx index 7b6a7864..23848239 100644 --- a/sites/mainweb/app/(portal)/admin/hackathons/[id]/page.tsx +++ b/sites/mainweb/app/(portal)/admin/hackathons/[id]/page.tsx @@ -13,7 +13,8 @@ import { AnalyticsTab } from "@/components/admin/hackathons/AnalyticsTab"; import { EventsTab } from "@/components/admin/hackathons/EventsTab"; import { JudgesTab } from "@/components/admin/hackathons/JudgesTab"; import { AnnouncementsTab } from "@/components/admin/hackathons/AnnouncementsTab"; -import { Gavel, Megaphone } from "lucide-react"; +import { TableCards } from "@/components/admin/hackathons/TableCards"; +import { Gavel, Megaphone, QrCode } from "lucide-react"; type Tab = | "events" @@ -21,7 +22,8 @@ type Tab = | "attendees" | "analytics" | "judges" - | "announcements"; + | "announcements" + | "tables"; export default function AdminHackathonDashboard() { const { status } = useSession(); @@ -59,6 +61,11 @@ export default function AdminHackathonDashboard() { icon: , }, { id: "judges", label: "Judges", icon: }, + { + id: "tables", + label: "Table Cards", + icon: , + }, { id: "announcements", label: "Email", @@ -189,6 +196,7 @@ export default function AdminHackathonDashboard() { )} {activeTab === "judges" && } + {activeTab === "tables" && } {activeTab === "announcements" && ( )} @@ -196,7 +204,11 @@ export default function AdminHackathonDashboard() { {/* MOBILE BOTTOM NAVIGATION - Enhanced */} -
+ {/* Scrolls horizontally rather than a fixed 5-column grid: there are + seven tabs, so Table Cards and Email were laid out off the right edge + of the phone an organiser actually carries — unreachable, with nothing + to suggest they existed. */} +
{/* Top gradient overlay */}
@@ -204,7 +216,7 @@ export default function AdminHackathonDashboard() {