diff --git a/src/components/music/ApiKeySettings.tsx b/src/components/music/ApiKeySettings.tsx new file mode 100644 index 0000000..7e9b48e --- /dev/null +++ b/src/components/music/ApiKeySettings.tsx @@ -0,0 +1,162 @@ +import { useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Settings, Key, Eye, EyeOff, Check, X, AlertCircle } from 'lucide-react'; +import { useMusicApiKeys } from '@/hooks/useMusicApiKeys'; + +interface ApiKeySettingsProps { + isOpen: boolean; + onClose: () => void; + playSound: (sound: string) => void; +} + +export const ApiKeySettings = ({ isOpen, onClose, playSound }: ApiKeySettingsProps) => { + const { apiKeys, saveApiKeys, hasYoutubeKey, hasSoundcloudKey } = useMusicApiKeys(); + const [youtubeKey, setYoutubeKey] = useState(apiKeys.youtubeApiKey); + const [soundcloudId, setSoundcloudId] = useState(apiKeys.soundcloudClientId); + const [showYoutubeKey, setShowYoutubeKey] = useState(false); + const [showSoundcloudKey, setShowSoundcloudKey] = useState(false); + + const handleSave = () => { + playSound('click'); + saveApiKeys({ + youtubeApiKey: youtubeKey.trim(), + soundcloudClientId: soundcloudId.trim(), + }); + onClose(); + }; + + const handleClose = () => { + playSound('click'); + // Reset to current saved values + setYoutubeKey(apiKeys.youtubeApiKey); + setSoundcloudId(apiKeys.soundcloudClientId); + onClose(); + }; + + return ( + + {isOpen && ( + + e.stopPropagation()} + className="w-full max-w-md border border-primary bg-background box-glow p-6 space-y-6" + > +
+
+ +

API Keys

+
+ +
+ +
+ +

+ API keys are stored locally in your browser. They are never sent to our servers. +

+
+ +
+ {/* YouTube API Key */} +
+
+ + {hasYoutubeKey && ( + + Connected + + )} +
+
+ setYoutubeKey(e.target.value)} + placeholder="AIzaSy..." + className="w-full px-3 py-2 pr-10 bg-background border border-primary/30 focus:border-primary font-pixel text-sm text-foreground placeholder:text-foreground/40 outline-none transition-colors" + /> + +
+

+ Get from Google Cloud Console +

+
+ + {/* SoundCloud Client ID */} +
+
+ + {hasSoundcloudKey && ( + + Connected + + )} +
+
+ setSoundcloudId(e.target.value)} + placeholder="Your Client ID..." + className="w-full px-3 py-2 pr-10 bg-background border border-primary/30 focus:border-primary font-pixel text-sm text-foreground placeholder:text-foreground/40 outline-none transition-colors" + /> + +
+

+ Get from SoundCloud Developers +

