feat: auth & admin

This commit is contained in:
2026-06-03 10:41:53 +03:00
parent 612d0f0125
commit 7dc59fb3c4
120 changed files with 4683 additions and 2159 deletions
+128 -29
View File
@@ -1,6 +1,9 @@
import { useParams, useNavigate } from 'react-router';
import { ScrollArea, IconButton, Button } from 'modern-sk';
import { useGetAlbumQuery, useGetAlbumTracksQuery } from '../../api/endpoints/library';
import {
useGetAlbumQuery,
useGetAlbumTracksQuery,
} from '../../api/endpoints/library';
import { TrackRow } from '../../components/track/TrackRow';
import { LoadingSkeleton } from '../../components/common/LoadingSkeleton';
import { ErrorState } from '../../components/common/ErrorState';
@@ -19,11 +22,20 @@ export function AlbumDetailPage() {
const tracksQuery = useGetAlbumTracksQuery(albumId ?? '', { skip: !albumId });
if (albumQuery.isLoading || tracksQuery.isLoading) {
return <div style={{ padding: '1.5rem' }}><LoadingSkeleton rows={10} /></div>;
return (
<div style={{ padding: '1.5rem' }}>
<LoadingSkeleton rows={10} />
</div>
);
}
if (albumQuery.isError) {
return <ErrorState message="Failed to load album" onRetry={() => albumQuery.refetch()} />;
return (
<ErrorState
message="Failed to load album"
onRetry={() => albumQuery.refetch()}
/>
);
}
const album = albumQuery.data;
@@ -32,52 +44,139 @@ export function AlbumDetailPage() {
const handlePlayAll = () => {
if (!tracks.length || !album) return;
dispatch(setQueue({
entries: tracks.map((t) => ({
trackId: t.id,
title: t.title,
artistName: t.artistName,
albumTitle: t.albumTitle,
durationMs: t.durationMs,
albumArtUrl: t.albumArtUrl,
})),
source: 'album',
sourceId: album.id,
sourceName: album.title,
}));
dispatch(
setQueue({
entries: tracks.map((t) => ({
trackId: t.id,
title: t.title,
artistName: t.artistName,
albumTitle: t.albumTitle,
durationMs: t.durationMs,
albumArtUrl: t.albumArtUrl,
})),
source: 'album',
sourceId: album.id,
sourceName: album.title,
}),
);
};
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
{/* header */}
<div style={{ padding: '1.25rem 1.5rem', borderBottom: '1px solid var(--color-border)', display: 'flex', alignItems: 'center', gap: '1rem', flexShrink: 0 }}>
<IconButton variant="ghost" size="sm" onClick={() => navigate(-1)} aria-label="Back"></IconButton>
<div style={{ display: 'flex', gap: '1.5rem', alignItems: 'flex-end', flex: 1 }}>
<div
style={{
padding: '1.25rem 1.5rem',
borderBottom: '1px solid var(--color-border)',
display: 'flex',
alignItems: 'center',
gap: '1rem',
flexShrink: 0,
}}
>
<IconButton
variant="ghost"
size="sm"
onClick={() => navigate(-1)}
aria-label="Back"
>
</IconButton>
<div
style={{
display: 'flex',
gap: '1.5rem',
alignItems: 'flex-end',
flex: 1,
}}
>
{artUrl ? (
<img src={artUrl} alt={album?.title} width={96} height={96} style={{ borderRadius: 8, objectFit: 'cover', flexShrink: 0 }} />
<img
src={artUrl}
alt={album?.title}
width={96}
height={96}
style={{ borderRadius: 8, objectFit: 'cover', flexShrink: 0 }}
/>
) : (
<div style={{ width: 96, height: 96, borderRadius: 8, background: 'var(--color-surface-3)', flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '2.5rem' }}>💿</div>
<div
style={{
width: 96,
height: 96,
borderRadius: 8,
background: 'var(--color-surface-3)',
flexShrink: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '2.5rem',
}}
>
💿
</div>
)}
<div>
<p style={{ margin: 0, fontSize: '0.75rem', color: 'var(--color-text-3)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Album</p>
<h1 style={{ margin: '0.25rem 0', fontSize: '1.5rem', fontWeight: 700 }}>{album?.title}</h1>
<p style={{ margin: 0, color: 'var(--color-text-2)', fontSize: '0.875rem' }}>
<p
style={{
margin: 0,
fontSize: '0.75rem',
color: 'var(--color-text-3)',
textTransform: 'uppercase',
letterSpacing: '0.05em',
}}
>
Album
</p>
<h1
style={{
margin: '0.25rem 0',
fontSize: '1.5rem',
fontWeight: 700,
}}
>
{album?.title}
</h1>
<p
style={{
margin: 0,
color: 'var(--color-text-2)',
fontSize: '0.875rem',
}}
>
{album?.artistName}
{album?.year && ` · ${album.year}`}
{album && ` · ${album.trackCount} tracks · ${formatDuration(album.totalDurationMs)}`}
{album &&
` · ${album.trackCount} tracks · ${formatDuration(album.totalDurationMs)}`}
</p>
</div>
</div>
<Button variant="primary" onClick={handlePlayAll} disabled={!tracks.length}> Play</Button>
<Button
variant="primary"
onClick={handlePlayAll}
disabled={!tracks.length}
>
Play
</Button>
</div>
{/* tracks */}
<ScrollArea style={{ flex: 1 }}>
{tracksQuery.isLoading && <LoadingSkeleton rows={10} />}
{tracksQuery.isError && <ErrorState message="Failed to load tracks" onRetry={() => tracksQuery.refetch()} />}
{!tracksQuery.isLoading && !tracksQuery.isError && tracks.length === 0 && (
<EmptyState icon="♫" title="No tracks" description="This album has no tracks." />
{tracksQuery.isError && (
<ErrorState
message="Failed to load tracks"
onRetry={() => tracksQuery.refetch()}
/>
)}
{!tracksQuery.isLoading &&
!tracksQuery.isError &&
tracks.length === 0 && (
<EmptyState
icon="♫"
title="No tracks"
description="This album has no tracks."
/>
)}
{tracks.map((track, i) => (
<TrackRow key={track.id} track={track} index={i} />
))}