feat: nginx reverse proxy, Spotify import, overlay system, UI overhaul
- Add nginx as single entry point: /api/* → backend, /* → web - NEXT_PUBLIC_API_URL="" so all API calls are relative (go through nginx) - Add Spotify playlist import (Client Credentials OAuth, up to 500 tracks) - Add Yandex/Spotify tabbed import UI on /playlists - Add stream overlay system (SSE + polling fallback, 9 styles) - Reorganize pages into (main) route group - Add QueuePanel, VersionsPanel, Toaster components - Add overlay settings tab in /settings Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,28 @@
|
||||
'use client'
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { usePartyStore } from '@/store/partyStore'
|
||||
import { fetchTopCharts } from '@/lib/api'
|
||||
import type { SearchResult } from '@/types'
|
||||
|
||||
const SEARCH_HISTORY_KEY = 'pm_search_history'
|
||||
const MAX_HISTORY = 8
|
||||
|
||||
function getSearchHistory(): string[] {
|
||||
if (typeof window === 'undefined') return []
|
||||
try { return JSON.parse(localStorage.getItem(SEARCH_HISTORY_KEY) ?? '[]') } catch { return [] }
|
||||
}
|
||||
|
||||
function addToSearchHistory(q: string) {
|
||||
const prev = getSearchHistory().filter((s) => s !== q)
|
||||
const next = [q, ...prev].slice(0, MAX_HISTORY)
|
||||
try { localStorage.setItem(SEARCH_HISTORY_KEY, JSON.stringify(next)) } catch {}
|
||||
}
|
||||
|
||||
function clearSearchHistory() {
|
||||
try { localStorage.removeItem(SEARCH_HISTORY_KEY) } catch {}
|
||||
}
|
||||
|
||||
function TopChartsCard({ tracks }: { tracks: string[] }) {
|
||||
const { loadPlaylist } = usePartyStore()
|
||||
const [launched, setLaunched] = useState(false)
|
||||
@@ -48,24 +66,34 @@ function TopChartsCard({ tracks }: { tracks: string[] }) {
|
||||
export default function SoloTab() {
|
||||
const [search, setSearch] = useState('')
|
||||
const [topTracks, setTopTracks] = useState<string[] | null>(null)
|
||||
const [searchHistory, setSearchHistory] = useState<string[]>([])
|
||||
const { loadPlaylist } = usePartyStore()
|
||||
|
||||
useEffect(() => {
|
||||
setSearchHistory(getSearchHistory())
|
||||
fetchTopCharts().then((results: SearchResult[]) => {
|
||||
if (!results.length) { setTopTracks([]); return }
|
||||
setTopTracks(
|
||||
results.map((r) => (r.artist ? `${r.artist} — ${r.title}` : r.title))
|
||||
)
|
||||
setTopTracks(results.map((r) => (r.artist ? `${r.artist} — ${r.title}` : r.title)))
|
||||
})
|
||||
}, [])
|
||||
|
||||
const handleSearch = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
const q = search.trim()
|
||||
if (!q) return
|
||||
loadPlaylist([q])
|
||||
const doSearch = useCallback((q: string) => {
|
||||
if (!q.trim()) return
|
||||
addToSearchHistory(q.trim())
|
||||
setSearchHistory(getSearchHistory())
|
||||
loadPlaylist([q.trim()])
|
||||
setSearch('')
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}, [loadPlaylist])
|
||||
|
||||
const handleSearch = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
doSearch(search)
|
||||
}
|
||||
|
||||
const handleClearHistory = () => {
|
||||
clearSearchHistory()
|
||||
setSearchHistory([])
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -100,6 +128,31 @@ export default function SoloTab() {
|
||||
▶
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{searchHistory.length > 0 && (
|
||||
<div className="mt-3">
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<span className="text-[10px] text-muted font-display uppercase tracking-[1px]">Недавние</span>
|
||||
<button
|
||||
onClick={handleClearHistory}
|
||||
className="text-[10px] text-muted hover:text-[#ff6b6b] transition-colors cursor-pointer"
|
||||
>
|
||||
Очистить
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{searchHistory.map((q) => (
|
||||
<button
|
||||
key={q}
|
||||
onClick={() => doSearch(q)}
|
||||
className="text-[11px] px-2.5 py-1 rounded-[7px] bg-surface2 text-muted hover:text-app-text hover:bg-white/[0.07] transition-all cursor-pointer truncate max-w-[200px]"
|
||||
>
|
||||
{q}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user