search bar, design update
This commit is contained in:
parent
573f3cb35b
commit
873bf64a4e
7 changed files with 1438 additions and 922 deletions
File diff suppressed because it is too large
Load diff
|
@ -1,26 +1,146 @@
|
|||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { searchSpotifyTracks, isLoggedIn } from '../utils/spotifyAuth.js';
|
||||
|
||||
function AddSongModal({ onClose }) {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [searchResults, setSearchResults] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleBackdropClick = (e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = async (e) => {
|
||||
const query = e.target.value;
|
||||
setSearchQuery(query);
|
||||
setError('');
|
||||
|
||||
if (query.length > 2) {
|
||||
if (!isLoggedIn()) {
|
||||
setError('Please login to Spotify to search for songs');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const results = await searchSpotifyTracks(query);
|
||||
setSearchResults(results);
|
||||
} catch (err) {
|
||||
setError(err.message === 'Token expired' ? 'Session expired. Please login again.' : 'Search failed. Please try again.');
|
||||
setSearchResults([]);
|
||||
if (err.message === 'Token expired') {
|
||||
setTimeout(() => window.location.reload(), 2000);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
} else {
|
||||
setSearchResults([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyPress = async (e) => {
|
||||
if (e.key === 'Enter' && searchQuery.trim() && searchQuery.length > 2) {
|
||||
if (!isLoggedIn()) {
|
||||
setError('Please login to Spotify to search for songs');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const results = await searchSpotifyTracks(searchQuery);
|
||||
setSearchResults(results);
|
||||
setError('');
|
||||
} catch (err) {
|
||||
setError(err.message === 'Token expired' ? 'Session expired. Please login again.' : 'Search failed. Please try again.');
|
||||
setSearchResults([]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="modal-backdrop" onClick={handleBackdropClick}>
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h2>Add Song</h2>
|
||||
<button className="close-btn" onClick={onClose}>
|
||||
<div className="add-song-modal-backdrop" onClick={handleBackdropClick}>
|
||||
<div className="add-song-modal-content">
|
||||
<div className="add-song-modal-header">
|
||||
<h2>Add Song to Station</h2>
|
||||
<button className="add-song-close-btn" onClick={onClose}>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="modal-body">
|
||||
<p>Song addition functionality coming soon...</p>
|
||||
<div className="add-song-modal-body">
|
||||
<div className="add-song-search-container">
|
||||
<div className="add-song-search-box">
|
||||
<svg className="search-icon" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>
|
||||
</svg>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search for songs, artists, or albums..."
|
||||
value={searchQuery}
|
||||
onChange={handleSearch}
|
||||
onKeyPress={handleKeyPress}
|
||||
autoFocus
|
||||
/>
|
||||
{isLoading && (
|
||||
<div className="search-loading">
|
||||
<div className="loading-spinner"></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="add-song-results-container">
|
||||
{error && (
|
||||
<div className="add-song-error">
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
)}
|
||||
{searchQuery.length === 0 ? (
|
||||
<div className="add-song-placeholder">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/>
|
||||
</svg>
|
||||
<p>Search for music to add to your station</p>
|
||||
<p className="placeholder-subtitle">Enter song title, artist, or album name</p>
|
||||
</div>
|
||||
) : searchQuery.length <= 2 ? (
|
||||
<div className="add-song-placeholder">
|
||||
<p>Type at least 3 characters to search</p>
|
||||
</div>
|
||||
) : searchResults.length === 0 && !isLoading && !error ? (
|
||||
<div className="add-song-placeholder">
|
||||
<p>No results found for "{searchQuery}"</p>
|
||||
<p className="placeholder-subtitle">Try different keywords</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="add-song-results-list">
|
||||
{searchResults.map(song => (
|
||||
<div key={song.id} className="add-song-result-item">
|
||||
<img src={song.coverUrl} alt={`${song.title} cover`} className="result-song-cover" />
|
||||
<div className="result-song-info">
|
||||
<h4>{song.title}</h4>
|
||||
<p className="result-artist">{song.artist}</p>
|
||||
<p className="result-album">{song.album}</p>
|
||||
</div>
|
||||
<button className="add-song-btn" onClick={() => console.log('Adding song:', song)}>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
||||
</svg>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -9,50 +9,78 @@ function CreateStation() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="create-station">
|
||||
<header className="create-station-header">
|
||||
<h1>Create a Station on Serena</h1>
|
||||
</header>
|
||||
|
||||
<main className="create-station-content">
|
||||
<div className="join-method-section">
|
||||
<h2>How should people be able to join your station?</h2>
|
||||
|
||||
<div className="radio-option">
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
name="joinMethod"
|
||||
value="password"
|
||||
checked={joinMethod === 'password'}
|
||||
onChange={(e) => setJoinMethod(e.target.value)}
|
||||
/>
|
||||
Password
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{joinMethod === 'password' && (
|
||||
<div className="password-input-section">
|
||||
<label htmlFor="station-password">Station Password:</label>
|
||||
<input
|
||||
type="password"
|
||||
id="station-password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Enter station password"
|
||||
/>
|
||||
<div className="create-station-container">
|
||||
<div className="create-station-main">
|
||||
{/* Left Section - Vinyl Animation */}
|
||||
<div className="create-station-left-section">
|
||||
<div className="create-station-vinyl-container">
|
||||
<div className="create-station-vinyl-record">
|
||||
<div className="vinyl-center"></div>
|
||||
<div className="vinyl-grooves"></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Animated Music Notes */}
|
||||
<div className="create-station-music-notes">
|
||||
<div className="music-note note-1">♪</div>
|
||||
<div className="music-note note-2">♫</div>
|
||||
<div className="music-note note-3">♪</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="create-station-final-btn"
|
||||
onClick={handleCreateStation}
|
||||
disabled={joinMethod !== 'password' || !password.trim()}
|
||||
>
|
||||
Create Radio Station
|
||||
</button>
|
||||
</main>
|
||||
{/* Right Section - Create Station Form */}
|
||||
<div className="create-station-right-section">
|
||||
<div className="create-station-content">
|
||||
<header className="create-station-header">
|
||||
<h1>Create a Station on Serena</h1>
|
||||
</header>
|
||||
|
||||
<main className="create-station-form">
|
||||
<div className="join-method-section">
|
||||
<h2>How should people be able to join your station?</h2>
|
||||
|
||||
<div className="radio-option">
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
name="joinMethod"
|
||||
value="password"
|
||||
checked={joinMethod === 'password'}
|
||||
onChange={(e) => setJoinMethod(e.target.value)}
|
||||
/>
|
||||
<span className="radio-custom"></span>
|
||||
Password
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{joinMethod === 'password' && (
|
||||
<div className="password-input-section">
|
||||
<label htmlFor="station-password">Station Password:</label>
|
||||
<input
|
||||
type="password"
|
||||
id="station-password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Enter station password"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="create-station-final-btn"
|
||||
onClick={handleCreateStation}
|
||||
disabled={joinMethod !== 'password' || !password.trim()}
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
||||
</svg>
|
||||
Create Radio Station
|
||||
</button>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -14,24 +14,52 @@ function Home() {
|
|||
|
||||
return (
|
||||
<div className="home-container">
|
||||
<div className="content">
|
||||
<h1 className="title">Radio Station Hub</h1>
|
||||
<p className="subtitle">Create or join a radio station to share music with friends</p>
|
||||
|
||||
<div className="button-container">
|
||||
<button
|
||||
className="action-button primary"
|
||||
onClick={handleCreateStation}
|
||||
>
|
||||
Create Radio Station
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="action-button secondary"
|
||||
onClick={handleJoinStation}
|
||||
>
|
||||
Join Radio Station
|
||||
</button>
|
||||
<div className="home-main">
|
||||
{/* Left Section - Logo/Animation */}
|
||||
<div className="home-left-section">
|
||||
<div className="home-vinyl-container">
|
||||
<div className="home-vinyl-record">
|
||||
<div className="vinyl-center"></div>
|
||||
<div className="vinyl-grooves"></div>
|
||||
</div>
|
||||
|
||||
{/* Animated Music Notes */}
|
||||
<div className="home-music-notes">
|
||||
<div className="music-note note-1">♪</div>
|
||||
<div className="music-note note-2">♫</div>
|
||||
<div className="music-note note-3">♪</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Section - Content */}
|
||||
<div className="home-right-section">
|
||||
<div className="home-content">
|
||||
<h1 className="home-title">Serena Hub</h1>
|
||||
<p className="home-subtitle">Create or join a radio station to share music with friends</p>
|
||||
|
||||
<div className="home-button-container">
|
||||
<button
|
||||
className="home-action-button primary"
|
||||
onClick={handleCreateStation}
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
||||
</svg>
|
||||
Create Radio Station
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="home-action-button secondary"
|
||||
onClick={handleJoinStation}
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M15 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm-9-2V7H4v3H1v2h3v3h2v-3h3v-2H6zm9 4c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
|
||||
</svg>
|
||||
Join Radio Station
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -13,50 +13,78 @@ function JoinStation() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="join-station">
|
||||
<header className="join-station-header">
|
||||
<h1>Join a Station on Serena</h1>
|
||||
</header>
|
||||
|
||||
<main className="join-station-content">
|
||||
<div className="verify-method-section">
|
||||
<h2>How would you like to verify access?</h2>
|
||||
|
||||
<div className="radio-option">
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
name="verifyMethod"
|
||||
value="password"
|
||||
checked={verifyMethod === 'password'}
|
||||
onChange={(e) => setVerifyMethod(e.target.value)}
|
||||
/>
|
||||
Password
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{verifyMethod === 'password' && (
|
||||
<div className="password-input-section">
|
||||
<label htmlFor="station-password">Station Password:</label>
|
||||
<input
|
||||
type="password"
|
||||
id="station-password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Enter station password"
|
||||
/>
|
||||
<div className="join-station-container">
|
||||
<div className="join-station-main">
|
||||
{/* Left Section - Vinyl Animation */}
|
||||
<div className="join-station-left-section">
|
||||
<div className="join-station-vinyl-container">
|
||||
<div className="join-station-vinyl-record">
|
||||
<div className="vinyl-center"></div>
|
||||
<div className="vinyl-grooves"></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Animated Music Notes */}
|
||||
<div className="join-station-music-notes">
|
||||
<div className="music-note note-1">♪</div>
|
||||
<div className="music-note note-2">♫</div>
|
||||
<div className="music-note note-3">♪</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="join-station-final-btn"
|
||||
onClick={handleJoinStation}
|
||||
disabled={verifyMethod !== 'password' || !password.trim()}
|
||||
>
|
||||
Join Station
|
||||
</button>
|
||||
</main>
|
||||
{/* Right Section - Join Station Form */}
|
||||
<div className="join-station-right-section">
|
||||
<div className="join-station-content">
|
||||
<header className="join-station-header">
|
||||
<h1>Join a Station on Serena</h1>
|
||||
</header>
|
||||
|
||||
<main className="join-station-form">
|
||||
<div className="verify-method-section">
|
||||
<h2>How would you like to verify access?</h2>
|
||||
|
||||
<div className="radio-option">
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
name="verifyMethod"
|
||||
value="password"
|
||||
checked={verifyMethod === 'password'}
|
||||
onChange={(e) => setVerifyMethod(e.target.value)}
|
||||
/>
|
||||
<span className="radio-custom"></span>
|
||||
Password
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{verifyMethod === 'password' && (
|
||||
<div className="password-input-section">
|
||||
<label htmlFor="station-password">Station Password:</label>
|
||||
<input
|
||||
type="password"
|
||||
id="station-password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Enter station password"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="join-station-final-btn"
|
||||
onClick={handleJoinStation}
|
||||
disabled={verifyMethod !== 'password' || !password.trim()}
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M15 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm-9-2V7H4v3H1v2h3v3h2v-3h3v-2H6zm9 4c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
|
||||
</svg>
|
||||
Join Radio Station
|
||||
</button>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,30 +1,27 @@
|
|||
import React, { useState } from 'react';
|
||||
import AddSongModal from './AddSongModal';
|
||||
import LoginButton from '../components/LoginButton';
|
||||
|
||||
function StationPage() {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [currentSong, setCurrentSong] = useState({
|
||||
title: "No song playing",
|
||||
artist: "Add songs to get started",
|
||||
isPlaying: false
|
||||
isPlaying: false,
|
||||
progress: 0,
|
||||
duration: 180 // 3 minutes in seconds
|
||||
});
|
||||
|
||||
const [nextSong] = useState({
|
||||
title: "No song queued",
|
||||
artist: "No Artist Queued"
|
||||
});
|
||||
|
||||
// Empty song list - no songs initially
|
||||
const recommendedSongs = [];
|
||||
|
||||
const handlePlayPause = () => {
|
||||
setCurrentSong(prev => ({ ...prev, isPlaying: !prev.isPlaying }));
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
console.log('Next song');
|
||||
};
|
||||
|
||||
const handlePrevious = () => {
|
||||
console.log('Previous song');
|
||||
};
|
||||
|
||||
const openModal = () => {
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
@ -34,34 +31,38 @@ function StationPage() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="station-page">
|
||||
<div className="animated-background">
|
||||
{[...Array(20)].map((_, i) => (
|
||||
<div key={i} className={`star star-${i + 1}`}></div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="station-content">
|
||||
<header className="station-header">
|
||||
<h1>Serena Station</h1>
|
||||
<p className="station-subtitle">Collaborative Music Experience</p>
|
||||
<LoginButton />
|
||||
</header>
|
||||
|
||||
<div className="media-controls-section">
|
||||
<div className="current-song">
|
||||
<h3>{currentSong.title}</h3>
|
||||
<p>{currentSong.artist}</p>
|
||||
</div>
|
||||
|
||||
<div className="media-controls">
|
||||
<button className="control-btn" onClick={handlePrevious}>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div className="station-page-desktop">
|
||||
{/* Main Layout */}
|
||||
<div className="desktop-main">
|
||||
{/* Left Section */}
|
||||
<div className="left-section">
|
||||
{/* Vinyl Record */}
|
||||
<div className="vinyl-container">
|
||||
<div className={`vinyl-record-desktop ${currentSong.isPlaying ? 'spinning' : ''}`}>
|
||||
<div className="vinyl-center"></div>
|
||||
<div className="vinyl-grooves"></div>
|
||||
</div>
|
||||
|
||||
<button className="control-btn play-pause" onClick={handlePlayPause}>
|
||||
{/* Animated Music Notes */}
|
||||
{currentSong.isPlaying && (
|
||||
<div className="music-notes-desktop">
|
||||
<div className="music-note note-1">♪</div>
|
||||
<div className="music-note note-2">♫</div>
|
||||
<div className="music-note note-3">♪</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Next Song Info */}
|
||||
<div className="next-song-info">
|
||||
<p className="next-label">Next song:</p>
|
||||
<h4>{nextSong.title}</h4>
|
||||
<p className="next-artist">{nextSong.artist}</p>
|
||||
</div>
|
||||
|
||||
{/* Bottom Play Icon */}
|
||||
<div className="bottom-play-icon">
|
||||
<button className="mini-play-btn" onClick={handlePlayPause}>
|
||||
{currentSong.isPlaying ? (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>
|
||||
|
@ -72,43 +73,55 @@ function StationPage() {
|
|||
</svg>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button className="control-btn" onClick={handleNext}>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="songs-section">
|
||||
<div className="section-header">
|
||||
<h2>Song Queue</h2>
|
||||
<button className="add-song-btn" onClick={openModal}>
|
||||
Add Song
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="songs-list">
|
||||
{recommendedSongs.length === 0 ? (
|
||||
<div className="empty-songs-state">
|
||||
<p>No songs in queue yet. Add some songs to get started!</p>
|
||||
</div>
|
||||
) : (
|
||||
recommendedSongs.map(song => (
|
||||
<div key={song.id} className="song-item">
|
||||
<div className="song-info">
|
||||
<h4>{song.title}</h4>
|
||||
<p>{song.artist}</p>
|
||||
</div>
|
||||
<span className="song-duration">{song.duration}</span>
|
||||
{/* Right Section - Recommendations */}
|
||||
<div className="right-section">
|
||||
<div className="recommendations-container">
|
||||
<div className="recommendations-header">
|
||||
<h2>Song Suggestions</h2>
|
||||
<button className="add-suggestion-btn" onClick={openModal}>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="recommendations-list">
|
||||
{recommendedSongs.length === 0 ? (
|
||||
<div style={{ textAlign: 'center', padding: '40px', color: '#aaa' }}>
|
||||
<p>No songs suggested yet</p>
|
||||
<p style={{ fontSize: '14px', marginTop: '10px' }}>Add songs to get started!</p>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
) : (
|
||||
recommendedSongs.map(song => (
|
||||
<div key={song.id} className="recommendation-item">
|
||||
<img src={song.coverUrl} alt={`${song.title} cover`} className="song-cover" />
|
||||
<div className="song-info">
|
||||
<h4>{song.title}</h4>
|
||||
<p className="artist">{song.artist}</p>
|
||||
<p className="album">{song.album}</p>
|
||||
</div>
|
||||
<span className="duration">{song.duration}</span>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="progress-container">
|
||||
<div className="progress-bar">
|
||||
<div
|
||||
className="progress-fill"
|
||||
style={{ width: `${(currentSong.progress / currentSong.duration) * 100}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isModalOpen && <AddSongModal onClose={closeModal} />}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -38,3 +38,39 @@ export const removeAccessToken = () => {
|
|||
export const isLoggedIn = () => {
|
||||
return !!getAccessToken();
|
||||
};
|
||||
|
||||
export const searchSpotifyTracks = async (query) => {
|
||||
const token = getAccessToken();
|
||||
if (!token) {
|
||||
throw new Error('No access token available');
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`https://api.spotify.com/v1/search?q=${encodeURIComponent(query)}&type=track&limit=20`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
removeAccessToken();
|
||||
throw new Error('Token expired');
|
||||
}
|
||||
throw new Error('Search failed');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.tracks.items.map((track) => ({
|
||||
id: track.id,
|
||||
title: track.name,
|
||||
artist: track.artists.map((artist) => artist.name).join(', '),
|
||||
album: track.album.name,
|
||||
coverUrl: track.album.images[0]?.url || '',
|
||||
duration: track.duration_ms,
|
||||
previewUrl: track.preview_url,
|
||||
spotifyUrl: track.external_urls.spotify,
|
||||
}));
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue