search bar, design update

This commit is contained in:
Tobias 2025-08-02 02:00:11 +02:00
parent 573f3cb35b
commit 873bf64a4e
7 changed files with 1438 additions and 922 deletions

File diff suppressed because it is too large Load diff

View file

@ -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>

View file

@ -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>
);
}

View file

@ -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>

View file

@ -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>
);
}

View file

@ -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>
);

View file

@ -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,
}));
};