diff --git a/src/components/dialogs/SaveBracketDialog.tsx b/src/components/dialogs/SaveBracketDialog.tsx
index 69aea80..d920aff 100644
--- a/src/components/dialogs/SaveBracketDialog.tsx
+++ b/src/components/dialogs/SaveBracketDialog.tsx
@@ -1,6 +1,9 @@
"use client";
-import { Save } from "lucide-react";
+import { useAuth } from "@clerk/nextjs";
+import { accountRequest } from "@/lib/account-client";
+import { PLAYOFF_SEASON_YEAR } from "@/data/teams";
+import { Loader2, Save } from "lucide-react";
import { nanoid } from "nanoid";
import { useState } from "react";
import { toast } from "sonner";
@@ -24,12 +27,13 @@ interface SaveBracketDialogProps {
}
export function SaveBracketDialog({ open, onOpenChange }: SaveBracketDialogProps) {
+ const { userId } = useAuth();
const { bracket, setBracketName, setSubtitle } = useBracket();
const [name, setName] = useState(bracket.name);
const [subtitle, setSubtitleValue] = useState(bracket.subtitle || "");
const [isSaving, setIsSaving] = useState(false);
- const handleSave = () => {
+ const handleSave = async () => {
setIsSaving(true);
try {
const trimmedName = name.trim();
@@ -46,7 +50,17 @@ export function SaveBracketDialog({ open, onOpenChange }: SaveBracketDialogProps
setBracketName(trimmedName);
setSubtitle(trimmedSubtitle);
- saveBracket(bracketToSave);
+ if (userId) {
+ await accountRequest(
+ "/api/brackets",
+ "POST",
+ { seasonYear: PLAYOFF_SEASON_YEAR, state: bracketToSave },
+ undefined,
+ userId,
+ );
+ } else {
+ saveBracket(bracketToSave);
+ }
toast.success("Bracket saved!", {
description: trimmedName
? `"${trimmedName}" has been saved.`
@@ -54,7 +68,7 @@ export function SaveBracketDialog({ open, onOpenChange }: SaveBracketDialogProps
});
onOpenChange(false);
} catch (error) {
- toast.error("Failed to save bracket");
+ toast.error(error instanceof Error ? error.message : "Failed to save bracket");
} finally {
setIsSaving(false);
}
@@ -69,8 +83,9 @@ export function SaveBracketDialog({ open, onOpenChange }: SaveBracketDialogProps
Save Bracket
- Save your current bracket to access it later. You can save multiple versions with
- different names.
+ {userId
+ ? "Save a private copy to your account and access it on any device."
+ : "Save a copy in this browser. Sign in to save brackets across devices."}
@@ -131,7 +146,13 @@ export function SaveBracketDialog({ open, onOpenChange }: SaveBracketDialogProps
disabled={isSaving}
className="bg-gradient-to-r from-red-600 to-blue-600 hover:from-red-700 hover:to-blue-700 md:h-11 md:px-6 md:text-base"
>
- {isSaving ? "Saving..." : "Save Bracket"}
+ {isSaving && (
+
+ )}
+ Save Bracket
diff --git a/src/contexts/BracketContext.tsx b/src/contexts/BracketContext.tsx
index 24886be..5766370 100644
--- a/src/contexts/BracketContext.tsx
+++ b/src/contexts/BracketContext.tsx
@@ -1,5 +1,7 @@
"use client";
+import { useAuth } from "@clerk/nextjs";
+
import {
createContext,
type ReactNode,
@@ -61,9 +63,35 @@ function getMatchupRound(matchupId: string): RoundName | null {
return null;
}
-export function BracketProvider({ children }: { children: ReactNode }) {
- const storedUser = getStoredUser();
- const storedBracket = getCurrentBracket();
+type ProviderProps = { children: ReactNode; initialBracket?: BracketState; persist?: boolean };
+export function BracketProvider(props: ProviderProps) {
+ const { userId, isLoaded } = useAuth();
+ // Mount editable state only after account identity is known. Remounting a
+ // temporary guest tree when Clerk loads can otherwise discard typed input.
+ if (!isLoaded)
+ return (
+
+ Loading bracket…
+
+ );
+ return (
+
+ );
+}
+
+function BracketProviderState({
+ children,
+ initialBracket,
+ persist,
+ ownerId,
+}: ProviderProps & { ownerId?: string }) {
+ const storedUser = ownerId ? null : getStoredUser();
+ const storedBracket = initialBracket ?? (persist ? getCurrentBracket(ownerId) : null);
// Migrate old brackets that don't have lockedRounds
const migratedBracket = storedBracket
@@ -86,10 +114,10 @@ export function BracketProvider({ children }: { children: ReactNode }) {
// Auto-save to localStorage on changes
useEffect(() => {
- if (bracket.userName) {
- saveCurrentBracket(bracket);
+ if (persist && bracket.userName) {
+ saveCurrentBracket(bracket, ownerId);
}
- }, [bracket]);
+ }, [bracket, persist, ownerId]);
// Track if we have live games for SSE subscription
const hasLiveGames = hasInProgressGames(bracket.liveResults);
diff --git a/src/lib/account-client.ts b/src/lib/account-client.ts
new file mode 100644
index 0000000..3616abb
--- /dev/null
+++ b/src/lib/account-client.ts
@@ -0,0 +1,40 @@
+export class AccountRequestError extends Error {
+ constructor(
+ public status: number,
+ message: string,
+ ) {
+ super(message);
+ }
+}
+
+export async function accountRequest
(
+ path: string,
+ method = "GET",
+ body?: unknown,
+ signal?: AbortSignal,
+ ownerId?: string | null,
+): Promise {
+ const response = await fetch(path, {
+ method,
+ signal,
+ headers: ownerId ? { "X-Bracket-Owner": ownerId } : {},
+ cache: "no-store",
+ credentials: "same-origin",
+ ...(body === undefined
+ ? {}
+ : {
+ headers: {
+ "Content-Type": "application/json",
+ ...(ownerId ? { "X-Bracket-Owner": ownerId } : {}),
+ },
+ body: JSON.stringify(body),
+ }),
+ });
+ const data = await response.json().catch(() => ({}));
+ if (!response.ok)
+ throw new AccountRequestError(
+ response.status,
+ data.error || "Unable to reach account storage. Please retry.",
+ );
+ return data as T;
+}
diff --git a/src/lib/bracket-api.ts b/src/lib/bracket-api.ts
index dd1f24b..c7a1371 100644
--- a/src/lib/bracket-api.ts
+++ b/src/lib/bracket-api.ts
@@ -11,6 +11,9 @@ export async function withAccount(request: Request, action: (ownerId: string) =>
try {
const { userId } = await auth();
if (!userId) return json({ error: "Sign in to access your saved brackets." }, 401);
+ const expectedOwner = request.headers.get("x-bracket-owner");
+ if (expectedOwner && expectedOwner !== userId)
+ return json({ error: "Your account changed. Reload before continuing." }, 401);
if (request.method !== "GET") {
const origin = request.headers.get("origin");
if (origin && origin !== new URL(request.url).origin)
diff --git a/src/lib/storage.ts b/src/lib/storage.ts
index 80114bf..5073751 100644
--- a/src/lib/storage.ts
+++ b/src/lib/storage.ts
@@ -5,6 +5,10 @@ const USER_KEY = `${STORAGE_PREFIX}user`;
const BRACKETS_KEY = `${STORAGE_PREFIX}brackets`;
const CURRENT_KEY = `${STORAGE_PREFIX}current`;
+function storageKey(key: string, ownerId?: string): string {
+ return ownerId ? `${key}:account:${ownerId}` : key;
+}
+
function isClient(): boolean {
return typeof window !== "undefined";
}
@@ -31,20 +35,20 @@ export function clearStoredUser(): void {
}
// Saved brackets storage
-export function getSavedBrackets(): SavedBracket[] {
+export function getSavedBrackets(ownerId?: string): SavedBracket[] {
if (!isClient()) return [];
try {
- const data = localStorage.getItem(BRACKETS_KEY);
+ const data = localStorage.getItem(storageKey(BRACKETS_KEY, ownerId));
return data ? JSON.parse(data) : [];
} catch {
return [];
}
}
-export function saveBracket(bracket: BracketState): string {
+export function saveBracket(bracket: BracketState, ownerId?: string): string {
if (!isClient()) return bracket.id;
- const brackets = getSavedBrackets();
+ const brackets = getSavedBrackets(ownerId);
const existingIndex = brackets.findIndex((b) => b.id === bracket.id);
const savedBracket: SavedBracket = {
@@ -62,40 +66,43 @@ export function saveBracket(bracket: BracketState): string {
brackets.push(savedBracket);
}
- localStorage.setItem(BRACKETS_KEY, JSON.stringify(brackets));
+ localStorage.setItem(storageKey(BRACKETS_KEY, ownerId), JSON.stringify(brackets));
return bracket.id;
}
-export function loadBracket(id: string): BracketState | null {
- const brackets = getSavedBrackets();
+export function loadBracket(id: string, ownerId?: string): BracketState | null {
+ const brackets = getSavedBrackets(ownerId);
const saved = brackets.find((b) => b.id === id);
return saved?.state || null;
}
-export function deleteBracket(id: string): void {
+export function deleteBracket(id: string, ownerId?: string): void {
if (!isClient()) return;
- const brackets = getSavedBrackets();
+ const brackets = getSavedBrackets(ownerId);
const filtered = brackets.filter((b) => b.id !== id);
- localStorage.setItem(BRACKETS_KEY, JSON.stringify(filtered));
+ localStorage.setItem(storageKey(BRACKETS_KEY, ownerId), JSON.stringify(filtered));
}
// Current session storage (auto-save)
-export function getCurrentBracket(): BracketState | null {
+export function getCurrentBracket(ownerId?: string): BracketState | null {
if (!isClient()) return null;
try {
- const data = localStorage.getItem(CURRENT_KEY);
+ const data = localStorage.getItem(storageKey(CURRENT_KEY, ownerId));
return data ? JSON.parse(data) : null;
} catch {
return null;
}
}
-export function saveCurrentBracket(bracket: BracketState): void {
+export function saveCurrentBracket(bracket: BracketState, ownerId?: string): void {
if (!isClient()) return;
- localStorage.setItem(CURRENT_KEY, JSON.stringify({ ...bracket, updatedAt: Date.now() }));
+ localStorage.setItem(
+ storageKey(CURRENT_KEY, ownerId),
+ JSON.stringify({ ...bracket, updatedAt: Date.now() }),
+ );
}
-export function clearCurrentBracket(): void {
+export function clearCurrentBracket(ownerId?: string): void {
if (!isClient()) return;
- localStorage.removeItem(CURRENT_KEY);
+ localStorage.removeItem(storageKey(CURRENT_KEY, ownerId));
}