mirror of
https://github.com/bnair123/MusicAnalyser.git
synced 2026-02-25 11:46:07 +00:00
feat(frontend): add Archives, update Playlists with composition, add Navbar
This commit is contained in:
209
frontend/src/components/Archives.jsx
Normal file
209
frontend/src/components/Archives.jsx
Normal file
@@ -0,0 +1,209 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
import { Card, Typography, Spin, Drawer, Empty, Tag, Button } from 'antd';
|
||||
import { CalendarOutlined, RightOutlined, HistoryOutlined, RobotOutlined } from '@ant-design/icons';
|
||||
import NarrativeSection from './NarrativeSection';
|
||||
import StatsGrid from './StatsGrid';
|
||||
import TrackList from './TrackList';
|
||||
import Navbar from './Navbar';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
const Archives = () => {
|
||||
const [snapshots, setSnapshots] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedSnapshot, setSelectedSnapshot] = useState(null);
|
||||
const [drawerVisible, setDrawerVisible] = useState(false);
|
||||
|
||||
const fetchSnapshots = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await axios.get('/api/snapshots');
|
||||
setSnapshots(response.data);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch snapshots:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchSnapshots();
|
||||
}, []);
|
||||
|
||||
const handleSnapshotClick = (snapshot) => {
|
||||
setSelectedSnapshot(snapshot);
|
||||
setDrawerVisible(true);
|
||||
};
|
||||
|
||||
const closeDrawer = () => {
|
||||
setDrawerVisible(false);
|
||||
setSelectedSnapshot(null);
|
||||
};
|
||||
|
||||
// Helper to safely parse JSON if it comes as string (though axios usually handles it)
|
||||
const safeParse = (data) => {
|
||||
if (typeof data === 'string') {
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar showRefresh={false} />
|
||||
|
||||
<main className="w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<Title level={2} className="text-white !mb-0 flex items-center gap-3">
|
||||
<HistoryOutlined className="text-primary" />
|
||||
Archives
|
||||
</Title>
|
||||
<Text className="text-slate-400">
|
||||
{snapshots.length} Snapshot{snapshots.length !== 1 && 's'} Found
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex justify-center p-12">
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
) : snapshots.length === 0 ? (
|
||||
<div className="glass-panel p-12 text-center rounded-xl border border-dashed border-slate-700">
|
||||
<Empty description={<span className="text-slate-400">No archives found yet.</span>} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{snapshots.map((snap) => {
|
||||
const narrative = safeParse(snap.narrative_report);
|
||||
const metrics = safeParse(snap.metrics_payload);
|
||||
const date = new Date(snap.created_at);
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={snap.id}
|
||||
hoverable
|
||||
className="bg-slate-800 border-slate-700 shadow-xl transition-all duration-300 hover:border-primary/50 group"
|
||||
onClick={() => handleSnapshotClick(snap)}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<Tag icon={<CalendarOutlined />} color="blue">
|
||||
{date.toLocaleDateString(undefined, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' })}
|
||||
</Tag>
|
||||
<Text className="text-slate-500 text-xs font-mono">
|
||||
{date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Text className="text-slate-400 text-xs uppercase tracking-wider block mb-1">Vibe</Text>
|
||||
<Title level={4} className="!mt-0 !mb-1 text-white truncate">
|
||||
{metrics?.vibe || 'Unknown Vibe'}
|
||||
</Title>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="text-slate-400 text-xs uppercase tracking-wider block mb-1">Musical Era</Text>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-primary font-bold">
|
||||
{metrics?.era?.musical_age || 'N/A'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t border-slate-700/50 flex justify-end">
|
||||
<Button type="text" className="text-slate-400 group-hover:text-primary flex items-center gap-1 pl-0">
|
||||
View Details <RightOutlined className="text-xs" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Drawer
|
||||
title={
|
||||
<div className="flex items-center gap-3">
|
||||
<CalendarOutlined className="text-primary" />
|
||||
<span className="text-white">
|
||||
Snapshot: {selectedSnapshot && new Date(selectedSnapshot.created_at).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
placement="right"
|
||||
width={800} // Wide drawer
|
||||
onClose={closeDrawer}
|
||||
open={drawerVisible}
|
||||
className="bg-[#0f172a] text-white"
|
||||
styles={{
|
||||
header: { background: '#1e293b', borderBottom: '1px solid #334155' },
|
||||
body: { background: '#0f172a', padding: '24px' },
|
||||
mask: { backdropFilter: 'blur(4px)' }
|
||||
}}
|
||||
>
|
||||
{selectedSnapshot && (
|
||||
<div className="space-y-8">
|
||||
{/* Reuse components but pass the specific snapshot data */}
|
||||
<NarrativeSection
|
||||
narrative={safeParse(selectedSnapshot.narrative_report)}
|
||||
vibe={safeParse(selectedSnapshot.metrics_payload)?.vibe}
|
||||
/>
|
||||
|
||||
<StatsGrid metrics={safeParse(selectedSnapshot.metrics_payload)} />
|
||||
|
||||
{/* Playlist Compositions if available */}
|
||||
{selectedSnapshot.playlist_composition && (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<HistoryOutlined className="text-blue-400 text-xl" />
|
||||
<Title level={3} className="!mb-0 text-white">Archived Playlists</Title>
|
||||
</div>
|
||||
|
||||
{/* We need to parse playlist composition if it's stored as JSON string */}
|
||||
{(() => {
|
||||
const playlists = safeParse(selectedSnapshot.playlist_composition);
|
||||
if (!playlists) return <Text className="text-slate-500">No playlist data archived.</Text>;
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-6">
|
||||
{playlists.six_hour && (
|
||||
<Card className="bg-slate-800 border-slate-700" title={<span className="text-blue-400">Short & Sweet (6h)</span>}>
|
||||
<div className="mb-4">
|
||||
<Text className="text-gray-400 text-xs uppercase tracking-wider block mb-1">Theme</Text>
|
||||
<Text className="text-white font-medium block">{playlists.six_hour.theme}</Text>
|
||||
<Paragraph className="text-gray-400 text-sm italic mt-1">{playlists.six_hour.reasoning}</Paragraph>
|
||||
</div>
|
||||
<TrackList tracks={playlists.six_hour.composition} />
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{playlists.daily && (
|
||||
<Card className="bg-slate-800 border-slate-700" title={<span className="text-purple-400">Daily Devotion</span>}>
|
||||
<div className="mb-4">
|
||||
<Text className="text-gray-400 text-xs uppercase tracking-wider block mb-1">Strategy</Text>
|
||||
<Text className="text-white font-medium block">Daily Mix</Text>
|
||||
</div>
|
||||
<TrackList tracks={playlists.daily.composition} />
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Drawer>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Archives;
|
||||
Reference in New Issue
Block a user