diff --git a/app/app.css b/app/app.css index f456645..e27fd40 100644 --- a/app/app.css +++ b/app/app.css @@ -4,7 +4,7 @@ @custom-variant dark (&:is(.dark *)); @theme { - --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif, + --font-sans: "Barlow", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; } @@ -51,42 +51,43 @@ html { color-scheme: dark; + font-size: 18px; } :root { --radius: 0.625rem; - --background: oklch(0.13 0.015 255); - --foreground: oklch(0.95 0.01 255); - --card: oklch(0.18 0.02 255); - --card-foreground: oklch(0.95 0.01 255); - --popover: oklch(0.18 0.02 255); - --popover-foreground: oklch(0.95 0.01 255); - --primary: oklch(0.623 0.214 259.815); - --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.22 0.025 255); - --secondary-foreground: oklch(0.95 0.01 255); - --muted: oklch(0.22 0.025 255); - --muted-foreground: oklch(0.65 0.02 255); - --accent: oklch(0.25 0.03 255); - --accent-foreground: oklch(0.95 0.01 255); + --background: #14171e; + --foreground: #ffffff; + --card: #1c1f26; + --card-foreground: #ffffff; + --popover: #1c1f26; + --popover-foreground: #ffffff; + --primary: #adf661; + --primary-foreground: #14171e; + --secondary: #1c1f26; + --secondary-foreground: #ffffff; + --muted: #1c1f26; + --muted-foreground: rgb(255 255 255 / 55%); + --accent: #2ce1c1; + --accent-foreground: #14171e; --destructive: oklch(0.65 0.22 25); - --border: oklch(1 0 0 / 8%); - --input: oklch(1 0 0 / 12%); - --ring: oklch(0.623 0.214 259.815 / 50%); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); + --border: rgb(255 255 255 / 10%); + --input: rgb(255 255 255 / 12%); + --ring: #adf661; + --chart-1: #adf661; + --chart-2: #2ce1c1; + --chart-3: oklch(0.646 0.222 41.116); --chart-4: oklch(0.828 0.189 84.429); --chart-5: oklch(0.769 0.188 70.08); - --sidebar: oklch(0.18 0.02 255); - --sidebar-foreground: oklch(0.95 0.01 255); - --sidebar-primary: oklch(0.623 0.214 259.815); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.25 0.03 255); - --sidebar-accent-foreground: oklch(0.95 0.01 255); - --sidebar-border: oklch(1 0 0 / 8%); - --sidebar-ring: oklch(0.623 0.214 259.815 / 50%); - --electric: oklch(0.623 0.214 259.815); + --sidebar: #1c1f26; + --sidebar-foreground: #ffffff; + --sidebar-primary: #adf661; + --sidebar-primary-foreground: #14171e; + --sidebar-accent: #2ce1c1; + --sidebar-accent-foreground: #14171e; + --sidebar-border: rgb(255 255 255 / 10%); + --sidebar-ring: #adf661; + --electric: #2ce1c1; --amber-accent: oklch(0.79 0.17 70); --coral-accent: oklch(0.68 0.19 35); } @@ -98,6 +99,10 @@ html { body { @apply bg-background text-foreground; } + .bg-card { + background: #1c1f26; + border-color: #2a2e38 !important; + } } /* NProgress theme override */ diff --git a/app/components/league/CreateLeagueCard.stories.tsx b/app/components/league/CreateLeagueCard.stories.tsx new file mode 100644 index 0000000..59cade3 --- /dev/null +++ b/app/components/league/CreateLeagueCard.stories.tsx @@ -0,0 +1,15 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { CreateLeagueCard } from "./CreateLeagueCard"; + +const meta: Meta = { + title: "League/CreateLeagueCard", + component: CreateLeagueCard, + parameters: { + layout: "padded", + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/app/components/league/CreateLeagueCard.tsx b/app/components/league/CreateLeagueCard.tsx new file mode 100644 index 0000000..fd415d9 --- /dev/null +++ b/app/components/league/CreateLeagueCard.tsx @@ -0,0 +1,21 @@ +import { Swords } from "lucide-react"; +import { Link } from "react-router"; +import { Button } from "~/components/ui/button"; +import { Card, CardContent } from "~/components/ui/card"; +import { SectionCardHeader } from "~/components/ui/SectionCardHeader"; + +export function CreateLeagueCard() { + return ( + + + +

