Initial commit: party-mix-app with prefetch cache, audio preload optimizations
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
136
apps/web/src/components/Queue/QueueCard.tsx
Normal file
136
apps/web/src/components/Queue/QueueCard.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
'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(200,255,0,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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user