POL-127: Add organizations table and championship ownership
- Create organizations table with Alembic migration (3-phase: create table, migrate data, drop old column) - Add org_id FK on championships linking to organizations - Refactor all schemas into one-class-per-file packages (auth, championship, organization, participant, registration, user) - Update CRUD layer with selectinload for organization relationships - Update frontend types and components to use nested organization object - Remove phantom Championship fields (subtitle, venue, accent_color) from frontend Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,7 @@ import { RegistrationTimeline } from "@/components/registrations/RegistrationTim
|
||||
import { StatusBadge } from "@/components/shared/StatusBadge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { MapPin, Building2, Calendar, CreditCard, Film, ExternalLink } from "lucide-react";
|
||||
import { MapPin, Calendar, CreditCard, Film, ExternalLink } from "lucide-react";
|
||||
|
||||
export default function ChampionshipDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = use(params);
|
||||
@@ -54,7 +54,6 @@ export default function ChampionshipDetailPage({ params }: { params: Promise<{ i
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="font-display text-3xl font-bold tracking-wide">{championship.title}</h1>
|
||||
{championship.subtitle && <p className="text-muted-foreground mt-1">{championship.subtitle}</p>}
|
||||
</div>
|
||||
<StatusBadge status={championship.status} />
|
||||
</div>
|
||||
@@ -67,12 +66,6 @@ export default function ChampionshipDetailPage({ params }: { params: Promise<{ i
|
||||
<span className="text-muted-foreground">{championship.location}</span>
|
||||
</div>
|
||||
)}
|
||||
{championship.venue && (
|
||||
<div className="flex items-center gap-2.5 rounded-xl bg-surface-elevated border border-border/30 px-4 py-3 text-sm">
|
||||
<Building2 size={16} className="text-purple-accent shrink-0" />
|
||||
<span className="text-muted-foreground">{championship.venue}</span>
|
||||
</div>
|
||||
)}
|
||||
{eventDate && (
|
||||
<div className="flex items-center gap-2.5 rounded-xl bg-surface-elevated border border-border/30 px-4 py-3 text-sm">
|
||||
<Calendar size={16} className="text-gold-accent shrink-0" />
|
||||
|
||||
@@ -64,13 +64,13 @@ export default function ProfilePage() {
|
||||
<span className="text-sm text-foreground">{user.phone}</span>
|
||||
</div>
|
||||
)}
|
||||
{user.organization_name && (
|
||||
{user.organization?.name && (
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<span className="flex items-center gap-2 text-sm text-dim">
|
||||
<Building2 size={14} />
|
||||
Organization
|
||||
</span>
|
||||
<span className="text-sm text-foreground">{user.organization_name}</span>
|
||||
<span className="text-sm text-foreground">{user.organization.name}</span>
|
||||
</div>
|
||||
)}
|
||||
{user.instagram_handle && (
|
||||
|
||||
@@ -35,10 +35,10 @@ export function UserCard({ user, onApprove, onReject, isActing }: Props) {
|
||||
<span className={`h-2 w-2 rounded-full shrink-0 ${STATUS_DOT[user.status] ?? "bg-dim"}`} />
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">{user.email}</p>
|
||||
{user.organization_name && (
|
||||
{user.organization?.name && (
|
||||
<p className="flex items-center gap-1.5 text-sm text-muted-foreground">
|
||||
<Building2 size={12} className="text-dim" />
|
||||
{user.organization_name}
|
||||
{user.organization.name}
|
||||
</p>
|
||||
)}
|
||||
{user.phone && (
|
||||
|
||||
@@ -27,6 +27,6 @@ export const authApi = {
|
||||
|
||||
me: () => apiClient.get<UserOut>("/auth/me").then((r) => r.data),
|
||||
|
||||
updateMe: (data: Partial<Pick<UserOut, "full_name" | "phone" | "organization_name" | "instagram_handle">>) =>
|
||||
updateMe: (data: Partial<Pick<UserOut, "full_name" | "phone" | "instagram_handle">> & { organization_name?: string }) =>
|
||||
apiClient.patch<UserOut>("/auth/me", data).then((r) => r.data),
|
||||
};
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
export interface OrganizationBrief {
|
||||
id: string;
|
||||
name: string;
|
||||
instagram: string | null;
|
||||
logo_url: string | null;
|
||||
}
|
||||
|
||||
export interface Championship {
|
||||
id: string;
|
||||
org_id: string | null;
|
||||
title: string;
|
||||
subtitle: string | null;
|
||||
description: string | null;
|
||||
location: string | null;
|
||||
venue: string | null;
|
||||
event_date: string | null;
|
||||
registration_open_at: string | null;
|
||||
registration_close_at: string | null;
|
||||
@@ -14,7 +20,7 @@ export interface Championship {
|
||||
status: "draft" | "open" | "closed" | "completed";
|
||||
source: string;
|
||||
image_url: string | null;
|
||||
accent_color: string | null;
|
||||
organization: OrganizationBrief | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
export interface OrganizationOut {
|
||||
id: string;
|
||||
name: string;
|
||||
instagram: string | null;
|
||||
email: string | null;
|
||||
city: string | null;
|
||||
logo_url: string | null;
|
||||
verified: boolean;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface UserOut {
|
||||
id: string;
|
||||
email: string;
|
||||
@@ -5,7 +16,7 @@ export interface UserOut {
|
||||
phone: string | null;
|
||||
role: "member" | "organizer" | "admin";
|
||||
status: "pending" | "approved" | "rejected";
|
||||
organization_name: string | null;
|
||||
instagram_handle: string | null;
|
||||
organization: OrganizationOut | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user