diff --git a/src/components/Index/FestivalSetContext.tsx b/src/components/Index/FestivalSetContext.tsx new file mode 100644 index 0000000..f6906c4 --- /dev/null +++ b/src/components/Index/FestivalSetContext.tsx @@ -0,0 +1,90 @@ +import { createContext, useContext, ReactNode, useMemo } from "react"; +import { FestivalSet } from "@/services/queries"; + +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; + use24Hour?: boolean; + + // Computed helpers + isMultiArtist: boolean; + getVoteCount: (voteType: number) => number; +} + +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; + use24Hour?: boolean; +} + +export function FestivalSetProvider({ + children, + set, + userVote, + userKnowledge, + votingLoading, + onVote, + onKnowledgeToggle, + onAuthRequired, + 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, + use24Hour, + isMultiArtist, + getVoteCount, + }; + + return ( + + {children} + + ); +} + +export function useFestivalSet() { + const context = useContext(FestivalSetContext); + if (!context) { + throw new Error("useFestivalSet must be used within a FestivalSetProvider"); + } + return context; +} diff --git a/src/components/Index/GenreBadge.tsx b/src/components/Index/GenreBadge.tsx index 91d4e90..8a7c7d9 100644 --- a/src/components/Index/GenreBadge.tsx +++ b/src/components/Index/GenreBadge.tsx @@ -3,9 +3,10 @@ import { useGenres } from "@/hooks/queries/useGenresQuery"; interface GenreBadgeProps { genreId: string; + size?: "default" | "sm"; } -export function GenreBadge({ genreId }: GenreBadgeProps) { +export function GenreBadge({ genreId, size = "default" }: GenreBadgeProps) { const { genres, loading, error } = useGenres(); if (loading || error) return null; @@ -16,7 +17,9 @@ export function GenreBadge({ genreId }: GenreBadgeProps) { return ( {genre.name} diff --git a/src/components/Index/MultiArtistSetCard.tsx b/src/components/Index/MultiArtistSetCard.tsx new file mode 100644 index 0000000..cfd29f7 --- /dev/null +++ b/src/components/Index/MultiArtistSetCard.tsx @@ -0,0 +1,30 @@ +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { SetHeader } from "./shared/SetHeader"; +import { SetImage } from "./shared/SetImage"; +import { SetMetadata } from "./shared/SetMetadata"; +import { SetDescription } from "./shared/SetDescription"; +import { SetVotingButtons } from "./shared/SetVotingButtons"; + +export function MultiArtistSetCard() { + return ( + + + {/* Set Image with Mixed Artists */} + + +
+
+ + +
+
+ + +
+ + + + +
+ ); +} diff --git a/src/components/Index/MultiArtistSetListItem.tsx b/src/components/Index/MultiArtistSetListItem.tsx new file mode 100644 index 0000000..c0a7a6b --- /dev/null +++ b/src/components/Index/MultiArtistSetListItem.tsx @@ -0,0 +1,50 @@ +import { SetHeader } from "./shared/SetHeader"; +import { SetImage } from "./shared/SetImage"; +import { SetMetadata } from "./shared/SetMetadata"; +import { SetDescription } from "./shared/SetDescription"; +import { SetVotingButtons } from "./shared/SetVotingButtons"; + +export function MultiArtistSetListItem() { + return ( +
+ {/* Mobile Layout (sm and below) */} +
+
+ +
+ + +
+
+ + + + +
+ + {/* Desktop Layout (md and above) */} +
+ + +
+
+
+ + + +
+
+ + +
+ +
+ +
+
+
+ ); +} diff --git a/src/components/Index/SetCard.tsx b/src/components/Index/SetCard.tsx deleted file mode 100644 index 6c05cb6..0000000 --- a/src/components/Index/SetCard.tsx +++ /dev/null @@ -1,298 +0,0 @@ -import { Link } from "react-router-dom"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { Star, Heart, X, MapPin, Clock, Eye, EyeOff } from "lucide-react"; -import { ArtistImageLoader } from "@/components/ArtistImageLoader"; -import { useToast } from "@/hooks/use-toast"; -import { formatTimeRange } from "@/lib/timeUtils"; -import { FestivalSet } from "@/services/queries"; -import { GenreBadge } from "./GenreBadge"; - -interface ArtistCardProps { - set: FestivalSet; - userVote?: number; - userKnowledge?: boolean; - votingLoading?: boolean; - onVote: ( - setId: string, - voteType: number, - ) => Promise<{ requiresAuth: boolean }>; - onKnowledgeToggle: (setId: string) => Promise<{ requiresAuth: boolean }>; - onAuthRequired: () => void; - use24Hour?: boolean; -} - -export const SetCard = ({ - set, - userVote, - userKnowledge, - votingLoading, - onVote, - onKnowledgeToggle, - onAuthRequired, - use24Hour = false, -}: ArtistCardProps) => { - const { toast } = useToast(); - - const handleVote = async (voteType: number) => { - const result = await onVote(set.id, voteType); - if (result.requiresAuth) { - onAuthRequired(); - } - }; - - const handleKnowledgeToggle = async () => { - const result = await onKnowledgeToggle(set.id); - if (result.requiresAuth) { - onAuthRequired(); - } else { - // Show toast notification - const newKnowledgeState = !userKnowledge; - toast({ - title: `${set.name} is ${newKnowledgeState ? "known" : "unknown"}`, - duration: 2000, - }); - } - }; - - const getVoteCount = (voteType: number) => { - return set.votes.filter((vote) => vote.vote_type === voteType).length; - }; - - const getSocialPlatformLogo = (url: string) => { - if (url.includes("spotify.com")) { - return { - logo: "https://storage.googleapis.com/pr-newsroom-wp/1/2023/05/Spotify_Primary_Logo_RGB_Green.png", - platform: "Spotify", - color: "text-green-400 hover:text-green-300", - }; - } - if (url.includes("soundcloud.com")) { - return { - logo: "https://d21buns5ku92am.cloudfront.net/26628/documents/54546-1717072325-sc-logo-cloud-black-7412d7.svg", - platform: "SoundCloud", - color: "text-orange-400 hover:text-orange-300", - }; - } - return null; - }; - - return ( - - - {/* Artist Image - clickable for details */} - - - - -
-
-
- {set.name} - - {/* Knowledge Toggle */} - - - {/* Social Links - small icons next to name */} - {set.artists[0]?.spotify_url && - getSocialPlatformLogo(set.artists[0]?.spotify_url) && ( - - )} - {set.artists[0]?.soundcloud_url && - getSocialPlatformLogo(set.artists[0]?.soundcloud_url) && ( - - )} -
- - {set.artists - ?.flatMap((a) => a.artist_music_genres || []) - .filter( - (genre, index, self) => - self.findIndex( - (g) => g.music_genre_id === genre.music_genre_id, - ) === index, - ) - .map((genre) => ( - - ))} - - {/* Stage and Time Information */} -
- {set.stages?.name && ( -
- - {set.stages.name} -
- )} - {formatTimeRange(set.time_start, set.time_end, use24Hour) && ( -
- - - {formatTimeRange(set.time_start, set.time_end, use24Hour)} - -
- )} -
-
-
- - {set.description && ( - - {set.description} - - )} -
- - - {/* Voting System */} -
-
- - {votingLoading && ( -
- )} -
-
- - {votingLoading && ( -
- )} -
-
- - {votingLoading && ( -
- )} -
-
- - - ); -}; diff --git a/src/components/Index/SetListItem.tsx b/src/components/Index/SetListItem.tsx deleted file mode 100644 index b851bdb..0000000 --- a/src/components/Index/SetListItem.tsx +++ /dev/null @@ -1,492 +0,0 @@ -import { Link } from "react-router-dom"; -import { Button } from "@/components/ui/button"; -import { Star, Heart, X, MapPin, Clock, Eye, EyeOff } from "lucide-react"; -import { useToast } from "@/hooks/use-toast"; -import { formatTimeRange } from "@/lib/timeUtils"; -import { User } from "@supabase/supabase-js"; -import { FestivalSet } from "@/services/queries"; -import { ArtistImageLoader } from "../ArtistImageLoader"; -import { GenreBadge } from "./GenreBadge"; - -interface SetListItemProps { - set: FestivalSet; - userVote?: number; - userKnowledge?: boolean; - votingLoading?: boolean; - onVote: ( - setId: string, - voteType: number, - ) => Promise<{ requiresAuth: boolean }>; - onKnowledgeToggle: (setId: string) => Promise<{ requiresAuth: boolean }>; - onAuthRequired: () => void; - user?: User; - use24Hour?: boolean; -} - -export function SetListItem({ - set, - userVote, - userKnowledge, - votingLoading, - onVote, - onKnowledgeToggle, - onAuthRequired, - use24Hour = false, -}: SetListItemProps) { - const { toast } = useToast(); - - const handleVote = async (voteType: number) => { - const result = await onVote(set.id, voteType); - if (result.requiresAuth) { - onAuthRequired(); - } - }; - - const handleKnowledgeToggle = async () => { - const result = await onKnowledgeToggle(set.id); - if (result.requiresAuth) { - onAuthRequired(); - } else { - // Show toast notification - const newKnowledgeState = !userKnowledge; - toast({ - title: `${set.name} is ${newKnowledgeState ? "known" : "unknown"}`, - duration: 2000, - }); - } - }; - - const getVoteCount = (voteType: number) => { - return set.votes.filter((vote) => vote.vote_type === voteType).length; - }; - - const getSocialPlatformLogo = (url: string) => { - if (url.includes("spotify.com")) { - return { - logo: "https://storage.googleapis.com/pr-newsroom-wp/1/2023/05/Spotify_Primary_Logo_RGB_Green.png", - platform: "Spotify", - color: "text-green-400 hover:text-green-300", - }; - } - if (url.includes("soundcloud.com")) { - return { - logo: "https://d21buns5ku92am.cloudfront.net/26628/documents/54546-1717072325-sc-logo-cloud-black-7412d7.svg", - platform: "SoundCloud", - color: "text-orange-400 hover:text-orange-300", - }; - } - return null; - }; - - return ( -
- {/* Mobile Layout (sm and below) */} -
- {/* Top Row: Image + Basic Info */} -
- - - -
-
-

- {set.name} -

- - {/* Knowledge Toggle */} - - - {/* Social Links - small icons next to name */} - {set.artists[0]?.spotify_url && - getSocialPlatformLogo(set.artists[0]?.spotify_url) && ( - - )} - {set.artists[0]?.soundcloud_url && - getSocialPlatformLogo(set.artists[0]?.soundcloud_url) && ( - - )} -
- -
- {set.artists - ?.flatMap((a) => a.artist_music_genres || []) - .filter( - (genre, index, self) => - self.findIndex( - (g) => g.music_genre_id === genre.music_genre_id, - ) === index, - ) - .map((genre) => ( - - ))} - {set.stages?.name && ( -
- - {set.stages.name} -
- )} - {formatTimeRange(set.time_start, set.time_end, use24Hour) && ( -
- - - {formatTimeRange(set.time_start, set.time_end, use24Hour)} - -
- )} -
-
-
- - {/* Description */} - {set.description && ( -

- {set.description} -

- )} - - {/* Voting Buttons */} -
- - - - {votingLoading && ( -
- )} -
-
- - {/* Desktop Layout (md and above) */} -
- {/* Artist Image - clickable for details */} - - - - - {/* Main Content */} -
-
-
-
-

- {set.name} -

- - {/* Knowledge Toggle */} - - - {/* Social Links - small icons next to name */} - {set.artists[0]?.spotify_url && - getSocialPlatformLogo(set.artists[0]?.spotify_url) && ( - - )} - {set.artists[0]?.soundcloud_url && - getSocialPlatformLogo(set.artists[0]?.soundcloud_url) && ( - - )} -
- -
- {set.artists - ?.flatMap((a) => a.artist_music_genres || []) - .filter( - (genre, index, self) => - self.findIndex( - (g) => g.music_genre_id === genre.music_genre_id, - ) === index, - ) - .map((genre) => ( - - ))} - {set.stages?.name && ( -
- - {set.stages.name} -
- )} - {formatTimeRange(set.time_start, set.time_end, use24Hour) && ( -
- - - {formatTimeRange(set.time_start, set.time_end, use24Hour)} - -
- )} -
-
-
- - {set.description && ( -

- {set.description} -

- )} -
- - {/* Actions */} -
- {/* Voting System */} - - - - {votingLoading && ( -
- )} -
-
-
- ); -} diff --git a/src/components/Index/SetsPanel.tsx b/src/components/Index/SetsPanel.tsx index c0c5a57..d7d2399 100644 --- a/src/components/Index/SetsPanel.tsx +++ b/src/components/Index/SetsPanel.tsx @@ -3,32 +3,31 @@ import { User } from "@supabase/supabase-js"; import { useOfflineVoting } from "@/hooks/useOfflineVoting"; import { FestivalSet } from "@/services/queries"; -import { SetCard } from "./SetCard"; -import { SetListItem } from "./SetListItem"; import { EmptyArtistsState } from "./EmptyArtistsState"; +import { FestivalSetProvider } from "./FestivalSetContext"; +import { SingleArtistSetListItem } from "./SingleArtistSetListItem"; +import { MultiArtistSetListItem } from "./MultiArtistSetListItem"; export function SetsPanel({ sets, - isGrid, user, use24Hour, openAuthDialog, onLockSort, }: { sets: Array; - isGrid: boolean; user: User | null; use24Hour: boolean; openAuthDialog(): void; onLockSort: () => void; }) { - const handleVoteWithLock = async (setId: string, voteType: number) => { + 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, @@ -39,35 +38,10 @@ export function SetsPanel({ return ; } - if (isGrid) { - return ( -
- {sets.map((set) => ( - ({ - requiresAuth: !user, - })} - onAuthRequired={openAuthDialog} - use24Hour={use24Hour} - /> - ))} -
- ); - } - return (
{sets.map((set) => ( - + > + {set.artists.length > 1 ? ( + + ) : ( + + )} + ))}
); diff --git a/src/components/Index/SingleArtistSetCard.tsx b/src/components/Index/SingleArtistSetCard.tsx new file mode 100644 index 0000000..217a9f9 --- /dev/null +++ b/src/components/Index/SingleArtistSetCard.tsx @@ -0,0 +1,29 @@ +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { SetHeader } from "./shared/SetHeader"; +import { SetImage } from "./shared/SetImage"; +import { SetMetadata } from "./shared/SetMetadata"; +import { SetDescription } from "./shared/SetDescription"; +import { SetVotingButtons } from "./shared/SetVotingButtons"; + +export function SingleArtistSetCard() { + return ( + + + + +
+
+ + +
+
+ + +
+ + + + +
+ ); +} diff --git a/src/components/Index/SingleArtistSetListItem.tsx b/src/components/Index/SingleArtistSetListItem.tsx new file mode 100644 index 0000000..626a790 --- /dev/null +++ b/src/components/Index/SingleArtistSetListItem.tsx @@ -0,0 +1,51 @@ +import { SetHeader } from "./shared/SetHeader"; +import { SetImage } from "./shared/SetImage"; +import { SetMetadata } from "./shared/SetMetadata"; +import { SetDescription } from "./shared/SetDescription"; +import { SetVotingButtons } from "./shared/SetVotingButtons"; + +export function SingleArtistSetListItem() { + return ( +
+ {/* Mobile Layout (sm and below) */} +
+
+ +
+ + + +
+
+ + + + +
+ + {/* Desktop Layout (md and above) */} +
+ + +
+
+
+ + + +
+
+ + +
+ +
+ +
+
+
+ ); +} diff --git a/src/components/Index/shared/MultiArtistInfo.tsx b/src/components/Index/shared/MultiArtistInfo.tsx new file mode 100644 index 0000000..018df8a --- /dev/null +++ b/src/components/Index/shared/MultiArtistInfo.tsx @@ -0,0 +1,29 @@ +import { SocialPlatformLinkList } from "./SocialPlatformLinkList"; + +interface Artist { + id: string; + name: string; + spotify_url?: string | null; + soundcloud_url?: string | null; +} + +interface MultiArtistInfoProps { + artists: Artist[]; + size?: "sm" | "md"; +} + +export function MultiArtistInfo({ + artists, + size = "md", +}: MultiArtistInfoProps) { + return ( +
+ {artists.map((artist) => ( +
+ {artist.name} + +
+ ))} +
+ ); +} diff --git a/src/components/Index/shared/SetDescription.tsx b/src/components/Index/shared/SetDescription.tsx new file mode 100644 index 0000000..a128b26 --- /dev/null +++ b/src/components/Index/shared/SetDescription.tsx @@ -0,0 +1,35 @@ +import { CardDescription } from "@/components/ui/card"; +import { MultiArtistInfo } from "./MultiArtistInfo"; +import { useFestivalSet } from "../FestivalSetContext"; + +interface SetDescriptionProps { + className?: string; +} + +export function SetDescription({ + className = "text-purple-200 text-sm leading-relaxed", +}: SetDescriptionProps) { + const { set, isMultiArtist } = useFestivalSet(); + + if (isMultiArtist) { + return ( + +
+
{set.description}
+
+ Artists:{" "} + +
+
+
+ ); + } + + if (set.description) { + return ( + {set.description} + ); + } + + return null; +} diff --git a/src/components/Index/shared/SetHeader.tsx b/src/components/Index/shared/SetHeader.tsx new file mode 100644 index 0000000..ba6e82d --- /dev/null +++ b/src/components/Index/shared/SetHeader.tsx @@ -0,0 +1,74 @@ +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { CardTitle } from "@/components/ui/card"; +import { Eye, EyeOff, Users } from "lucide-react"; +import { SocialPlatformLinkList } from "./SocialPlatformLinkList"; +import { useFestivalSet } from "../FestivalSetContext"; + +interface SetHeaderProps { + size?: "sm" | "lg"; +} + +export function SetHeader({ size = "lg" }: SetHeaderProps) { + const { + set, + userKnowledge, + onKnowledgeToggle, + onAuthRequired, + isMultiArtist, + } = useFestivalSet(); + + async function handleKnowledgeToggle() { + const result = await onKnowledgeToggle(set.id); + if (result.requiresAuth) { + onAuthRequired(); + } + } + const titleClass = + size === "sm" + ? "text-white text-lg font-semibold truncate" + : "text-white text-xl"; + + return ( +
+ {set.name} + + {isMultiArtist && ( + + + {set.artists.length} + + )} + + {/* Knowledge Toggle */} + + + {/* Social Platform Links */} + {!isMultiArtist && set.artists.length > 0 && ( + + )} +
+ ); +} diff --git a/src/components/Index/shared/SetImage.tsx b/src/components/Index/shared/SetImage.tsx new file mode 100644 index 0000000..0b2850b --- /dev/null +++ b/src/components/Index/shared/SetImage.tsx @@ -0,0 +1,39 @@ +import { Link } from "react-router-dom"; +import { ArtistImageLoader } from "@/components/ArtistImageLoader"; +import { MixedArtistImage } from "@/components/SetDetail/MixedArtistImage"; +import { useFestivalSet } from "../FestivalSetContext"; + +interface SetImageProps { + className?: string; + size?: "sm" | "md" | "lg"; +} + +export function SetImage({ className = "", size = "lg" }: SetImageProps) { + const { set, isMultiArtist } = useFestivalSet(); + + const sizeClasses = { + sm: "w-12 h-12", + md: "w-16 h-16", + lg: "aspect-square w-full mb-4", + }; + + const containerClass = `${sizeClasses[size]} ${className} overflow-hidden rounded-lg hover:opacity-90 transition-opacity cursor-pointer`; + + return ( + + {isMultiArtist ? ( + + ) : ( + + )} + + ); +} diff --git a/src/components/Index/shared/SetMetadata.tsx b/src/components/Index/shared/SetMetadata.tsx new file mode 100644 index 0000000..bf8e592 --- /dev/null +++ b/src/components/Index/shared/SetMetadata.tsx @@ -0,0 +1,54 @@ +import { MapPin, Clock } from "lucide-react"; +import { formatTimeRange } from "@/lib/timeUtils"; +import { GenreBadge } from "../GenreBadge"; +import { useFestivalSet } from "../FestivalSetContext"; + +export function SetMetadata() { + const { set, use24Hour } = useFestivalSet(); + const uniqueGenres = set.artists + ?.flatMap((a) => a.artist_music_genres || []) + .filter( + (genre, index, self) => + self.findIndex((g) => g.music_genre_id === genre.music_genre_id) === + index, + ); + + const timeRangeFormatted = formatTimeRange( + set.time_start, + set.time_end, + use24Hour, + ); + + return ( +
+ {/* Genres */} + {uniqueGenres.length > 0 && ( +
+ {uniqueGenres?.map((genre) => ( + + ))} +
+ )} + + {/* Stage and Time Information */} +
+ {set.stages?.name && ( +
+ + {set.stages.name} +
+ )} + {timeRangeFormatted && ( +
+ + {timeRangeFormatted} +
+ )} +
+
+ ); +} diff --git a/src/components/Index/shared/SetVotingButtons.tsx b/src/components/Index/shared/SetVotingButtons.tsx new file mode 100644 index 0000000..4e9983e --- /dev/null +++ b/src/components/Index/shared/SetVotingButtons.tsx @@ -0,0 +1,100 @@ +import { Button } from "@/components/ui/button"; +import { Star, Heart, X } from "lucide-react"; +import { useFestivalSet } from "../FestivalSetContext"; + +interface SetVotingButtonsProps { + size?: "sm" | "default"; + layout?: "horizontal" | "vertical"; +} + +export function SetVotingButtons({ + size = "default", + layout = "vertical", +}: SetVotingButtonsProps) { + const { set, userVote, votingLoading, onVote, onAuthRequired, getVoteCount } = + useFestivalSet(); + + const handleVote = async (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"; + + return ( +
+
+ + {votingLoading && ( +
+ )} +
+ +
+ + {votingLoading && ( +
+ )} +
+ +
+ + {votingLoading && ( +
+ )} +
+
+ ); +} diff --git a/src/components/Index/shared/SocialPlatformLink.tsx b/src/components/Index/shared/SocialPlatformLink.tsx new file mode 100644 index 0000000..71e0061 --- /dev/null +++ b/src/components/Index/shared/SocialPlatformLink.tsx @@ -0,0 +1,41 @@ +import { Button } from "@/components/ui/button"; +import { getSocialPlatformLogo } from "./SocialPlatformUtils"; + +interface SocialPlatformLinkProps { + url: string; + artistName: string; + size?: "sm" | "md"; +} + +export function SocialPlatformLink({ + url, + artistName, + size = "md", +}: SocialPlatformLinkProps) { + const platformInfo = getSocialPlatformLogo(url); + + if (!platformInfo) { + return null; + } + + const buttonSize = size === "sm" ? "h-5 w-5" : "h-6 w-6"; + const imageSize = size === "sm" ? "h-2.5 w-2.5" : "h-3 w-3"; + + return ( + + ); +} diff --git a/src/components/Index/shared/SocialPlatformLinkList.tsx b/src/components/Index/shared/SocialPlatformLinkList.tsx new file mode 100644 index 0000000..12dae31 --- /dev/null +++ b/src/components/Index/shared/SocialPlatformLinkList.tsx @@ -0,0 +1,37 @@ +import { SocialPlatformLink } from "./SocialPlatformLink"; + +interface Artist { + id: string; + name: string; + spotify_url?: string | null; + soundcloud_url?: string | null; +} + +interface SocialPlatformLinkListProps { + artist: Artist; + size?: "sm" | "md"; +} + +export function SocialPlatformLinkList({ + artist, + size = "md", +}: SocialPlatformLinkListProps) { + return ( +
+ {artist.spotify_url && ( + + )} + {artist.soundcloud_url && ( + + )} +
+ ); +} diff --git a/src/components/Index/shared/SocialPlatformUtils.tsx b/src/components/Index/shared/SocialPlatformUtils.tsx new file mode 100644 index 0000000..9d8cffd --- /dev/null +++ b/src/components/Index/shared/SocialPlatformUtils.tsx @@ -0,0 +1,19 @@ +import socialPlatformLogos from "./social-platform-logos.json"; + +export function getSocialPlatformLogo(url: string) { + if (url.includes("spotify.com")) { + return { + logo: socialPlatformLogos.spotify.logo, + platform: "Spotify", + color: "text-green-400 hover:text-green-300", + }; + } + if (url.includes("soundcloud.com")) { + return { + logo: socialPlatformLogos.soundcloud.logo, + platform: "SoundCloud", + color: "text-orange-400 hover:text-orange-300", + }; + } + return null; +} diff --git a/src/components/Index/shared/social-platform-logos.json b/src/components/Index/shared/social-platform-logos.json new file mode 100644 index 0000000..0bae194 --- /dev/null +++ b/src/components/Index/shared/social-platform-logos.json @@ -0,0 +1,8 @@ +{ + "spotify": { + "logo": "https://storage.googleapis.com/pr-newsroom-wp/1/2023/05/Spotify_Primary_Logo_RGB_Green.png" + }, + "soundcloud": { + "logo": "https://d21buns5ku92am.cloudfront.net/26628/documents/54546-1717072325-sc-logo-cloud-black-7412d7.svg" + } +} diff --git a/src/components/SetDetail/IndividualArtistCard.tsx b/src/components/SetDetail/IndividualArtistCard.tsx new file mode 100644 index 0000000..282cbec --- /dev/null +++ b/src/components/SetDetail/IndividualArtistCard.tsx @@ -0,0 +1,106 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { ExternalLink, Music, Play } from "lucide-react"; +import { Artist } from "@/services/queries"; +import { GenreBadge } from "../Index/GenreBadge"; +import { ArtistImageLoader } from "../ArtistImageLoader"; + +interface IndividualArtistCardProps { + artist: Artist; + showFullDetails?: boolean; +} + +export function IndividualArtistCard({ + artist, + showFullDetails = false, +}: IndividualArtistCardProps) { + return ( + + {/* Artist Image */} +
+ +
+ + + + {artist.name} + + + {/* Genres */} +
+ {artist.artist_music_genres + ?.slice(0, showFullDetails ? undefined : 2) + .map((genre) => ( + + ))} + {!showFullDetails && + artist.artist_music_genres && + artist.artist_music_genres.length > 2 && ( + + +{artist.artist_music_genres.length - 2} + + )} +
+
+ + + {/* Description (only if showFullDetails) */} + {showFullDetails && artist.description && ( +

+ {artist.description} +

+ )} + + {/* External Links */} +
+ {artist.spotify_url && ( + + )} + {artist.soundcloud_url && ( + + )} +
+
+
+ ); +} diff --git a/src/components/SetDetail/MixedArtistImage.tsx b/src/components/SetDetail/MixedArtistImage.tsx new file mode 100644 index 0000000..529cf9a --- /dev/null +++ b/src/components/SetDetail/MixedArtistImage.tsx @@ -0,0 +1,149 @@ +import { Artist } from "@/services/queries"; +import { ArtistImageLoader } from "../ArtistImageLoader"; + +interface MixedArtistImageProps { + artists: Artist[]; + setName: string; + className?: string; +} + +export function MixedArtistImage({ + artists, + setName, + className = "", +}: MixedArtistImageProps) { + const artistsWithImages = artists.filter((artist) => artist.image_url); + + // If no images available, show placeholder + if (artistsWithImages.length === 0) { + return ( +
+
+
🎵
+ {/*
{setName}
*/} +
+
+ ); + } + + // Single image - just show it + if (artistsWithImages.length === 1) { + return ( + + ); + } + + // Two images - split vertically + if (artistsWithImages.length === 2) { + return ( +
+
+
+ +
+
+ +
+
+ {/* Overlay with set name */} + {/*
+
+
+ {setName} +
+
+
*/} +
+ ); + } + + // Three images - one large, two small + if (artistsWithImages.length === 3) { + return ( +
+
+ {/* Main image takes left 2/3 */} +
+ +
+ {/* Two smaller images stack on right 1/3 */} +
+
+ +
+
+ +
+
+
+ {/* Overlay with set name */} + {/*
+
+
+ {setName} +
+
+
*/} +
+ ); + } + + // Four or more images - 2x2 grid with overflow indicator + return ( +
+
+ {artistsWithImages.slice(0, 4).map((artist, index) => ( +
+ + {/* Show count overlay on last image if more than 4 */} + {index === 3 && artistsWithImages.length > 4 && ( +
+
+ +{artistsWithImages.length - 4} +
+
+ )} +
+ ))} +
+ {/* Overlay with set name */} + {/*
+
+
+ {setName} +
+
+
*/} +
+ ); +} diff --git a/src/components/SetDetail/MultiArtistSetInfoCard.tsx b/src/components/SetDetail/MultiArtistSetInfoCard.tsx new file mode 100644 index 0000000..2fe9e4b --- /dev/null +++ b/src/components/SetDetail/MultiArtistSetInfoCard.tsx @@ -0,0 +1,139 @@ +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Clock, MapPin, Users } from "lucide-react"; +import { ArtistVotingButtons } from "./SetVotingButtons"; +import { FestivalSet } from "@/services/queries"; +import { formatTimeRange } from "@/lib/timeUtils"; +import { GenreBadge } from "../Index/GenreBadge"; +import { IndividualArtistCard } from "./IndividualArtistCard"; + +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( + (artist) => artist.artist_music_genres || [], + ); + const uniqueGenres = allGenres.filter( + (genre, index, self) => + index === + self.findIndex((g) => g.music_genre_id === genre.music_genre_id), + ); + + return ( +
+ {/* Main Set Info Card */} + + +
+
+ + {set.name} + + + {/* Set Summary */} +
+
+ + + {set.artists.length} Artists + +
+
+ + {/* Genres */} +
+ {uniqueGenres.map((genre) => ( + + ))} + {netVoteScore !== 0 && ( + 0 + ? "border-green-400 text-green-400" + : "border-red-400 text-red-400" + }`} + > + Score: {netVoteScore > 0 ? "+" : ""} + {netVoteScore} + + )} +
+ + {/* Performance Information */} +
+ {set.stages?.name && ( +
+ + {set.stages.name} +
+ )} + {formatTimeRange(set.time_start, set.time_end, use24Hour) && ( +
+ + + {formatTimeRange(set.time_start, set.time_end, use24Hour)} + +
+ )} +
+
+
+ {set.description && ( + + {set.description} + + )} +
+ + {/* Voting System */} + + +
+ + {/* Individual Artist Cards */} +
+

+ + Artists in this Set +

+
+ {set.artists.map((artist) => ( + + ))} +
+
+
+ ); +} diff --git a/src/components/SetDetail/SetInfoCard.tsx b/src/components/SetDetail/SetInfoCard.tsx index 4f1f501..459a5c3 100644 --- a/src/components/SetDetail/SetInfoCard.tsx +++ b/src/components/SetDetail/SetInfoCard.tsx @@ -7,14 +7,14 @@ import { } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; -import { ExternalLink, Music, Play, Clock, MapPin } from "lucide-react"; +import { Clock, MapPin, ExternalLink, Music, Play } from "lucide-react"; import { ArtistVotingButtons } from "./SetVotingButtons"; -import { Artist } from "@/services/queries"; +import { FestivalSet } from "@/services/queries"; import { formatTimeRange } from "@/lib/timeUtils"; import { GenreBadge } from "../Index/GenreBadge"; -interface ArtistInfoCardProps { - artist: Artist; +interface SetInfoCardProps { + set: FestivalSet; userVote: number | null; netVoteScore: number; onVote: (voteType: number) => void; @@ -22,14 +22,15 @@ interface ArtistInfoCardProps { use24Hour?: boolean; } -export const ArtistInfoCard = ({ - artist, +export function SetInfoCard({ + set, userVote, netVoteScore, onVote, getVoteCount, use24Hour = false, -}: ArtistInfoCardProps) => { +}: SetInfoCardProps) { + const artist = set.artists[0]; return (
@@ -37,7 +38,7 @@ export const ArtistInfoCard = ({
- {artist.name} + {set.name}
{artist.artist_music_genres?.map((genre) => ( @@ -63,34 +64,26 @@ export const ArtistInfoCard = ({ {/* Performance Information */}
- {artist.stage && ( + {set.stages?.name && (
- {artist.stage} + {set.stages.name}
)} - {formatTimeRange( - artist.time_start, - artist.time_end, - use24Hour, - ) && ( + {formatTimeRange(set.time_start, set.time_end, use24Hour) && (
- {formatTimeRange( - artist.time_start, - artist.time_end, - use24Hour, - )} + {formatTimeRange(set.time_start, set.time_end, use24Hour)}
)}
- {artist.description && ( + {(set.description || artist.description) && ( - {artist.description} + {set.description || artist.description} )} @@ -137,4 +130,4 @@ export const ArtistInfoCard = ({
); -}; +} diff --git a/src/components/SetDetail/SetVotingButtons.tsx b/src/components/SetDetail/SetVotingButtons.tsx index 128da05..8089bce 100644 --- a/src/components/SetDetail/SetVotingButtons.tsx +++ b/src/components/SetDetail/SetVotingButtons.tsx @@ -7,11 +7,11 @@ interface ArtistVotingButtonsProps { getVoteCount: (voteType: number) => number; } -export const ArtistVotingButtons = ({ +export function ArtistVotingButtons({ userVote, onVote, getVoteCount, -}: ArtistVotingButtonsProps) => { +}: ArtistVotingButtonsProps) { return (
); -}; +} diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index dc3b01d..b42ae32 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -48,7 +48,7 @@ const CardDescription = React.forwardRef< HTMLParagraphElement, React.HTMLAttributes >(({ className, ...props }, ref) => ( -

{ + async function handleVoteAction(artistId: string, voteType: number) { const result = await handleVote(artistId, voteType); if (result.requiresAuth) { setShowAuthDialog(true); } - }; + } return (

@@ -137,7 +134,6 @@ export default function EditionView() { {urlState.mainView === "list" && ( setShowAuthDialog(true)} diff --git a/src/pages/SetDetails.tsx b/src/pages/SetDetails.tsx index 8b7c675..c20b3dc 100644 --- a/src/pages/SetDetails.tsx +++ b/src/pages/SetDetails.tsx @@ -1,7 +1,9 @@ import { useParams } from "react-router-dom"; import { AppHeader } from "@/components/AppHeader"; import { ArtistImageCard } from "@/components/SetDetail/SetImageCard"; -import { ArtistInfoCard } from "@/components/SetDetail/SetInfoCard"; +import { MixedArtistImage } from "@/components/SetDetail/MixedArtistImage"; +import { SetInfoCard } from "@/components/SetDetail/SetInfoCard"; +import { MultiArtistSetInfoCard } from "@/components/SetDetail/MultiArtistSetInfoCard"; import { ArtistNotFoundState } from "@/components/SetDetail/SetNotFoundState"; import { ArtistLoadingState } from "@/components/SetDetail/SetLoadingState"; import { SetGroupVoting } from "@/components/SetDetail/SetGroupVoting"; @@ -31,36 +33,58 @@ export function SetDetails() { return ; } - const artist = currentSet.artists[0]; + const isMultiArtistSet = currentSet.artists.length > 1; + const primaryArtist = currentSet.artists[0]; return (
- {/* Artist Header */} -
- + {/* Set Header */} + {isMultiArtistSet ? ( +
+ {/* Mixed Image for Multi-Artist Sets */} + - -
+ +
+ ) : ( +
+ {/* Single Artist Image */} + + + +
+ )} - {/* Artist Group Voting Section */} + {/* Set Group Voting Section */}
- {/* Artist Notes Section */} + {/* Set Notes Section */}