166 lines
5.3 KiB
React
166 lines
5.3 KiB
React
|
|
import { useState, useEffect } from 'react'
|
||
|
|
import { createPortal } from 'react-dom'
|
||
|
|
import { X, Search, Music, Heart } from 'lucide-react'
|
||
|
|
import { searchMusic, getFavorites } from '../utils/musicApi'
|
||
|
|
import { hapticFeedback } from '../utils/telegram'
|
||
|
|
import './MusicPickerModal.css'
|
||
|
|
|
||
|
|
export default function MusicPickerModal({ onClose, onSelect }) {
|
||
|
|
const [activeTab, setActiveTab] = useState('favorites')
|
||
|
|
const [query, setQuery] = useState('')
|
||
|
|
const [searchResults, setSearchResults] = useState([])
|
||
|
|
const [favorites, setFavorites] = useState([])
|
||
|
|
const [loading, setLoading] = useState(false)
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (activeTab === 'favorites') {
|
||
|
|
loadFavorites()
|
||
|
|
}
|
||
|
|
}, [activeTab])
|
||
|
|
|
||
|
|
const loadFavorites = async () => {
|
||
|
|
try {
|
||
|
|
setLoading(true)
|
||
|
|
const data = await getFavorites({ limit: 50 })
|
||
|
|
setFavorites(data.tracks || [])
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Ошибка загрузки избранного:', error)
|
||
|
|
} finally {
|
||
|
|
setLoading(false)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleSearch = async () => {
|
||
|
|
if (!query.trim()) return
|
||
|
|
|
||
|
|
try {
|
||
|
|
setLoading(true)
|
||
|
|
hapticFeedback('light')
|
||
|
|
const results = await searchMusic(query.trim(), 'tracks')
|
||
|
|
setSearchResults(results.tracks || [])
|
||
|
|
hapticFeedback('success')
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Ошибка поиска:', error)
|
||
|
|
hapticFeedback('error')
|
||
|
|
} finally {
|
||
|
|
setLoading(false)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleSelectTrack = (track) => {
|
||
|
|
hapticFeedback('light')
|
||
|
|
onSelect(track)
|
||
|
|
onClose()
|
||
|
|
}
|
||
|
|
|
||
|
|
const renderTrack = (track) => (
|
||
|
|
<div
|
||
|
|
key={track._id}
|
||
|
|
className="music-picker-track"
|
||
|
|
onClick={() => handleSelectTrack(track)}
|
||
|
|
>
|
||
|
|
<div className="track-cover-small">
|
||
|
|
{track.coverImage ? (
|
||
|
|
<img src={track.coverImage} alt={track.title} />
|
||
|
|
) : (
|
||
|
|
<Music size={20} />
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
<div className="track-info-small">
|
||
|
|
<div className="track-title-small">{track.title}</div>
|
||
|
|
<div className="track-artist-small">{track.artist?.name || 'Unknown'}</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
|
||
|
|
return createPortal(
|
||
|
|
<div className="music-picker-overlay" onClick={onClose}>
|
||
|
|
<div className="music-picker-modal" onClick={(e) => e.stopPropagation()}>
|
||
|
|
<div className="music-picker-header">
|
||
|
|
<h2>Выбрать трек</h2>
|
||
|
|
<button className="close-btn" onClick={onClose}>
|
||
|
|
<X size={24} />
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="music-picker-tabs">
|
||
|
|
<button
|
||
|
|
className={`picker-tab ${activeTab === 'favorites' ? 'active' : ''}`}
|
||
|
|
onClick={() => setActiveTab('favorites')}
|
||
|
|
>
|
||
|
|
<Heart size={18} />
|
||
|
|
Избранное
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
className={`picker-tab ${activeTab === 'search' ? 'active' : ''}`}
|
||
|
|
onClick={() => setActiveTab('search')}
|
||
|
|
>
|
||
|
|
<Search size={18} />
|
||
|
|
Поиск
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="music-picker-content">
|
||
|
|
{activeTab === 'search' && (
|
||
|
|
<div className="search-section">
|
||
|
|
<div className="search-input-wrapper">
|
||
|
|
<Search size={18} />
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
placeholder="Поиск треков..."
|
||
|
|
value={query}
|
||
|
|
onChange={(e) => setQuery(e.target.value)}
|
||
|
|
onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
|
||
|
|
/>
|
||
|
|
<button onClick={handleSearch} disabled={!query.trim() || loading}>
|
||
|
|
Найти
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{loading ? (
|
||
|
|
<div className="picker-loading">
|
||
|
|
<div className="spinner" />
|
||
|
|
<p>Поиск...</p>
|
||
|
|
</div>
|
||
|
|
) : searchResults.length === 0 && query ? (
|
||
|
|
<div className="picker-empty">
|
||
|
|
<p>Ничего не найдено</p>
|
||
|
|
</div>
|
||
|
|
) : searchResults.length > 0 ? (
|
||
|
|
<div className="tracks-list-picker">
|
||
|
|
{searchResults.map(renderTrack)}
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
<div className="picker-empty">
|
||
|
|
<Music size={48} color="var(--text-secondary)" />
|
||
|
|
<p>Введите запрос для поиска</p>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{activeTab === 'favorites' && (
|
||
|
|
loading ? (
|
||
|
|
<div className="picker-loading">
|
||
|
|
<div className="spinner" />
|
||
|
|
<p>Загрузка...</p>
|
||
|
|
</div>
|
||
|
|
) : favorites.length === 0 ? (
|
||
|
|
<div className="picker-empty">
|
||
|
|
<Heart size={48} color="var(--text-secondary)" />
|
||
|
|
<p>Нет избранных треков</p>
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
<div className="tracks-list-picker">
|
||
|
|
{favorites.map(renderTrack)}
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>,
|
||
|
|
document.body
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|