+ Invite friends, pick your sports, and start drafting. +

+ +
+
+ ); +} diff --git a/app/components/league/LeagueAvatar.tsx b/app/components/league/LeagueAvatar.tsx new file mode 100644 index 0000000..4774053 --- /dev/null +++ b/app/components/league/LeagueAvatar.tsx @@ -0,0 +1,52 @@ +const AVATAR_COLORS = [ + "#adf661", + "#2ce1c1", + "#8b5cf6", + "#f59e0b", + "#ef4444", + "#3b82f6", +]; + +function hashLeagueId(id: string): number { + let hash = 0; + for (let i = 0; i < id.length; i++) { + hash = (hash * 31 + id.charCodeAt(i)) & 0xffff; + } + return hash; +} + +interface LeagueAvatarProps { + leagueId: string; + leagueName: string; + /** Tailwind size classes, e.g. "h-10 w-10" (default) or "h-5 w-5" */ + className?: string; + /** Font size class, e.g. "text-sm" (default) or "text-[10px]" */ + textClassName?: string; +} + +export function LeagueAvatar({ + leagueId, + leagueName, + className = "h-10 w-10", + textClassName = "text-sm", +}: LeagueAvatarProps) { + const color = AVATAR_COLORS[hashLeagueId(leagueId) % AVATAR_COLORS.length]; + const initials = + leagueName + .split(/\s+/) + .filter(Boolean) + .slice(0, 2) + .map((w) => w[0].toUpperCase()) + .join("") || "?"; + + return ( +
+ {initials} +
+ ); +} diff --git a/app/components/league/LeagueRow.stories.tsx b/app/components/league/LeagueRow.stories.tsx new file mode 100644 index 0000000..335df58 --- /dev/null +++ b/app/components/league/LeagueRow.stories.tsx @@ -0,0 +1,99 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { LeagueRow } from "./LeagueRow"; + +const meta: Meta = { + title: "League/LeagueRow", + component: LeagueRow, + parameters: { + layout: "padded", + }, +}; + +export default meta; +type Story = StoryObj; + +const baseLeague = { + leagueId: "league-abc-123", + leagueName: "Alpha Dynasty", + numSports: 2, +}; + +export const DraftInProgress: Story = { + args: { + ...baseLeague, + status: "draft", + seasonId: "season-xyz-456", + }, +}; + +export const ActiveRankUp: Story = { + args: { + ...baseLeague, + leagueName: "Hardcourt Elites", + leagueId: "league-hce-999", + numSports: 1, + status: "active", + currentRank: 4, + previousRank: 6, + totalPoints: 1422.8, + }, +}; + +export const ActiveRankDown: Story = { + args: { + ...baseLeague, + leagueName: "Monday Night Madness", + leagueId: "league-mnm-777", + numSports: 3, + status: "active", + currentRank: 7, + previousRank: 5, + totalPoints: 987.5, + }, +}; + +export const ActiveNoChange: Story = { + args: { + ...baseLeague, + leagueName: "The Bracket Boys", + leagueId: "league-tbb-555", + numSports: 2, + status: "active", + currentRank: 1, + previousRank: 1, + totalPoints: 2810.0, + }, +}; + +export const ActiveNoStanding: Story = { + args: { + ...baseLeague, + leagueName: "Fresh Start", + leagueId: "league-fs-222", + numSports: 2, + status: "active", + }, +}; + +export const PreDraft: Story = { + args: { + ...baseLeague, + leagueName: "Summer Smash League", + leagueId: "league-ssl-333", + numSports: 4, + status: "pre_draft", + seasonId: "season-ssl-999", + }, +}; + +export const Completed: Story = { + args: { + ...baseLeague, + leagueName: "2023 World Tour", + leagueId: "league-wt-111", + numSports: 2, + status: "completed", + currentRank: 3, + totalPoints: 3150.2, + }, +}; diff --git a/app/components/league/LeagueRow.tsx b/app/components/league/LeagueRow.tsx new file mode 100644 index 0000000..7db8546 --- /dev/null +++ b/app/components/league/LeagueRow.tsx @@ -0,0 +1,234 @@ +import { formatDistanceToNow } from "date-fns"; +import { Link } from "react-router"; +import { Button } from "~/components/ui/button"; +import { LeagueAvatar } from "./LeagueAvatar"; + +export interface LeagueRowProps { + leagueId: string; + leagueName: string; + numSports: number; + status: "draft" | "active" | "pre_draft" | "completed"; + seasonId?: string; + currentRank?: number; + totalPoints?: number; + previousRank?: number; + completionPercentage?: number; + draftDateTime?: string | null; + picksUntilMyTurn?: number; + draftPosition?: number; +} + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +function ordinal(n: number): string { + const s = ["th", "st", "nd", "rd"]; + const v = n % 100; + return n + (s[(v - 20) % 10] ?? s[v] ?? s[0]); +} + + +// ─── Shared stat column ─────────────────────────────────────────────────────── + +function StatColumn({ + label, + children, +}: { + label: string; + children: React.ReactNode; +}) { + return ( +
+

