Skip to content

Commit e852bd0

Browse files
authored
Merge pull request #87 from RelistenNet/filters-sort
First pass implementation of filters and sorting
2 parents 9b11c2a + 428715c commit e852bd0

24 files changed

+969
-272
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"react-dom": "19.1.0",
4141
"react-redux": "^8.1.2",
4242
"react-timeago": "^8.2.0",
43+
"react-use-cookie": "^1.6.1",
4344
"redux": "^4.2.1",
4445
"tailwind-merge": "^3.3.1",
4546
"thenby": "^1.3.4",

pnpm-lock.yaml

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app/(content)/today/page.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
import TodayTrack from '@/components/TodayTrack';
22
import RelistenAPI from '@/lib/RelistenAPI';
33
import { getCurrentMonthDay } from '@/lib/timezone';
4-
import { groupBy } from '@/lib/utils';
5-
import { Artist, Day } from '@/types';
4+
import { Day } from '@/types';
65

76
export default async function Page() {
87
const currentMonthDay = await getCurrentMonthDay();
98

109
const data = await RelistenAPI.fetchTodayShows(currentMonthDay.month, currentMonthDay.day);
1110

12-
const artists: Artist[] = data.map((day: Day) => ({
13-
...day,
14-
artistName: day.artist?.name,
15-
}));
16-
const groupedBy: Day[][] = groupBy(artists, 'artistName');
11+
// Group by artist name manually since artist is a nested property
12+
const groupedBy = data.reduce((acc, day) => {
13+
const artistName = day.artist?.name || 'Unknown Artist';
14+
if (!acc[artistName]) {
15+
acc[artistName] = [];
16+
}
17+
acc[artistName].push(day);
18+
return acc;
19+
}, {} as Record<string, Day[]>);
1720

1821
return (
1922
<div className="mx-auto w-full max-w-3xl flex-1 px-4 py-8">

src/app/error.tsx

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,22 @@ export default function Error({
1818
<div className="max-w-lg w-full mx-auto text-center">
1919
<div className="mb-8">
2020
<div className="w-20 h-20 mx-auto bg-red-100 rounded-full flex items-center justify-center mb-4">
21-
<svg className="w-10 h-10 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
22-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
21+
<svg
22+
className="w-10 h-10 text-red-600"
23+
fill="none"
24+
stroke="currentColor"
25+
viewBox="0 0 24 24"
26+
>
27+
<path
28+
strokeLinecap="round"
29+
strokeLinejoin="round"
30+
strokeWidth={2}
31+
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
32+
/>
2333
</svg>
2434
</div>
2535

26-
<h1 className="text-3xl font-semibold text-gray-900 mb-3">
27-
Oops! Something went wrong
28-
</h1>
36+
<h1 className="text-3xl font-semibold text-gray-900 mb-3">Oops! Something went wrong</h1>
2937

3038
<p className="text-lg text-foreground-muted mb-8">
3139
We encountered an error while loading this page. Don't worry, it's not your fault.
@@ -49,7 +57,7 @@ export default function Error({
4957
</button>
5058

5159
<button
52-
onClick={() => window.location.href = '/'}
60+
onClick={() => (window.location.href = '/')}
5361
className="bg-gray-100 text-gray-900 px-6 py-3 rounded-lg hover:bg-gray-200 transition-colors"
5462
>
5563
Go to homepage
@@ -64,22 +72,28 @@ export default function Error({
6472
</summary>
6573
<div className="space-y-2">
6674
<div>
67-
<span className="text-xs font-semibold text-gray-500 uppercase tracking-wide">Error Message:</span>
75+
<span className="text-xs font-semibold text-gray-500 uppercase tracking-wide">
76+
Error Message:
77+
</span>
6878
<p className="text-sm text-red-700 font-mono bg-red-50 p-2 rounded mt-1">
6979
{error.message}
7080
</p>
7181
</div>
7282
{error.digest && (
7383
<div>
74-
<span className="text-xs font-semibold text-gray-500 uppercase tracking-wide">Error Digest:</span>
84+
<span className="text-xs font-semibold text-gray-500 uppercase tracking-wide">
85+
Error Digest:
86+
</span>
7587
<p className="text-sm text-gray-700 font-mono bg-gray-50 p-2 rounded mt-1">
7688
{error.digest}
7789
</p>
7890
</div>
7991
)}
8092
{error.stack && (
8193
<div>
82-
<span className="text-xs font-semibold text-gray-500 uppercase tracking-wide">Stack Trace:</span>
94+
<span className="text-xs font-semibold text-gray-500 uppercase tracking-wide">
95+
Stack Trace:
96+
</span>
8397
<pre className="text-xs text-gray-700 bg-gray-50 p-3 rounded mt-1 overflow-auto max-h-40">
8498
{error.stack}
8599
</pre>
@@ -91,4 +105,4 @@ export default function Error({
91105
</div>
92106
</div>
93107
);
94-
}
108+
}

src/components/ArtistsColumn.tsx

Lines changed: 7 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,14 @@
11
import RelistenAPI from '@/lib/RelistenAPI';
2-
import { groupBy, simplePluralize } from '../lib/utils';
3-
import { Artist } from '../types';
4-
import Column from './Column';
5-
import Row from './Row';
6-
import RowHeader from './RowHeader';
7-
import { DEFAULT_ARTIST_SLUG } from '@/lib/defaultArtist';
8-
9-
const byObject = {
10-
phish: 'Phish.in',
11-
};
2+
import { getServerFilters } from '@/lib/serverFilterCookies';
3+
import ArtistsColumnWithControls from './ArtistsColumnWithControls';
124

135
const ArtistsColumn = async () => {
14-
const artists = await RelistenAPI.fetchArtists();
6+
const [artists, initialFilters] = await Promise.all([
7+
RelistenAPI.fetchArtists(),
8+
getServerFilters('root', true),
9+
]);
1510

16-
return (
17-
<Column heading="Bands">
18-
{artists &&
19-
Object.entries(groupBy(Object.values(artists), 'featured'))
20-
.sort(([a], [b]) => b.localeCompare(a))
21-
.map(([type, artists]: [string, Artist[]]) => [
22-
<RowHeader key={`header-${type}`}>{type === '1' ? 'Featured' : 'Bands'}</RowHeader>,
23-
...artists.map((artist: Artist, idx: number) => (
24-
<Row
25-
key={[idx, artist.id].join(':')}
26-
href={`/${artist.slug}`}
27-
activeSegments={{ artistSlug: artist.slug }}
28-
fallbackParams={{ artistSlug: DEFAULT_ARTIST_SLUG }}
29-
>
30-
<div>
31-
<div>{artist.name}</div>
32-
{byObject[String(artist.slug)] && (
33-
<span className="text-xs text-foreground-muted">
34-
Powered by {byObject[String(artist.slug)]}
35-
</span>
36-
)}
37-
</div>
38-
<div className="min-w-[20%] text-right text-xs text-foreground-muted">
39-
<div>{simplePluralize('show', artist.show_count)}</div>
40-
<div>{simplePluralize('tape', artist.source_count)}</div>
41-
</div>
42-
</Row>
43-
)),
44-
])}
45-
</Column>
46-
);
11+
return <ArtistsColumnWithControls artists={artists} initialFilters={initialFilters} />;
4712
};
4813

4914
export default ArtistsColumn;
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
'use client';
2+
3+
import { useMemo } from 'react';
4+
import { groupBy, simplePluralize } from '../lib/utils';
5+
import { Artist } from '../types';
6+
import { useFilterState } from '@/hooks/useFilterState';
7+
import { FilterState } from '@/lib/filterCookies';
8+
import ColumnWithToggleControls from './ColumnWithToggleControls';
9+
import Row from './Row';
10+
import RowHeader from './RowHeader';
11+
12+
const byObject = {
13+
phish: 'Phish.in',
14+
};
15+
16+
type ArtistsColumnWithControlsProps = {
17+
artists: Artist[];
18+
initialFilters?: FilterState;
19+
};
20+
21+
const ArtistsColumnWithControls = ({ artists, initialFilters }: ArtistsColumnWithControlsProps) => {
22+
const { alphaAsc, toggleFilter, clearFilters } = useFilterState(initialFilters, 'root');
23+
24+
const toggles = [
25+
{
26+
type: 'sort' as const,
27+
isActive: alphaAsc, // Show as active when Z-A (ascending)
28+
onToggle: () => toggleFilter('alpha'),
29+
title: alphaAsc ? 'Z-A' : 'A-Z',
30+
},
31+
];
32+
33+
const processedArtists = useMemo(() => {
34+
const grouped = groupBy(artists, 'featured');
35+
const sortedGroups = Object.entries(grouped).sort(([a], [b]) => b.localeCompare(a));
36+
37+
return sortedGroups.map(([type, groupArtists]) => {
38+
const sorted = [...groupArtists];
39+
40+
// Apply alphabetical sorting (default is desc/A-Z when no filter set)
41+
if (alphaAsc) {
42+
// Z-A (ascending)
43+
sorted.sort((a, b) => (b.name || '').localeCompare(a.name || ''));
44+
} else {
45+
// Default: A-Z (descending)
46+
sorted.sort((a, b) => (a.name || '').localeCompare(b.name || ''));
47+
}
48+
49+
return [type, sorted] as [string, Artist[]];
50+
});
51+
}, [artists, alphaAsc]);
52+
53+
const totalArtistCount = artists.length;
54+
const filteredArtistCount = processedArtists.reduce(
55+
(acc, [, groupArtists]) => acc + groupArtists.length,
56+
0
57+
);
58+
59+
return (
60+
<ColumnWithToggleControls
61+
heading="Bands"
62+
toggles={toggles}
63+
filteredCount={filteredArtistCount}
64+
totalCount={totalArtistCount}
65+
onClearFilters={clearFilters}
66+
>
67+
{processedArtists.map(([type, groupArtists]) => [
68+
<RowHeader key={`header-${type}`}>{type === '1' ? 'Featured' : 'Bands'}</RowHeader>,
69+
...groupArtists.map((artist: Artist, idx: number) => (
70+
<Row
71+
key={[idx, artist.id].join(':')}
72+
href={`/${artist.slug}`}
73+
activeSegments={{ artistSlug: artist.slug }}
74+
>
75+
<div>
76+
<div>{artist.name}</div>
77+
{byObject[String(artist.slug)] && (
78+
<span className="text-xs text-foreground-muted">
79+
Powered by {byObject[String(artist.slug)]}
80+
</span>
81+
)}
82+
</div>
83+
<div className="min-w-[20%] text-right text-xs text-foreground-muted">
84+
<div>{simplePluralize('show', artist.show_count)}</div>
85+
<div>{simplePluralize('tape', artist.source_count)}</div>
86+
</div>
87+
</Row>
88+
)),
89+
])}
90+
</ColumnWithToggleControls>
91+
);
92+
};
93+
94+
export default ArtistsColumnWithControls;

0 commit comments

Comments
 (0)