feat: add recent plays history modal
- GET /api/me/recent endpoint returning last 50 play events with track and artist info - RecentPlays modal component with time-ago display - "Recent plays" button in user dropdown menu - Clicking a track in history starts playback Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { SearchDropdown } from '../SearchDropdown'
|
||||
import { RecentPlays } from './RecentPlays'
|
||||
import styles from './header.module.css'
|
||||
|
||||
type SearchResultItem = {
|
||||
@@ -19,10 +20,11 @@ type HeaderProps = {
|
||||
searchOpen: boolean
|
||||
searchResults: SearchResultItem[]
|
||||
onSearchSelect: (type: string, slug: string) => void
|
||||
onPlayTrack: (slug: string) => void
|
||||
user: UserInfo
|
||||
}
|
||||
|
||||
function UserMenu({ user }: { user: UserInfo }) {
|
||||
function UserMenu({ user, onShowRecent }: { user: UserInfo; onShowRecent: () => void }) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
@@ -52,6 +54,9 @@ function UserMenu({ user }: { user: UserInfo }) {
|
||||
<div className={styles.userName}>{user.name ?? user.sub}</div>
|
||||
{user.email && <div className={styles.userEmail}>{user.email}</div>}
|
||||
</div>
|
||||
<button className={styles.userAction} onClick={() => { setOpen(false); onShowRecent() }}>
|
||||
Recent plays
|
||||
</button>
|
||||
<a href="/auth/logout" className={styles.userLogout}>Sign out</a>
|
||||
</div>
|
||||
)}
|
||||
@@ -63,31 +68,42 @@ export function Header({
|
||||
searchOpen,
|
||||
searchResults,
|
||||
onSearchSelect,
|
||||
onPlayTrack,
|
||||
user,
|
||||
}: HeaderProps) {
|
||||
const [showRecent, setShowRecent] = useState(false)
|
||||
|
||||
return (
|
||||
<header className={styles.header}>
|
||||
<div className={styles.headerLogo}>
|
||||
<button className="btn-menu">☰</button>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<circle cx="9" cy="18" r="3" />
|
||||
<circle cx="18" cy="15" r="3" />
|
||||
<path d="M12 18V6l9-3v3" />
|
||||
</svg>
|
||||
Furumi
|
||||
<span className={styles.headerVersion}>v</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
|
||||
<div className="search-wrap">
|
||||
<input id="searchInput" placeholder="Search..." />
|
||||
<SearchDropdown
|
||||
isOpen={searchOpen}
|
||||
results={searchResults}
|
||||
onSelect={onSearchSelect}
|
||||
/>
|
||||
<>
|
||||
<header className={styles.header}>
|
||||
<div className={styles.headerLogo}>
|
||||
<button className="btn-menu">☰</button>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<circle cx="9" cy="18" r="3" />
|
||||
<circle cx="18" cy="15" r="3" />
|
||||
<path d="M12 18V6l9-3v3" />
|
||||
</svg>
|
||||
Furumi
|
||||
<span className={styles.headerVersion}>v</span>
|
||||
</div>
|
||||
<UserMenu user={user} />
|
||||
</div>
|
||||
</header>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
|
||||
<div className="search-wrap">
|
||||
<input id="searchInput" placeholder="Search..." />
|
||||
<SearchDropdown
|
||||
isOpen={searchOpen}
|
||||
results={searchResults}
|
||||
onSelect={onSearchSelect}
|
||||
/>
|
||||
</div>
|
||||
<UserMenu user={user} onShowRecent={() => setShowRecent(true)} />
|
||||
</div>
|
||||
</header>
|
||||
{showRecent && (
|
||||
<RecentPlays
|
||||
onClose={() => setShowRecent(false)}
|
||||
onPlay={onPlayTrack}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user