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 [];
+}