Replace all hardcoded #c8ff00 / rgba(200,255,0,...) with CSS custom properties (--accent, --accent-rgb) so the accent color updates live. Add themeStore (Zustand + localStorage) with 6 presets (Лайм, Синий, Розовый, Фиолет, Оранж, Минт). Add ThemeApplier component that syncs CSS vars on load. Add color picker UI section in ExtraTab. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
137 lines
5.5 KiB
TypeScript
137 lines
5.5 KiB
TypeScript
'use client'
|
||
|
||
import { useState, useRef, useCallback } from 'react'
|
||
import { usePartyStore } from '@/store/partyStore'
|
||
|
||
export default function QueueCard() {
|
||
const { queue, curIdx, setCurIdx, generateMix, reorderQueue } = usePartyStore()
|
||
const [open, setOpen] = useState(false)
|
||
const dragSrcIdx = useRef<number | null>(null)
|
||
|
||
const handleQueueClick = (e: React.MouseEvent, idx: number) => {
|
||
if ((e.target as HTMLElement).closest('.drag-handle')) return
|
||
setCurIdx(idx)
|
||
}
|
||
|
||
const onDragStart = useCallback((idx: number, el: HTMLElement) => {
|
||
dragSrcIdx.current = idx
|
||
el.classList.add('dragging')
|
||
}, [])
|
||
|
||
const onDragEnd = useCallback((el: HTMLElement) => {
|
||
el.classList.remove('dragging')
|
||
document.querySelectorAll('.q-item').forEach((i) => i.classList.remove('drag-over'))
|
||
}, [])
|
||
|
||
const onDragOver = useCallback((e: React.DragEvent, el: HTMLElement) => {
|
||
e.preventDefault()
|
||
document.querySelectorAll('.q-item').forEach((i) => i.classList.remove('drag-over'))
|
||
el.classList.add('drag-over')
|
||
}, [])
|
||
|
||
const onDrop = useCallback(
|
||
(e: React.DragEvent, tgtIdx: number, el: HTMLElement) => {
|
||
e.preventDefault()
|
||
el.classList.remove('drag-over')
|
||
if (dragSrcIdx.current === null || dragSrcIdx.current === tgtIdx) return
|
||
reorderQueue(dragSrcIdx.current, tgtIdx)
|
||
dragSrcIdx.current = null
|
||
},
|
||
[reorderQueue],
|
||
)
|
||
|
||
if (!queue.length) return null
|
||
|
||
return (
|
||
<div className="bg-surface border border-white/[0.07] rounded-app overflow-hidden mb-4">
|
||
<div
|
||
onClick={() => setOpen((o) => !o)}
|
||
className="flex items-center justify-between px-4 py-3 cursor-pointer hover:bg-surface2 transition-colors duration-100 select-none sm:px-3"
|
||
>
|
||
<div className="flex items-center gap-2">
|
||
<span className="font-display text-[11px] font-bold tracking-[1.2px] uppercase text-muted">
|
||
Очередь · {queue.length} треков
|
||
</span>
|
||
<button
|
||
onClick={(e) => { e.stopPropagation(); generateMix() }}
|
||
className="px-2.5 py-0.5 text-[11px] border border-white/[0.07] rounded-lg bg-transparent text-muted hover:bg-surface2 hover:text-app-text transition-all duration-150 cursor-pointer"
|
||
>
|
||
↺
|
||
</button>
|
||
</div>
|
||
<span className={`text-muted text-[11px] transition-transform duration-200 ${open ? 'rotate-180' : ''}`}>▼</span>
|
||
</div>
|
||
|
||
<div
|
||
className="overflow-hidden transition-all duration-300"
|
||
style={{ maxHeight: open ? '500px' : '0' }}
|
||
>
|
||
<div className="flex flex-col max-h-[500px] overflow-y-auto">
|
||
{queue.map((item, i) => {
|
||
const active = i === curIdx
|
||
return (
|
||
<div
|
||
key={i}
|
||
data-idx={i}
|
||
draggable
|
||
className="q-item flex items-center gap-2 px-4 py-2 border-b border-white/[0.07] last:border-b-0 cursor-pointer hover:bg-surface2 transition-colors duration-100 select-none relative sm:px-3"
|
||
style={{ background: active ? 'rgba(var(--accent-rgb),0.04)' : undefined }}
|
||
onClick={(e) => handleQueueClick(e, i)}
|
||
onDragStart={(e) => onDragStart(i, e.currentTarget)}
|
||
onDragEnd={(e) => onDragEnd(e.currentTarget)}
|
||
onDragOver={(e) => onDragOver(e, e.currentTarget)}
|
||
onDrop={(e) => onDrop(e, i, e.currentTarget)}
|
||
>
|
||
<div
|
||
className="drag-handle text-muted cursor-grab shrink-0 p-1 opacity-40 hover:opacity-80 transition-opacity duration-150 flex items-center touch-none"
|
||
title="Перетащить"
|
||
>
|
||
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
|
||
<circle cx="9" cy="5" r="1.5" />
|
||
<circle cx="9" cy="12" r="1.5" />
|
||
<circle cx="9" cy="19" r="1.5" />
|
||
<circle cx="15" cy="5" r="1.5" />
|
||
<circle cx="15" cy="12" r="1.5" />
|
||
<circle cx="15" cy="19" r="1.5" />
|
||
</svg>
|
||
</div>
|
||
|
||
{active ? (
|
||
<div className="flex items-end gap-[1.5px] w-3 h-3 shrink-0">
|
||
<div className="queue-bar" />
|
||
<div className="queue-bar" />
|
||
<div className="queue-bar" />
|
||
</div>
|
||
) : (
|
||
<span className="text-[11px] text-muted w-[18px] text-right shrink-0 font-display">{i + 1}</span>
|
||
)}
|
||
|
||
{item.img ? (
|
||
<img
|
||
src={item.img}
|
||
alt=""
|
||
className="w-7 h-7 rounded-[5px] object-cover shrink-0 bg-surface2"
|
||
onError={(e) => ((e.target as HTMLImageElement).style.display = 'none')}
|
||
/>
|
||
) : (
|
||
<div className="w-7 h-7 rounded-[5px] bg-surface2 shrink-0" />
|
||
)}
|
||
|
||
<span className="flex-1 text-[13px] text-app-text whitespace-nowrap overflow-hidden text-ellipsis">
|
||
{item.title}
|
||
</span>
|
||
<span
|
||
className="text-[10px] px-1.5 py-0.5 rounded-[5px] shrink-0 font-medium"
|
||
style={{ background: item.color.bg, color: item.color.text }}
|
||
>
|
||
{item.owner}
|
||
</span>
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|