diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 93048e9..c369eab 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -16,7 +16,10 @@ "mcp__ide__getDiagnostics", "Bash(git checkout:*)", "Bash(mkdir:*)", - "Bash(npx oxlint:*)" + "Bash(npx oxlint:*)", + "WebSearch", + "WebFetch(domain:tanstack.com)", + "Bash(node:*)" ], "deny": [] } diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..ef04759 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,7 @@ +# Copilot Custom Instructions + +see CLAUDE.md for more details and rules. + +## Function Definitions After Return in React Components + +In this codebase, it is acceptable and preferred to define helper functions (such as event handlers) after the main component’s return statement. This style improves readability by keeping the primary component logic at the top and allowing additional details to be found below. JavaScript and TypeScript support function hoisting for function declarations, so this pattern is safe and intentional. Please do not flag this as a style issue in reviews. diff --git a/.gitignore b/.gitignore index 52e3438..a1678ad 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,4 @@ tests/screenshots/ supabase/.temp supabase/.branches .vercel +dev-dist \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index b482d32..b5c0b35 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -114,6 +114,10 @@ src/ - **Forms**: ALL forms must use react-hook-form with proper validation. Never use plain HTML forms or manual state management for form inputs. Use @hookform/resolvers for validation schemas when needed. - **Long Components**: Break long components (>150 lines) into smaller focused pieces. Follow the FilterSortControls pattern of primary controls + expandable sections. +#### Function Definitions After Return in React Components + +In this codebase, it is acceptable and preferred to define helper functions (such as event handlers) after the main component’s return statement. This style improves readability by keeping the primary component logic at the top and allowing additional details to be found below. JavaScript and TypeScript support function hoisting for function declarations, so this pattern is safe and intentional. Please do not flag this as a style issue in reviews. + ### Important Notes - Server runs on port 8080 (not standard 3000) diff --git a/package-lock.json b/package-lock.json index d8176e2..6517449 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,8 +39,10 @@ "@radix-ui/react-tooltip": "^1.1.4", "@supabase/supabase-js": "^2.50.0", "@tailwindcss/line-clamp": "^0.4.4", + "@tanstack/query-async-storage-persister": "^5.86.0", "@tanstack/react-query": "^5.56.2", "@tanstack/react-query-devtools": "^5.81.2", + "@tanstack/react-query-persist-client": "^5.85.9", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.0.0", @@ -4476,10 +4478,46 @@ "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, + "node_modules/@tanstack/query-async-storage-persister": { + "version": "5.86.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-async-storage-persister/-/query-async-storage-persister-5.86.0.tgz", + "integrity": "sha512-+/TXcM08kwxJ0Ny/2HN6MPae43mvLOBQxHSbtVHQlCQO3rm/ewEnxFtU1ktXhEyTAo7ABAENu5eGXqmHChmMIA==", + "license": "MIT", + "dependencies": { + "@tanstack/query-persist-client-core": "5.86.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-async-storage-persister/node_modules/@tanstack/query-core": { + "version": "5.86.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.86.0.tgz", + "integrity": "sha512-Y6ibQm6BXbw6w1p3a5LrPn8Ae64M0dx7hGmnhrm9P+XAkCCKXOwZN0J5Z1wK/0RdNHtR9o+sWHDXd4veNI60tQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-async-storage-persister/node_modules/@tanstack/query-persist-client-core": { + "version": "5.86.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-persist-client-core/-/query-persist-client-core-5.86.0.tgz", + "integrity": "sha512-EHHfmqkqHd1Rak2xv4ZT3SwZuSAZ09D6WzuqA8DIXsB/Ir3H9HB/3O1jJe2/LhmgIYSf6k8jYiblK14dvqhd0w==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.86.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tanstack/query-core": { - "version": "5.81.5", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.81.5.tgz", - "integrity": "sha512-ZJOgCy/z2qpZXWaj/oxvodDx07XcQa9BF92c0oINjHkoqUPsmm3uG08HpTaviviZ/N9eP1f9CM7mKSEkIo7O1Q==", + "version": "5.85.9", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.85.9.tgz", + "integrity": "sha512-5fxb9vwyftYE6KFLhhhDyLr8NO75+Wpu7pmTo+TkwKmMX2oxZDoLwcqGP8ItKSpUMwk3urWgQDZfyWr5Jm9LsQ==", "license": "MIT", "funding": { "type": "github", @@ -4496,13 +4534,26 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tanstack/query-persist-client-core": { + "version": "5.85.9", + "resolved": "https://registry.npmjs.org/@tanstack/query-persist-client-core/-/query-persist-client-core-5.85.9.tgz", + "integrity": "sha512-er7HfMjn6TQanWG5nudjRNZbo7ahf7IIdWN5kU7L2qRZ2kcw89TQZAZ74GIQsumOXZD7sUcHG2dylveFZNxlZA==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.85.9" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tanstack/react-query": { - "version": "5.81.5", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.81.5.tgz", - "integrity": "sha512-lOf2KqRRiYWpQT86eeeftAGnjuTR35myTP8MXyvHa81VlomoAWNEd8x5vkcAfQefu0qtYCvyqLropFZqgI2EQw==", + "version": "5.85.9", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.85.9.tgz", + "integrity": "sha512-2T5zgSpcOZXGkH/UObIbIkGmUPQqZqn7esVQFXLOze622h4spgWf5jmvrqAo9dnI13/hyMcNsF1jsoDcb59nJQ==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.81.5" + "@tanstack/query-core": "5.85.9" }, "funding": { "type": "github", @@ -4529,6 +4580,23 @@ "react": "^18 || ^19" } }, + "node_modules/@tanstack/react-query-persist-client": { + "version": "5.85.9", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-persist-client/-/react-query-persist-client-5.85.9.tgz", + "integrity": "sha512-h75Xyt/XDtk1oRmli5znQRXclT/iZpseTK7ScqEjaiZmPSREgg2mJb1blo2SB1swSidDNsCtnENLNH43PP0/9w==", + "license": "MIT", + "dependencies": { + "@tanstack/query-persist-client-core": "5.85.9" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.85.9", + "react": "^18 || ^19" + } + }, "node_modules/@types/d3-array": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", diff --git a/package.json b/package.json index 6a73878..795ae07 100644 --- a/package.json +++ b/package.json @@ -55,8 +55,10 @@ "@radix-ui/react-tooltip": "^1.1.4", "@supabase/supabase-js": "^2.50.0", "@tailwindcss/line-clamp": "^0.4.4", + "@tanstack/query-async-storage-persister": "^5.86.0", "@tanstack/react-query": "^5.56.2", "@tanstack/react-query-devtools": "^5.81.2", + "@tanstack/react-query-persist-client": "^5.85.9", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.0.0", diff --git a/src/App.tsx b/src/App.tsx index f35ba49..19d32fe 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,6 +3,7 @@ import { Toaster as Sonner } from "@/components/ui/sonner"; import { TooltipProvider } from "@/components/ui/tooltip"; import { BrowserRouter } from "react-router-dom"; import { CookieConsentBanner } from "@/components/layout/legal/CookieConsentBanner"; +import { OfflineIndicator } from "@/components/ui/OfflineIndicator"; import { getSubdomainInfo, shouldRedirectFromWww, @@ -40,6 +41,7 @@ function App() { + ); } diff --git a/src/components/router/EditionRoutes.tsx b/src/components/router/EditionRoutes.tsx index fdb3238..b9951b3 100644 --- a/src/components/router/EditionRoutes.tsx +++ b/src/components/router/EditionRoutes.tsx @@ -8,7 +8,7 @@ import { MapTab } from "@/pages/EditionView/tabs/MapTab"; import { InfoTab } from "@/pages/EditionView/tabs/InfoTab"; import { SocialTab } from "@/pages/EditionView/tabs/SocialTab"; import { ScheduleTabTimeline } from "@/pages/EditionView/tabs/ScheduleTab/TimelineTab"; -import { ScheduleTabList } from "@/pages/EditionView/tabs/ScheduleTab/ListTab"; +import { ScheduleTabList } from "@/pages/EditionView/tabs/ScheduleTab/list/ListTab"; import { ScheduleTab } from "@/pages/EditionView/tabs/ScheduleTab"; interface EditionRoutesProps { diff --git a/src/components/ui/OfflineIndicator.tsx b/src/components/ui/OfflineIndicator.tsx new file mode 100644 index 0000000..2020440 --- /dev/null +++ b/src/components/ui/OfflineIndicator.tsx @@ -0,0 +1,16 @@ +import { useOnlineStatus } from "@/hooks/useOnlineStatus"; +import { Badge } from "@/components/ui/badge"; +import { WifiOff } from "lucide-react"; + +export function OfflineIndicator() { + const isOnline = useOnlineStatus(); + + if (isOnline) return null; + + return ( + + + Offline + + ); +} diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx index 2420ebe..5e385d1 100644 --- a/src/contexts/AuthContext.tsx +++ b/src/contexts/AuthContext.tsx @@ -9,7 +9,6 @@ import { import { User } from "@supabase/supabase-js"; import { supabase } from "@/integrations/supabase/client"; import { useProfileQuery } from "@/hooks/queries/auth/useProfile"; -import { profileOfflineService } from "@/services/profileOfflineService"; import { useToast } from "@/hooks/use-toast"; import { AuthDialog } from "@/components/AuthDialog/AuthDialog"; import { Profile } from "@/hooks/queries/auth/useProfile"; @@ -60,7 +59,7 @@ export function AuthProvider({ children }: AuthProviderProps) { if (event === "SIGNED_OUT") { // For sign out, use the current user state from closure if (user?.id) { - await profileOfflineService.clearCachedProfile(user.id); + // await profileOfflineService.clearCachedProfile(user.id); } } @@ -127,7 +126,7 @@ export function AuthProvider({ children }: AuthProviderProps) { async function signOut() { // Clear cached profile before signing out if (user?.id) { - await profileOfflineService.clearCachedProfile(user.id); + // await profileOfflineService.clearCachedProfile(user.id); } await supabase.auth.signOut(); } diff --git a/src/hooks/queries/artists/notes/useSaveNoteMutation.ts b/src/hooks/queries/artists/notes/useCreateNoteMutation.ts similarity index 79% rename from src/hooks/queries/artists/notes/useSaveNoteMutation.ts rename to src/hooks/queries/artists/notes/useCreateNoteMutation.ts index 9661c4c..a191bf7 100644 --- a/src/hooks/queries/artists/notes/useSaveNoteMutation.ts +++ b/src/hooks/queries/artists/notes/useCreateNoteMutation.ts @@ -3,17 +3,19 @@ import { useToast } from "@/hooks/use-toast"; import { supabase } from "@/integrations/supabase/client"; import { artistNotesKeys } from "./types"; -async function saveArtistNote(variables: { - artistId: string; +async function createNote({ + setId, + userId, + noteContent, +}: { + setId: string; userId: string; noteContent: string; }) { - const { artistId, userId, noteContent } = variables; - const { data, error } = await supabase .from("artist_notes") .upsert({ - artist_id: artistId, + artist_id: setId, user_id: userId, note_content: noteContent, }) @@ -24,16 +26,16 @@ async function saveArtistNote(variables: { return data; } -export function useSaveNoteMutation() { +export function useCreateNoteMutation() { const queryClient = useQueryClient(); const { toast } = useToast(); return useMutation({ - mutationFn: saveArtistNote, + mutationFn: createNote, onSuccess: (_, variables) => { // Invalidate and refetch notes for this artist queryClient.invalidateQueries({ - queryKey: artistNotesKeys.notes(variables.artistId), + queryKey: artistNotesKeys.notes(variables.setId), }); toast({ title: "Success", diff --git a/src/hooks/queries/auth/useProfile.ts b/src/hooks/queries/auth/useProfile.ts index 17d7f6f..b2ce635 100644 --- a/src/hooks/queries/auth/useProfile.ts +++ b/src/hooks/queries/auth/useProfile.ts @@ -1,8 +1,6 @@ import { useQuery } from "@tanstack/react-query"; import { supabase } from "@/integrations/supabase/client"; import type { Database } from "@/integrations/supabase/types"; -import { profileOfflineService } from "@/services/profileOfflineService"; -import { useOfflineProfileToast } from "@/hooks/useOfflineProfileToast"; export type Profile = Database["public"]["Tables"]["profiles"]["Row"]; @@ -27,53 +25,10 @@ async function fetchProfile(userId: string) { return data; } -// Hook with offline support export function useProfileQuery(userId: string | undefined) { - const { showOfflineProfileToast, isOnline } = useOfflineProfileToast(); - return useQuery({ queryKey: profileKeys.detail(userId), - queryFn: async () => { - if (!userId) return null; - - try { - // Try online fetch first - if (isOnline) { - const profile = await fetchProfile(userId); - // Cache successful fetch - await profileOfflineService.cacheProfile(userId, profile); - return profile; - } else { - // Use cached data when offline - const cachedProfile = - await profileOfflineService.getCachedProfile(userId); - if (cachedProfile) { - showOfflineProfileToast(); - return cachedProfile; - } - throw new Error("No profile data available offline"); - } - } catch (error) { - // Fallback to cache on error - if (isOnline) { - console.error("Online profile fetch failed, using cache:", error); - const cachedProfile = - await profileOfflineService.getCachedProfile(userId); - if (cachedProfile) { - showOfflineProfileToast(); - return cachedProfile; - } - } - throw error; - } - }, + queryFn: () => fetchProfile(userId!), enabled: !!userId, - staleTime: 5 * 60 * 1000, // 5 minutes - gcTime: 24 * 60 * 60 * 1000, // 24 hours (was cacheTime) - retry: (failureCount, _error) => { - // Don't retry if we're offline and have cached data - if (!isOnline) return false; - return failureCount < 2; - }, }); } diff --git a/src/hooks/queries/auth/useUpdateProfile.ts b/src/hooks/queries/auth/useUpdateProfile.ts index 72e60fb..d71936f 100644 --- a/src/hooks/queries/auth/useUpdateProfile.ts +++ b/src/hooks/queries/auth/useUpdateProfile.ts @@ -2,7 +2,6 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useToast } from "@/hooks/use-toast"; import { supabase } from "@/integrations/supabase/client"; import { profileKeys } from "./useProfile"; -import { profileOfflineService } from "@/services/profileOfflineService"; // Mutation function async function updateProfile(variables: { @@ -56,13 +55,7 @@ export function useUpdateProfileMutation() { return useMutation({ mutationFn: updateProfile, - onSuccess: async (data, variables) => { - // Update the profile cache - queryClient.setQueryData(profileKeys.detail(variables.userId), data); - - // Update offline cache - await profileOfflineService.cacheProfile(variables.userId, data); - + onSuccess: async (_data, variables) => { // Invalidate to ensure consistency queryClient.invalidateQueries({ queryKey: profileKeys.detail(variables.userId), diff --git a/src/hooks/queries/useOfflineArtistsQuery.ts b/src/hooks/queries/useOfflineArtistsQuery.ts deleted file mode 100644 index b6cc8da..0000000 --- a/src/hooks/queries/useOfflineArtistsQuery.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { useEffect, useRef } from "react"; -import { supabase } from "@/integrations/supabase/client"; -import { useOnlineStatus, useOfflineData } from "@/hooks/useOffline"; -import { Artist, useArtistsQuery } from "./artists/useArtists"; -import { RealtimeChannel } from "@supabase/supabase-js"; - -// Note: Currently not using merge logic, but keeping for future enhancement - -export function useOfflineArtistsQuery() { - const queryClient = useQueryClient(); - const channelRef = useRef(null); - const isOnline = useOnlineStatus(); - const { offlineReady, saveArtistsOffline, getArtistsOffline } = - useOfflineData(); - - // Get online data when available - const { - data: onlineArtists = [], - isLoading: onlineLoading, - error: onlineError, - refetch, - } = useArtistsQuery(); - - // Query for offline data - const offlineQuery = useQuery({ - queryKey: ["artists", "offline"], - queryFn: getArtistsOffline, - enabled: offlineReady && !isOnline, - staleTime: Infinity, // Offline data doesn't expire - }); - - // Main query that combines online and offline data - const combinedQuery = useQuery({ - queryKey: ["artists", "combined", isOnline], - queryFn: async (): Promise<{ - artists: Artist[]; - dataSource: "online" | "offline"; - }> => { - if (isOnline && onlineArtists.length > 0) { - // Save to offline storage when online data is available - if (offlineReady) { - await saveArtistsOffline(onlineArtists); - } - return { artists: onlineArtists, dataSource: "online" }; - } else if (!isOnline && offlineQuery.data) { - // Use offline data when offline - return { artists: offlineQuery.data, dataSource: "offline" }; - } else if (isOnline && !onlineArtists.length && offlineQuery.data) { - // Fallback to offline if online fails but offline available - return { artists: offlineQuery.data, dataSource: "offline" }; - } - - return { artists: [], dataSource: isOnline ? "online" : "offline" }; - }, - enabled: - (isOnline && !onlineLoading) || - (!isOnline && !!offlineQuery.data) || - (offlineReady && !isOnline), - staleTime: isOnline ? 5 * 60 * 1000 : Infinity, // 5 min for online, infinite for offline - }); - - // Set up real-time subscriptions when online - useEffect(() => { - if (!isOnline) { - // Clean up any existing channel when going offline - if (channelRef.current) { - supabase.removeChannel(channelRef.current); - channelRef.current = null; - } - return; - } - - // Clean up any existing channel first - if (channelRef.current) { - supabase.removeChannel(channelRef.current); - channelRef.current = null; - } - - try { - // Create unique channel name to prevent conflicts - const channelName = `artists-changes-${Date.now()}-${Math.random().toString(36).substring(7)}`; - - const artistsChannel = supabase - .channel(channelName) - .on( - "postgres_changes", - { - event: "*", - schema: "public", - table: "artists", - }, - () => { - // Invalidate queries to refetch fresh data - queryClient.invalidateQueries({ queryKey: ["artists"] }); - }, - ) - .subscribe((_, err) => { - if (err) { - console.error("Subscription error:", err); - } - }); - - channelRef.current = artistsChannel; - } catch (err) { - console.error("Failed to create subscription channel:", err); - } - - return () => { - if (channelRef.current) { - try { - supabase.removeChannel(channelRef.current); - } catch (err) { - console.error("Error cleaning up channel:", err); - } - channelRef.current = null; - } - }; - }, [queryClient, isOnline]); - - async function fetchArtists() { - if (isOnline) { - refetch(); - combinedQuery.refetch(); - } else { - offlineQuery.refetch(); - combinedQuery.refetch(); - } - } - - return { - artists: combinedQuery.data?.artists || [], - dataSource: - combinedQuery.data?.dataSource || (isOnline ? "online" : "offline"), - loading: combinedQuery.isLoading || onlineLoading, - error: combinedQuery.error || onlineError || offlineQuery.error, - fetchArtists, - refetch: fetchArtists, - }; -} diff --git a/src/hooks/queries/useOfflineNotesQuery.ts b/src/hooks/queries/useOfflineNotesQuery.ts deleted file mode 100644 index 2db1745..0000000 --- a/src/hooks/queries/useOfflineNotesQuery.ts +++ /dev/null @@ -1,265 +0,0 @@ -import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import { supabase } from "@/integrations/supabase/client"; -import { useToast } from "@/hooks/use-toast"; -import { offlineStorage } from "@/lib/offlineStorage"; -import { useOnlineStatus, useOfflineQueue } from "@/hooks/useOffline"; -import { SetNote } from "./artists/notes/types"; - -interface OfflineNote { - id: string; - setId: string; - content: string; - userId: string; - timestamp: number; - synced: boolean; -} - -function mergeOfflineAndServerNotes( - offlineNotes: OfflineNote[], - serverNotes: SetNote[], -): SetNote[] { - // Transform offline notes to match server format - const processedOfflineNotes = offlineNotes.map((note) => ({ - id: note.id, - set_id: note.setId, - user_id: note.userId, - note_content: note.content, - created_at: new Date(note.timestamp).toISOString(), - updated_at: new Date(note.timestamp).toISOString(), - author_username: "You (offline)", - author_email: "", - })); - - // Merge offline and server notes (remove duplicates, server takes precedence) - const mergedNotes = [...serverNotes]; - processedOfflineNotes.forEach((offlineNote) => { - if (!mergedNotes.find((serverNote) => serverNote.id === offlineNote.id)) { - mergedNotes.push(offlineNote); - } - }); - - return mergedNotes.sort( - (a, b) => - new Date(b.created_at).getTime() - new Date(a.created_at).getTime(), - ); -} - -async function fetchNotesWithOffline( - setId: string, - isOnline: boolean, -): Promise { - // Always load offline notes first - const offlineNotes = await offlineStorage.getNotes(setId); - - if (!isOnline) { - // Transform offline notes to match expected format - return offlineNotes.map((note) => ({ - id: note.id, - set_id: note.setId, - user_id: note.userId, - note_content: note.content, - created_at: new Date(note.timestamp).toISOString(), - updated_at: new Date(note.timestamp).toISOString(), - author_username: "You (offline)", - author_email: "", - })); - } - - // Fetch server notes when online - try { - const { data: serverNotes, error } = await supabase - .from("artist_notes") - .select("*") - .eq("artist_id", setId) - .order("created_at", { ascending: false }); - - if (error) { - throw new Error(error.message); - } - - if (!serverNotes || serverNotes.length === 0) { - // Return transformed offline notes if no server notes - return offlineNotes.map((note) => ({ - id: note.id, - set_id: note.setId, - user_id: note.userId, - note_content: note.content, - created_at: new Date(note.timestamp).toISOString(), - updated_at: new Date(note.timestamp).toISOString(), - author_username: "You (offline)", - author_email: "", - })); - } - - // Get author profiles for server notes - const userIds = serverNotes.map((note) => note.user_id); - const { data: profilesData } = await supabase - .from("profiles") - .select("id, username, email") - .in("id", userIds); - - const serverNotesWithAuthor = serverNotes.map(({ artist_id, ...note }) => { - const profile = profilesData?.find((p) => p.id === note.user_id); - return { - ...note, - set_id: artist_id, - author_username: profile?.username || undefined, - author_email: profile?.email || undefined, - }; - }); - - // Merge offline and server notes - return mergeOfflineAndServerNotes(offlineNotes, serverNotesWithAuthor); - } catch (error) { - console.error("Error fetching server notes:", error); - // Fall back to offline notes only - return offlineNotes.map((note) => ({ - id: note.id, - set_id: note.setId, - user_id: note.userId, - note_content: note.content, - created_at: new Date(note.timestamp).toISOString(), - updated_at: new Date(note.timestamp).toISOString(), - author_username: "You (offline)", - author_email: "", - })); - } -} - -export function useOfflineNotesQuery(setId: string, userId: string | null) { - const isOnline = useOnlineStatus(); - - return useQuery({ - queryKey: ["notes", setId, userId], - queryFn: () => fetchNotesWithOffline(setId, isOnline), - enabled: !!setId && !!userId, - staleTime: 2 * 60 * 1000, // 2 minutes - notes don't change often - refetchOnWindowFocus: isOnline, // Only refetch on focus when online - }); -} - -export function useSaveNoteMutation(setId: string) { - const queryClient = useQueryClient(); - const { toast } = useToast(); - const isOnline = useOnlineStatus(); - const { updateQueueSize } = useOfflineQueue(); - - return useMutation({ - mutationFn: async ({ - noteContent, - userId, - }: { - noteContent: string; - userId: string; - }) => { - // Save to offline storage first - const offlineNoteId = await offlineStorage.saveNote({ - setId: setId, - content: noteContent, - userId, - timestamp: Date.now(), - synced: false, - }); - - // If online, also save to server - if (isOnline) { - try { - const { data, error } = await supabase - .from("artist_notes") - .upsert({ - artist_id: setId, - user_id: userId, - note_content: noteContent, - }) - .select() - .single(); - - if (error) { - throw new Error(error.message); - } - - // Mark as synced since it's now saved online - await offlineStorage.markNoteSynced(offlineNoteId); - - return data; - } catch (error) { - console.error("Error saving note to server:", error); - // Keep in offline storage for later sync - updateQueueSize(); - throw error; - } - } else { - // Offline mode - add to queue - updateQueueSize(); - return { id: offlineNoteId, offline: true }; - } - }, - onSuccess: () => { - // Invalidate and refetch notes - queryClient.invalidateQueries({ - queryKey: ["notes", setId], - }); - - toast({ - title: "Success", - description: isOnline ? "Note saved" : "Note saved offline", - }); - }, - onError: (error: Error) => { - toast({ - title: "Error", - description: error.message || "Failed to save note", - variant: "destructive", - }); - }, - }); -} - -export function useDeleteNoteMutation(setId: string) { - const queryClient = useQueryClient(); - const { toast } = useToast(); - const isOnline = useOnlineStatus(); - - return useMutation({ - mutationFn: async (noteId: string) => { - // Try to delete from offline storage first - try { - await offlineStorage.deleteNote(noteId); - } catch (error) { - console.error("Note not found in offline storage:", error); - } - - // If online, also delete from server - if (isOnline) { - const { error } = await supabase - .from("artist_notes") - .delete() - .eq("id", noteId); - - if (error) { - throw new Error(error.message); - } - } - - return noteId; - }, - onSuccess: () => { - // Invalidate and refetch notes - queryClient.invalidateQueries({ - queryKey: ["notes", setId], - }); - - toast({ - title: "Success", - description: "Note deleted", - }); - }, - onError: (error: Error) => { - toast({ - title: "Error", - description: error.message || "Failed to delete note", - variant: "destructive", - }); - }, - }); -} diff --git a/src/hooks/queries/useOfflineSetsQuery.ts b/src/hooks/queries/useOfflineSetsQuery.ts deleted file mode 100644 index 89642c6..0000000 --- a/src/hooks/queries/useOfflineSetsQuery.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { useEffect, useRef } from "react"; -import { supabase } from "@/integrations/supabase/client"; -import { useOnlineStatus, useOfflineData } from "@/hooks/useOffline"; -import { useSetsQuery } from "./sets/useSets"; -import { RealtimeChannel } from "@supabase/supabase-js"; -import { FestivalSet, setsKeys } from "./sets/useSets"; -import { userVotesKeys } from "./voting/useUserVotes"; - -export function useOfflineSetsQuery() { - const queryClient = useQueryClient(); - const channelRef = useRef(null); - const isOnline = useOnlineStatus(); - const { offlineReady, saveSetsOffline, getSetsOffline } = useOfflineData(); - - // Get online data when available - const { - data: onlineSets = [], - isLoading: onlineLoading, - error: onlineError, - refetch, - } = useSetsQuery(); - - // Query for offline data - const offlineQuery = useQuery({ - queryKey: ["sets", "offline"], - queryFn: getSetsOffline, - enabled: offlineReady && !isOnline, - staleTime: Infinity, // Offline data doesn't expire - }); - - // Main query that combines online and offline data - const combinedQuery = useQuery({ - queryKey: ["sets", "combined", isOnline], - queryFn: async (): Promise<{ - sets: FestivalSet[]; - dataSource: "online" | "offline"; - }> => { - if (isOnline && onlineSets.length > 0) { - // Save to offline storage when online data is available - if (offlineReady) { - await saveSetsOffline(onlineSets); - } - return { sets: onlineSets, dataSource: "online" }; - } else if (!isOnline && offlineQuery.data) { - // Use offline data when offline - return { sets: offlineQuery.data, dataSource: "offline" }; - } else if (isOnline && !onlineSets.length && offlineQuery.data) { - // Fallback to offline if online fails but offline available - return { sets: offlineQuery.data, dataSource: "offline" }; - } - - return { sets: [], dataSource: isOnline ? "online" : "offline" }; - }, - enabled: - (isOnline && !onlineLoading) || - (!isOnline && !!offlineQuery.data) || - (offlineReady && !isOnline), - staleTime: isOnline ? 5 * 60 * 1000 : Infinity, // 5 min for online, infinite for offline - }); - - // Set up real-time subscriptions when online - useEffect(() => { - if (!isOnline) { - // Clean up any existing channel when going offline - if (channelRef.current) { - supabase.removeChannel(channelRef.current); - channelRef.current = null; - } - return; - } - - // Clean up any existing channel first - if (channelRef.current) { - supabase.removeChannel(channelRef.current); - channelRef.current = null; - } - - try { - // Create unique channel name to prevent conflicts - const channelName = `sets-changes-${Date.now()}-${Math.random().toString(36).substring(7)}`; - - const setsChannel = supabase - .channel(channelName) - .on( - "postgres_changes", - { - event: "*", - schema: "public", - table: "sets", - }, - () => { - // Invalidate queries to refetch fresh data - queryClient.invalidateQueries({ queryKey: setsKeys.all }); - queryClient.invalidateQueries({ queryKey: userVotesKeys.all }); - }, - ) - .subscribe((_, err) => { - if (err) { - console.error("Subscription error:", err); - } - }); - - channelRef.current = setsChannel; - } catch (err) { - console.error("Failed to create subscription channel:", err); - } - - return () => { - if (channelRef.current) { - try { - supabase.removeChannel(channelRef.current); - } catch (err) { - console.error("Error cleaning up channel:", err); - } - channelRef.current = null; - } - }; - }, [queryClient, isOnline]); - - async function fetchSets() { - if (isOnline) { - refetch(); - combinedQuery.refetch(); - } else { - offlineQuery.refetch(); - combinedQuery.refetch(); - } - } - - return { - sets: combinedQuery.data?.sets || [], - dataSource: - combinedQuery.data?.dataSource || (isOnline ? "online" : "offline"), - loading: combinedQuery.isLoading || onlineLoading, - error: combinedQuery.error || onlineError || offlineQuery.error, - fetchSets, - refetch: fetchSets, - }; -} diff --git a/src/hooks/queries/useOfflineVotingQuery.ts b/src/hooks/queries/useOfflineVotingQuery.ts deleted file mode 100644 index cdcef63..0000000 --- a/src/hooks/queries/useOfflineVotingQuery.ts +++ /dev/null @@ -1,304 +0,0 @@ -import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import { supabase } from "@/integrations/supabase/client"; -import { useToast } from "@/components/ui/use-toast"; -import { offlineStorage } from "@/lib/offlineStorage"; -import { useOnlineStatus, useOfflineQueue } from "@/hooks/useOffline"; -import type { User } from "@supabase/supabase-js"; -import { FestivalSet, setsKeys } from "./sets/useSets"; -import { userVotesKeys } from "./voting/useUserVotes"; - -interface OfflineVote { - id: string; - setId: string; - voteType: number; - userId: string; - timestamp: number; - synced: boolean; -} - -function mergeOfflineAndServerVotes( - offlineVotes: OfflineVote[], - serverVotes: Record, -): Record { - // Start with server votes (authoritative when online) - const mergedVotes = { ...serverVotes }; - - // Add offline votes that aren't on server yet - offlineVotes.forEach((vote) => { - const voteId = vote.setId; - // Only use offline vote if no server vote exists - if (!(voteId in mergedVotes)) { - mergedVotes[voteId] = vote.voteType; - } - }); - - return mergedVotes; -} - -async function fetchUserVotesWithOffline( - userId: string, - isOnline: boolean, -): Promise> { - // Always load offline votes first - const offlineVotes = await offlineStorage.getVotes(); - const userOfflineVotes = offlineVotes.filter( - (vote) => vote.userId === userId, - ); - - if (!isOnline) { - // Transform offline votes to expected format - return userOfflineVotes.reduce( - (acc, vote) => { - acc[vote.setId] = vote.voteType; - return acc; - }, - {} as Record, - ); - } - - // Fetch server votes when online - try { - const { data, error } = await supabase - .from("votes") - .select("set_id, vote_type") - .eq("user_id", userId); - - if (error) { - console.error("Error fetching server votes:", error); - // Fall back to offline votes only - return userOfflineVotes.reduce( - (acc, vote) => { - acc[vote.setId] = vote.voteType; - return acc; - }, - {} as Record, - ); - } - - const serverVotes: Record = {}; - (data || []).forEach((vote) => { - // Prioritize set-based votes over legacy artist votes - if (vote.set_id) { - serverVotes[vote.set_id] = vote.vote_type; - } - }); - - // Merge offline and server votes - return mergeOfflineAndServerVotes(userOfflineVotes, serverVotes); - } catch (error) { - console.error("Error loading user votes:", error); - // Fall back to offline votes only - return userOfflineVotes.reduce( - (acc, vote) => { - acc[vote.setId] = vote.voteType; - return acc; - }, - {} as Record, - ); - } -} - -export function useOfflineVotingQuery(user: User | null) { - const isOnline = useOnlineStatus(); - - return useQuery({ - queryKey: userVotesKeys.user(user?.id || ""), - queryFn: () => fetchUserVotesWithOffline(user!.id, isOnline), - enabled: !!user?.id, - staleTime: isOnline ? 30 * 1000 : Infinity, // 30 seconds for online, infinite for offline - refetchOnWindowFocus: isOnline, - }); -} - -export function useOfflineVoteMutation( - user: User | null, - onVoteUpdate?: () => void, -) { - const queryClient = useQueryClient(); - const { toast } = useToast(); - const isOnline = useOnlineStatus(); - const { updateQueueSize } = useOfflineQueue(); - - return useMutation({ - mutationFn: async ({ - setId, - voteType, - }: { - setId: string; - voteType: number; - }) => { - if (!user) { - throw new Error("User not authenticated"); - } - - const userId = user.id; - - // Get current vote to determine if this is a toggle - const currentVotes = - (queryClient.getQueryData(userVotesKeys.user(userId)) as Record< - string, - number - >) || {}; - const existingVote = currentVotes[setId]; - const isToggle = existingVote === voteType; - - // Save to offline storage first - if (isToggle) { - // Find and remove the existing vote - const offlineVotes = await offlineStorage.getVotes(); - const existingVote = offlineVotes.find( - (v) => v.userId === userId && v.setId === setId, - ); - if (existingVote) { - await offlineStorage.deleteVote(existingVote.id); - } - } else { - // Add/update vote - await offlineStorage.saveVote({ - userId, - setId, - voteType, - timestamp: Date.now(), - synced: false, - }); - } - - // Update query cache immediately for optimistic updates - queryClient.setQueryData( - userVotesKeys.user(userId), - (oldData: Record = {}) => { - const newData = { ...oldData }; - if (isToggle) { - delete newData[setId]; - } else { - newData[setId] = voteType; - } - return newData; - }, - ); - - // Also update the sets cache to reflect vote count changes - queryClient.setQueriesData({ queryKey: setsKeys.all }, (oldData: any) => { - if (!oldData) return oldData; - - // Handle different data structures (could be array of sets or object with sets property) - function updateSetsArray(sets: Array) { - return sets.map((set: FestivalSet) => { - if (set.id === setId) { - const updatedVotes = [...(set.votes || [])]; - - // Remove existing vote from this user for this set - const existingVoteIndex = updatedVotes.findIndex( - (vote: any) => vote.user_id === userId, - ); - if (existingVoteIndex !== -1) { - updatedVotes.splice(existingVoteIndex, 1); - } - - // Add new vote if not a toggle - if (!isToggle) { - updatedVotes.push({ - vote_type: voteType, - user_id: userId, - }); - } - - return { - ...set, - votes: updatedVotes, - }; - } - return set; - }); - } - - // Handle array of sets - if (Array.isArray(oldData)) { - return updateSetsArray(oldData); - } - - // Handle object with sets property - if (oldData.sets && Array.isArray(oldData.sets)) { - return { - ...oldData, - sets: updateSetsArray(oldData.sets), - }; - } - - return oldData; - }); - - // If online, sync immediately - if (isOnline) { - try { - if (isToggle) { - // Remove vote from server - const { error } = await supabase - .from("votes") - .delete() - .eq("user_id", userId) - .eq("set_id", setId); - - if (error) throw error; - } else { - // Add/update vote on server - const { error } = await supabase.from("votes").upsert( - { - user_id: userId, - vote_type: voteType, - set_id: setId, - }, - { - onConflict: "user_id,set_id", - }, - ); - - if (error) throw error; - } - - // Mark as synced in offline storage - if (!isToggle) { - // Find and mark the vote as synced - const offlineVotes = await offlineStorage.getVotes(); - const voteToSync = offlineVotes.find( - (v) => v.userId === userId && v.setId === setId, - ); - if (voteToSync) { - await offlineStorage.markVoteSynced(voteToSync.id); - } - } - } catch (error) { - console.error("Error syncing vote to server:", error); - // Keep in offline storage for later sync - updateQueueSize(); - throw error; - } - } else { - // Offline mode - add to queue - updateQueueSize(); - } - - // Trigger callback if provided - if (onVoteUpdate) { - onVoteUpdate(); - } - - return isToggle ? null : voteType; - }, - onSettled: () => { - // Revert optimistic update on error - if (user?.id) { - queryClient.invalidateQueries({ - queryKey: userVotesKeys.user(user.id), - }); - } - }, - onError: (error: any) => { - toast({ - title: "Error", - description: error.message || "Failed to save vote", - variant: "destructive", - }); - }, - }); -} diff --git a/src/hooks/queries/voting/useGroupVotes.ts b/src/hooks/queries/voting/useGroupVotes.ts index 3365cdc..0c498fb 100644 --- a/src/hooks/queries/voting/useGroupVotes.ts +++ b/src/hooks/queries/voting/useGroupVotes.ts @@ -1,7 +1,5 @@ import { useQuery } from "@tanstack/react-query"; import { supabase } from "@/integrations/supabase/client"; -import { offlineStorage } from "@/lib/offlineStorage"; -import { useOnlineStatus } from "@/hooks/useOffline"; export interface GroupVote { vote_type: number; @@ -75,33 +73,10 @@ async function fetchGroupVotes( }); } -// Hook with offline support export function useGroupVotesQuery(setId: string, groupId: string) { - const isOnline = useOnlineStatus(); - return useQuery({ queryKey: groupVotesKeys.votes(setId, groupId), - queryFn: async (): Promise => { - if (!setId || !groupId) return []; - - try { - if (isOnline) { - return await fetchGroupVotes(setId, groupId); - } else { - return await offlineStorage.getSetGroupVotes(setId, groupId); - } - } catch (error) { - console.error("Error fetching group votes:", error); - return []; - } - }, + queryFn: () => fetchGroupVotes(setId, groupId), enabled: !!setId && !!groupId, - staleTime: 30 * 1000, // 30 seconds - gcTime: 5 * 60 * 1000, // 5 minutes - retry: (failureCount) => { - // Don't retry if we're offline and have no cached data - if (!isOnline) return false; - return failureCount < 2; - }, }); } diff --git a/src/hooks/useOffline.ts b/src/hooks/useOffline.ts deleted file mode 100644 index 44fd25f..0000000 --- a/src/hooks/useOffline.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { useState, useEffect, useCallback } from "react"; -import { offlineStorage } from "@/lib/offlineStorage"; -import { Artist } from "@/hooks/queries/artists/useArtists"; -import { FestivalSet } from "@/hooks/queries/sets/useSets"; -export function useOnlineStatus() { - const [isOnline, setIsOnline] = useState(navigator.onLine); - - useEffect(() => { - function handleOnline() { - return setIsOnline(true); - } - function handleOffline() { - return setIsOnline(false); - } - - window.addEventListener("online", handleOnline); - window.addEventListener("offline", handleOffline); - - return () => { - window.removeEventListener("online", handleOnline); - window.removeEventListener("offline", handleOffline); - }; - }, []); - - return isOnline; -} - -export function useOfflineQueue() { - const [queueSize, setQueueSize] = useState(0); - const [syncing, setSyncing] = useState(false); - - const updateQueueSize = useCallback(async () => { - try { - const [unsyncedVotes, unsyncedNotes] = await Promise.all([ - offlineStorage.getUnsyncedVotes(), - offlineStorage.getUnsyncedNotes(), - ]); - setQueueSize(unsyncedVotes.length + unsyncedNotes.length); - } catch (error) { - console.error("Error updating queue size:", error); - } - }, []); - - const syncQueue = useCallback(async () => { - if (syncing) return; - - setSyncing(true); - try { - // Get unsynced items - const [unsyncedVotes, unsyncedNotes] = await Promise.all([ - offlineStorage.getUnsyncedVotes(), - offlineStorage.getUnsyncedNotes(), - ]); - - // Sync votes - for (const vote of unsyncedVotes) { - try { - // Here you would sync with your backend - // For now, we'll just mark as synced - await offlineStorage.markVoteSynced(vote.id); - } catch (error) { - console.error("Error syncing vote:", error); - } - } - - // Sync notes - for (const note of unsyncedNotes) { - try { - // Here you would sync with your backend - // For now, we'll just mark as synced - await offlineStorage.markNoteSynced(note.id); - } catch (error) { - console.error("Error syncing note:", error); - } - } - - await updateQueueSize(); - } catch (error) { - console.error("Error syncing queue:", error); - } finally { - setSyncing(false); - } - }, [syncing, updateQueueSize]); - - useEffect(() => { - updateQueueSize(); - }, [updateQueueSize]); - - return { - queueSize, - syncing, - syncQueue, - updateQueueSize, - }; -} - -export function useOfflineData() { - const [offlineReady, setOfflineReady] = useState(false); - - useEffect(() => { - async function initOfflineStorage() { - try { - await offlineStorage.init(); - setOfflineReady(true); - } catch (error) { - console.error("Error initializing offline storage:", error); - } - } - - initOfflineStorage(); - }, []); - - const saveArtistsOffline = useCallback(async (artists: Artist[]) => { - try { - await offlineStorage.saveArtists(artists); - } catch (error) { - console.error("Error saving artists offline:", error); - } - }, []); - - const getArtistsOffline = useCallback(async () => { - try { - return await offlineStorage.getArtists(); - } catch (error) { - console.error("Error getting artists offline:", error); - return []; - } - }, []); - - const saveSetsOffline = useCallback(async (artists: FestivalSet[]) => { - try { - await offlineStorage.saveSets(artists); - } catch (error) { - console.error("Error saving artists offline:", error); - } - }, []); - - const getSetsOffline = useCallback(async () => { - try { - return await offlineStorage.getSets(); - } catch (error) { - console.error("Error getting artists offline:", error); - return []; - } - }, []); - - return { - offlineReady, - saveArtistsOffline, - getArtistsOffline, - saveSetsOffline, - getSetsOffline, - }; -} diff --git a/src/hooks/useOfflineArtistData.ts b/src/hooks/useOfflineArtistData.ts deleted file mode 100644 index f473955..0000000 --- a/src/hooks/useOfflineArtistData.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { useOfflineArtistsQuery } from "./queries/useOfflineArtistsQuery"; -import { useArchiveArtistMutation } from "./queries/artists/useArchiveArtist"; - -export function useOfflineArtistData() { - // Use the new React Query-based offline artists hook - const { artists, dataSource, loading, error, fetchArtists, refetch } = - useOfflineArtistsQuery(); - - // Keep the archive mutation separate - const archiveArtistMutation = useArchiveArtistMutation(); - - return { - artists, - loading, - error, - dataSource, - fetchArtists, - archiveArtist: archiveArtistMutation.mutate, - archivingArtist: archiveArtistMutation.isPending, - refetch, - }; -} diff --git a/src/hooks/useOfflineNotes.ts b/src/hooks/useOfflineNotes.ts deleted file mode 100644 index b1294cd..0000000 --- a/src/hooks/useOfflineNotes.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { useCallback } from "react"; -import { - useOfflineNotesQuery, - useSaveNoteMutation, - useDeleteNoteMutation, -} from "./queries/useOfflineNotesQuery"; - -export function useOfflineNotes(setId: string, userId: string | null) { - // Use React Query for data fetching - const { - data: notes = [], - isLoading: loading, - error, - refetch, - } = useOfflineNotesQuery(setId, userId); - - // Use React Query mutations - const saveNoteMutation = useSaveNoteMutation(setId); - const deleteNoteMutation = useDeleteNoteMutation(setId); - - const saveNote = useCallback( - async (noteContent: string) => { - if (!setId || !userId) return false; - - try { - await saveNoteMutation.mutateAsync({ noteContent, userId }); - return true; - } catch (error) { - console.error("Error saving note:", error); - return false; - } - }, - [saveNoteMutation, setId, userId], - ); - - const deleteNote = useCallback( - async (noteId: string) => { - if (!noteId) return false; - - try { - await deleteNoteMutation.mutateAsync(noteId); - return true; - } catch (error) { - console.error("Error deleting note:", error); - return false; - } - }, - [deleteNoteMutation], - ); - - const refreshNotes = useCallback(() => { - refetch(); - }, [refetch]); - - return { - notes, - loading, - saving: saveNoteMutation.isPending, - error: error?.message || null, - saveNote, - deleteNote, - refreshNotes, - }; -} diff --git a/src/hooks/useOfflineProfileToast.ts b/src/hooks/useOfflineProfileToast.ts deleted file mode 100644 index af61390..0000000 --- a/src/hooks/useOfflineProfileToast.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { useCallback } from "react"; -import { useOnlineStatus } from "./useOffline"; -import { useToast } from "@/hooks/use-toast"; - -export function useOfflineProfileToast() { - const isOnline = useOnlineStatus(); - const { toast } = useToast(); - - const showOfflineProfileToast = useCallback(() => { - if (!isOnline) { - toast({ - title: "Using cached profile", - description: - "Profile data is from offline cache (will sync when online)", - }); - } - }, [isOnline, toast]); - - return { - showOfflineProfileToast, - isOnline, - }; -} diff --git a/src/hooks/useOfflineSetsData.ts b/src/hooks/useOfflineSetsData.ts deleted file mode 100644 index 784a544..0000000 --- a/src/hooks/useOfflineSetsData.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { useOfflineSetsQuery } from "./queries/useOfflineSetsQuery"; - -export function useOfflineSetsData() { - // Use the new React Query-based offline sets hook - const { sets, dataSource, loading, error, fetchSets, refetch } = - useOfflineSetsQuery(); - - return { - sets, - loading, - error, - dataSource, - fetchArtists: fetchSets, // Keep original API name for compatibility - refetch, - }; -} diff --git a/src/hooks/useOfflineVoting.ts b/src/hooks/useOfflineVoting.ts deleted file mode 100644 index 03313b8..0000000 --- a/src/hooks/useOfflineVoting.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { useState, useCallback } from "react"; -import { - useOfflineVotingQuery, - useOfflineVoteMutation, -} from "./queries/useOfflineVotingQuery"; -import type { User } from "@supabase/supabase-js"; - -export function useOfflineVoting(user: User | null, onVoteUpdate?: () => void) { - const [votingLoading, setVotingLoading] = useState>( - {}, - ); - - const { - data: userVotes = {}, - isLoading, - error, - } = useOfflineVotingQuery(user); - - const voteMutation = useOfflineVoteMutation(user, onVoteUpdate); - - const handleVote = useCallback( - async (setId: string, voteType: number) => { - if (!user) { - return { requiresAuth: true }; - } - - if (votingLoading[setId]) { - return { requiresAuth: false }; - } - - try { - setVotingLoading((prev) => ({ ...prev, [setId]: true })); - - await voteMutation.mutateAsync({ setId, voteType }); - - return { requiresAuth: false }; - } catch (error) { - console.error("Error voting:", error); - return { requiresAuth: false, error: true }; - } finally { - setVotingLoading((prev) => ({ ...prev, [setId]: false })); - } - }, - [user, votingLoading, voteMutation], - ); - - const getUserVote = useCallback( - (itemId: string): number | undefined => { - return userVotes[itemId]; - }, - [userVotes], - ); - - return { - userVotes, - loading: isLoading, - error: error?.message || null, - votingLoading, - handleVote, - getUserVote, - }; -} diff --git a/src/hooks/useOnlineStatus.ts b/src/hooks/useOnlineStatus.ts new file mode 100644 index 0000000..5e1def5 --- /dev/null +++ b/src/hooks/useOnlineStatus.ts @@ -0,0 +1,24 @@ +import { useState, useEffect } from "react"; + +export function useOnlineStatus() { + const [isOnline, setIsOnline] = useState(navigator.onLine); + + useEffect(() => { + function handleOnline() { + return setIsOnline(true); + } + function handleOffline() { + return setIsOnline(false); + } + + window.addEventListener("online", handleOnline); + window.addEventListener("offline", handleOffline); + + return () => { + window.removeEventListener("online", handleOnline); + window.removeEventListener("offline", handleOffline); + }; + }, []); + + return isOnline; +} diff --git a/src/hooks/useVoteCount.ts b/src/hooks/useVoteCount.ts new file mode 100644 index 0000000..8c19cef --- /dev/null +++ b/src/hooks/useVoteCount.ts @@ -0,0 +1,23 @@ +import { useCallback, useMemo } from "react"; +import { FestivalSet } from "./queries/sets/useSets"; + +export function useVoteCount(set: FestivalSet | undefined) { + const voteCounts = useMemo(() => { + if (!set?.votes) { + return {}; + } + + const counts: Record = {}; + for (const vc of set.votes) { + counts[vc.vote_type] = (counts[vc.vote_type] || 0) + 1; + } + return counts; + }, [set?.votes]); + + const getVoteCount = useCallback( + (voteType: number) => voteCounts[voteType] || 0, + [voteCounts], + ); + + return { getVoteCount }; +} diff --git a/src/lib/offlineStorage.ts b/src/lib/offlineStorage.ts deleted file mode 100644 index 5e05bbb..0000000 --- a/src/lib/offlineStorage.ts +++ /dev/null @@ -1,311 +0,0 @@ -import { Artist } from "@/hooks/queries/artists/useArtists"; -import { FestivalSet } from "@/hooks/queries/sets/useSets"; -import { openDB, IDBPDatabase } from "idb"; - -interface OfflineVote { - id: string; - setId: string; - voteType: number; - userId: string; - timestamp: number; - synced: boolean; -} - -interface OfflineNote { - id: string; - setId: string; // New field for set-based notes - content: string; - userId: string; - timestamp: number; - synced: boolean; -} - -export type OfflineProfile = { - created_at: string; - email: string | null; - id: string; - username: string | null; - completed_onboarding: boolean | null; -}; - -export type OfflineSetting = { - action: "archive"; - artistId: string; - timestamp: number; - synced: boolean; -}; - -class OfflineStorageManager { - private db: IDBPDatabase | null = null; - private dbName = "festival-offline-db"; - private version = 2; - - async init(): Promise { - this.db = await openDB(this.dbName, this.version, { - upgrade(db) { - // Artists store - if (!db.objectStoreNames.contains("artists")) { - db.createObjectStore("artists", { keyPath: "id" }); - } - - // Sets store - if (!db.objectStoreNames.contains("sets")) { - db.createObjectStore("sets", { keyPath: "id" }); - } - - // Votes store - if (!db.objectStoreNames.contains("votes")) { - const votesStore = db.createObjectStore("votes", { keyPath: "id" }); - votesStore.createIndex("setId", "setId", { unique: false }); - votesStore.createIndex("synced", "synced", { unique: false }); - } - - // Notes store - if (!db.objectStoreNames.contains("notes")) { - const notesStore = db.createObjectStore("notes", { keyPath: "id" }); - notesStore.createIndex("setId", "setId", { unique: false }); - notesStore.createIndex("synced", "synced", { unique: false }); - } - - // Schedule store - if (!db.objectStoreNames.contains("schedule")) { - db.createObjectStore("schedule", { keyPath: "id" }); - } - - // Settings store - if (!db.objectStoreNames.contains("settings")) { - db.createObjectStore("settings", { keyPath: "key" }); - } - }, - }); - } - - async ensureDB(): Promise { - if (!this.db) { - await this.init(); - } - return this.db!; - } - - // Artists methods - async saveArtists(artists: Artist[]): Promise { - const db = await this.ensureDB(); - const tx = db.transaction("artists", "readwrite"); - const store = tx.objectStore("artists"); - await Promise.all(artists.map((artist) => store.put(artist))); - await tx.done; - } - - async getArtists(): Promise { - const db = await this.ensureDB(); - return db.getAll("artists"); - } - - async getArtist(id: string): Promise { - const db = await this.ensureDB(); - return db.get("artists", id); - } - - // Sets methods - async saveSets(sets: FestivalSet[]): Promise { - const db = await this.ensureDB(); - const tx = db.transaction("sets", "readwrite"); - const store = tx.objectStore("sets"); - await Promise.all(sets.map((set) => store.put(set))); - await tx.done; - } - - async getSets(): Promise { - const db = await this.ensureDB(); - return db.getAll("sets"); - } - - async getSet(id: string): Promise { - const db = await this.ensureDB(); - const set = await db.get("sets", id); - - if (!set) { - return null; - } - - // Get all votes for this set from offline storage - const votes = await this.getVotes(id); - - // Transform offline votes to match server format - const transformedVotes = votes.map((vote) => ({ - vote_type: vote.voteType, - user_id: vote.userId, - })); - - return { - ...set, - votes: transformedVotes, - }; - } - - // Votes methods - async saveVote(vote: Omit): Promise { - const db = await this.ensureDB(); - const id = `vote_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; - const voteWithId = { ...vote, id }; - await db.put("votes", voteWithId); - return id; - } - - async getVotes(setId?: string): Promise { - const db = await this.ensureDB(); - if (setId) { - const tx = db.transaction("votes", "readonly"); - const setIndex = tx.objectStore("votes").index("setId"); - return setIndex.getAll(setId); - } - return db.getAll("votes"); - } - - async getUnsyncedVotes(): Promise { - const db = await this.ensureDB(); - const allVotes = await db.getAll("votes"); - return allVotes.filter((vote: OfflineVote) => !vote.synced); - } - - async markVoteSynced(id: string): Promise { - const db = await this.ensureDB(); - const vote = await db.get("votes", id); - if (vote) { - await db.put("votes", { ...vote, synced: true }); - } - } - - async deleteVote(id: string): Promise { - const db = await this.ensureDB(); - await db.delete("votes", id); - } - - // Notes methods - async saveNote(note: Omit): Promise { - const db = await this.ensureDB(); - const id = `note_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; - const noteWithId = { ...note, id }; - await db.put("notes", noteWithId); - return id; - } - - async getNotes(id?: string): Promise { - const db = await this.ensureDB(); - if (id) { - try { - const tx = db.transaction("notes", "readonly"); - const setIndex = tx.objectStore("notes").index("setId"); - return setIndex.getAll(id); - } catch (err) { - console.error("Error fetching notes for set:", err); - return []; - } - } - return db.getAll("notes"); - } - - async getUnsyncedNotes(): Promise { - const db = await this.ensureDB(); - const allNotes = await db.getAll("notes"); - return allNotes.filter((note: OfflineNote) => !note.synced); - } - - async markNoteSynced(id: string): Promise { - const db = await this.ensureDB(); - const note = await db.get("notes", id); - if (note) { - await db.put("notes", { ...note, synced: true }); - } - } - - async deleteNote(id: string): Promise { - const db = await this.ensureDB(); - await db.delete("notes", id); - } - - // Profile methods (secure offline caching) - async saveProfile(userId: string, profile: OfflineProfile): Promise { - const db = await this.ensureDB(); - // Only cache non-sensitive profile data - const safeProfile = { - id: profile.id, - username: profile.username, - email: profile.email, // Email is generally safe to cache - created_at: profile.created_at, - timestamp: Date.now(), - }; - await db.put("settings", { key: `profile_${userId}`, value: safeProfile }); - } - - async getProfile(userId: string): Promise { - const db = await this.ensureDB(); - const result = await db.get("settings", `profile_${userId}`); - return result?.value; - } - - async clearProfile(userId: string): Promise { - const db = await this.ensureDB(); - await db.delete("settings", `profile_${userId}`); - } - - // Settings methods - async saveSetting(key: string, value: OfflineSetting): Promise { - const db = await this.ensureDB(); - await db.put("settings", { key, value }); - } - - async getSetting(key: string): Promise { - const db = await this.ensureDB(); - const result = await db.get("settings", key); - return result?.value; - } - - // Group voting methods - async getSetGroupVotes( - setId: string, - _groupId: string, - ): Promise< - Array<{ - vote_type: number; - user_id: string; - username: string | null; - }> - > { - try { - // Get all votes for this set - const setVotes = await this.getVotes(setId); - - // For now, return all votes since we don't have group member filtering offline - // In a real implementation, you'd also cache group member data - return setVotes.map((vote) => ({ - vote_type: vote.voteType, - user_id: vote.userId, - username: null, // We don't store usernames in offline votes currently - })); - } catch (error) { - console.error("Error fetching offline group votes:", error); - return []; - } - } - - // Utility methods - async clear(): Promise { - const db = await this.ensureDB(); - const tx = db.transaction( - ["artists", "sets", "votes", "notes", "schedule", "settings"], - "readwrite", - ); - await Promise.all([ - tx.objectStore("artists").clear(), - tx.objectStore("sets").clear(), - tx.objectStore("votes").clear(), - tx.objectStore("notes").clear(), - tx.objectStore("schedule").clear(), - tx.objectStore("settings").clear(), - ]); - await tx.done; - } -} - -export const offlineStorage = new OfflineStorageManager(); diff --git a/src/lib/voteConfig.ts b/src/lib/voteConfig.ts index ffc1ddd..0234fc2 100644 --- a/src/lib/voteConfig.ts +++ b/src/lib/voteConfig.ts @@ -51,12 +51,27 @@ export const VOTE_CONFIG = { }, } as const; +export type VoteConfig = { + value: -1 | 1 | 2; + label: string; + icon: typeof Star; + bgColor: string; + iconColor: string; + textColor: string; + descColor: string; + circleColor: string; + buttonSelected: string; + buttonUnselected: string; + spinnerColor: string; + description: string; +}; + export function getVoteConfig(voteValue: number): VoteType | undefined { return ( VOTES_TYPES.find((key) => VOTE_CONFIG[key].value === voteValue) || undefined ); } -export function getVoteValue(voteType: VoteType): -1 | 1 | 2 | undefined { +export function getVoteValue(voteType: VoteType): -1 | 1 | 2 { return VOTE_CONFIG[voteType].value; } diff --git a/src/main.tsx b/src/main.tsx index af72862..fc7b13f 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,15 +1,22 @@ import { createRoot } from "react-dom/client"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { QueryClient } from "@tanstack/react-query"; +import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client"; +import { createAsyncStoragePersister } from "@tanstack/query-async-storage-persister"; + import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import App from "./App.tsx"; import "./index.css"; -// Create a client +const persister = createAsyncStoragePersister({ + storage: window.localStorage, +}); + +// Create a client with offline-first configuration const queryClient = new QueryClient({ defaultOptions: { queries: { - staleTime: 5 * 60 * 1000, // 5 minutes - gcTime: 10 * 60 * 1000, // 10 minutes + staleTime: 15 * 60 * 1000, // 15 minutes - longer for offline support + gcTime: 24 * 60 * 60 * 1000, // 24 hours - keep data longer for offline retry: (failureCount, error) => { // Don't retry on 4xx errors if ( @@ -23,6 +30,7 @@ const queryClient = new QueryClient({ return failureCount < 2; }, refetchOnWindowFocus: false, + refetchOnReconnect: true, // Refetch when coming back online }, mutations: { retry: 1, @@ -31,8 +39,11 @@ const queryClient = new QueryClient({ }); createRoot(document.getElementById("root")!).render( - + - , + , ); diff --git a/src/pages/EditionView/tabs/ArtistsTab/ArtistsTab.tsx b/src/pages/EditionView/tabs/ArtistsTab/ArtistsTab.tsx index 1251569..cddc188 100644 --- a/src/pages/EditionView/tabs/ArtistsTab/ArtistsTab.tsx +++ b/src/pages/EditionView/tabs/ArtistsTab/ArtistsTab.tsx @@ -1,4 +1,3 @@ -import { useAuth } from "@/contexts/AuthContext"; import { FilterSortControls } from "./filters/FilterSortControls"; import { useSetFiltering } from "./useSetFiltering"; import { useUrlState } from "@/hooks/useUrlState"; @@ -7,7 +6,6 @@ import { useSetsByEditionQuery } from "@/hooks/queries/sets/useSetsByEdition"; import { useFestivalEdition } from "@/contexts/FestivalEditionContext"; export function ArtistsTab() { - const { user, showAuthDialog } = useAuth(); const { state: urlState, updateUrlState, clearFilters } = useUrlState(); const { edition } = useFestivalEdition(); @@ -40,9 +38,7 @@ export function ArtistsTab() {
showAuthDialog()} onLockSort={() => lockCurrentOrder(updateUrlState)} />
diff --git a/src/pages/EditionView/tabs/ArtistsTab/FestivalSetContext.tsx b/src/pages/EditionView/tabs/ArtistsTab/FestivalSetContext.tsx index a1d2cbc..cacf39f 100644 --- a/src/pages/EditionView/tabs/ArtistsTab/FestivalSetContext.tsx +++ b/src/pages/EditionView/tabs/ArtistsTab/FestivalSetContext.tsx @@ -1,22 +1,10 @@ -import { createContext, useContext, ReactNode, useMemo } from "react"; +import { createContext, useContext, ReactNode } from "react"; import { FestivalSet } from "@/hooks/queries/sets/useSets"; interface FestivalSetContextValue { set: FestivalSet; - userVote?: number; - userKnowledge?: boolean; - votingLoading?: boolean; - onVote: ( - setId: string, - voteType: number, - ) => Promise<{ requiresAuth: boolean }>; - onKnowledgeToggle: (setId: string) => Promise<{ requiresAuth: boolean }>; - onAuthRequired: () => void; + onLockSort(): void; use24Hour?: boolean; - - // Computed helpers - isMultiArtist: boolean; - getVoteCount: (voteType: number) => number; } const FestivalSetContext = createContext(null); @@ -24,54 +12,20 @@ const FestivalSetContext = createContext(null); interface FestivalSetProviderProps { children: ReactNode; set: FestivalSet; - userVote?: number; - userKnowledge?: boolean; - votingLoading?: boolean; - onVote: ( - setId: string, - voteType: number, - ) => Promise<{ requiresAuth: boolean }>; - onKnowledgeToggle: (setId: string) => Promise<{ requiresAuth: boolean }>; - onAuthRequired: () => void; + onLockSort(): void; use24Hour?: boolean; } export function FestivalSetProvider({ children, set, - userVote, - userKnowledge, - votingLoading, - onVote, - onKnowledgeToggle, - onAuthRequired, + onLockSort, use24Hour = false, }: FestivalSetProviderProps) { - const isMultiArtist = set.artists.length > 1; - - const voteCounts = useMemo(() => { - const counts: Record = {}; - for (const vc of set.votes) { - counts[vc.vote_type] = (counts[vc.vote_type] || 0) + 1; - } - return counts; - }, [set.votes]); - - function getVoteCount(voteType: number) { - return voteCounts[voteType] || 0; - } - const contextValue: FestivalSetContextValue = { set, - userVote, - userKnowledge, - votingLoading, - onVote, - onKnowledgeToggle, - onAuthRequired, + onLockSort, use24Hour, - isMultiArtist, - getVoteCount, }; return ( diff --git a/src/pages/EditionView/tabs/ArtistsTab/SetCard/SetDescription.tsx b/src/pages/EditionView/tabs/ArtistsTab/SetCard/SetDescription.tsx index a128b26..aceb42d 100644 --- a/src/pages/EditionView/tabs/ArtistsTab/SetCard/SetDescription.tsx +++ b/src/pages/EditionView/tabs/ArtistsTab/SetCard/SetDescription.tsx @@ -9,7 +9,8 @@ interface SetDescriptionProps { export function SetDescription({ className = "text-purple-200 text-sm leading-relaxed", }: SetDescriptionProps) { - const { set, isMultiArtist } = useFestivalSet(); + const { set } = useFestivalSet(); + const isMultiArtist = set.artists.length > 1; if (isMultiArtist) { return ( diff --git a/src/pages/EditionView/tabs/ArtistsTab/SetCard/SetHeader.tsx b/src/pages/EditionView/tabs/ArtistsTab/SetCard/SetHeader.tsx index 501306e..b12870a 100644 --- a/src/pages/EditionView/tabs/ArtistsTab/SetCard/SetHeader.tsx +++ b/src/pages/EditionView/tabs/ArtistsTab/SetCard/SetHeader.tsx @@ -9,7 +9,8 @@ interface SetHeaderProps { } export function SetHeader({ size = "lg" }: SetHeaderProps) { - const { set, isMultiArtist } = useFestivalSet(); + const { set } = useFestivalSet(); + const isMultiArtist = set.artists.length > 1; const titleClass = size === "sm" @@ -30,8 +31,7 @@ export function SetHeader({ size = "lg" }: SetHeaderProps) { )} - {/* Social Platform Links */} - {!isMultiArtist && set.artists.length > 0 && ( + {set.artists.length === 1 && ( 1; const sizeClasses = { sm: "w-12 h-12", diff --git a/src/pages/EditionView/tabs/ArtistsTab/SetCard/SetVotingButtons.tsx b/src/pages/EditionView/tabs/ArtistsTab/SetCard/SetVotingButtons.tsx index 9c25afa..7e677d8 100644 --- a/src/pages/EditionView/tabs/ArtistsTab/SetCard/SetVotingButtons.tsx +++ b/src/pages/EditionView/tabs/ArtistsTab/SetCard/SetVotingButtons.tsx @@ -1,6 +1,10 @@ import { Button } from "@/components/ui/button"; -import { VOTE_CONFIG, VOTES_TYPES } from "@/lib/voteConfig"; +import { VOTE_CONFIG, VOTES_TYPES, type VoteConfig } from "@/lib/voteConfig"; import { useFestivalSet } from "../FestivalSetContext"; +import { useUserVotes } from "@/hooks/queries/voting/useUserVotes"; +import { useVote } from "@/hooks/queries/voting/useVote"; +import { useAuth } from "@/contexts/AuthContext"; +import { useVoteCount } from "@/hooks/useVoteCount"; interface SetVotingButtonsProps { size?: "sm" | "default"; @@ -11,56 +15,104 @@ export function SetVotingButtons({ size = "default", layout = "vertical", }: SetVotingButtonsProps) { - const { set, userVote, votingLoading, onVote, onAuthRequired, getVoteCount } = - useFestivalSet(); + const { user, showAuthDialog } = useAuth(); + + const { set, onLockSort } = useFestivalSet(); + const { getVoteCount } = useVoteCount(set); + const userVotesQuery = useUserVotes(user?.id); + const voteMutation = useVote(); + + const userVoteForSet = userVotesQuery.data?.[set.id]; - async function handleVote(voteType: number) { - const result = await onVote(set.id, voteType); - if (result.requiresAuth) { - onAuthRequired(); - } - } const containerClass = layout === "horizontal" ? "flex items-center gap-2" : "space-y-3"; - const buttonClass = layout === "horizontal" ? "" : "flex-1"; - - const voteButtons = VOTES_TYPES.map((voteType) => ({ - config: VOTE_CONFIG[voteType], - vote: VOTE_CONFIG[voteType].value, - })); + const voteButtons = VOTES_TYPES.map((voteType) => VOTE_CONFIG[voteType]); return (
- {votingLoading && ( + {userVotesQuery.isLoading && (
)} - {voteButtons.map(({ config, vote }) => { - const IconComponent = config.icon; - const isSelected = userVote === vote; - + {voteButtons.map((config) => { return ( -
- -
+ handleVote(config.value)} + voteCount={getVoteCount(config.value)} + isVoting={voteMutation.isPending} + size={size} + layout={layout} + /> ); })}
); + + function handleVote(voteType: number) { + if (!user?.id) { + showAuthDialog(); + + return; + } + + const setId = set.id; + + voteMutation.mutate( + { + setId, + voteType, + userId: user?.id, + existingVote: userVoteForSet, + }, + { + onSuccess() { + onLockSort(); + }, + }, + ); + } +} + +function VoteButton({ + config, + layout, + isSelected, + size, + onClick, + voteCount, + isVoting, +}: { + isSelected: boolean; + config: VoteConfig; + size?: "sm" | "default"; + layout?: "horizontal" | "vertical"; + onClick(): void; + voteCount: number; + isVoting: boolean; +}) { + const buttonClass = layout === "horizontal" ? "" : "flex-1"; + const IconComponent = config.icon; + + return ( +
+ +
+ ); } diff --git a/src/pages/EditionView/tabs/ArtistsTab/SetsPanel.tsx b/src/pages/EditionView/tabs/ArtistsTab/SetsPanel.tsx index 2febdd5..224eeb6 100644 --- a/src/pages/EditionView/tabs/ArtistsTab/SetsPanel.tsx +++ b/src/pages/EditionView/tabs/ArtistsTab/SetsPanel.tsx @@ -1,6 +1,3 @@ -import { User } from "@supabase/supabase-js"; - -import { useOfflineVoting } from "@/hooks/useOfflineVoting"; import { FestivalSet } from "@/hooks/queries/sets/useSets"; import { EmptyArtistsState } from "./EmptyArtistsState"; @@ -9,30 +6,13 @@ import { SetListItem } from "./SetListItem"; export function SetsPanel({ sets, - user, use24Hour, - openAuthDialog, onLockSort, }: { sets: Array; - user: User | null; use24Hour: boolean; - openAuthDialog(): void; onLockSort: () => void; }) { - async function handleVoteWithLock(setId: string, voteType: number) { - const result = await handleVote(setId, voteType); - if (!result.requiresAuth) { - onLockSort(); - } - return result; - } - - const { userVotes, votingLoading, handleVote } = useOfflineVoting( - user, - undefined, // Remove the refresh callback to prevent auto re-sorting - ); - if (sets.length === 0) { return ; } @@ -43,14 +23,7 @@ export function SetsPanel({ ({ - requiresAuth: !user, - })} - onAuthRequired={openAuthDialog} + onLockSort={onLockSort} use24Hour={use24Hour} > diff --git a/src/pages/EditionView/tabs/ScheduleTab.tsx b/src/pages/EditionView/tabs/ScheduleTab.tsx index e78a100..7e88c6f 100644 --- a/src/pages/EditionView/tabs/ScheduleTab.tsx +++ b/src/pages/EditionView/tabs/ScheduleTab.tsx @@ -1,4 +1,4 @@ -import { ScheduleNavigation } from "@/pages/EditionView/tabs/ScheduleTab/ScheduleNavigation"; +import { ScheduleNavigation } from "./ScheduleTab/ScheduleNavigation"; import { Outlet } from "react-router-dom"; export function ScheduleTab() { diff --git a/src/pages/EditionView/tabs/ScheduleTab/ListTab.tsx b/src/pages/EditionView/tabs/ScheduleTab/ListTab.tsx deleted file mode 100644 index dc1408f..0000000 --- a/src/pages/EditionView/tabs/ScheduleTab/ListTab.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { ListSchedule } from "@/pages/EditionView/tabs/ScheduleTab/list/ListSchedule"; -import { ListFilters } from "@/pages/EditionView/tabs/ScheduleTab/ListFilters"; -import { useAuth } from "@/contexts/AuthContext"; -import { useOfflineVoting } from "@/hooks/useOfflineVoting"; - -export function ScheduleTabList() { - const { user, showAuthDialog } = useAuth(); - const { userVotes, handleVote } = useOfflineVoting(user); - - async function handleVoteAction(artistId: string, voteType: number) { - const result = await handleVote(artistId, voteType); - if (result.requiresAuth) { - showAuthDialog(); - } - } - - return ( - <> - - - - ); -} diff --git a/src/pages/EditionView/tabs/ScheduleTab/TimelineTab.tsx b/src/pages/EditionView/tabs/ScheduleTab/TimelineTab.tsx index 1c4b83d..d23ada8 100644 --- a/src/pages/EditionView/tabs/ScheduleTab/TimelineTab.tsx +++ b/src/pages/EditionView/tabs/ScheduleTab/TimelineTab.tsx @@ -1,17 +1,5 @@ -import { useAuth } from "@/contexts/AuthContext"; -import { useOfflineVoting } from "@/hooks/useOfflineVoting"; -import { Timeline } from "@/pages/EditionView/tabs/ScheduleTab/horizontal/Timeline"; +import { Timeline } from "./horizontal/Timeline"; export function ScheduleTabTimeline() { - const { user, showAuthDialog } = useAuth(); - const { userVotes, handleVote } = useOfflineVoting(user); - - async function handleVoteAction(artistId: string, voteType: number) { - const result = await handleVote(artistId, voteType); - if (result.requiresAuth) { - showAuthDialog(); - } - } - - return ; + return ; } diff --git a/src/pages/EditionView/tabs/ScheduleTab/VoteButtons.tsx b/src/pages/EditionView/tabs/ScheduleTab/VoteButtons.tsx index 32cfb6b..168b5f2 100644 --- a/src/pages/EditionView/tabs/ScheduleTab/VoteButtons.tsx +++ b/src/pages/EditionView/tabs/ScheduleTab/VoteButtons.tsx @@ -8,14 +8,20 @@ import { import { cn } from "@/lib/utils"; import type { ScheduleSet } from "@/hooks/useScheduleData"; import { useMemo } from "react"; +import { useAuth } from "@/contexts/AuthContext"; +import { useUserVotes } from "@/hooks/queries/voting/useUserVotes"; +import { useVote } from "@/hooks/queries/voting/useVote"; interface VoteButtonsProps { set: ScheduleSet; - userVote?: number; - onVote?: (setId: string, voteType: number) => void; } -export function VoteButtons({ set, userVote, onVote }: VoteButtonsProps) { +export function VoteButtons({ set }: VoteButtonsProps) { + const { user, showAuthDialog } = useAuth(); + const userVotesQuery = useUserVotes(user?.id); + const voteMutation = useVote(); + + const userVote = userVotesQuery.data?.[set.id]; const userVoteType = useMemo(() => { return userVote ? getVoteConfig(userVote) : undefined; }, [userVote]); @@ -47,7 +53,7 @@ export function VoteButtons({ set, userVote, onVote }: VoteButtonsProps) { handleVote(voteType)} + onVote={() => handleVote(getVoteValue(voteType))} count={votesMap[voteType]} value={userVoteType} /> @@ -56,11 +62,18 @@ export function VoteButtons({ set, userVote, onVote }: VoteButtonsProps) {
); - function handleVote(voteType: VoteType) { - const value = getVoteValue(voteType); - if (onVote && value) { - onVote(set.id, value); + async function handleVote(voteType: number) { + if (!user) { + showAuthDialog(); + return; } + + voteMutation.mutate({ + setId: set.id, + voteType, + userId: user.id, + existingVote: userVote, + }); } } diff --git a/src/pages/EditionView/tabs/ScheduleTab/horizontal/SetBlock.tsx b/src/pages/EditionView/tabs/ScheduleTab/horizontal/SetBlock.tsx index 3f52399..157fa6c 100644 --- a/src/pages/EditionView/tabs/ScheduleTab/horizontal/SetBlock.tsx +++ b/src/pages/EditionView/tabs/ScheduleTab/horizontal/SetBlock.tsx @@ -6,11 +6,9 @@ import { VoteButtons } from "../VoteButtons"; interface SetBlockProps { set: ScheduleSet; - userVote?: number; - onVote?: (setId: string, voteType: number) => void; } -export function SetBlock({ set, userVote, onVote }: SetBlockProps) { +export function SetBlock({ set }: SetBlockProps) { return ( @@ -22,7 +20,7 @@ export function SetBlock({ set, userVote, onVote }: SetBlockProps) { )} - + ); diff --git a/src/pages/EditionView/tabs/ScheduleTab/horizontal/StageRow.tsx b/src/pages/EditionView/tabs/ScheduleTab/horizontal/StageRow.tsx index c1ed9ea..5340ac7 100644 --- a/src/pages/EditionView/tabs/ScheduleTab/horizontal/StageRow.tsx +++ b/src/pages/EditionView/tabs/ScheduleTab/horizontal/StageRow.tsx @@ -7,16 +7,9 @@ interface StageRowProps { sets: HorizontalTimelineSet[]; }; totalWidth: number; - userVotes: Record; - onVote: (artistId: string, voteType: number) => void; } -export function StageRow({ - stage, - totalWidth, - userVotes, - onVote, -}: StageRowProps) { +export function StageRow({ stage, totalWidth }: StageRowProps) { return (
{/* Timeline Track */} @@ -37,11 +30,7 @@ export function StageRow({ }} >
- +
); diff --git a/src/pages/EditionView/tabs/ScheduleTab/horizontal/Timeline.tsx b/src/pages/EditionView/tabs/ScheduleTab/horizontal/Timeline.tsx index 40621fd..2dcbed2 100644 --- a/src/pages/EditionView/tabs/ScheduleTab/horizontal/Timeline.tsx +++ b/src/pages/EditionView/tabs/ScheduleTab/horizontal/Timeline.tsx @@ -9,12 +9,7 @@ import { useTimelineUrlState } from "@/hooks/useTimelineUrlState"; import { format } from "date-fns"; import { useStagesByEditionQuery } from "@/hooks/queries/stages/useStagesByEdition"; -interface TimelineProps { - userVotes: Record; - onVote: (artistId: string, voteType: number) => void; -} - -export function Timeline({ userVotes, onVote }: TimelineProps) { +export function Timeline() { const { edition } = useFestivalEdition(); const { data: editionSets = [], isLoading: setsLoading } = useEditionSetsQuery(edition?.id); @@ -117,17 +112,9 @@ export function Timeline({ userVotes, onVote }: TimelineProps) { return (
- {/* Unified Timeline Container */}
- {/* Fixed Stage Labels Column */} - - {/* Scrollable Timeline Content */} - +
); diff --git a/src/pages/EditionView/tabs/ScheduleTab/horizontal/TimelineContainer.tsx b/src/pages/EditionView/tabs/ScheduleTab/horizontal/TimelineContainer.tsx index e60c669..605fb58 100644 --- a/src/pages/EditionView/tabs/ScheduleTab/horizontal/TimelineContainer.tsx +++ b/src/pages/EditionView/tabs/ScheduleTab/horizontal/TimelineContainer.tsx @@ -5,15 +5,9 @@ import type { TimelineData } from "@/lib/timelineCalculator"; interface TimelineContainerProps { timelineData: TimelineData; - userVotes: Record; - onVote: (artistId: string, voteType: number) => void; } -export function TimelineContainer({ - timelineData, - userVotes, - onVote, -}: TimelineContainerProps) { +export function TimelineContainer({ timelineData }: TimelineContainerProps) { const scrollContainerRef = useRef(null); return ( @@ -35,8 +29,6 @@ export function TimelineContainer({ key={stage.name} stage={stage} totalWidth={timelineData.totalWidth} - userVotes={userVotes} - onVote={onVote} /> ))} diff --git a/src/pages/EditionView/tabs/ScheduleTab/ListFilters.tsx b/src/pages/EditionView/tabs/ScheduleTab/list/ListFilters.tsx similarity index 92% rename from src/pages/EditionView/tabs/ScheduleTab/ListFilters.tsx rename to src/pages/EditionView/tabs/ScheduleTab/list/ListFilters.tsx index 3e49e7a..06afecd 100644 --- a/src/pages/EditionView/tabs/ScheduleTab/ListFilters.tsx +++ b/src/pages/EditionView/tabs/ScheduleTab/list/ListFilters.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; -import { DayFilterSelect } from "./DayFilterSelect"; -import { TimeFilterSelect } from "./TimeFilterSelect"; -import { StageFilterButtons } from "./StageFilterButtons"; +import { DayFilterSelect } from "../DayFilterSelect"; +import { TimeFilterSelect } from "../TimeFilterSelect"; +import { StageFilterButtons } from "../StageFilterButtons"; import { useTimelineUrlState } from "@/hooks/useTimelineUrlState"; import { FilterToggle } from "@/components/filters/FilterToggle"; import { FilterContainer } from "@/components/filters/FilterContainer"; diff --git a/src/pages/EditionView/tabs/ScheduleTab/list/ListSchedule.tsx b/src/pages/EditionView/tabs/ScheduleTab/list/ListSchedule.tsx index 2309064..bd0aa59 100644 --- a/src/pages/EditionView/tabs/ScheduleTab/list/ListSchedule.tsx +++ b/src/pages/EditionView/tabs/ScheduleTab/list/ListSchedule.tsx @@ -8,17 +8,12 @@ import type { ScheduleSet } from "@/hooks/useScheduleData"; import { useTimelineUrlState } from "@/hooks/useTimelineUrlState"; import { useStagesByEditionQuery } from "@/hooks/queries/stages/useStagesByEdition"; -interface ListScheduleProps { - userVotes: Record; - onVote: (artistId: string, voteType: number) => void; -} - interface TimeSlot { time: Date; sets: (ScheduleSet & { stageName: string })[]; } -export function ListSchedule({ userVotes, onVote }: ListScheduleProps) { +export function ListSchedule() { const { edition } = useFestivalEdition(); const { data: editionSets = [], isLoading: setsLoading } = useEditionSetsQuery(edition?.id); @@ -158,8 +153,6 @@ export function ListSchedule({ userVotes, onVote }: ListScheduleProps) { key={slot.time.toISOString()} timeSlot={slot} showDateHeader={showDateHeader} - userVotes={userVotes} - onVote={onVote} /> ); })} diff --git a/src/pages/EditionView/tabs/ScheduleTab/list/ListTab.tsx b/src/pages/EditionView/tabs/ScheduleTab/list/ListTab.tsx new file mode 100644 index 0000000..586cbe3 --- /dev/null +++ b/src/pages/EditionView/tabs/ScheduleTab/list/ListTab.tsx @@ -0,0 +1,11 @@ +import { ListSchedule } from "./ListSchedule"; +import { ListFilters } from "./ListFilters"; + +export function ScheduleTabList() { + return ( + <> + + + + ); +} diff --git a/src/pages/EditionView/tabs/ScheduleTab/list/MobileSetCard.tsx b/src/pages/EditionView/tabs/ScheduleTab/list/MobileSetCard.tsx index e43692a..6125839 100644 --- a/src/pages/EditionView/tabs/ScheduleTab/list/MobileSetCard.tsx +++ b/src/pages/EditionView/tabs/ScheduleTab/list/MobileSetCard.tsx @@ -7,11 +7,9 @@ import type { ScheduleSet } from "@/hooks/useScheduleData"; interface MobileSetCardProps { set: ScheduleSet & { stageName: string }; - userVote?: number; - onVote?: (setId: string, voteType: number) => void; } -export function MobileSetCard({ set, userVote, onVote }: MobileSetCardProps) { +export function MobileSetCard({ set }: MobileSetCardProps) { const duration = set.startTime && set.endTime ? differenceInMinutes(set.endTime, set.startTime) @@ -23,7 +21,7 @@ export function MobileSetCard({ set, userVote, onVote }: MobileSetCardProps) { {/* Artist name */}
{set.name} @@ -54,8 +52,7 @@ export function MobileSetCard({ set, userVote, onVote }: MobileSetCardProps) { )}
- {/* Vote buttons */} - + ); diff --git a/src/pages/EditionView/tabs/ScheduleTab/list/TimeSlotGroup.tsx b/src/pages/EditionView/tabs/ScheduleTab/list/TimeSlotGroup.tsx index 170c779..ac0719a 100644 --- a/src/pages/EditionView/tabs/ScheduleTab/list/TimeSlotGroup.tsx +++ b/src/pages/EditionView/tabs/ScheduleTab/list/TimeSlotGroup.tsx @@ -11,15 +11,11 @@ interface TimeSlot { interface TimeSlotGroupProps { timeSlot: TimeSlot; showDateHeader: boolean; - userVotes: Record; - onVote: (artistId: string, voteType: number) => void; } export function TimeSlotGroup({ timeSlot, showDateHeader, - userVotes, - onVote, }: TimeSlotGroupProps) { return (
@@ -44,40 +40,22 @@ export function TimeSlotGroup({
- {/* Sets for this time slot */}
{/* Mobile: Stack all sets vertically */}
{timeSlot.sets.map((set) => ( - + ))}
{/* Desktop: Show sets side by side when space allows */}
{timeSlot.sets.length === 1 ? ( - // Single set - take full width - + ) : ( - // Multiple sets - grid layout
{timeSlot.sets.map((set) => ( - + ))}
)} diff --git a/src/pages/EditionView/tabs/ScheduleTab/list/VerticalArtistScheduleBlock.tsx b/src/pages/EditionView/tabs/ScheduleTab/list/VerticalArtistScheduleBlock.tsx deleted file mode 100644 index f2134d5..0000000 --- a/src/pages/EditionView/tabs/ScheduleTab/list/VerticalArtistScheduleBlock.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { Card, CardContent } from "@/components/ui/card"; -import { Link } from "react-router-dom"; -import { Clock } from "lucide-react"; -import { format } from "date-fns"; -import { VoteButtons } from "../VoteButtons"; -import { useMemo } from "react"; -import { VerticalTimelineSet } from "@/lib/timelineCalculator"; - -interface VerticalArtistScheduleBlockProps { - set: VerticalTimelineSet; - userVote?: number; - onVote?: (setId: string, voteType: number) => void; -} - -export function VerticalArtistScheduleBlock({ - set, - userVote, - onVote, -}: VerticalArtistScheduleBlockProps) { - const height = set.verticalPosition?.height || 60; - const isCompact = height < 80; - const isVeryCompact = height < 60; - - const timeString = useMemo(() => { - if (!set.startTime || !set.endTime) return ""; - - if (isVeryCompact) { - return format(set.startTime, "H:mm"); - } - - const start = format(set.startTime, "H"); - const end = format(set.endTime, "H"); - const startMinutes = set.startTime.getMinutes(); - const endMinutes = set.endTime.getMinutes(); - const startStr = startMinutes === 0 ? start : format(set.startTime, "H:mm"); - const endStr = endMinutes === 0 ? end : format(set.endTime, "H:mm"); - - return `${startStr}-${endStr}`; - }, [set.startTime, set.endTime, isVeryCompact]); - - return ( - - -
- - {set.name} - -
- - {!isVeryCompact && timeString && ( -
- - - {timeString} - -
- )} - -
- -
-
-
- ); -} diff --git a/src/pages/EditionView/tabs/ScheduleTab/list/VerticalStageColumn.tsx b/src/pages/EditionView/tabs/ScheduleTab/list/VerticalStageColumn.tsx deleted file mode 100644 index 0997eed..0000000 --- a/src/pages/EditionView/tabs/ScheduleTab/list/VerticalStageColumn.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { VerticalArtistScheduleBlock } from "./VerticalArtistScheduleBlock"; -import type { VerticalTimelineSet } from "@/lib/timelineCalculator"; - -interface VerticalStageColumnProps { - stage: { - name: string; - sets: VerticalTimelineSet[]; - }; - totalHeight: number; - userVotes: Record; - onVote: (artistId: string, voteType: number) => void; -} - -export function VerticalStageColumn({ - stage, - totalHeight, - userVotes, - onVote, -}: VerticalStageColumnProps) { - return ( -
-
- {stage.sets.map((set) => { - if (!set.verticalPosition) return null; - - return ( -
-
- -
-
- ); - })} -
-
- ); -} diff --git a/src/pages/EditionView/tabs/ScheduleTab/list/VerticalStageLabels.tsx b/src/pages/EditionView/tabs/ScheduleTab/list/VerticalStageLabels.tsx deleted file mode 100644 index 4262502..0000000 --- a/src/pages/EditionView/tabs/ScheduleTab/list/VerticalStageLabels.tsx +++ /dev/null @@ -1,17 +0,0 @@ -interface VerticalStageLabelsProps { - stages: Array<{ name: string }>; -} - -export function VerticalStageLabels({ stages }: VerticalStageLabelsProps) { - return ( -
- {stages.map((stage) => ( -
-
- {stage.name} -
-
- ))} -
- ); -} diff --git a/src/pages/EditionView/tabs/ScheduleTab/list/VerticalTimeScale.tsx b/src/pages/EditionView/tabs/ScheduleTab/list/VerticalTimeScale.tsx deleted file mode 100644 index ecdf509..0000000 --- a/src/pages/EditionView/tabs/ScheduleTab/list/VerticalTimeScale.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { format, differenceInMinutes } from "date-fns"; -import { useRef } from "react"; - -interface VerticalTimeScaleProps { - timeSlots: Date[]; - totalHeight: number; - scrollContainerRef?: React.RefObject; -} - -const dateFormat = "MMMM d"; - -export function VerticalTimeScale({ - timeSlots, - totalHeight, -}: VerticalTimeScaleProps) { - const timeScaleRef = useRef(null); - - const dateChanges = timeSlots.reduce( - (changes, timeSlot, index) => { - if (index === 0) { - changes.push({ date: timeSlot, position: 20 }); - } else { - const prevDate = format(timeSlots[index - 1], "yyyy-MM-dd"); - const currentDate = format(timeSlot, "yyyy-MM-dd"); - if (prevDate !== currentDate) { - const midnightOfNewDate = new Date(timeSlot); - midnightOfNewDate.setHours(0, 0, 0, 0); - - const festivalStart = timeSlots[0]; - const minutesFromStart = differenceInMinutes( - midnightOfNewDate, - festivalStart, - ); - const position = minutesFromStart * 2 + 20; // 2px per minute - - changes.push({ date: midnightOfNewDate, position }); - } - } - return changes; - }, - [] as Array<{ date: Date; position: number }>, - ); - - return ( -
- {/* Date change indicators */} - {dateChanges.map((dateChange, index) => { - const nextDateChange = dateChanges[index + 1]; - const fullHeight = nextDateChange - ? nextDateChange.position - dateChange.position - : totalHeight - dateChange.position; - - const space = 5; - const height = fullHeight - space; - const top = dateChange.position; - - return ( -
- {format(dateChange.date, dateFormat)} -
- ); - })} - - {/* Time slots */} - {timeSlots.map((timeSlot, index) => ( -
-
- {format(timeSlot, "HH:mm")} -
-
-
- ))} - -
-
- ); -} diff --git a/src/pages/EditionView/tabs/ScheduleTab/list/VerticalTimelineContainer.tsx b/src/pages/EditionView/tabs/ScheduleTab/list/VerticalTimelineContainer.tsx deleted file mode 100644 index ebd6fee..0000000 --- a/src/pages/EditionView/tabs/ScheduleTab/list/VerticalTimelineContainer.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { useRef } from "react"; -import { VerticalTimeScale } from "./VerticalTimeScale"; -import { VerticalStageColumn } from "./VerticalStageColumn"; -import type { VerticalTimelineData } from "@/lib/timelineCalculator"; - -interface VerticalTimelineContainerProps { - timelineData: VerticalTimelineData; - userVotes: Record; - onVote: (artistId: string, voteType: number) => void; -} - -export function VerticalTimelineContainer({ - timelineData, - userVotes, - onVote, -}: VerticalTimelineContainerProps) { - const scrollContainerRef = useRef(null); - - return ( -
- {/* Time Scale */} - - - {/* Stage Columns */} -
- {timelineData.stages.map((stage) => ( - - ))} -
-
- ); -} diff --git a/src/pages/SetDetails.tsx b/src/pages/SetDetails.tsx index 1a66085..e11b9b6 100644 --- a/src/pages/SetDetails.tsx +++ b/src/pages/SetDetails.tsx @@ -7,12 +7,12 @@ import { MultiArtistSetInfoCard } from "./SetDetails/MultiArtistSetInfoCard"; import { ArtistNotFoundState } from "./SetDetails/SetNotFoundState"; import { ArtistLoadingState } from "./SetDetails/SetLoadingState"; import { SetGroupVoting } from "./SetDetails/SetGroupVoting"; -import { ArtistNotes } from "./SetDetails/SetNotes"; +import { SetNotes } from "./SetDetails/SetNotes"; import { useUrlState } from "@/hooks/useUrlState"; -import { useSetDetail } from "./SetDetails/useSetDetail"; import { useSetBySlugQuery } from "@/hooks/queries/sets/useSetBySlug"; import { useFestivalEdition } from "@/contexts/FestivalEditionContext"; import { useAuth } from "@/contexts/AuthContext"; +import { useVoteCount } from "@/hooks/useVoteCount"; export function SetDetails() { const { user } = useAuth(); @@ -23,10 +23,12 @@ export function SetDetails() { slug: setSlug, editionId: edition?.id, }); - const { userVote, loading, handleVote, getVoteCount, netVoteScore } = - useSetDetail(setQuery.data?.id); - if (loading) { + const { getVoteCount } = useVoteCount(setQuery.data); + + const netVoteScore = 2 * getVoteCount(2) + getVoteCount(1) - getVoteCount(-1); + + if (setQuery.isLoading) { return ; } @@ -54,16 +56,12 @@ export function SetDetails() {
) : (
- {/* Single Artist Image */}
@@ -87,7 +82,7 @@ export function SetDetails() { {/* Set Notes Section */}
- +
diff --git a/src/pages/SetDetails/MultiArtistSetInfoCard.tsx b/src/pages/SetDetails/MultiArtistSetInfoCard.tsx index 8ce04db..31cf41f 100644 --- a/src/pages/SetDetails/MultiArtistSetInfoCard.tsx +++ b/src/pages/SetDetails/MultiArtistSetInfoCard.tsx @@ -7,7 +7,7 @@ import { } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Clock, Users } from "lucide-react"; -import { ArtistVotingButtons } from "./SetVotingButtons"; +import { SetVotingButtons } from "./SetVotingButtons"; import { FestivalSet } from "@/hooks/queries/sets/useSets"; import { formatTimeRange } from "@/lib/timeUtils"; import { GenreBadge } from "@/components/GenreBadge"; @@ -16,19 +16,13 @@ import { StagePin } from "@/components/StagePin"; interface MultiArtistSetInfoCardProps { set: FestivalSet; - userVote: number | null; netVoteScore: number; - onVote: (voteType: number) => void; - getVoteCount: (voteType: number) => number; use24Hour?: boolean; } export function MultiArtistSetInfoCard({ set, - userVote, netVoteScore, - onVote, - getVoteCount, use24Hour = false, }: MultiArtistSetInfoCardProps) { const allGenres = set.artists.flatMap( @@ -105,12 +99,7 @@ export function MultiArtistSetInfoCard({ )} - {/* Voting System */} - + diff --git a/src/pages/SetDetails/SetInfoCard.tsx b/src/pages/SetDetails/SetInfoCard.tsx index f0aa3a0..71c30d8 100644 --- a/src/pages/SetDetails/SetInfoCard.tsx +++ b/src/pages/SetDetails/SetInfoCard.tsx @@ -8,7 +8,7 @@ import { import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Clock, ExternalLink, Music, Play } from "lucide-react"; -import { ArtistVotingButtons } from "./SetVotingButtons"; +import { SetVotingButtons } from "./SetVotingButtons"; import { FestivalSet } from "@/hooks/queries/sets/useSets"; import { formatTimeRange } from "@/lib/timeUtils"; import { GenreBadge } from "@/components/GenreBadge"; @@ -16,19 +16,13 @@ import { StagePin } from "@/components/StagePin"; interface SetInfoCardProps { set: FestivalSet; - userVote: number | null; netVoteScore: number; - onVote: (voteType: number) => void; - getVoteCount: (voteType: number) => number; use24Hour?: boolean; } export function SetInfoCard({ set, - userVote, netVoteScore, - onVote, - getVoteCount, use24Hour = false, }: SetInfoCardProps) { const artist = set.artists[0]; @@ -84,12 +78,7 @@ export function SetInfoCard({ )} - {/* Voting System */} - + {/* External Links */} {(artist.spotify_url || artist.soundcloud_url) && ( diff --git a/src/pages/SetDetails/SetNotFoundState.tsx b/src/pages/SetDetails/SetNotFoundState.tsx index 54c5f78..e130bd0 100644 --- a/src/pages/SetDetails/SetNotFoundState.tsx +++ b/src/pages/SetDetails/SetNotFoundState.tsx @@ -1,14 +1,16 @@ import { Link } from "react-router-dom"; import { Button } from "@/components/ui/button"; import { ArrowLeft, Music } from "lucide-react"; +import { useFestivalEdition } from "@/contexts/FestivalEditionContext"; export function ArtistNotFoundState() { + const { basePath } = useFestivalEdition(); return (

Set not found

- +