Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"react-dom": "19.1.0",
"react-redux": "^8.1.2",
"react-timeago": "^8.2.0",
"react-use-cookie": "^1.6.1",
"redux": "^4.2.1",
"tailwind-merge": "^3.3.1",
"thenby": "^1.3.4",
Expand Down
13 changes: 13 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 10 additions & 7 deletions src/app/(content)/today/page.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import TodayTrack from '@/components/TodayTrack';
import RelistenAPI from '@/lib/RelistenAPI';
import { getCurrentMonthDay } from '@/lib/timezone';
import { groupBy } from '@/lib/utils';
import { Artist, Day } from '@/types';
import { Day } from '@/types';

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

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

const artists: Artist[] = data.map((day: Day) => ({
...day,
artistName: day.artist?.name,
}));
const groupedBy: Day[][] = groupBy(artists, 'artistName');
// Group by artist name manually since artist is a nested property
const groupedBy = data.reduce((acc, day) => {
const artistName = day.artist?.name || 'Unknown Artist';
if (!acc[artistName]) {
acc[artistName] = [];
}
acc[artistName].push(day);
return acc;
}, {} as Record<string, Day[]>);

return (
<div className="mx-auto w-full max-w-3xl flex-1 px-4 py-8">
Expand Down
34 changes: 24 additions & 10 deletions src/app/error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,22 @@ export default function Error({
<div className="max-w-lg w-full mx-auto text-center">
<div className="mb-8">
<div className="w-20 h-20 mx-auto bg-red-100 rounded-full flex items-center justify-center mb-4">
<svg className="w-10 h-10 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
<svg
className="w-10 h-10 text-red-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>

<h1 className="text-3xl font-semibold text-gray-900 mb-3">
Oops! Something went wrong
</h1>
<h1 className="text-3xl font-semibold text-gray-900 mb-3">Oops! Something went wrong</h1>

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

<button
onClick={() => window.location.href = '/'}
onClick={() => (window.location.href = '/')}
className="bg-gray-100 text-gray-900 px-6 py-3 rounded-lg hover:bg-gray-200 transition-colors"
>
Go to homepage
Expand All @@ -64,22 +72,28 @@ export default function Error({
</summary>
<div className="space-y-2">
<div>
<span className="text-xs font-semibold text-gray-500 uppercase tracking-wide">Error Message:</span>
<span className="text-xs font-semibold text-gray-500 uppercase tracking-wide">
Error Message:
</span>
<p className="text-sm text-red-700 font-mono bg-red-50 p-2 rounded mt-1">
{error.message}
</p>
</div>
{error.digest && (
<div>
<span className="text-xs font-semibold text-gray-500 uppercase tracking-wide">Error Digest:</span>
<span className="text-xs font-semibold text-gray-500 uppercase tracking-wide">
Error Digest:
</span>
<p className="text-sm text-gray-700 font-mono bg-gray-50 p-2 rounded mt-1">
{error.digest}
</p>
</div>
)}
{error.stack && (
<div>
<span className="text-xs font-semibold text-gray-500 uppercase tracking-wide">Stack Trace:</span>
<span className="text-xs font-semibold text-gray-500 uppercase tracking-wide">
Stack Trace:
</span>
<pre className="text-xs text-gray-700 bg-gray-50 p-3 rounded mt-1 overflow-auto max-h-40">
{error.stack}
</pre>
Expand All @@ -91,4 +105,4 @@ export default function Error({
</div>
</div>
);
}
}
49 changes: 7 additions & 42 deletions src/components/ArtistsColumn.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,14 @@
import RelistenAPI from '@/lib/RelistenAPI';
import { groupBy, simplePluralize } from '../lib/utils';
import { Artist } from '../types';
import Column from './Column';
import Row from './Row';
import RowHeader from './RowHeader';
import { DEFAULT_ARTIST_SLUG } from '@/lib/defaultArtist';

const byObject = {
phish: 'Phish.in',
};
import { getServerFilters } from '@/lib/serverFilterCookies';
import ArtistsColumnWithControls from './ArtistsColumnWithControls';

const ArtistsColumn = async () => {
const artists = await RelistenAPI.fetchArtists();
const [artists, initialFilters] = await Promise.all([
RelistenAPI.fetchArtists(),
getServerFilters('root', true),
]);

return (
<Column heading="Bands">
{artists &&
Object.entries(groupBy(Object.values(artists), 'featured'))
.sort(([a], [b]) => b.localeCompare(a))
.map(([type, artists]: [string, Artist[]]) => [
<RowHeader key={`header-${type}`}>{type === '1' ? 'Featured' : 'Bands'}</RowHeader>,
...artists.map((artist: Artist, idx: number) => (
<Row
key={[idx, artist.id].join(':')}
href={`/${artist.slug}`}
activeSegments={{ artistSlug: artist.slug }}
fallbackParams={{ artistSlug: DEFAULT_ARTIST_SLUG }}
>
<div>
<div>{artist.name}</div>
{byObject[String(artist.slug)] && (
<span className="text-xs text-foreground-muted">
Powered by {byObject[String(artist.slug)]}
</span>
)}
</div>
<div className="min-w-[20%] text-right text-xs text-foreground-muted">
<div>{simplePluralize('show', artist.show_count)}</div>
<div>{simplePluralize('tape', artist.source_count)}</div>
</div>
</Row>
)),
])}
</Column>
);
return <ArtistsColumnWithControls artists={artists} initialFilters={initialFilters} />;
};

export default ArtistsColumn;
94 changes: 94 additions & 0 deletions src/components/ArtistsColumnWithControls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
'use client';

import { useMemo } from 'react';
import { groupBy, simplePluralize } from '../lib/utils';
import { Artist } from '../types';
import { useFilterState } from '@/hooks/useFilterState';
import { FilterState } from '@/lib/filterCookies';
import ColumnWithToggleControls from './ColumnWithToggleControls';
import Row from './Row';
import RowHeader from './RowHeader';

const byObject = {
phish: 'Phish.in',
};

type ArtistsColumnWithControlsProps = {
artists: Artist[];
initialFilters?: FilterState;
};

const ArtistsColumnWithControls = ({ artists, initialFilters }: ArtistsColumnWithControlsProps) => {
const { alphaAsc, toggleFilter, clearFilters } = useFilterState(initialFilters, 'root');

const toggles = [
{
type: 'sort' as const,
isActive: alphaAsc, // Show as active when Z-A (ascending)
onToggle: () => toggleFilter('alpha'),
title: alphaAsc ? 'Z-A' : 'A-Z',
},
];

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];

// 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[]];
});
}, [artists, alphaAsc]);

const totalArtistCount = artists.length;
const filteredArtistCount = processedArtists.reduce(
(acc, [, groupArtists]) => acc + groupArtists.length,
0
);

return (
<ColumnWithToggleControls
heading="Bands"
toggles={toggles}
filteredCount={filteredArtistCount}
totalCount={totalArtistCount}
onClearFilters={clearFilters}
>
{processedArtists.map(([type, groupArtists]) => [
<RowHeader key={`header-${type}`}>{type === '1' ? 'Featured' : 'Bands'}</RowHeader>,
...groupArtists.map((artist: Artist, idx: number) => (
<Row
key={[idx, artist.id].join(':')}
href={`/${artist.slug}`}
activeSegments={{ artistSlug: artist.slug }}
>
<div>
<div>{artist.name}</div>
{byObject[String(artist.slug)] && (
<span className="text-xs text-foreground-muted">
Powered by {byObject[String(artist.slug)]}
</span>
)}
</div>
<div className="min-w-[20%] text-right text-xs text-foreground-muted">
<div>{simplePluralize('show', artist.show_count)}</div>
<div>{simplePluralize('tape', artist.source_count)}</div>
</div>
</Row>
)),
])}
</ColumnWithToggleControls>
);
};

export default ArtistsColumnWithControls;
Loading