feat: add user support with play event tracking
Backend (Rust API): - Add users and play_events tables (migration 0005) - Extract full user identity from JWT (sub, username, email, name) and pass AuthUser via request extensions to all handlers - Auto-upsert user in background on every authenticated request - POST /api/tracks/:slug/play endpoint to record play events - Allow POST method in CORS Frontend (Node player): - Call recordPlay() when a track starts playing - Add user profile avatar with dropdown menu (name, email, sign out) - Pass user info from App through FurumiPlayer to Header Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { SearchDropdown } from '../SearchDropdown'
|
||||
import styles from './header.module.css'
|
||||
|
||||
@@ -8,16 +9,61 @@ type SearchResultItem = {
|
||||
detail?: string
|
||||
}
|
||||
|
||||
type UserInfo = {
|
||||
sub: string
|
||||
name?: string
|
||||
email?: string
|
||||
}
|
||||
|
||||
type HeaderProps = {
|
||||
searchOpen: boolean
|
||||
searchResults: SearchResultItem[]
|
||||
onSearchSelect: (type: string, slug: string) => void
|
||||
user: UserInfo
|
||||
}
|
||||
|
||||
function UserMenu({ user }: { user: UserInfo }) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
function handleClick(e: MouseEvent) {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
|
||||
}
|
||||
document.addEventListener('mousedown', handleClick)
|
||||
return () => document.removeEventListener('mousedown', handleClick)
|
||||
}, [])
|
||||
|
||||
const initials = (user.name ?? user.sub)
|
||||
.split(' ')
|
||||
.map((w) => w[0])
|
||||
.slice(0, 2)
|
||||
.join('')
|
||||
.toUpperCase()
|
||||
|
||||
return (
|
||||
<div className={styles.userMenu} ref={ref}>
|
||||
<button className={styles.userAvatar} onClick={() => setOpen(!open)} title={user.name ?? user.sub}>
|
||||
{initials}
|
||||
</button>
|
||||
{open && (
|
||||
<div className={styles.userDropdown}>
|
||||
<div className={styles.userInfo}>
|
||||
<div className={styles.userName}>{user.name ?? user.sub}</div>
|
||||
{user.email && <div className={styles.userEmail}>{user.email}</div>}
|
||||
</div>
|
||||
<a href="/auth/logout" className={styles.userLogout}>Sign out</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function Header({
|
||||
searchOpen,
|
||||
searchResults,
|
||||
onSearchSelect,
|
||||
user,
|
||||
}: HeaderProps) {
|
||||
return (
|
||||
<header className={styles.header}>
|
||||
@@ -40,6 +86,7 @@ export function Header({
|
||||
onSelect={onSearchSelect}
|
||||
/>
|
||||
</div>
|
||||
<UserMenu user={user} />
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user