Files
vynl/frontend/src/pages/Recommendations.tsx
root 5215e8c792 Add playlist export and SEO meta tags
Add text/CSV export endpoints for playlists and saved recommendations.
Add export buttons to PlaylistDetail and Recommendations pages.
Add Open Graph and Twitter meta tags to index.html for better SEO.
2026-03-31 20:49:07 -05:00

166 lines
5.6 KiB
TypeScript

import { useState, useEffect } from 'react'
import { Loader2, Clock, Heart, Sparkles, Download } from 'lucide-react'
import { useAuth } from '../lib/auth'
import { getRecommendationHistory, getSavedRecommendations, toggleSaveRecommendation, exportSaved, type RecommendationItem } from '../lib/api'
import RecommendationCard from '../components/RecommendationCard'
type Tab = 'saved' | 'history'
export default function Recommendations() {
const { user } = useAuth()
const [tab, setTab] = useState<Tab>('saved')
const [saved, setSaved] = useState<RecommendationItem[]>([])
const [history, setHistory] = useState<RecommendationItem[]>([])
const [loading, setLoading] = useState(true)
const [savingIds, setSavingIds] = useState<Set<string>>(new Set())
useEffect(() => {
const load = async () => {
try {
const [savedData, historyData] = await Promise.all([
getSavedRecommendations(),
getRecommendationHistory(),
])
setSaved(savedData)
setHistory(historyData)
} catch {
// silent
} finally {
setLoading(false)
}
}
load()
}, [])
const handleToggleSave = async (id: string) => {
setSavingIds((prev) => new Set(prev).add(id))
try {
const { saved: isSaved } = await toggleSaveRecommendation(id)
const updater = (items: RecommendationItem[]) =>
items.map((r) => (r.id === id ? { ...r, saved: isSaved } : r))
setSaved(updater)
setHistory(updater)
// If unsaved, remove from saved tab
if (!isSaved) {
setSaved((prev) => prev.filter((r) => r.id !== id))
}
// If saved, add to saved tab if not already there
if (isSaved) {
const item = history.find((r) => r.id === id)
if (item) {
setSaved((prev) =>
prev.some((r) => r.id === id) ? prev : [...prev, { ...item, saved: true }]
)
}
}
} catch {
// silent
} finally {
setSavingIds((prev) => {
const next = new Set(prev)
next.delete(id)
return next
})
}
}
const items = tab === 'saved' ? saved : history
if (loading) {
return (
<div className="flex items-center justify-center py-20">
<Loader2 className="w-8 h-8 text-purple animate-spin" />
</div>
)
}
return (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold text-charcoal">Recommendations</h1>
<p className="text-charcoal-muted mt-1">Your discovered music</p>
</div>
{/* Daily remaining */}
{!user?.is_pro && (
<div className="flex items-center gap-2 px-4 py-3 bg-amber-50 border border-amber-200 rounded-xl text-sm">
<Sparkles className="w-4 h-4 text-amber-500 flex-shrink-0" />
<span className="text-amber-700">
<strong>{user?.daily_recommendations_remaining ?? 0}</strong> recommendations remaining today (free tier)
</span>
</div>
)}
{/* Export + Tabs */}
<div className="flex items-center gap-4">
{tab === 'saved' && saved.length > 0 && (
<button
onClick={() => exportSaved()}
className="flex items-center gap-2 px-4 py-2.5 bg-purple-50 text-purple text-sm font-medium rounded-xl hover:bg-purple-100 transition-colors cursor-pointer border-none"
>
<Download className="w-4 h-4" />
Export Saved
</button>
)}
</div>
<div className="flex gap-1 bg-purple-50 p-1 rounded-xl w-fit">
<button
onClick={() => setTab('saved')}
className={`flex items-center gap-2 px-5 py-2.5 rounded-lg text-sm font-medium transition-colors cursor-pointer border-none ${
tab === 'saved'
? 'bg-white text-purple shadow-sm'
: 'bg-transparent text-charcoal-muted hover:text-charcoal'
}`}
>
<Heart className="w-4 h-4" />
Saved ({saved.length})
</button>
<button
onClick={() => setTab('history')}
className={`flex items-center gap-2 px-5 py-2.5 rounded-lg text-sm font-medium transition-colors cursor-pointer border-none ${
tab === 'history'
? 'bg-white text-purple shadow-sm'
: 'bg-transparent text-charcoal-muted hover:text-charcoal'
}`}
>
<Clock className="w-4 h-4" />
History ({history.length})
</button>
</div>
{/* List */}
{items.length === 0 ? (
<div className="bg-white rounded-2xl border border-purple-100 p-12 text-center">
<div className="w-16 h-16 rounded-2xl bg-purple-50 flex items-center justify-center mx-auto mb-4">
{tab === 'saved' ? (
<Heart className="w-8 h-8 text-purple" />
) : (
<Clock className="w-8 h-8 text-purple" />
)}
</div>
<h2 className="text-xl font-semibold text-charcoal mb-2">
{tab === 'saved' ? 'No saved recommendations' : 'No recommendations yet'}
</h2>
<p className="text-charcoal-muted max-w-md mx-auto">
{tab === 'saved'
? 'Tap the heart icon on recommendations to save them here'
: 'Head to the Discover page to get your first recommendations'}
</p>
</div>
) : (
<div className="space-y-3">
{items.map((rec) => (
<RecommendationCard
key={rec.id}
recommendation={rec}
onToggleSave={handleToggleSave}
saving={savingIds.has(rec.id)}
/>
))}
</div>
)}
</div>
)
}