- Created SpotifyContext for account management * Add/remove Spotify accounts with email and display name * Persistent storage to localStorage * Automatic account activation when assigned to zone - Implemented zone-to-account mapping system * Each zone can be assigned a Spotify account * Multiple zones can share one account * Switching between sources preserves account assignment - Enhanced ZoneCard component: * Account selector dropdown when Spotify is selected * Display account name/email under zone name * Auto-select first account when switching to Spotify * Green-tinted account dropdown for visual distinction - Created SpotifyAccountManager component: * Add new Spotify accounts with email and display name * List all configured accounts * Remove accounts (cleans up zone mappings) * Collapsible form for adding new accounts * Glassmorphic styling with green accent - Updated Audio page: * New 'Accounts' tab for Spotify account management * Accessible alongside Zones, Radio, and Library tabs * Smooth tab transitions with animations - Architecture supports: * Real Spotify API integration (ready for OAuth) * Multiple accounts simultaneously * Spotify Connect per account (one instance per account) * Zone grouping with shared account control - Build: 70 modules, 1.25 MB (345 KB gzipped) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
181 lines
4.5 KiB
JavaScript
181 lines
4.5 KiB
JavaScript
import { useState } from 'react'
|
|
import { useSpotify } from '../../contexts/SpotifyContext.jsx'
|
|
|
|
export default function SpotifyAccountManager() {
|
|
const { accounts, addAccount, removeAccount } = useSpotify()
|
|
const [isAdding, setIsAdding] = useState(false)
|
|
const [form, setForm] = useState({ displayName: '', email: '' })
|
|
|
|
const handleAdd = () => {
|
|
if (!form.email) return
|
|
|
|
addAccount({
|
|
id: `spotify-${Date.now()}`,
|
|
email: form.email,
|
|
displayName: form.displayName || form.email.split('@')[0],
|
|
addedAt: new Date().toISOString(),
|
|
})
|
|
|
|
setForm({ displayName: '', email: '' })
|
|
setIsAdding(false)
|
|
}
|
|
|
|
return (
|
|
<div style={styles.container}>
|
|
<div style={styles.header}>
|
|
<span style={styles.title}>🎵 Spotify Accounts</span>
|
|
<button
|
|
className="icon ghost"
|
|
onClick={() => setIsAdding(!isAdding)}
|
|
style={{ fontSize: 18 }}
|
|
title="Add account"
|
|
>
|
|
+
|
|
</button>
|
|
</div>
|
|
|
|
{isAdding && (
|
|
<div style={styles.form}>
|
|
<input
|
|
type="text"
|
|
placeholder="Display name (optional)"
|
|
value={form.displayName}
|
|
onChange={e => setForm({ ...form, displayName: e.target.value })}
|
|
style={styles.input}
|
|
/>
|
|
<input
|
|
type="email"
|
|
placeholder="Email"
|
|
value={form.email}
|
|
onChange={e => setForm({ ...form, email: e.target.value })}
|
|
style={styles.input}
|
|
/>
|
|
<div style={styles.formButtons}>
|
|
<button
|
|
className="primary"
|
|
onClick={handleAdd}
|
|
style={{ flex: 1, fontSize: 12 }}
|
|
>
|
|
Add
|
|
</button>
|
|
<button
|
|
className="ghost"
|
|
onClick={() => setIsAdding(false)}
|
|
style={{ flex: 1, fontSize: 12 }}
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div style={styles.list}>
|
|
{accounts.length === 0 ? (
|
|
<div style={styles.empty}>No Spotify accounts</div>
|
|
) : (
|
|
accounts.map(account => (
|
|
<div key={account.id} style={styles.item}>
|
|
<div style={styles.itemInfo}>
|
|
<div style={styles.itemName}>{account.displayName}</div>
|
|
<div style={styles.itemEmail}>{account.email}</div>
|
|
</div>
|
|
<button
|
|
className="icon ghost"
|
|
onClick={() => removeAccount(account.id)}
|
|
style={{ fontSize: 14, color: '#ef4444' }}
|
|
title="Remove account"
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const styles = {
|
|
container: {
|
|
background: 'var(--glass-bg)',
|
|
backdropFilter: 'blur(var(--glass-blur))',
|
|
WebkitBackdropFilter: 'blur(var(--glass-blur))',
|
|
border: '1px solid rgba(255, 255, 255, 0.1)',
|
|
borderRadius: 'var(--radius-lg)',
|
|
padding: 14,
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: 10,
|
|
},
|
|
header: {
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
paddingBottom: 8,
|
|
borderBottom: '1px solid rgba(255, 255, 255, 0.1)',
|
|
},
|
|
title: {
|
|
fontSize: 12,
|
|
fontWeight: 700,
|
|
color: 'var(--text)',
|
|
},
|
|
form: {
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: 8,
|
|
padding: 10,
|
|
background: 'rgba(29, 185, 84, 0.08)',
|
|
borderRadius: 'var(--radius)',
|
|
border: '1px solid rgba(29, 185, 84, 0.2)',
|
|
},
|
|
input: {
|
|
padding: '8px 10px',
|
|
background: 'rgba(255, 255, 255, 0.05)',
|
|
border: '1px solid rgba(255, 255, 255, 0.1)',
|
|
borderRadius: 'var(--radius)',
|
|
color: 'var(--text)',
|
|
fontSize: 12,
|
|
fontFamily: 'var(--font-ui)',
|
|
},
|
|
formButtons: {
|
|
display: 'flex',
|
|
gap: 8,
|
|
},
|
|
list: {
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: 6,
|
|
maxHeight: 300,
|
|
overflowY: 'auto',
|
|
},
|
|
empty: {
|
|
fontSize: 11,
|
|
color: 'var(--muted)',
|
|
textAlign: 'center',
|
|
padding: 12,
|
|
},
|
|
item: {
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
padding: 8,
|
|
background: 'rgba(29, 185, 84, 0.08)',
|
|
border: '1px solid rgba(29, 185, 84, 0.15)',
|
|
borderRadius: 'var(--radius)',
|
|
},
|
|
itemInfo: {
|
|
flex: 1,
|
|
minWidth: 0,
|
|
},
|
|
itemName: {
|
|
fontSize: 12,
|
|
fontWeight: 600,
|
|
color: 'var(--text)',
|
|
},
|
|
itemEmail: {
|
|
fontSize: 10,
|
|
color: 'var(--muted)',
|
|
marginTop: 2,
|
|
},
|
|
}
|