From e1c29fbacdba4e57dfe8ea6173a7fd9fbdc63e00 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Thu, 4 Sep 2025 02:08:35 +0300 Subject: [PATCH 1/9] feat(voting): remove offline voting --- src/components/router/EditionRoutes.tsx | 2 +- src/hooks/queries/useOfflineVotingQuery.ts | 304 ------------------ src/hooks/useOfflineVoting.ts | 62 ---- src/hooks/useVoteCount.ts | 23 ++ src/lib/voteConfig.ts | 17 +- .../tabs/ArtistsTab/ArtistsTab.tsx | 4 - .../tabs/ArtistsTab/FestivalSetContext.tsx | 56 +--- .../ArtistsTab/SetCard/SetDescription.tsx | 3 +- .../tabs/ArtistsTab/SetCard/SetHeader.tsx | 3 +- .../tabs/ArtistsTab/SetCard/SetImage.tsx | 3 +- .../ArtistsTab/SetCard/SetVotingButtons.tsx | 126 +++++--- .../EditionView/tabs/ArtistsTab/SetsPanel.tsx | 29 +- src/pages/EditionView/tabs/ScheduleTab.tsx | 2 +- .../EditionView/tabs/ScheduleTab/ListTab.tsx | 23 -- .../tabs/ScheduleTab/TimelineTab.tsx | 16 +- .../tabs/ScheduleTab/VoteButtons.tsx | 29 +- .../tabs/ScheduleTab/horizontal/SetBlock.tsx | 6 +- .../tabs/ScheduleTab/horizontal/StageRow.tsx | 15 +- .../tabs/ScheduleTab/horizontal/Timeline.tsx | 17 +- .../horizontal/TimelineContainer.tsx | 10 +- .../ScheduleTab/{ => list}/ListFilters.tsx | 6 +- .../tabs/ScheduleTab/list/ListSchedule.tsx | 9 +- .../tabs/ScheduleTab/list/ListTab.tsx | 11 + .../tabs/ScheduleTab/list/MobileSetCard.tsx | 7 +- .../tabs/ScheduleTab/list/TimeSlotGroup.tsx | 28 +- .../list/VerticalArtistScheduleBlock.tsx | 73 ----- .../ScheduleTab/list/VerticalStageColumn.tsx | 51 --- .../ScheduleTab/list/VerticalStageLabels.tsx | 17 - .../ScheduleTab/list/VerticalTimeScale.tsx | 95 ------ .../list/VerticalTimelineContainer.tsx | 44 --- src/pages/SetDetails.tsx | 10 +- .../SetDetails/MultiArtistSetInfoCard.tsx | 15 +- src/pages/SetDetails/SetInfoCard.tsx | 15 +- src/pages/SetDetails/SetVotingButtons.tsx | 92 ++++-- src/pages/SetDetails/useSetDetail.ts | 25 +- 35 files changed, 258 insertions(+), 990 deletions(-) delete mode 100644 src/hooks/queries/useOfflineVotingQuery.ts delete mode 100644 src/hooks/useOfflineVoting.ts create mode 100644 src/hooks/useVoteCount.ts delete mode 100644 src/pages/EditionView/tabs/ScheduleTab/ListTab.tsx rename src/pages/EditionView/tabs/ScheduleTab/{ => list}/ListFilters.tsx (92%) create mode 100644 src/pages/EditionView/tabs/ScheduleTab/list/ListTab.tsx delete mode 100644 src/pages/EditionView/tabs/ScheduleTab/list/VerticalArtistScheduleBlock.tsx delete mode 100644 src/pages/EditionView/tabs/ScheduleTab/list/VerticalStageColumn.tsx delete mode 100644 src/pages/EditionView/tabs/ScheduleTab/list/VerticalStageLabels.tsx delete mode 100644 src/pages/EditionView/tabs/ScheduleTab/list/VerticalTimeScale.tsx delete mode 100644 src/pages/EditionView/tabs/ScheduleTab/list/VerticalTimelineContainer.tsx 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/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/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/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/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/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..ae902ba 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 > 0; 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..ba7e9b5 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 > 0; const titleClass = size === "sm" diff --git a/src/pages/EditionView/tabs/ArtistsTab/SetCard/SetImage.tsx b/src/pages/EditionView/tabs/ArtistsTab/SetCard/SetImage.tsx index 40f4230..3bcc069 100644 --- a/src/pages/EditionView/tabs/ArtistsTab/SetCard/SetImage.tsx +++ b/src/pages/EditionView/tabs/ArtistsTab/SetCard/SetImage.tsx @@ -9,7 +9,8 @@ interface SetImageProps { } export function SetImage({ className = "", size = "lg" }: SetImageProps) { - const { set, isMultiArtist } = useFestivalSet(); + const { set } = useFestivalSet(); + const isMultiArtist = set.artists.length > 0; 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..da825ad 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) @@ -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..148a6a1 100644 --- a/src/pages/SetDetails.tsx +++ b/src/pages/SetDetails.tsx @@ -23,8 +23,7 @@ export function SetDetails() { slug: setSlug, editionId: edition?.id, }); - const { userVote, loading, handleVote, getVoteCount, netVoteScore } = - useSetDetail(setQuery.data?.id); + const { loading, netVoteScore } = useSetDetail(setQuery.data?.id); if (loading) { return ; @@ -54,16 +53,12 @@ export function SetDetails() {
) : (
- {/* Single Artist Image */}
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/SetVotingButtons.tsx b/src/pages/SetDetails/SetVotingButtons.tsx index a526fb4..5c0947b 100644 --- a/src/pages/SetDetails/SetVotingButtons.tsx +++ b/src/pages/SetDetails/SetVotingButtons.tsx @@ -1,6 +1,63 @@ import { Button } from "@/components/ui/button"; +import { useAuth } from "@/contexts/AuthContext"; +import { FestivalSet } from "@/hooks/queries/sets/useSets"; +import { useUserVotes } from "@/hooks/queries/voting/useUserVotes"; +import { useVote } from "@/hooks/queries/voting/useVote"; +import { useVoteCount } from "@/hooks/useVoteCount"; import { VOTE_CONFIG, getVoteConfig } from "@/lib/voteConfig"; +interface SetVotingButtonsProps { + set: FestivalSet; +} + +export function SetVotingButtons({ set }: SetVotingButtonsProps) { + const { user, showAuthDialog } = useAuth(); + const { getVoteCount } = useVoteCount(set); + const userVotesQuery = useUserVotes(user?.id); + const voteMutation = useVote(); + + const setId = set.id; + const userVoteForSet = userVotesQuery.data?.[setId]; + + return ( +
+ handleVote(2)} + count={getVoteCount(2)} + /> + handleVote(1)} + count={getVoteCount(1)} + /> + handleVote(-1)} + count={getVoteCount(-1)} + /> +
+ ); + + function handleVote(voteType: number) { + if (!user?.id) { + showAuthDialog(); + + return; + } + + voteMutation.mutate({ + setId, + voteType, + userId: user?.id, + existingVote: userVoteForSet, + }); + } +} + interface VoteButtonProps { voteType: number; isActive: boolean; @@ -30,38 +87,3 @@ function VoteButton({ voteType, isActive, onClick, count }: VoteButtonProps) { ); } - -interface ArtistVotingButtonsProps { - userVote: number | null; - onVote: (voteType: number) => void; - getVoteCount: (voteType: number) => number; -} - -export function ArtistVotingButtons({ - userVote, - onVote, - getVoteCount, -}: ArtistVotingButtonsProps) { - return ( -
- onVote(2)} - count={getVoteCount(2)} - /> - onVote(1)} - count={getVoteCount(1)} - /> - onVote(-1)} - count={getVoteCount(-1)} - /> -
- ); -} diff --git a/src/pages/SetDetails/useSetDetail.ts b/src/pages/SetDetails/useSetDetail.ts index 26ddaab..5d47345 100644 --- a/src/pages/SetDetails/useSetDetail.ts +++ b/src/pages/SetDetails/useSetDetail.ts @@ -1,8 +1,8 @@ import { useMemo } from "react"; import { useAuth } from "@/contexts/AuthContext"; -import { useOfflineVoting } from "@/hooks/useOfflineVoting"; import { useUserPermissionsQuery } from "@/hooks/queries/auth/useUserPermissions"; import { useOfflineSetsData } from "@/hooks/useOfflineSetsData"; +import { useVoteCount } from "@/hooks/useVoteCount"; export function useSetDetail(setId: string | undefined) { const { user, loading: authLoading } = useAuth(); @@ -10,42 +10,25 @@ export function useSetDetail(setId: string | undefined) { useUserPermissionsQuery(user?.id, "edit_artists"); const setsQuery = useOfflineSetsData(); - const { userVotes, handleVote } = useOfflineVoting(user); const sets = setsQuery.sets; const currentSet = useMemo(() => { if (!setId || !sets.length) { - return null; + return undefined; } - return sets.find((a) => a.id === setId) || null; + return sets.find((a) => a.id === setId); }, [setId, sets]); - async function handleVoteAction(voteType: number) { - if (!setId) return; - await handleVote(setId, voteType); - } - - function getVoteCount(voteType: number) { - if (!currentSet) return 0; - return ( - currentSet.votes?.filter((vote) => vote.vote_type === voteType).length || - 0 - ); - } + const { getVoteCount } = useVoteCount(currentSet); const netVoteScore = currentSet ? 2 * getVoteCount(2) + getVoteCount(1) - getVoteCount(-1) : 0; - const userVote = userVotes[setId || ""] || null; - return { - userVote, loading: authLoading || isLoadingPermissions || setsQuery.loading, canEdit, - handleVote: handleVoteAction, - getVoteCount, netVoteScore, }; } From cb1d4f9032ff0c569460f04086a7fc3ad9cda58a Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Thu, 4 Sep 2025 02:33:47 +0300 Subject: [PATCH 2/9] feat(notes): remove offline notes --- ...teMutation.ts => useCreateNoteMutation.ts} | 18 +- src/hooks/queries/useOfflineNotesQuery.ts | 265 ------------------ src/hooks/queries/useOfflineSetsQuery.ts | 140 --------- src/hooks/useOfflineNotes.ts | 64 ----- src/hooks/useOfflineSetsData.ts | 16 -- src/pages/SetDetails.tsx | 13 +- src/pages/SetDetails/SetNotes.tsx | 136 ++------- src/pages/SetDetails/notes/CreateNoteForm.tsx | 70 +++++ src/pages/SetDetails/notes/SetNoteItem.tsx | 47 ++++ src/pages/SetDetails/useSetDetail.ts | 34 --- 10 files changed, 166 insertions(+), 637 deletions(-) rename src/hooks/queries/artists/notes/{useSaveNoteMutation.ts => useCreateNoteMutation.ts} (79%) delete mode 100644 src/hooks/queries/useOfflineNotesQuery.ts delete mode 100644 src/hooks/queries/useOfflineSetsQuery.ts delete mode 100644 src/hooks/useOfflineNotes.ts delete mode 100644 src/hooks/useOfflineSetsData.ts create mode 100644 src/pages/SetDetails/notes/CreateNoteForm.tsx create mode 100644 src/pages/SetDetails/notes/SetNoteItem.tsx delete mode 100644 src/pages/SetDetails/useSetDetail.ts 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/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/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/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/pages/SetDetails.tsx b/src/pages/SetDetails.tsx index 148a6a1..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,9 +23,12 @@ export function SetDetails() { slug: setSlug, editionId: edition?.id, }); - const { loading, 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 ; } @@ -79,7 +82,7 @@ export function SetDetails() { {/* Set Notes Section */}
- +
diff --git a/src/pages/SetDetails/SetNotes.tsx b/src/pages/SetDetails/SetNotes.tsx index e998e82..6a56201 100644 --- a/src/pages/SetDetails/SetNotes.tsx +++ b/src/pages/SetDetails/SetNotes.tsx @@ -7,22 +7,22 @@ import { CardTitle, } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; -import { Textarea } from "@/components/ui/textarea"; -import { Trash2, Edit3, Save, X, StickyNote } from "lucide-react"; -import { useOfflineNotes } from "@/hooks/useOfflineNotes"; +import { Edit3, StickyNote } from "lucide-react"; +import { useArtistNotesQuery } from "@/hooks/queries/artists/notes/useArtistNotes"; +import { SetNoteItem } from "./notes/SetNoteItem"; +import { CreateNoteForm } from "./notes/CreateNoteForm"; -interface ArtistNotesProps { - artistId: string; +interface SetNotesProps { + setId: string; userId: string | null; } -export function ArtistNotes({ artistId, userId }: ArtistNotesProps) { - const { notes, loading, saving, saveNote, deleteNote } = useOfflineNotes( - artistId, - userId, - ); +export function SetNotes({ setId, userId }: SetNotesProps) { + const notesQuery = useArtistNotesQuery(setId); + const [isEditing, setIsEditing] = useState(false); - const [noteContent, setNoteContent] = useState(""); + + const notes = notesQuery.data; if (!userId) { return ( @@ -40,29 +40,7 @@ export function ArtistNotes({ artistId, userId }: ArtistNotesProps) { ); } - async function handleSave() { - const success = await saveNote(noteContent.trim()); - if (success) { - setIsEditing(false); - } - } - - async function handleDelete(noteId: string) { - if (window.confirm("Are you sure you want to delete this note?")) { - const success = await deleteNote(noteId); - if (success) { - setNoteContent(""); - setIsEditing(false); - } - } - } - - function handleCancel() { - setNoteContent(""); - setIsEditing(false); - } - - if (loading) { + if (notesQuery.isLoading) { return ( @@ -95,88 +73,36 @@ export function ArtistNotes({ artistId, userId }: ArtistNotesProps) { {isEditing ? ( -
-