From 3bda1f5fce194c6c853e913cf3773bad655f536b Mon Sep 17 00:00:00 2001 From: Kevin Mulcrone Date: Tue, 22 Jul 2025 06:46:22 -0600 Subject: [PATCH 1/6] feat(artists): mark as favorite --- src/components/ArtistsColumn.tsx | 12 +++- src/components/ArtistsColumnWithControls.tsx | 60 ++++++++++++++------ src/components/ColumnWithToggleControls.tsx | 2 +- src/components/YearsColumn.tsx | 5 +- src/components/YearsColumnWithControls.tsx | 16 ++++++ src/hooks/useFavoriteState.ts | 59 +++++++++++++++++++ src/lib/favoriteCookies.ts | 39 +++++++++++++ src/lib/serverFavoriteCookies.ts | 17 ++++++ 8 files changed, 189 insertions(+), 21 deletions(-) create mode 100644 src/hooks/useFavoriteState.ts create mode 100644 src/lib/favoriteCookies.ts create mode 100644 src/lib/serverFavoriteCookies.ts 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..83785f7 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,37 @@ 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]; + console.log('processing artists...') + 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 +91,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..e7b0b85 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(); }); @@ -23,6 +25,7 @@ const YearsColumn = async ({ artistSlug }: Pick) => { artistName={artist?.name} artistYears={artistYears} 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..6bbbca4 --- /dev/null +++ b/src/hooks/useFavoriteState.ts @@ -0,0 +1,59 @@ +'use client'; + +import { useCallback, useMemo } from 'react'; +import useCookie from 'react-use-cookie'; +import { useRouter } from 'next/navigation'; + +export function useFavoriteState(initialFavorites: string[]) { + const router = useRouter(); + const defaultValue = initialFavorites ? JSON.stringify(initialFavorites) : '[]'; + const [cookieValue, setCookieValue] = useCookie('relisten_favorites:artists', 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: 365, + SameSite: 'Lax', + }); + + router.refresh(); + }, + [setCookieValue, router] + ); + + const toggleFavorite = useCallback( + (artistId: string) => { + let updatedFavorites = [...favorites]; + + if (favorites.includes(artistId)) { + updatedFavorites = updatedFavorites.filter((favorite) => favorite !== artistId); + } else { + updatedFavorites.push(artistId); + } + + setFavorites([...updatedFavorites]); + }, + [favorites, setFavorites] + ); + + return { + favorites, + toggleFavorite, + isFavorite, + }; +} diff --git a/src/lib/favoriteCookies.ts b/src/lib/favoriteCookies.ts new file mode 100644 index 0000000..321b094 --- /dev/null +++ b/src/lib/favoriteCookies.ts @@ -0,0 +1,39 @@ +import { SORT_DIRECTION } from '@/hooks/useFilterState'; + +export type FilterState = { + date?: SORT_DIRECTION; + sbd?: boolean; + alpha?: SORT_DIRECTION; +}; + +export type FilterKey = string; // e.g., "/grateful-dead:filters" or "/grateful-dead/1977:filters" + +// Get filter key based on pathname +export function getFilterKey(pathname: string): FilterKey { + // Remove trailing slashes + const cleanPath = pathname.replace(/\/$/, ''); + + // For artist pages like /grateful-dead, /phish, etc. + const artistMatch = cleanPath.match(/^\/([a-z-]+)$/); + if (artistMatch) { + return `${cleanPath}:filters`; + } + + // For year pages like /grateful-dead/1977, /phish/1998, etc. + const yearMatch = cleanPath.match(/^\/([a-z-]+)\/\d{4}$/); + if (yearMatch) { + // Use the artist-level filters for year pages + return `/${yearMatch[1]}:filters`; + } + + // For show pages like /grateful-dead/1977/05/08, use artist-level filters + const showMatch = cleanPath.match(/^\/([a-z-]+)\/\d{4}\/\d{2}\/\d{2}$/); + if (showMatch) { + return `/${showMatch[1]}:filters`; + } + + // For other pages, use the full path + return `${cleanPath}:filters`; +} + +// Legacy functions - now using hook approach in useFilterState diff --git a/src/lib/serverFavoriteCookies.ts b/src/lib/serverFavoriteCookies.ts new file mode 100644 index 0000000..49e476f --- /dev/null +++ b/src/lib/serverFavoriteCookies.ts @@ -0,0 +1,17 @@ +import { cookies } from 'next/headers'; + +// Server-side function to read filter cookies +export async function getServerFavorites(): Promise { + const cookieStore = await cookies(); + + try { + const value = cookieStore.get('relisten_favorites:artists')?.value; + if (value) { + return JSON.parse(value); + } + } catch (error) { + console.error('Error parsing favorites cookie on server:', error); + } + + return []; +} From 2dd0860e6d820ef5ee82a84bad362d914411a9d8 Mon Sep 17 00:00:00 2001 From: Kevin Mulcrone Date: Tue, 22 Jul 2025 06:56:24 -0600 Subject: [PATCH 2/6] cleanup --- src/components/ArtistsColumnWithControls.tsx | 1 - src/lib/favoriteCookies.ts | 39 -------------------- 2 files changed, 40 deletions(-) delete mode 100644 src/lib/favoriteCookies.ts diff --git a/src/components/ArtistsColumnWithControls.tsx b/src/components/ArtistsColumnWithControls.tsx index 83785f7..c70c129 100644 --- a/src/components/ArtistsColumnWithControls.tsx +++ b/src/components/ArtistsColumnWithControls.tsx @@ -44,7 +44,6 @@ const ArtistsColumnWithControls = ({ ]; const processedArtists = useMemo(() => { - console.log('processing artists...') try { const favoritesGroup = artists.filter( (artist) => artist.uuid && favorites.includes(artist.uuid) diff --git a/src/lib/favoriteCookies.ts b/src/lib/favoriteCookies.ts deleted file mode 100644 index 321b094..0000000 --- a/src/lib/favoriteCookies.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { SORT_DIRECTION } from '@/hooks/useFilterState'; - -export type FilterState = { - date?: SORT_DIRECTION; - sbd?: boolean; - alpha?: SORT_DIRECTION; -}; - -export type FilterKey = string; // e.g., "/grateful-dead:filters" or "/grateful-dead/1977:filters" - -// Get filter key based on pathname -export function getFilterKey(pathname: string): FilterKey { - // Remove trailing slashes - const cleanPath = pathname.replace(/\/$/, ''); - - // For artist pages like /grateful-dead, /phish, etc. - const artistMatch = cleanPath.match(/^\/([a-z-]+)$/); - if (artistMatch) { - return `${cleanPath}:filters`; - } - - // For year pages like /grateful-dead/1977, /phish/1998, etc. - const yearMatch = cleanPath.match(/^\/([a-z-]+)\/\d{4}$/); - if (yearMatch) { - // Use the artist-level filters for year pages - return `/${yearMatch[1]}:filters`; - } - - // For show pages like /grateful-dead/1977/05/08, use artist-level filters - const showMatch = cleanPath.match(/^\/([a-z-]+)\/\d{4}\/\d{2}\/\d{2}$/); - if (showMatch) { - return `/${showMatch[1]}:filters`; - } - - // For other pages, use the full path - return `${cleanPath}:filters`; -} - -// Legacy functions - now using hook approach in useFilterState From 9ea05a6b3e03f884cd822774d6c2619747e4b2c5 Mon Sep 17 00:00:00 2001 From: Kevin Mulcrone Date: Tue, 22 Jul 2025 07:07:42 -0600 Subject: [PATCH 3/6] pass in artistId --- src/components/YearsColumn.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/YearsColumn.tsx b/src/components/YearsColumn.tsx index e7b0b85..2f5713a 100644 --- a/src/components/YearsColumn.tsx +++ b/src/components/YearsColumn.tsx @@ -24,6 +24,7 @@ const YearsColumn = async ({ artistSlug }: Pick) => { artistSlug={artistSlug} artistName={artist?.name} artistYears={artistYears} + artistId={artist?.uuid} initialFilters={initialFilters} initialFavorites={initialFavorites} > From aadfbe509b8620b3784d489a21720bef38b20aad Mon Sep 17 00:00:00 2001 From: Kevin Mulcrone Date: Tue, 22 Jul 2025 11:11:04 -0600 Subject: [PATCH 4/6] artist favorite const --- src/hooks/useFavoriteState.ts | 3 ++- src/lib/constants.ts | 2 ++ src/lib/serverFavoriteCookies.ts | 5 ++++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/hooks/useFavoriteState.ts b/src/hooks/useFavoriteState.ts index 6bbbca4..75150e2 100644 --- a/src/hooks/useFavoriteState.ts +++ b/src/hooks/useFavoriteState.ts @@ -3,11 +3,12 @@ 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('relisten_favorites:artists', defaultValue); + const [_cookieValue, setCookieValue] = useCookie(FAVORITE_ARTIST_COOKIE_NAME, defaultValue); const favorites = useMemo(() => { try { 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 index 49e476f..ea2374c 100644 --- a/src/lib/serverFavoriteCookies.ts +++ b/src/lib/serverFavoriteCookies.ts @@ -1,11 +1,14 @@ 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('relisten_favorites:artists')?.value; + const value = cookieStore.get(FAVORITE_ARTIST_COOKIE_NAME)?.value; if (value) { return JSON.parse(value); } From ecf1ecb87a6e36415ac69dea3c59556f9071f843 Mon Sep 17 00:00:00 2001 From: Kevin Mulcrone Date: Tue, 22 Jul 2025 11:13:11 -0600 Subject: [PATCH 5/6] 10 year cookie --- src/hooks/useFavoriteState.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useFavoriteState.ts b/src/hooks/useFavoriteState.ts index 75150e2..2b81d8e 100644 --- a/src/hooks/useFavoriteState.ts +++ b/src/hooks/useFavoriteState.ts @@ -28,7 +28,7 @@ export function useFavoriteState(initialFavorites: string[]) { const setFavorites = useCallback( (updatedFavorites: string[]) => { setCookieValue(JSON.stringify(updatedFavorites), { - days: 365, + days: 3650, // 10 years SameSite: 'Lax', }); From 3ca1a8984a4625a55ee9a79033efaec00399d8d1 Mon Sep 17 00:00:00 2001 From: Kevin Mulcrone Date: Tue, 5 Aug 2025 13:25:07 -0600 Subject: [PATCH 6/6] use set --- src/hooks/useFavoriteState.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/hooks/useFavoriteState.ts b/src/hooks/useFavoriteState.ts index 2b81d8e..3fb854f 100644 --- a/src/hooks/useFavoriteState.ts +++ b/src/hooks/useFavoriteState.ts @@ -39,12 +39,12 @@ export function useFavoriteState(initialFavorites: string[]) { const toggleFavorite = useCallback( (artistId: string) => { - let updatedFavorites = [...favorites]; + const updatedFavorites = new Set([...initialFavorites]); - if (favorites.includes(artistId)) { - updatedFavorites = updatedFavorites.filter((favorite) => favorite !== artistId); + if (updatedFavorites.has(artistId)) { + updatedFavorites.delete(artistId); } else { - updatedFavorites.push(artistId); + updatedFavorites.add(artistId); } setFavorites([...updatedFavorites]);