+
+
+ +
+ + +
+
+
+ )} +
+ ); +}; diff --git a/src/components/music/SoundCloudPlayer.tsx b/src/components/music/SoundCloudPlayer.tsx new file mode 100644 index 0000000..43ca66f --- /dev/null +++ b/src/components/music/SoundCloudPlayer.tsx @@ -0,0 +1,401 @@ +import { useState, useEffect, useRef, useCallback } from 'react'; +import { motion } from 'framer-motion'; +import { Search, Play, Pause, Loader2, Heart, Clock, Volume2, Music2 } from 'lucide-react'; +import { useMusicApiKeys } from '@/hooks/useMusicApiKeys'; +import type { SCWidget } from '@/types/media-apis.d'; + +interface SoundCloudTrack { + id: number; + title: string; + user: { username: string }; + artwork_url: string | null; + duration: number; + permalink_url: string; + stream_url?: string; +} + +interface SoundCloudPlayerProps { + playSound: (sound: string) => void; + onOpenSettings: () => void; +} + +// SoundCloud mix/set search queries +const SOUNDCLOUD_GENRES = [ + { label: 'House', query: 'house mix set' }, + { label: 'Techno', query: 'techno mix set' }, + { label: 'Drum & Bass', query: 'drum and bass mix' }, + { label: 'Trance', query: 'trance mix set' }, + { label: 'Dubstep', query: 'dubstep mix' }, + { label: 'Deep House', query: 'deep house mix' }, + { label: 'Ambient', query: 'ambient mix' }, + { label: 'Lo-Fi', query: 'lofi mix beats' }, +]; + +const FAVORITES_KEY = 'soundcloud-favorites'; + +const formatDuration = (ms: number): string => { + const totalSeconds = Math.floor(ms / 1000); + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; + + if (hours > 0) { + return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; + } + return `${minutes}:${seconds.toString().padStart(2, '0')}`; +}; + +export const SoundCloudPlayer = ({ playSound, onOpenSettings }: SoundCloudPlayerProps) => { + const { apiKeys, hasSoundcloudKey } = useMusicApiKeys(); + const [searchQuery, setSearchQuery] = useState(''); + const [tracks, setTracks] = useState([]); + const [favorites, setFavorites] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [currentTrack, setCurrentTrack] = useState(null); + const [isPlaying, setIsPlaying] = useState(false); + const [volume, setVolume] = useState(80); + const [selectedGenre, setSelectedGenre] = useState(null); + const [showFavorites, setShowFavorites] = useState(false); + const widgetRef = useRef(null); + const iframeRef = useRef(null); + + // Load favorites from localStorage + useEffect(() => { + const stored = localStorage.getItem(FAVORITES_KEY); + if (stored) { + try { + setFavorites(JSON.parse(stored)); + } catch (e) { + console.error('Failed to parse favorites'); + } + } + }, []); + + // Load SoundCloud Widget API + useEffect(() => { + if (!document.getElementById('sc-widget-script')) { + const script = document.createElement('script'); + script.id = 'sc-widget-script'; + script.src = 'https://w.soundcloud.com/player/api.js'; + document.body.appendChild(script); + } + }, []); + + const searchTracks = useCallback(async (query: string) => { + if (!hasSoundcloudKey || !query.trim()) return; + + setIsLoading(true); + setError(null); + + try { + // Search for mixes/sets (longer duration tracks) + const response = await fetch( + `https://api.soundcloud.com/tracks?q=${encodeURIComponent(query)}&client_id=${apiKeys.soundcloudClientId}&limit=20&duration[from]=600000` + ); + + if (!response.ok) { + throw new Error('Failed to search SoundCloud'); + } + + const data: SoundCloudTrack[] = await response.json(); + setTracks(data); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to search'); + console.error('SoundCloud search error:', err); + } finally { + setIsLoading(false); + } + }, [hasSoundcloudKey, apiKeys.soundcloudClientId]); + + const handleSearch = () => { + playSound('click'); + setSelectedGenre(null); + setShowFavorites(false); + searchTracks(searchQuery); + }; + + const handleGenreClick = (genre: typeof SOUNDCLOUD_GENRES[0]) => { + playSound('click'); + setSelectedGenre(genre.label); + setShowFavorites(false); + setSearchQuery(genre.query); + searchTracks(genre.query); + }; + + const playTrack = (track: SoundCloudTrack) => { + playSound('click'); + setCurrentTrack(track); + setIsPlaying(true); + + // Use widget to play + if (iframeRef.current && window.SC) { + const widget = window.SC.Widget(iframeRef.current); + widgetRef.current = widget; + + // Load and play the track + widget.load(track.permalink_url, { + auto_play: true, + buying: false, + sharing: false, + download: false, + show_artwork: false, + show_playcount: false, + show_user: false, + callback: () => { + widget.setVolume(volume); + widget.bind(window.SC.Widget.Events.PLAY, () => setIsPlaying(true)); + widget.bind(window.SC.Widget.Events.PAUSE, () => setIsPlaying(false)); + widget.bind(window.SC.Widget.Events.FINISH, () => { + // Play next track if available + const displayTracks = showFavorites ? favorites : tracks; + const currentIndex = displayTracks.findIndex(t => t.id === track.id); + if (currentIndex < displayTracks.length - 1) { + playTrack(displayTracks[currentIndex + 1]); + } + }); + } + }); + } + }; + + const togglePlay = () => { + playSound('click'); + if (!widgetRef.current) return; + + widgetRef.current.toggle(); + }; + + const handleVolumeChange = (newVolume: number) => { + setVolume(newVolume); + if (widgetRef.current) { + widgetRef.current.setVolume(newVolume); + } + }; + + const toggleFavorite = (track: SoundCloudTrack) => { + playSound('click'); + const isFavorite = favorites.some(f => f.id === track.id); + let updated: SoundCloudTrack[]; + + if (isFavorite) { + updated = favorites.filter(f => f.id !== track.id); + } else { + updated = [track, ...favorites]; + } + + setFavorites(updated); + localStorage.setItem(FAVORITES_KEY, JSON.stringify(updated)); + }; + + const displayTracks = showFavorites ? favorites : tracks; + + if (!hasSoundcloudKey) { + return ( +
+ +

+ SoundCloud Client ID required to search and play mixes +

+ +
+ ); + } + + return ( +
+ {/* Hidden SoundCloud widget iframe */} +