diff --git a/src/components/ArtistsColumn.tsx b/src/components/ArtistsColumn.tsx index df1e202..f4a2939 100644 --- a/src/components/ArtistsColumn.tsx +++ b/src/components/ArtistsColumn.tsx @@ -1,14 +1,22 @@ import RelistenAPI from '@/lib/RelistenAPI'; import { getServerFilters } from '@/lib/serverFilterCookies'; import ArtistsColumnWithControls from './ArtistsColumnWithControls'; +import { getServerFavorites } from '@/lib/serverFavoriteCookies'; const ArtistsColumn = async () => { - const [artists, initialFilters] = await Promise.all([ + const [artists, initialFilters, initialFavorites] = await Promise.all([ RelistenAPI.fetchArtists(), getServerFilters('root', true), + getServerFavorites(), ]); - return ; + return ( + + ); }; export default ArtistsColumn; diff --git a/src/components/ArtistsColumnWithControls.tsx b/src/components/ArtistsColumnWithControls.tsx index 2257752..c70c129 100644 --- a/src/components/ArtistsColumnWithControls.tsx +++ b/src/components/ArtistsColumnWithControls.tsx @@ -8,18 +8,31 @@ import { FilterState } from '@/lib/filterCookies'; import ColumnWithToggleControls from './ColumnWithToggleControls'; import Row from './Row'; import RowHeader from './RowHeader'; +import { useFavoriteState } from '@/hooks/useFavoriteState'; const byObject = { phish: 'Phish.in', }; +const artistGroups = { + 0: 'Bands', + 1: 'Featured', + 2: 'Favorites', +}; + type ArtistsColumnWithControlsProps = { artists: Artist[]; initialFilters?: FilterState; + initialFavorites: string[]; }; -const ArtistsColumnWithControls = ({ artists, initialFilters }: ArtistsColumnWithControlsProps) => { +const ArtistsColumnWithControls = ({ + artists, + initialFilters, + initialFavorites, +}: ArtistsColumnWithControlsProps) => { const { alphaAsc, toggleFilter, clearFilters } = useFilterState(initialFilters, 'root'); + const { favorites } = useFavoriteState(initialFavorites); const toggles = [ { @@ -31,24 +44,36 @@ const ArtistsColumnWithControls = ({ artists, initialFilters }: ArtistsColumnWit ]; const processedArtists = useMemo(() => { - const grouped = groupBy(artists, 'featured'); - const sortedGroups = Object.entries(grouped).sort(([a], [b]) => b.localeCompare(a)); - - return sortedGroups.map(([type, groupArtists]) => { - const sorted = [...groupArtists]; + try { + const favoritesGroup = artists.filter( + (artist) => artist.uuid && favorites.includes(artist.uuid) + ); - // Apply alphabetical sorting (default is desc/A-Z when no filter set) - if (alphaAsc) { - // Z-A (ascending) - sorted.sort((a, b) => (b.name || '').localeCompare(a.name || '')); - } else { - // Default: A-Z (descending) - sorted.sort((a, b) => (a.name || '').localeCompare(b.name || '')); + const grouped = groupBy(artists, 'featured'); + if (favoritesGroup.length) { + grouped[2] = favoritesGroup; } - return [type, sorted] as [string, Artist[]]; - }); - }, [artists, alphaAsc]); + const sortedGroups = Object.entries(grouped).sort(([a], [b]) => b.localeCompare(a)); + return sortedGroups.map(([type, groupArtists]) => { + const sorted = [...groupArtists]; + + // Apply alphabetical sorting (default is desc/A-Z when no filter set) + if (alphaAsc) { + // Z-A (ascending) + sorted.sort((a, b) => (b.name || '').localeCompare(a.name || '')); + } else { + // Default: A-Z (descending) + sorted.sort((a, b) => (a.name || '').localeCompare(b.name || '')); + } + + return [type, sorted] as [string, Artist[]]; + }); + } catch (error) { + console.error('Error processing artists:', error); + return []; + } + }, [artists, alphaAsc, favorites]); const totalArtistCount = artists.length; const filteredArtistCount = processedArtists.reduce( @@ -65,7 +90,7 @@ const ArtistsColumnWithControls = ({ artists, initialFilters }: ArtistsColumnWit onClearFilters={clearFilters} > {processedArtists.map(([type, groupArtists]) => [ - {type === '1' ? 'Featured' : 'Bands'}, + {artistGroups[type]}, ...groupArtists.map((artist: Artist, idx: number) => ( void; title: string; diff --git a/src/components/YearsColumn.tsx b/src/components/YearsColumn.tsx index b704a34..2f5713a 100644 --- a/src/components/YearsColumn.tsx +++ b/src/components/YearsColumn.tsx @@ -5,12 +5,14 @@ import { getServerFilters } from '@/lib/serverFilterCookies'; import YearsColumnWithControls from './YearsColumnWithControls'; import TodayInHistoryRow from './TodayInHistoryRow'; import RecentTapesRow from './RecentTapesRow'; +import { getServerFavorites } from '@/lib/serverFavoriteCookies'; const YearsColumn = async ({ artistSlug }: Pick) => { - const [artists, artistYears, initialFilters] = await Promise.all([ + const [artists, artistYears, initialFilters, initialFavorites] = await Promise.all([ RelistenAPI.fetchArtists(), RelistenAPI.fetchYears(artistSlug), getServerFilters(artistSlug || '', true), + getServerFavorites(), ]).catch(() => { notFound(); }); @@ -22,7 +24,9 @@ const YearsColumn = async ({ artistSlug }: Pick) => { artistSlug={artistSlug} artistName={artist?.name} artistYears={artistYears} + artistId={artist?.uuid} initialFilters={initialFilters} + initialFavorites={initialFavorites} > diff --git a/src/components/YearsColumnWithControls.tsx b/src/components/YearsColumnWithControls.tsx index 562f472..1995338 100644 --- a/src/components/YearsColumnWithControls.tsx +++ b/src/components/YearsColumnWithControls.tsx @@ -8,26 +8,35 @@ import sortActiveBands from '../lib/sortActiveBands'; import { simplePluralize } from '../lib/utils'; import ColumnWithToggleControls from './ColumnWithToggleControls'; import Row from './Row'; +import { Heart } from 'lucide-react'; +import { useFavoriteState } from '@/hooks/useFavoriteState'; +import cn from '@/lib/cn'; type YearsColumnWithControlsProps = { artistSlug?: string; artistName?: string; artistYears: Year[]; + artistId?: string; initialFilters?: FilterState; + initialFavorites: string[]; } & PropsWithChildren; const YearsColumnWithControls = ({ artistSlug, artistName, artistYears, + artistId, children, initialFilters, + initialFavorites, }: YearsColumnWithControlsProps) => { const { dateAsc, sbdOnly, toggleFilter, clearFilters } = useFilterState( initialFilters, artistSlug ); + const { toggleFavorite, isFavorite } = useFavoriteState(initialFavorites); + const toggles = [ { type: 'sort' as const, @@ -35,6 +44,13 @@ const YearsColumnWithControls = ({ onToggle: () => toggleFilter('date'), title: !dateAsc ? 'Newest First' : 'Oldest First', }, + { + type: 'favorite' as const, + isActive: isFavorite(artistId!), + onToggle: () => toggleFavorite(artistId!), + title: isFavorite(artistId!) ? 'Unfavorite' : 'Favorite', + icon: , + }, ]; const processedYears = useMemo(() => { diff --git a/src/hooks/useFavoriteState.ts b/src/hooks/useFavoriteState.ts new file mode 100644 index 0000000..3fb854f --- /dev/null +++ b/src/hooks/useFavoriteState.ts @@ -0,0 +1,60 @@ +'use client'; + +import { useCallback, useMemo } from 'react'; +import useCookie from 'react-use-cookie'; +import { useRouter } from 'next/navigation'; +import { FAVORITE_ARTIST_COOKIE_NAME } from '@/lib/constants'; + +export function useFavoriteState(initialFavorites: string[]) { + const router = useRouter(); + const defaultValue = initialFavorites ? JSON.stringify(initialFavorites) : '[]'; + const [_cookieValue, setCookieValue] = useCookie(FAVORITE_ARTIST_COOKIE_NAME, defaultValue); + + const favorites = useMemo(() => { + try { + return initialFavorites; + } catch { + return [] as string[]; + } + }, [initialFavorites]); + + const isFavorite = useCallback( + (artistId: string) => { + return favorites.includes(artistId); + }, + [favorites] + ); + + const setFavorites = useCallback( + (updatedFavorites: string[]) => { + setCookieValue(JSON.stringify(updatedFavorites), { + days: 3650, // 10 years + SameSite: 'Lax', + }); + + router.refresh(); + }, + [setCookieValue, router] + ); + + const toggleFavorite = useCallback( + (artistId: string) => { + const updatedFavorites = new Set([...initialFavorites]); + + if (updatedFavorites.has(artistId)) { + updatedFavorites.delete(artistId); + } else { + updatedFavorites.add(artistId); + } + + setFavorites([...updatedFavorites]); + }, + [favorites, setFavorites] + ); + + return { + favorites, + toggleFavorite, + isFavorite, + }; +} diff --git a/src/lib/constants.ts b/src/lib/constants.ts index cba9949..4accf9e 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1,2 +1,4 @@ // http://localhost:3823 export const API_DOMAIN = 'https://api.relisten.net'; + +export const FAVORITE_ARTIST_COOKIE_NAME = 'relisten_favorites:artists'; diff --git a/src/lib/serverFavoriteCookies.ts b/src/lib/serverFavoriteCookies.ts new file mode 100644 index 0000000..ea2374c --- /dev/null +++ b/src/lib/serverFavoriteCookies.ts @@ -0,0 +1,20 @@ +import { cookies } from 'next/headers'; +import { FAVORITE_ARTIST_COOKIE_NAME } from './constants'; + + + +// Server-side function to read filter cookies +export async function getServerFavorites(): Promise { + const cookieStore = await cookies(); + + try { + const value = cookieStore.get(FAVORITE_ARTIST_COOKIE_NAME)?.value; + if (value) { + return JSON.parse(value); + } + } catch (error) { + console.error('Error parsing favorites cookie on server:', error); + } + + return []; +}