diff --git a/app/api/music/tracks/route.ts b/app/api/music/tracks/route.ts new file mode 100644 index 0000000..48027c9 --- /dev/null +++ b/app/api/music/tracks/route.ts @@ -0,0 +1,73 @@ +// app/api/music/tracks/route.ts +import { NextResponse } from 'next/server'; +import { readdir } from 'fs/promises'; +import { join } from 'path'; + +export const dynamic = 'force-dynamic'; + +interface Track { + title: string; + fileName: string; + artist: string; + theme: string; +} + +function parseFileName(fileName: string): Track { + // Remove file extension + const nameWithoutExt = fileName.replace(/\.(mp4|mp3|wav|ogg)$/i, ''); + + // Parse format: [music_name]_[theme/genre].[format] + const parts = nameWithoutExt.split('_'); + + if (parts.length >= 2) { + const theme = parts[parts.length - 1].toLowerCase(); + const title = parts.slice(0, -1).join(' '); + + return { + title: title.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase()), + fileName: fileName, + artist: theme.charAt(0).toUpperCase() + theme.slice(1) + ' Collection', + theme: theme + }; + } + + // Fallback for files that don't follow the naming convention + return { + title: nameWithoutExt.replace(/-|_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()), + fileName: fileName, + artist: 'Unknown Artist', + theme: 'general' + }; +} + +export async function GET() { + try { + // Path to your music directory + const musicDir = join(process.cwd(), 'public', 'files', 'music'); + + // Read all files in the directory + const files = await readdir(musicDir); + + // Filter for audio/video files and parse them + const tracks = files + .filter(file => /\.(mp4|mp3|wav|ogg)$/i.test(file)) + .map(file => parseFileName(file)) + .sort((a, b) => { + // Sort by theme first, then by title + if (a.theme !== b.theme) { + return a.theme.localeCompare(b.theme); + } + return a.title.localeCompare(b.title); + }); + + return NextResponse.json({ tracks }); + } catch (error) { + console.error('Error reading music directory:', error); + + // Return fallback tracks if directory reading fails + return NextResponse.json({ + tracks: [ + ] + }); + } +} diff --git a/app/layout.tsx b/app/layout.tsx index 5126a2d..159066b 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -7,64 +7,69 @@ import InteractiveTopo from "@/components/interactiveTopo"; import "./globals.css"; export const metadata: Metadata = { - title: "OpenWheels Racing", - description: "Free racing community - Live dashboard and rankings", + title: "OpenWheels Racing", + description: "Free racing community - Live dashboard and rankings", }; export default function RootLayout({ - children, + children, }: { - children: React.ReactNode; + children: React.ReactNode; }) { - return ( - - -
- {/* Interactive topology effect */} - - - {/* Static topology background */} -
+ return ( + + +
+ {/* Interactive topology effect */} + - {/* Content wrapper */} -
- {/* Navbar - appears on all pages */} - + {/* Static topology background */} +
- {/* Page content */} -
- {children} -
+ {/* Content wrapper */} +
+ {/* Navbar - appears on all pages */} + - {/* Footer - appears on all pages */} -
-
-
- - - ); + {/* Page content */} +
+ {children} +
+ + {/* Footer - appears on all pages */} +
+
+
+ + + ); } function Footer() { - return ( - - ); + return ( + + ); } diff --git a/app/music/page.tsx b/app/music/page.tsx index 919580d..b0fc0a5 100644 --- a/app/music/page.tsx +++ b/app/music/page.tsx @@ -1,11 +1,581 @@ -export default function MusicPage() { +'use client'; + +import { useState, useRef, useEffect } from 'react'; + +// Configuration +const BASE_URL = "https://openwheels.racing/files/music/"; + +// Icons +function PlayIcon({ className = "w-6 h-6" }) { return ( -
-

Music Stream

-

This is still a work in progress. Stay tuned for updates!

- + + + + ); +} + +function PauseIcon({ className = "w-6 h-6" }) { + return ( + + + + + ); +} + +function SkipNextIcon({ className = "w-6 h-6" }) { + return ( + + + + + ); +} + +function SkipPrevIcon({ className = "w-6 h-6" }) { + return ( + + + + + ); +} + +function VideoIcon({ className = "w-5 h-5" }) { + return ( + + + + + ); +} + +function MusicIcon({ className = "w-5 h-5" }) { + return ( + + + + + + ); +} + +function RefreshIcon({ className = "w-5 h-5" }) { + return ( + + + + + + ); +} + +export default function MusicPage() { + const [tracks, setTracks] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [currentTrackIndex, setCurrentTrackIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [currentTime, setCurrentTime] = useState(0); + const [duration, setDuration] = useState(0); + const [selectedTheme, setSelectedTheme] = useState('all'); + const mediaRef = useRef(null); + + // Fetch available tracks from the server + const fetchTracks = async () => { + setLoading(true); + setError(null); + try { + const response = await fetch('/api/music/tracks'); + if (!response.ok) { + throw new Error('Failed to fetch tracks'); + } + const data = await response.json(); + setTracks(data.tracks || []); + } catch (err) { + console.error('Error fetching tracks:', err); + setError(err.message); + // Fallback to hardcoded tracks if API fails + setTracks([ + { title: "Alone Again", fileName: "AloneAgain.mp4", artist: "Unknown Artist", theme: "general" }, + { title: "House Music VOL1", fileName: "deephousetherapy.mp4", artist: "Deep House Therapy", theme: "house" }, + { title: "House Music VOL2", fileName: "videoplayback.mp4", artist: "Various Artists", theme: "house" }, + ]); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchTracks(); + }, []); + + // Group tracks by theme + const tracksByTheme = tracks.reduce((acc, track) => { + const theme = track.theme || 'general'; + if (!acc[theme]) { + acc[theme] = []; + } + acc[theme].push(track); + return acc; + }, {}); + + const themes = Object.keys(tracksByTheme).sort(); + + // Get filtered tracks based on selected theme + const filteredTracks = selectedTheme === 'all' + ? tracks + : tracksByTheme[selectedTheme] || []; + + const currentTrack = filteredTracks[currentTrackIndex]; + const isVideo = currentTrack?.fileName.toLowerCase().endsWith('.mp4'); + + useEffect(() => { + if (mediaRef.current && currentTrack) { + mediaRef.current.load(); + if (isPlaying) { + mediaRef.current.play().catch(err => { + console.error('Playback error:', err); + setIsPlaying(false); + }); + } + } + }, [currentTrackIndex, currentTrack]); + + const togglePlayPause = () => { + if (mediaRef.current) { + if (isPlaying) { + mediaRef.current.pause(); + } else { + mediaRef.current.play().catch(err => { + console.error('Playback error:', err); + }); + } + setIsPlaying(!isPlaying); + } + }; + + const nextTrack = () => { + if (filteredTracks.length > 0) { + setCurrentTrackIndex((prev) => (prev + 1) % filteredTracks.length); + setIsPlaying(true); + } + }; + + const prevTrack = () => { + if (filteredTracks.length > 0) { + setCurrentTrackIndex((prev) => (prev - 1 + filteredTracks.length) % filteredTracks.length); + setIsPlaying(true); + } + }; + + const handleTimeUpdate = () => { + if (mediaRef.current) { + setCurrentTime(mediaRef.current.currentTime); + setDuration(mediaRef.current.duration || 0); + } + }; + + const handleSeek = (e) => { + const rect = e.currentTarget.getBoundingClientRect(); + const x = e.clientX - rect.left; + const percentage = x / rect.width; + if (mediaRef.current) { + mediaRef.current.currentTime = percentage * duration; + } + }; + + const formatTime = (time) => { + if (isNaN(time)) return "0:00"; + const minutes = Math.floor(time / 60); + const seconds = Math.floor(time % 60); + return `${minutes}:${seconds.toString().padStart(2, '0')}`; + }; + + const handleThemeChange = (theme) => { + setSelectedTheme(theme); + setCurrentTrackIndex(0); + setIsPlaying(false); + }; + + if (loading) { + return ( +
+
+
+
+ + MUSIC STREAM +
+

+ AUDIO SYSTEM +

+

+ High-fidelity streaming for the racing community +

+
+
+
+
+
+

LOADING TRACKS...

+
+
+
+ ); + } + + if (!currentTrack) { + return ( +
+
+
+
+ + MUSIC STREAM +
+

+ AUDIO SYSTEM +

+

+ High-fidelity streaming for the racing community +

+
+
+
+
+
+

NO TRACKS AVAILABLE

+ {error &&

Error: {error}

} + +
+
+
+ ); + } + + return ( +
+ {/* Animated Background */} +
+
+
+ +
+
+
+ + MUSIC STREAM +
+

+ AUDIO SYSTEM +

+

+ High-fidelity streaming for the racing community +

+ + {/* Theme Filter */} +
+ FILTER: + + {themes.map(theme => ( + + ))} + +
+
+
+ +
+ {/* Main Player Container */} +
+ {/* Left Column - Now Playing */} +
+ {/* Video/Audio Display */} +
+ {isVideo ? ( + + ) : ( +
+ {/* Audio Visualizer */} +
+
+ {/* Pulsing circles */} + {[...Array(3)].map((_, i) => ( +
+ ))} + + {/* Center icon */} +
+ +
+
+
+ + {/* Audio element (hidden) */} + +
+ )} +
+ + {/* Track Info & Controls */} +
+ {/* Track Info */} +
+
+
+

+ {currentTrack.title} +

+

+ {currentTrack.artist} +

+
+
+
+ {isVideo ? : } + {isVideo ? 'VIDEO' : 'AUDIO'} +
+
+ {currentTrack.theme?.toUpperCase() || 'GENERAL'} +
+
+
+
+ + {/* Progress Bar */} +
+
+
+
+
+
+ {formatTime(currentTime)} + {formatTime(duration)} +
+
+ + {/* Controls */} +
+ + + + + +
+
+
+ + {/* Right Column - Playlist */} +
+
+

+ PLAYLIST + {selectedTheme !== 'all' && ` - ${selectedTheme.toUpperCase()}`} +

+
+
+ {filteredTracks.length === 0 ? ( +
+ No tracks in this category +
+ ) : ( + filteredTracks.map((track, index) => ( + + )) + )} +
+
+
+ + {/* Info Box */} +
+

+ SYSTEM INFO +

+
+
+

FORMAT

+

MP4 / AUDIO

+
+
+

QUALITY

+

HIGH FIDELITY

+
+
+

TRACKS

+

{tracks.length} AVAILABLE

+
+
+

STREAMING

+

PROGRESSIVE LOAD

+
+
+
+
+ + {/* Animations */} +
); }