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:
2026-04-25 12:40:22 +03:00
commit 0097fb5183
83 changed files with 11788 additions and 0 deletions

View 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>
)
}