design: improvements to music page
All checks were successful
Bump Dependencies / update-dependencies (push) Successful in 53s
All checks were successful
Bump Dependencies / update-dependencies (push) Successful in 53s
This commit is contained in:
parent
4294d16d53
commit
aa3bea690c
@ -33,3 +33,4 @@ You will need some environment variables set to properly self-host aidxnCC. They
|
|||||||
## To-Do
|
## To-Do
|
||||||
|
|
||||||
- [ ] Dockerize for easier deployment
|
- [ ] Dockerize for easier deployment
|
||||||
|
- [ ] Use MusicBrainz recording collections for displaying data about music
|
||||||
|
@ -2,14 +2,23 @@ import Header from '@/components/Header'
|
|||||||
import MusicWidget from '@/components/widgets/Music'
|
import MusicWidget from '@/components/widgets/Music'
|
||||||
import MusicInfo from '@/components/objects/MusicInfo'
|
import MusicInfo from '@/components/objects/MusicInfo'
|
||||||
import Footer from '@/components/Footer'
|
import Footer from '@/components/Footer'
|
||||||
|
import { Music as MusicNote } from "lucide-react";
|
||||||
|
|
||||||
export default function Music() {
|
export default function Music() {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col">
|
<div className="min-h-screen flex flex-col">
|
||||||
<Header />
|
<Header />
|
||||||
<main className="grow container mx-auto px-4 py-12">
|
<main className="grow container mx-auto px-4 py-12">
|
||||||
<MusicWidget />
|
<div className='mb-6 flex justify-center'>
|
||||||
<MusicInfo />
|
<MusicNote size={60} />
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl font-bold my-2 text-center text-gray-200" style={{ textShadow: '0 0 10px rgba(255, 255, 255, 0.5)' }}>
|
||||||
|
Music and Me
|
||||||
|
</h1>
|
||||||
|
<div className="flex justify-center max-w-2xl gap-16 mx-auto pt-8">
|
||||||
|
<MusicWidget />
|
||||||
|
<MusicInfo />
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
|
@ -13,16 +13,14 @@ const timePeriods: TimePeriod[] = [
|
|||||||
|
|
||||||
const MusicInfo: React.FC = () => {
|
const MusicInfo: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<div className="max-w-2xl mx-auto text-center text-gray-200">
|
<div>
|
||||||
{timePeriods.map((period) => (
|
{timePeriods.map((period) => (
|
||||||
<section key={period.slug} className="mb-12">
|
<section key={period.slug} className="mb-12">
|
||||||
<h2 className="text-2xl font-semibold mb-4">{period.title}</h2>
|
<h2 className="text-2xl font-semibold mb-4">{period.title}</h2>
|
||||||
<div className="flex justify-center">
|
<Button
|
||||||
<Button
|
href={`/time-periods/${period.slug}/what-was-going-on`}
|
||||||
href={`/time-periods/${period.slug}/what-was-going-on`}
|
label="WHAT WAS GOING ON"
|
||||||
label="WHAT WAS GOING ON"
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
52
components/objects/SeekBar.tsx
Normal file
52
components/objects/SeekBar.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
interface SeekBarProps {
|
||||||
|
duration: string
|
||||||
|
startPos: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SeekBar({ duration, startPos }: SeekBarProps) {
|
||||||
|
const getDurationInSeconds = (timeStr: string) => {
|
||||||
|
const parts = timeStr.split(":").map(Number)
|
||||||
|
if (parts.length === 3) {
|
||||||
|
return parts[0] * 3600 + parts[1] * 60 + parts[2]
|
||||||
|
} else {
|
||||||
|
return parts[0] * 60 + parts[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatTime = (seconds: number) => {
|
||||||
|
const hours = Math.floor(seconds / 3600)
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60)
|
||||||
|
const remainingSeconds = seconds % 60
|
||||||
|
|
||||||
|
if (hours > 0) {
|
||||||
|
return `${hours}:${minutes.toString().padStart(2, "0")}:${remainingSeconds.toString().padStart(2, "0")}`
|
||||||
|
}
|
||||||
|
return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalSeconds = getDurationInSeconds(duration)
|
||||||
|
const [currentSeconds] = React.useState(startPos)
|
||||||
|
const progress = (currentSeconds / totalSeconds) * 100
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full max-w-3xl mx-auto pt-4">
|
||||||
|
<div className="relative h-16 flex items-center">
|
||||||
|
<div className="absolute left-0 -top-0.5 text-sm">{formatTime(currentSeconds)}</div>
|
||||||
|
<div className="absolute right-0 -top-0.5 text-sm">{duration}</div>
|
||||||
|
|
||||||
|
<div className="w-full h-1 bg-gray-200 rounded-full">
|
||||||
|
<div className="h-full bg-primary rounded-full" style={{ width: `${progress}%` }} />
|
||||||
|
<div
|
||||||
|
className="absolute top-1/2 -translate-y-1/2 size-3 bg-white rounded-full shadow-lg"
|
||||||
|
style={{ left: `${progress}%`, marginLeft: "-6px" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,101 +1,127 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from "react"
|
||||||
import Image from 'next/image';
|
import Image from "next/image"
|
||||||
import { Play, SkipBack, SkipForward } from 'lucide-react';
|
import { Play, SkipBack, SkipForward } from "lucide-react"
|
||||||
import LoadingSpinner from '../objects/LoadingSpinner';
|
import LoadingSpinner from "../objects/LoadingSpinner"
|
||||||
|
import { SeekBar } from "@/components/objects/SeekBar"
|
||||||
|
|
||||||
interface Song {
|
interface Song {
|
||||||
albumArt: string;
|
albumArt: string
|
||||||
name: string;
|
name: string
|
||||||
artist: string;
|
artist: string
|
||||||
duration: string;
|
duration: string
|
||||||
link?: string;
|
link?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Period {
|
interface Period {
|
||||||
timePeriod: string;
|
timePeriod: string
|
||||||
songs: Song[];
|
songs: Song[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [timePeriod, setTimePeriod] = useState('Early Summer 2024');
|
const [timePeriod, setTimePeriod] = useState("Early Summer 2024")
|
||||||
const [songs, setSongs] = useState<Song[]>([]);
|
const [songs, setSongs] = useState<Song[]>([])
|
||||||
const [currentIndex, setCurrentIndex] = useState(0);
|
const [currentIndex, setCurrentIndex] = useState(0)
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
const [currentPosition, setCurrentPosition] = useState(0)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsLoading(true);
|
setIsLoading(true)
|
||||||
fetch('/data/music.json')
|
fetch("/data/music.json")
|
||||||
.then(response => response.json())
|
.then((response) => response.json())
|
||||||
.then((data: Period[]) => {
|
.then((data: Period[]) => {
|
||||||
const selectedPeriod = data.find((period) => period.timePeriod === timePeriod);
|
const selectedPeriod = data.find((period) => period.timePeriod === timePeriod)
|
||||||
const songsList = selectedPeriod ? selectedPeriod.songs : [];
|
const songsList = selectedPeriod ? selectedPeriod.songs : []
|
||||||
setSongs(songsList);
|
setSongs(songsList)
|
||||||
setCurrentIndex(Math.floor(Math.random() * songsList.length));
|
const newIndex = Math.floor(Math.random() * songsList.length)
|
||||||
setIsLoading(false);
|
setCurrentIndex(newIndex)
|
||||||
|
// Set initial random position for the selected song
|
||||||
|
if (songsList.length > 0) {
|
||||||
|
const durationInSeconds = parseDuration(songsList[newIndex]?.duration || "0:00")
|
||||||
|
setCurrentPosition(Math.floor(Math.random() * durationInSeconds))
|
||||||
|
}
|
||||||
|
setIsLoading(false)
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
console.error('Error fetching music data:', error);
|
console.error("Error fetching music data:", error)
|
||||||
setIsLoading(false);
|
setIsLoading(false)
|
||||||
});
|
})
|
||||||
}, [timePeriod]);
|
}, [timePeriod])
|
||||||
|
|
||||||
const handleNext = () => {
|
const handleNext = () => {
|
||||||
setCurrentIndex((currentIndex + 1) % songs.length);
|
setCurrentIndex((prevIndex) => {
|
||||||
};
|
const nextIndex = (prevIndex + 1) % songs.length
|
||||||
|
const durationInSeconds = parseDuration(songs[nextIndex].duration)
|
||||||
|
setCurrentPosition(Math.floor(Math.random() * durationInSeconds))
|
||||||
|
return nextIndex
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const handlePrevious = () => {
|
const handlePrevious = () => {
|
||||||
setCurrentIndex((currentIndex - 1 + songs.length) % songs.length);
|
setCurrentIndex((prevIndex) => {
|
||||||
};
|
const nextIndex = (prevIndex - 1 + songs.length) % songs.length
|
||||||
|
const durationInSeconds = parseDuration(songs[nextIndex].duration)
|
||||||
|
setCurrentPosition(Math.floor(Math.random() * durationInSeconds))
|
||||||
|
return nextIndex
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseDuration = (duration: string): number => {
|
||||||
|
const [minutes, seconds] = duration.split(":").map(Number)
|
||||||
|
return minutes * 60 + seconds
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-2xl mx-auto">
|
<div>
|
||||||
<section id="music-carousel" className="mb-12">
|
<section id="music-carousel" className="mb-12">
|
||||||
<h2 className="text-3xl font-semibold mb-4 text-gray-200">Music By Time Period</h2>
|
|
||||||
<div className="mb-4 pb-4">
|
|
||||||
<label htmlFor="timePeriod" className="text-gray-300">Time Period:</label>
|
|
||||||
<select
|
|
||||||
id="timePeriod"
|
|
||||||
value={timePeriod}
|
|
||||||
onChange={(e) => setTimePeriod(e.target.value)}
|
|
||||||
className="ml-2 p-2 bg-gray-700 text-gray-300 rounded-sm"
|
|
||||||
>
|
|
||||||
<option value="Early Summer 2024">Early Summer 2024</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isLoading && <LoadingSpinner />}
|
{isLoading && <LoadingSpinner />}
|
||||||
|
|
||||||
{!isLoading && songs.length > 0 && (
|
{!isLoading && songs.length > 0 && (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="text-center">
|
<Image
|
||||||
<Image
|
src={songs[currentIndex].albumArt || "/placeholder.svg"}
|
||||||
src={songs[currentIndex].albumArt}
|
alt={songs[currentIndex].name}
|
||||||
alt={songs[currentIndex].name}
|
width={300}
|
||||||
width={300}
|
height={300}
|
||||||
height={300}
|
className="mb-4 rounded-lg"
|
||||||
className="mx-auto mb-4 rounded-lg"
|
/>
|
||||||
/>
|
<h3 className="text-2xl font-bold text-gray-100">{songs[currentIndex].name}</h3>
|
||||||
<h3 className="text-2xl font-bold text-gray-100">{songs[currentIndex].name}</h3>
|
<p>{songs[currentIndex].artist}</p>
|
||||||
<p className="text-gray-300">{songs[currentIndex].artist}</p>
|
<SeekBar
|
||||||
<p className="text-gray-300">{songs[currentIndex].duration}</p>
|
key={`${currentIndex}-${currentPosition}`}
|
||||||
<div className="mt-4">
|
startPos={currentPosition}
|
||||||
<button onClick={handlePrevious} className="mr-4 text-gray-300">
|
duration={songs[currentIndex].duration}
|
||||||
<SkipBack className="w-8 h-8" />
|
/>
|
||||||
</button>
|
<div className="flex justify-center pb-2">
|
||||||
<button className="mr-4 text-gray-300" onClick={() => window.open(songs[currentIndex]?.link, '_blank')}>
|
<button onClick={handlePrevious} className="mr-4 cursor-pointer">
|
||||||
<Play className="w-8 h-8" />
|
<SkipBack className="w-8 h-8" />
|
||||||
</button>
|
</button>
|
||||||
<button onClick={handleNext} className="text-gray-300">
|
<button className="mr-4 cursor-pointer" onClick={() => window.open(songs[currentIndex]?.link, "_blank")}>
|
||||||
<SkipForward className="w-8 h-8" />
|
<Play className="w-8 h-8" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
<button onClick={handleNext} className="cursor-pointer">
|
||||||
|
<SkipForward className="w-8 h-8" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-col items-center mt-4">
|
||||||
|
<label htmlFor="timePeriod" className="font-bold uppercase text-sm pb-1">
|
||||||
|
Time Period
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="timePeriod"
|
||||||
|
value={timePeriod}
|
||||||
|
onChange={(e) => setTimePeriod(e.target.value)}
|
||||||
|
className="px-3 py-2 bg-gray-700 rounded-sm mb-2"
|
||||||
|
>
|
||||||
|
<option value="Early Summer 2024">Early Summer 2024</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user