-
- {m.name}
-
- {m.state}
-
-
-
- {p1?.team.name ?? '?'} vs {p2?.team.name ?? '?'}
- · {new Date(m.createdAt).toLocaleDateString()}
-
+
+{#if liveMatches.length > 0}
+
+
+
+ Live Now
+
+
+ {/if}
+
+
+
+ {#if liveMatches.length > 0}
+
Past Matches
+ {/if}
+
diff --git a/src/routes/(app)/teams/+page.server.ts b/src/routes/(app)/teams/+page.server.ts
new file mode 100644
index 0000000..b6a8030
--- /dev/null
+++ b/src/routes/(app)/teams/+page.server.ts
@@ -0,0 +1,120 @@
+import { db } from '$lib/server/db';
+import { team, teamMember, user } from '$lib/server/db/schema';
+import { eq } from 'drizzle-orm';
+import { redirect } from '@sveltejs/kit';
+import type { PageServerLoad, Actions } from './$types';
+
+export const load: PageServerLoad = async ({ locals }) => {
+ if (!locals.user) redirect(302, '/');
+
+ const teams = await db.query.team.findMany({
+ with: {
+ members: true
+ },
+ orderBy: (t, { desc }) => [desc(t.createdAt)]
+ });
+
+ // Fetch member user details
+ const teamsWithUsers = await Promise.all(
+ teams.map(async (t) => {
+ const membersWithUsers = await Promise.all(
+ t.members.map(async (m) => {
+ const u = await db.query.user.findFirst({
+ where: eq(user.id, m.userId)
+ });
+ return { ...m, user: u ? { id: u.id, name: u.name, image: u.image } : null };
+ })
+ );
+ return { ...t, members: membersWithUsers };
+ })
+ );
+
+ return { teams: teamsWithUsers, userId: locals.user.id };
+};
+
+export const actions: Actions = {
+ create: async ({ request, locals }) => {
+ if (!locals.user) redirect(302, '/');
+
+ const form = await request.formData();
+ const name = form.get('name')?.toString()?.trim();
+
+ if (!name) return { error: 'Team name is required' };
+
+ const [created] = await db
+ .insert(team)
+ .values({
+ name,
+ ownerId: locals.user.id,
+ isPersonal: false
+ })
+ .returning();
+
+ // Add creator as captain
+ await db.insert(teamMember).values({
+ teamId: created.id,
+ userId: locals.user.id,
+ role: 'captain'
+ });
+
+ return { success: true };
+ },
+
+ addMember: async ({ request, locals }) => {
+ if (!locals.user) redirect(302, '/');
+
+ const form = await request.formData();
+ const teamId = form.get('teamId')?.toString();
+ const username = form.get('username')?.toString()?.trim();
+
+ if (!teamId || !username) return { error: 'Team and username are required' };
+
+ // Find user by osu! username
+ const u = await db.query.user.findFirst({
+ where: eq(user.name, username)
+ });
+
+ if (!u) return { error: `User "${username}" not found. They need to log in first.` };
+
+ // Check not already member
+ const existing = await db.query.teamMember.findFirst({
+ where: (m, { and, eq }) => and(eq(m.teamId, teamId), eq(m.userId, u.id))
+ });
+
+ if (existing) return { error: `${username} is already on this team` };
+
+ await db.insert(teamMember).values({
+ teamId,
+ userId: u.id,
+ role: 'member'
+ });
+
+ return { success: true };
+ },
+
+ removeMember: async ({ request, locals }) => {
+ if (!locals.user) redirect(302, '/');
+
+ const form = await request.formData();
+ const memberId = form.get('memberId')?.toString();
+ if (!memberId) return { error: 'Missing member ID' };
+
+ await db.delete(teamMember).where(eq(teamMember.id, memberId));
+ return { success: true };
+ },
+
+ deleteTeam: async ({ request, locals }) => {
+ if (!locals.user) redirect(302, '/');
+
+ const form = await request.formData();
+ const teamId = form.get('teamId')?.toString();
+ if (!teamId) return { error: 'Missing team ID' };
+
+ // Only allow owner to delete
+ const t = await db.query.team.findFirst({ where: eq(team.id, teamId) });
+ if (!t || t.ownerId !== locals.user.id) return { error: 'Not authorized' };
+
+ await db.delete(team).where(eq(team.id, teamId));
+ return { success: true };
+ }
+};
diff --git a/src/routes/(app)/teams/+page.svelte b/src/routes/(app)/teams/+page.svelte
new file mode 100644
index 0000000..3c87bef
--- /dev/null
+++ b/src/routes/(app)/teams/+page.svelte
@@ -0,0 +1,229 @@
+
+
+
+
+
+
+
Teams
+
+ {data.teams.length} team{data.teams.length !== 1 ? 's' : ''}
+
+
+
(creating = !creating)}
+ class="rounded-md bg-accent px-4 py-2 text-sm font-600 text-surface-900 transition-colors hover:bg-accent-hover"
+ >
+ {creating ? 'Cancel' : 'New Team'}
+
+
+
+
+ {#if creating}
+
+ {/if}
+
+ {#if actionError}
+
{actionError}
+ {/if}
+
+
+ {#if myTeams.length > 0}
+
+
My Teams
+
+ {#each myTeams as t}
+
+
+
(expandedTeam = expandedTeam === t.id ? null : t.id)}
+ class="flex w-full items-center gap-4 p-4 text-left transition-colors hover:bg-surface-700"
+ >
+ {#if t.avatarUrl}
+
+ {:else}
+
+ {t.name.charAt(0).toUpperCase()}
+
+ {/if}
+
+
+ {t.name}
+ {#if t.isPersonal}
+ solo
+ {/if}
+
+
+ {t.members.length} member{t.members.length !== 1 ? 's' : ''}
+
+
+ ›
+
+
+
+ {#if expandedTeam === t.id}
+
+
+
+ {#each t.members as member}
+
+ {#if member.user?.image}
+
+ {:else}
+
+ {/if}
+
{member.user?.name ?? 'Unknown'}
+
{member.role}
+ {#if isOwner(t) && member.userId !== data.userId}
+
+ {/if}
+
+ {/each}
+
+
+
+ {#if isOwner(t) && !t.isPersonal}
+ {#if addingMemberTo === t.id}
+
+ {:else}
+
(addingMemberTo = t.id)}
+ class="mt-3 w-full rounded-md border border-dashed border-border py-2 text-xs text-text-secondary transition-colors hover:border-accent/40 hover:text-accent"
+ >
+ + Add Member
+
+ {/if}
+ {/if}
+
+
+ {#if isOwner(t) && !t.isPersonal}
+
+ {/if}
+
+ {/if}
+
+ {/each}
+
+
+ {/if}
+
+
+ {#if otherTeams.length > 0}
+
+
Other Teams
+
+ {#each otherTeams as t}
+
+ {#if t.avatarUrl}
+
+ {:else}
+
+ {t.name.charAt(0).toUpperCase()}
+
+ {/if}
+
+
{t.name}
+
{t.members.length} member{t.members.length !== 1 ? 's' : ''}
+
+
+ {/each}
+
+
+ {/if}
+
+
+ {#if data.teams.length === 0}
+
+
No teams yet. Your personal team is created when you sign in.
+
+ {/if}
+