+ {label} +

+
{children}
+
+ ); +} + +function StatDivider() { + return
; +} + +// ─── Season progress bar ────────────────────────────────────────────────────── + +function SeasonProgress({ pct, status }: { pct: number; status: "active" | "completed" }) { + const label = status === "completed" ? "100%" : `${pct}%`; + const fill = status === "completed" ? 100 : pct; + return ( +
+
+
+
+ {label} +
+ ); +} + +// ─── Rank change indicator ──────────────────────────────────────────────────── + +function RankChange({ current, previous }: { current: number; previous: number | undefined }) { + if (previous === undefined || previous === current) return null; + const delta = previous - current; + if (delta > 0) { + return ▲{delta}; + } + return ( + + ▼{Math.abs(delta)} + + ); +} + +// ─── Row variants ───────────────────────────────────────────────────────────── + +function DraftRow({ leagueId, leagueName, seasonId, picksUntilMyTurn }: LeagueRowProps) { + const picksLabel = + picksUntilMyTurn === 0 + ? "You're on the clock!" + : picksUntilMyTurn !== undefined + ? `Up in ${picksUntilMyTurn} pick${picksUntilMyTurn === 1 ? "" : "s"}` + : null; + + return ( +
+ +
+

{leagueName}

+
+
+ + + Draft in Progress + +
+ {picksLabel && ( + {picksLabel} + )} +
+
+ {seasonId && ( + + )} +
+ ); +} + +function ActiveRow({ + leagueId, + leagueName, + currentRank, + totalPoints, + previousRank, + completionPercentage = 0, +}: LeagueRowProps) { + const showStats = currentRank !== undefined || totalPoints !== undefined; + + return ( + + +
+

{leagueName}

+ +
+ {showStats && ( +
+ {currentRank !== undefined && ( + + #{currentRank} + + + )} + {currentRank !== undefined && totalPoints !== undefined && } + {totalPoints !== undefined && ( + + + {Math.round(totalPoints).toLocaleString("en-US")} + + + )} +
+ )} + + ); +} + +function PreDraftRow({ + leagueId, + leagueName, + draftDateTime, + draftPosition, +}: LeagueRowProps) { + let draftTimeValue = "Not scheduled"; + if (draftDateTime) { + const draftDate = new Date(draftDateTime); + draftTimeValue = + draftDate > new Date() + ? formatDistanceToNow(draftDate, { addSuffix: true }) + : "Starting soon"; + } + + return ( + + +
+

{leagueName}

+

Pre-Draft

+
+
+ + {draftTimeValue} + + {draftPosition !== undefined && ( + <> + + + + {ordinal(draftPosition)} + + + + )} +
+ + ); +} + +function CompletedRow({ + leagueId, + leagueName, + completionPercentage = 0, +}: LeagueRowProps) { + return ( + + +
+

{leagueName}

+ +
+ + ); +} + +// ─── Public export ──────────────────────────────────────────────────────────── + +export function LeagueRow(props: LeagueRowProps) { + if (props.status === "draft") return ; + if (props.status === "active") return ; + if (props.status === "pre_draft") return ; + return ; +} diff --git a/app/components/league/MyLeaguesCard.stories.tsx b/app/components/league/MyLeaguesCard.stories.tsx new file mode 100644 index 0000000..20f1f1e --- /dev/null +++ b/app/components/league/MyLeaguesCard.stories.tsx @@ -0,0 +1,96 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { MyLeaguesCard } from "./MyLeaguesCard"; + +const meta: Meta = { + title: "League/MyLeaguesCard", + component: MyLeaguesCard, + parameters: { + layout: "padded", + }, +}; + +export default meta; +type Story = StoryObj; + +export const MixedStatuses: Story = { + args: { + leagues: [ + { + leagueId: "league-abc-123", + leagueName: "Alpha Dynasty", + numSports: 2, + status: "active", + currentRank: 4, + previousRank: 6, + totalPoints: 1422.8, + }, + { + leagueId: "league-hce-999", + leagueName: "Hardcourt Elites", + numSports: 1, + status: "draft", + seasonId: "season-xyz-456", + }, + { + leagueId: "league-ssl-333", + leagueName: "Summer Smash", + numSports: 3, + status: "pre_draft", + }, + { + leagueId: "league-wt-111", + leagueName: "2023 World Tour", + numSports: 2, + status: "completed", + currentRank: 2, + totalPoints: 3150.2, + }, + ], + }, +}; + +export const DraftingOnly: Story = { + args: { + leagues: [ + { + leagueId: "league-abc-123", + leagueName: "World Aquatics Open", + numSports: 2, + status: "draft", + seasonId: "season-wao-789", + }, + ], + }, +}; + +export const ActiveOnly: Story = { + args: { + leagues: [ + { + leagueId: "league-abc-123", + leagueName: "Alpha Dynasty", + numSports: 2, + status: "active", + currentRank: 4, + previousRank: 6, + totalPoints: 1422.8, + }, + { + leagueId: "league-hce-999", + leagueName: "Hardcourt Elites", + numSports: 1, + status: "active", + currentRank: 12, + previousRank: 11, + totalPoints: 2810.0, + }, + ], + viewAllUrl: "/leagues", + }, +}; + +export const Empty: Story = { + args: { + leagues: [], + }, +}; diff --git a/app/components/league/MyLeaguesCard.tsx b/app/components/league/MyLeaguesCard.tsx new file mode 100644 index 0000000..ccfec9f --- /dev/null +++ b/app/components/league/MyLeaguesCard.tsx @@ -0,0 +1,59 @@ +import { Shield } from "lucide-react"; +import { Link } from "react-router"; +import { Card, CardContent } from "~/components/ui/card"; +import { SectionCardHeader } from "~/components/ui/SectionCardHeader"; +import { LeagueRow, type LeagueRowProps } from "./LeagueRow"; + +const STATUS_ORDER: Record = { + draft: 0, + active: 1, + pre_draft: 2, + completed: 3, +}; + +interface MyLeaguesCardProps { + leagues: LeagueRowProps[]; + viewAllUrl?: string; +} + +export function MyLeaguesCard({ leagues, viewAllUrl }: MyLeaguesCardProps) { + const sorted = leagues.toSorted( + (a, b) => STATUS_ORDER[a.status] - STATUS_ORDER[b.status] + ); + + const right = viewAllUrl ? ( + + View All Leagues + + ) : undefined; + + return ( + + + + {sorted.length === 0 ? ( +
+

+ You aren't a member of any leagues yet. +

+ + Create your first league → + +
+ ) : ( +
+ {sorted.map((league) => ( + + ))} +
+ )} +
+
+ ); +} diff --git a/app/components/navbar.tsx b/app/components/navbar.tsx index 6e83d40..40747b7 100644 --- a/app/components/navbar.tsx +++ b/app/components/navbar.tsx @@ -1,12 +1,5 @@ import { useState } from "react"; import { Link } from "react-router"; -import { - NavigationMenu, - NavigationMenuItem, - NavigationMenuLink, - NavigationMenuList, - navigationMenuTriggerStyle, -} from "~/components/ui/navigation-menu"; import { Sheet, SheetContent, @@ -14,8 +7,9 @@ import { SheetTitle, SheetTrigger, } from "~/components/ui/sheet"; +import { Button } from "~/components/ui/button"; import { SignedIn, SignedOut, SignInButton, UserButton } from "@clerk/react-router"; -import { MenuIcon } from "lucide-react"; +import { HelpCircle, MenuIcon, Settings } from "lucide-react"; interface NavbarProps { isAdmin: boolean; @@ -27,51 +21,38 @@ export function Navbar({ isAdmin }: NavbarProps) { return (
- {/* Logo */} - - Brackt - + {/* Left: Logo + Nav links */} +
+ + Brackt + + +
- {/* Desktop Navigation */} - - - - - Home - - - - - How To Play - - - - - Rules - - - - - Support - - - {isAdmin && ( - - - Admin - - - )} - - - - {/* Desktop Auth */} -
+ {/* Desktop Right: Support + Admin icons + Auth */} +
+ + + + {isAdmin && ( + + + + )} - + @@ -79,19 +60,33 @@ export function Navbar({ isAdmin }: NavbarProps) {
- {/* Mobile Menu */} -
+ {/* Mobile: icons + Auth + Hamburger */} +
+ + + + {isAdmin && ( + + + + )} - + - + + {/* + Desktop: 3-col grid, left column (My Leagues + Create) spans 2, right (Events) spans 1. + Mobile: single column stacked in order: My Leagues, Upcoming Events, Create League. + */} +
+ {/* My Leagues — order 1 on mobile, col-span-2 on desktop */} +
+ +
+ + {/* Upcoming Events — order 2 on mobile, right column on desktop spanning both rows */} +
+ +
+ + {/* Create a League — order 3 on mobile, below My Leagues on desktop */} +
+ +
- - {upcomingCalendarEvents.length > 0 && ( -
- -
- )} - - {leagues.length === 0 ? ( - - - No Leagues - - You aren't a member of any leagues yet - - - - - - - ) : ( -
- {leagues.map((league) => ( - - - - {league.name} - - Created {new Date(league.createdAt).toLocaleDateString()} - - - - {league.currentSeason ? ( -
-
-

- Current Season -

-

- {league.currentSeason.year} -

-
-
-

Status

-

- {league.currentSeason.status.replace("_", " ")} -

-
-
- ) : ( -

- No active season -

- )} -
-
- - ))} -
- )}
); } diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png index b047276..7200a0a 100644 Binary files a/public/apple-touch-icon.png and b/public/apple-touch-icon.png differ diff --git a/public/favicon-96x96.png b/public/favicon-96x96.png index 5c96732..d26adec 100644 Binary files a/public/favicon-96x96.png and b/public/favicon-96x96.png differ diff --git a/public/favicon.ico b/public/favicon.ico index e6ffcef..c9cd242 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/public/favicon.svg b/public/favicon.svg index b54c356..c038471 100644 --- a/public/favicon.svg +++ b/public/favicon.svg @@ -1 +1,43 @@ - \ No newline at end of file +RealFaviconGeneratorhttps://realfavicongenerator.net + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/logo.svg b/public/logo.svg index a5e1a54..b7c99e4 100644 --- a/public/logo.svg +++ b/public/logo.svg @@ -1,14 +1,28 @@ - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/logomark.svg b/public/logomark.svg new file mode 100644 index 0000000..90ae1b9 --- /dev/null +++ b/public/logomark.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/site.webmanifest b/public/site.webmanifest index ccf313a..59fe895 100644 --- a/public/site.webmanifest +++ b/public/site.webmanifest @@ -1,6 +1,6 @@ { - "name": "MyWebSite", - "short_name": "MySite", + "name": "Brackt", + "short_name": "Brackt", "icons": [ { "src": "/web-app-manifest-192x192.png", @@ -15,7 +15,7 @@ "purpose": "maskable" } ], - "theme_color": "#ffffff", - "background_color": "#ffffff", + "theme_color": "#2ce1c1", + "background_color": "#14171e", "display": "standalone" } \ No newline at end of file diff --git a/public/web-app-manifest-192x192.png b/public/web-app-manifest-192x192.png index d9eba2b..28af5a4 100644 Binary files a/public/web-app-manifest-192x192.png and b/public/web-app-manifest-192x192.png differ diff --git a/public/web-app-manifest-512x512.png b/public/web-app-manifest-512x512.png index 9093a20..a26fbe2 100644 Binary files a/public/web-app-manifest-512x512.png and b/public/web-app-manifest-512x512.png differ