From 26b4895695de294f81f3eb6222c3e9a5f35518d6 Mon Sep 17 00:00:00 2001 From: bnair123 Date: Tue, 30 Dec 2025 10:46:13 +0400 Subject: [PATCH] feat(frontend): add Archives, update Playlists with composition, add Navbar --- frontend/src/App.jsx | 10 +- frontend/src/components/Archives.jsx | 209 +++++++++++++++++++ frontend/src/components/Dashboard.jsx | 22 +- frontend/src/components/Navbar.jsx | 61 ++++++ frontend/src/components/PlaylistsSection.jsx | 5 + frontend/src/components/TrackList.jsx | 61 ++++++ 6 files changed, 347 insertions(+), 21 deletions(-) create mode 100644 frontend/src/components/Archives.jsx create mode 100644 frontend/src/components/Navbar.jsx create mode 100644 frontend/src/components/TrackList.jsx diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 8a68555..7a3eef7 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,8 +1,16 @@ +import React from 'react'; +import { BrowserRouter, Routes, Route } from 'react-router-dom'; import Dashboard from './components/Dashboard'; +import Archives from './components/Archives'; function App() { return ( - + + + } /> + } /> + + ); } diff --git a/frontend/src/components/Archives.jsx b/frontend/src/components/Archives.jsx new file mode 100644 index 0000000..7b3a7b2 --- /dev/null +++ b/frontend/src/components/Archives.jsx @@ -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 ( + <> + + +
+
+ + <HistoryOutlined className="text-primary" /> + Archives + + + {snapshots.length} Snapshot{snapshots.length !== 1 && 's'} Found + +
+ + {loading ? ( +
+ +
+ ) : snapshots.length === 0 ? ( +
+ No archives found yet.} /> +
+ ) : ( +
+ {snapshots.map((snap) => { + const narrative = safeParse(snap.narrative_report); + const metrics = safeParse(snap.metrics_payload); + const date = new Date(snap.created_at); + + return ( + handleSnapshotClick(snap)} + > +
+ } color="blue"> + {date.toLocaleDateString(undefined, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' })} + + + {date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })} + +
+ +
+
+ Vibe + + {metrics?.vibe || 'Unknown Vibe'} + +
+ +
+ Musical Era +
+ + {metrics?.era?.musical_age || 'N/A'} + +
+
+ +
+ +
+
+
+ ); + })} +
+ )} + + + + + Snapshot: {selectedSnapshot && new Date(selectedSnapshot.created_at).toLocaleDateString()} + + + } + 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 && ( +
+ {/* Reuse components but pass the specific snapshot data */} + + + + + {/* Playlist Compositions if available */} + {selectedSnapshot.playlist_composition && ( +
+
+ + Archived Playlists +
+ + {/* We need to parse playlist composition if it's stored as JSON string */} + {(() => { + const playlists = safeParse(selectedSnapshot.playlist_composition); + if (!playlists) return No playlist data archived.; + + return ( +
+ {playlists.six_hour && ( + Short & Sweet (6h)}> +
+ Theme + {playlists.six_hour.theme} + {playlists.six_hour.reasoning} +
+ +
+ )} + + {playlists.daily && ( + Daily Devotion}> +
+ Strategy + Daily Mix +
+ +
+ )} +
+ ); + })()} +
+ )} +
+ )} +
+
+ + ); +}; + +export default Archives; diff --git a/frontend/src/components/Dashboard.jsx b/frontend/src/components/Dashboard.jsx index f422cd8..5045ba3 100644 --- a/frontend/src/components/Dashboard.jsx +++ b/frontend/src/components/Dashboard.jsx @@ -7,6 +7,7 @@ import VibeRadar from './VibeRadar'; import HeatMap from './HeatMap'; import TopRotation from './TopRotation'; import ListeningLog from './ListeningLog'; +import Navbar from './Navbar'; import { Spin } from 'antd'; const API_BASE_URL = '/api'; @@ -80,26 +81,7 @@ const Dashboard = () => { return ( <> -
-
-
-
- equalizer -
-

SonicStats

-
-
- -
-
-
-
+ fetchData(true)} />
diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx new file mode 100644 index 0000000..aa9cdde --- /dev/null +++ b/frontend/src/components/Navbar.jsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { NavLink } from 'react-router-dom'; + +const Navbar = ({ onRefresh, showRefresh = true }) => { + return ( +
+
+
+ +
+ equalizer +
+

SonicStats

+
+ + +
+ +
+ {showRefresh && ( + + )} +
+
+
+
+ ); +}; + +export default Navbar; diff --git a/frontend/src/components/PlaylistsSection.jsx b/frontend/src/components/PlaylistsSection.jsx index 60f94ac..0f078cb 100644 --- a/frontend/src/components/PlaylistsSection.jsx +++ b/frontend/src/components/PlaylistsSection.jsx @@ -9,6 +9,7 @@ import { CustomerServiceOutlined } from '@ant-design/icons'; import Tooltip from './Tooltip'; +import TrackList from './TrackList'; const { Title, Text, Paragraph } = Typography; @@ -86,6 +87,8 @@ const PlaylistsSection = () => { "{playlists?.six_hour?.reasoning || 'Analyzing your recent listening patterns to find the perfect vibe.'}" + +
@@ -132,6 +135,8 @@ const PlaylistsSection = () => { A blend of 30 all-time favorites and 20 recent discoveries to keep your rotation fresh but familiar. + +
diff --git a/frontend/src/components/TrackList.jsx b/frontend/src/components/TrackList.jsx new file mode 100644 index 0000000..a535f91 --- /dev/null +++ b/frontend/src/components/TrackList.jsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { Button, Typography, Tooltip as AntTooltip, Avatar } from 'antd'; +import { + PlayCircleOutlined, + HistoryOutlined, + CustomerServiceOutlined, + RobotOutlined +} from '@ant-design/icons'; + +const { Text } = Typography; + +const TrackList = ({ tracks, maxHeight = "max-h-60" }) => { + if (!tracks || tracks.length === 0) return
No tracks available
; + + return ( +
+ {tracks.map((track, idx) => ( +
+
+ } /> +
+ {track.source === 'recommendation' ? ( + +
+ +
+
+ ) : ( + +
+ +
+
+ )} +
+
+
+ + {track.name} + + + {track.artist} + +
+
+
+
+ ))} +
+ ); +}; + +export default TrackList;