Files
party-mix-app/apps/web/src/components/Queue/QueueCard.tsx
Kirko 24856a644f feat: runtime accent color picker with 6 presets
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>
2026-04-25 12:59:58 +03:00

137 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
)
}