navbar changes, radio/webplayer rewrite + additional stations
This commit is contained in:
@ -23,9 +23,13 @@ initialize = () => {
|
|||||||
var sound = new Howl({
|
var sound = new Howl({
|
||||||
src: ["https://stream.codey.lol/sfm.ogg"],
|
src: ["https://stream.codey.lol/sfm.ogg"],
|
||||||
html5: true,
|
html5: true,
|
||||||
// onend: () => { sound.unload(); }
|
onloaderror: (id) => { console.log(`Fail: ${id}`) },
|
||||||
|
onplayerror: (id) => { console.log(`Fail b: ${id}`)},
|
||||||
|
onend: (e) => { sound.play(); }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log(sound);
|
||||||
|
|
||||||
let currentTime = 0;
|
let currentTime = 0;
|
||||||
let currentDuration = 0;
|
let currentDuration = 0;
|
||||||
let currentUUID = null;
|
let currentUUID = null;
|
||||||
|
@ -182,9 +182,9 @@ Custom
|
|||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
#alert {
|
.alert {
|
||||||
margin-top: 5px;
|
margin-top: 10%;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 1%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#spinner {
|
#spinner {
|
||||||
|
@ -53,6 +53,10 @@ section {
|
|||||||
padding: 1em;
|
padding: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.station-tabs {
|
||||||
|
top: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
.music-container {
|
.music-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@ -64,6 +68,7 @@ section {
|
|||||||
max-width: 700px;
|
max-width: 700px;
|
||||||
box-shadow: 1px 1px 5px 0 rgba(0, 0, 0, 0.3);
|
box-shadow: 1px 1px 5px 0 rgba(0, 0, 0, 0.3);
|
||||||
max-height: 290px;
|
max-height: 290px;
|
||||||
|
margin-bottom: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.album-cover {
|
.album-cover {
|
||||||
|
@ -8,7 +8,7 @@ import Alert from '@mui/joy/Alert';
|
|||||||
import WarningIcon from '@mui/icons-material/Warning';
|
import WarningIcon from '@mui/icons-material/Warning';
|
||||||
import CustomToastContainer from '../components/ToastProvider.jsx';
|
import CustomToastContainer from '../components/ToastProvider.jsx';
|
||||||
import LyricSearch from './LyricSearch.jsx';
|
import LyricSearch from './LyricSearch.jsx';
|
||||||
import 'primereact/resources/themes/bootstrap4-light-blue/theme.css'; // TEMP
|
import 'primereact/resources/themes/bootstrap4-light-blue/theme.css';
|
||||||
import 'primereact/resources/primereact.min.css';
|
import 'primereact/resources/primereact.min.css';
|
||||||
|
|
||||||
export default function Root({child}) {
|
export default function Root({child}) {
|
||||||
@ -24,6 +24,7 @@ export default function Root({child}) {
|
|||||||
closeOnClick={true}/>
|
closeOnClick={true}/>
|
||||||
<JoyUIRootIsland>
|
<JoyUIRootIsland>
|
||||||
<Alert
|
<Alert
|
||||||
|
className="alert"
|
||||||
startDecorator={<WarningIcon />}
|
startDecorator={<WarningIcon />}
|
||||||
variant="soft"
|
variant="soft"
|
||||||
color="danger">
|
color="danger">
|
||||||
|
@ -1,57 +1,243 @@
|
|||||||
import {useState, React} from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import jQuery from "jquery";
|
import { Howl, Howler } from "howler";
|
||||||
import Play from '@mui/icons-material/PlayArrow';
|
import Play from "@mui/icons-material/PlayArrow";
|
||||||
import Pause from '@mui/icons-material/Pause';
|
import Pause from "@mui/icons-material/Pause";
|
||||||
<style>
|
import "@styles/player.css";
|
||||||
@import "@styles/player.css";
|
|
||||||
</style>
|
|
||||||
|
|
||||||
export const render = false;
|
const API_URL = "https://api.codey.lol";
|
||||||
|
Howler.html5PoolSize = 32;
|
||||||
|
|
||||||
const PlayIcon = () => {
|
const STATIONS = {
|
||||||
return (
|
main: { label: "Main", streamPath: "/sfm.ogg" },
|
||||||
<Play />
|
rock: { label: "Rock", streamPath: "/rock.ogg" },
|
||||||
);
|
rap: { label: "Rap", streamPath: "/rap.ogg" },
|
||||||
|
electronic: { label: "Electronic", streamPath: "/electronic.ogg" },
|
||||||
|
classical: { label: "Classical", streamPath: "/classical.ogg" },
|
||||||
|
pop: { label: "Pop", streamPath: "/pop.ogg" },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Global interval tracking (required for Astro + client:only)
|
||||||
|
let activeInterval = null;
|
||||||
|
let currentStationForInterval = null;
|
||||||
|
|
||||||
|
function clearGlobalMetadataInterval() {
|
||||||
|
if (activeInterval) {
|
||||||
|
clearInterval(activeInterval);
|
||||||
|
activeInterval = null;
|
||||||
|
currentStationForInterval = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PauseIcon = () => {
|
|
||||||
return (
|
|
||||||
<Pause />
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Player() {
|
export function Player() {
|
||||||
const [isPlaying, setPlaying] = useState(false);
|
const [activeStation, setActiveStation] = useState("main");
|
||||||
window.isPlaying = isPlaying;
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
|
const [trackTitle, setTrackTitle] = useState("");
|
||||||
|
const [trackArtist, setTrackArtist] = useState("");
|
||||||
|
const [trackGenre, setTrackGenre] = useState("");
|
||||||
|
const [coverArt, setCoverArt] = useState("/images/radio_art_default.jpg");
|
||||||
|
const [elapsed, setElapsed] = useState(0);
|
||||||
|
const [duration, setDuration] = useState(0);
|
||||||
|
|
||||||
|
const soundRef = useRef(null);
|
||||||
|
const uuidRef = useRef(null);
|
||||||
|
const lastStationData = useRef(null);
|
||||||
|
|
||||||
|
const formatTime = (seconds) => {
|
||||||
|
const m = String(Math.floor(seconds / 60)).padStart(2, "0");
|
||||||
|
const s = String(Math.floor(seconds % 60)).padStart(2, "0");
|
||||||
|
return `${m}:${s}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const progress = duration > 0 ? (elapsed / duration) * 100 : 0;
|
||||||
|
|
||||||
|
// Create Howl instance on activeStation change
|
||||||
|
useEffect(() => {
|
||||||
|
if (soundRef.current) {
|
||||||
|
soundRef.current.unload();
|
||||||
|
soundRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const streamUrl = "https://stream.codey.lol" + STATIONS[activeStation].streamPath;
|
||||||
|
|
||||||
|
const howl = new Howl({
|
||||||
|
src: [streamUrl],
|
||||||
|
html5: true,
|
||||||
|
onend: () => howl.play(),
|
||||||
|
onplay: () => setIsPlaying(true),
|
||||||
|
onpause: () => setIsPlaying(false),
|
||||||
|
onstop: () => setIsPlaying(false),
|
||||||
|
onloaderror: (id, err) => console.error("Load error", err),
|
||||||
|
onplayerror: (id, err) => {
|
||||||
|
console.error("Play error", err);
|
||||||
|
setTimeout(() => howl.play(), 1000);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
soundRef.current = howl;
|
||||||
|
uuidRef.current = null;
|
||||||
|
lastStationData.current = null;
|
||||||
|
|
||||||
|
// Autoplay if previously playing
|
||||||
|
if (isPlaying) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (soundRef.current) {
|
||||||
|
soundRef.current.play();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
howl.stop();
|
||||||
|
howl.unload();
|
||||||
|
};
|
||||||
|
}, [activeStation]);
|
||||||
|
|
||||||
|
const togglePlayback = () => {
|
||||||
|
if (isPlaying) {
|
||||||
|
if (soundRef.current) {
|
||||||
|
soundRef.current.pause();
|
||||||
|
}
|
||||||
|
setIsPlaying(false);
|
||||||
|
} else {
|
||||||
|
// If a previous Howl exists, unload it
|
||||||
|
if (soundRef.current) {
|
||||||
|
soundRef.current.stop();
|
||||||
|
soundRef.current.unload();
|
||||||
|
}
|
||||||
|
|
||||||
|
const streamUrl =
|
||||||
|
"https://stream.codey.lol" +
|
||||||
|
STATIONS[activeStation].streamPath +
|
||||||
|
`?t=${Date.now()}`; // Cache-busting param
|
||||||
|
|
||||||
|
const newHowl = new Howl({
|
||||||
|
src: [streamUrl],
|
||||||
|
html5: true,
|
||||||
|
onend: () => newHowl.play(),
|
||||||
|
onplay: () => {
|
||||||
|
setIsPlaying(true);
|
||||||
|
},
|
||||||
|
onpause: () => setIsPlaying(false),
|
||||||
|
onstop: () => setIsPlaying(false),
|
||||||
|
onloaderror: (_, err) => console.error("Load error", err),
|
||||||
|
onplayerror: (_, err) => {
|
||||||
|
console.error("Play error", err);
|
||||||
|
setTimeout(() => newHowl.play(), 1000);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
soundRef.current = newHowl;
|
||||||
|
newHowl.play();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Metadata fetcher: global-safe
|
||||||
|
useEffect(() => {
|
||||||
|
clearGlobalMetadataInterval();
|
||||||
|
|
||||||
|
currentStationForInterval = activeStation;
|
||||||
|
|
||||||
|
const fetchTrackData = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_URL}/radio/np?station=${activeStation}`, {
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Ignore stale interval calls
|
||||||
|
if (currentStationForInterval !== activeStation) return;
|
||||||
|
|
||||||
|
if (data.artist === "N/A" && data.song === "N/A") {
|
||||||
|
if (lastStationData.current !== "offline") {
|
||||||
|
setTrackTitle("Offline");
|
||||||
|
setTrackArtist("");
|
||||||
|
setTrackGenre("");
|
||||||
|
setCoverArt("/images/radio_art_default.jpg");
|
||||||
|
setElapsed(0);
|
||||||
|
setDuration(0);
|
||||||
|
lastStationData.current = "offline";
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.uuid === uuidRef.current) {
|
||||||
|
if (lastStationData.current === data.uuid) {
|
||||||
|
setElapsed(data.elapsed);
|
||||||
|
setDuration(data.duration);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uuidRef.current = data.uuid;
|
||||||
|
lastStationData.current = data.uuid;
|
||||||
|
setTrackTitle(data.song);
|
||||||
|
setTrackArtist(data.artist);
|
||||||
|
setTrackGenre(data.genre !== "N/A" ? data.genre : "");
|
||||||
|
setCoverArt(`${API_URL}/radio/album_art?station=${activeStation}&_=${Date.now()}`);
|
||||||
|
setElapsed(data.elapsed);
|
||||||
|
setDuration(data.duration);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch track data:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchTrackData();
|
||||||
|
activeInterval = setInterval(fetchTrackData, 1000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearGlobalMetadataInterval();
|
||||||
|
};
|
||||||
|
}, [activeStation]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
|
<div className="station-tabs flex gap-2 justify-center mb-4 flex-wrap z-10 relative">
|
||||||
|
{Object.entries(STATIONS).map(([key, { label }]) => (
|
||||||
|
<button
|
||||||
|
key={key}
|
||||||
|
className={`px-3 py-1 rounded-full text-sm font-semibold transition-colors ${
|
||||||
|
activeStation === key
|
||||||
|
? "bg-neutral-800 text-white dark:bg-white dark:text-black"
|
||||||
|
: "bg-neutral-200 text-black dark:bg-neutral-700 dark:text-white"
|
||||||
|
}`}
|
||||||
|
onClick={() => setActiveStation(key)}
|
||||||
|
aria-pressed={activeStation === key}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="c-containter">
|
<div className="c-containter">
|
||||||
<div className="music-container">
|
<div className="music-container mt-8">
|
||||||
<section className="album-cover">
|
<section className="album-cover">
|
||||||
|
<img src={coverArt} className="cover" alt="Cover Art" />
|
||||||
<img src="https://api.codey.lol/radio/album_art" className="cover" alt="Cover Art" />
|
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
<section className="music-player">
|
<section className="music-player">
|
||||||
<h1 className='music-player__header'>serious.FM</h1>
|
<h1 className="music-player__header">serious.FM</h1>
|
||||||
<h1 className="music-player__title"></h1>
|
<h1 className="music-player__title">{trackTitle}</h1>
|
||||||
<h2 className="music-player__author"></h2>
|
<h2 className="music-player__author">{trackArtist}</h2>
|
||||||
<h2 className="music-player__genre"></h2>
|
{trackGenre && <h2 className="music-player__genre">{trackGenre}</h2>}
|
||||||
<div className="music-time">
|
<div className="music-time">
|
||||||
<p className="music-time__current"></p>
|
<p className="music-time__current">{formatTime(elapsed)}</p>
|
||||||
<p className="music-time__last"></p>
|
<p className="music-time__last">{formatTime(duration - elapsed)}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="music-bar" id="progress">
|
<div className="music-bar" id="progress">
|
||||||
<div id="length"></div>
|
<div id="length" style={{ width: `${progress}%` }}></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="music-control">
|
<div className="music-control">
|
||||||
<div className="music-control__play" id="play">
|
<div className="music-control__play" id="play">
|
||||||
{isPlaying == false && (<Play onClick={(e) => { setPlaying(!isPlaying); togglePlayback(); } } />)}
|
{!isPlaying ? (
|
||||||
{isPlaying && (<Pause onClick={(e) => { setPlaying(!isPlaying); togglePlayback(); }} />)}
|
<Play onClick={togglePlayback} />
|
||||||
|
) : (
|
||||||
|
<Pause onClick={togglePlayback} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
)};
|
);
|
||||||
|
}
|
||||||
|
@ -9,7 +9,7 @@ import { metaData } from "../config";
|
|||||||
import { SEO } from "astro-seo";
|
import { SEO } from "astro-seo";
|
||||||
import { getImagePath } from "astro-opengraph-images";
|
import { getImagePath } from "astro-opengraph-images";
|
||||||
import { JoyUIRootIsland } from "./Components"
|
import { JoyUIRootIsland } from "./Components"
|
||||||
import { useHtmlThemeAttr } from "../hooks/useHtmlThemeAttr"; // your existing theme hook
|
import { useHtmlThemeAttr } from "../hooks/useHtmlThemeAttr";
|
||||||
import { usePrimeReactThemeSwitcher } from "../hooks/usePrimeReactThemeSwitcher";
|
import { usePrimeReactThemeSwitcher } from "../hooks/usePrimeReactThemeSwitcher";
|
||||||
|
|
||||||
const { title, description = metaData.description, image } = Astro.props;
|
const { title, description = metaData.description, image } = Astro.props;
|
||||||
|
@ -4,6 +4,9 @@ import RandomMsg from "../components/RandomMsg";
|
|||||||
import { buildTime } from '../utils/buildTime.js';
|
import { buildTime } from '../utils/buildTime.js';
|
||||||
|
|
||||||
const YEAR = new Date().getFullYear();
|
const YEAR = new Date().getFullYear();
|
||||||
|
var ENVIRONMENT = (Astro.url.hostname === "localhost") ? "Dev": "Prod";
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<small class="block lg:mt-24 mt-16 text-[#1C1C1C] dark:text-[#D4D4D4] footer-text">
|
<small class="block lg:mt-24 mt-16 text-[#1C1C1C] dark:text-[#D4D4D4] footer-text">
|
||||||
@ -15,6 +18,7 @@ const YEAR = new Date().getFullYear();
|
|||||||
<div style="margin-top: 15px; bottom: 0%">
|
<div style="margin-top: 15px; bottom: 0%">
|
||||||
<small>Built: {buildTime} UTC</small>
|
<small>Built: {buildTime} UTC</small>
|
||||||
</div>
|
</div>
|
||||||
|
<span style="font-size: 0.75rem; font-style: italic; margin-left: 100%">{ENVIRONMENT}</span>
|
||||||
</div>
|
</div>
|
||||||
<style>
|
<style>
|
||||||
@media screen and (max-width: 480px) {
|
@media screen and (max-width: 480px) {
|
||||||
|
@ -2,79 +2,84 @@
|
|||||||
import { metaData } from "../config";
|
import { metaData } from "../config";
|
||||||
import { Icon } from "astro-icon/components";
|
import { Icon } from "astro-icon/components";
|
||||||
import ExitToApp from '@mui/icons-material/ExitToApp';
|
import ExitToApp from '@mui/icons-material/ExitToApp';
|
||||||
import HorizontalRuleIcon from '@mui/icons-material/HorizontalRule';
|
|
||||||
|
|
||||||
const navItems = {
|
const navItems = [
|
||||||
"/": { name: "Home", className: "", icon: null },
|
{ label: "Home", href: "/" },
|
||||||
"divider-1": { name: "", className: "", icon: HorizontalRuleIcon },
|
{ label: "Radio", href: "/radio" },
|
||||||
"/radio": { name: "Radio", className: "", icon: null },
|
{ blockSeparator: true },
|
||||||
"divider-2": { name: "", className: "", icon: HorizontalRuleIcon },
|
{ label: "Status", href: "https://status.boatson.boats", icon: ExitToApp },
|
||||||
"https://status.boatson.boats": { name: "Status", className: "", icon: ExitToApp },
|
{ label: "Git", href: "https://kode.boatson.boats", icon: ExitToApp },
|
||||||
"divider-3": { name: "", className: "", icon: HorizontalRuleIcon },
|
{ label: "Old Site", href: "https://old.codey.lol", icon: ExitToApp },
|
||||||
"https://kode.boatson.boats": { name: "Git", className: "", icon: ExitToApp },
|
];
|
||||||
"divider-4": { name: "", className: "", icon: HorizontalRuleIcon },
|
|
||||||
"https://old.codey.lol": { name: "Old Site", className: "", icon: ExitToApp },
|
const currentPath = Astro.url.pathname;
|
||||||
};
|
|
||||||
---
|
---
|
||||||
|
<script is:inline>
|
||||||
<nav class="lg:mb-16 mb-12 py-5">
|
toggleTheme = () => {
|
||||||
<div class="flex flex-col md:flex-row md:items-center justify-between">
|
console.log("Toggle!")
|
||||||
<div class="flex items-center">
|
const currentTheme = document.documentElement.getAttribute("data-theme");
|
||||||
<a href="/" class="text-3xl font-semibold header-text">
|
const newTheme = currentTheme === "dark" ? "light" : "dark";
|
||||||
|
document.dispatchEvent(new CustomEvent("set-theme", { detail: newTheme }));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<nav class="w-full px-4 py-4 bg-transparent">
|
||||||
|
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||||
|
<a href="/" class="text-xl font-semibold header-text whitespace-nowrap">
|
||||||
{metaData.title}
|
{metaData.title}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
|
||||||
<div class="flex flex-row gap-4 mt-6 md:mt-0 md:ml-auto items-center">
|
|
||||||
{
|
|
||||||
Object.entries(navItems).map(([path, { name, className, icon }]) =>
|
|
||||||
{
|
|
||||||
let isVisualSeparator = icon === HorizontalRuleIcon
|
|
||||||
|
|
||||||
if (isVisualSeparator) {
|
<ul class="flex flex-wrap items-center gap-2 text-sm text-neutral-700 dark:text-neutral-300">
|
||||||
|
{navItems.map((item, index) => {
|
||||||
|
if (item.blockSeparator) {
|
||||||
return (
|
return (
|
||||||
<HorizontalRuleIcon client:load
|
<li class="text-neutral-500 dark:text-neutral-500 px-2 select-none" aria-hidden="true">
|
||||||
size="sm"
|
‖
|
||||||
className="hr"
|
</li>
|
||||||
sx={{
|
);
|
||||||
color: 'inherit',
|
|
||||||
}} />
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isExternal = item.href?.startsWith("http");
|
||||||
|
const isActive = item.href === currentPath;
|
||||||
|
const nextItem = navItems[index + 1];
|
||||||
|
const shouldShowThinBar = nextItem && !nextItem.blockSeparator;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<li>
|
||||||
<a
|
<a
|
||||||
href={path}
|
href={item.href}
|
||||||
class={`transition-all hover:text-neutral-800 dark:hover:text-neutral-200 flex align-middle relative`}>
|
class={`flex items-center gap-1 px-2 py-1 rounded-md transition-colors
|
||||||
{name}
|
hover:bg-neutral-200 dark:hover:bg-neutral-800
|
||||||
|
${isActive ? "font-semibold underline underline-offset-4" : ""}`}
|
||||||
|
target={isExternal ? "_blank" : undefined}
|
||||||
|
rel={isExternal ? "noopener noreferrer" : undefined}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
{item.icon === ExitToApp && <ExitToApp className="w-4 h-4" client:load />}
|
||||||
</a>
|
</a>
|
||||||
)})
|
</li>
|
||||||
}
|
{shouldShowThinBar && (
|
||||||
|
<li class="text-neutral-400 dark:text-neutral-600 hidden sm:inline select-none" aria-hidden="true">
|
||||||
|
|
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<li>
|
||||||
<button
|
<button
|
||||||
id="theme-toggle"
|
id="theme-toggle"
|
||||||
aria-label="Toggle theme"
|
aria-label="Toggle theme"
|
||||||
class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90">
|
type="button"
|
||||||
|
class="flex items-center justify-center px-2 py-1 rounded-md hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-opacity"
|
||||||
|
onclick="toggleTheme()"
|
||||||
|
>
|
||||||
<Icon
|
<Icon
|
||||||
name="fa6-solid:circle-half-stroke"
|
name="fa6-solid:circle-half-stroke"
|
||||||
class="h-[14px] w-[14px] text-[#1c1c1c] dark:text-[#D4D4D4]"
|
class="h-4 w-4 text-[#1c1c1c] dark:text-[#D4D4D4]"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<script is:inline>
|
|
||||||
function setTheme(theme) {
|
|
||||||
document.dispatchEvent(new CustomEvent("set-theme", { detail: theme }));
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleTheme() {
|
|
||||||
const currentTheme = document.documentElement.getAttribute("data-theme");
|
|
||||||
const newTheme = currentTheme === "dark" ? "light" : "dark";
|
|
||||||
setTheme(newTheme);
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener("astro:page-load", () => {
|
|
||||||
document
|
|
||||||
.getElementById("theme-toggle")
|
|
||||||
.addEventListener("click", toggleTheme);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
@ -11,6 +11,5 @@ import "@styles/player.css";
|
|||||||
</Root>
|
</Root>
|
||||||
<script is:inline src="/scripts/jquery/dist/jquery.js" />
|
<script is:inline src="/scripts/jquery/dist/jquery.js" />
|
||||||
<script is:inline src="/scripts/howler/dist/howler.js" />
|
<script is:inline src="/scripts/howler/dist/howler.js" />
|
||||||
<script is:inline src="/scripts/player.js" />
|
|
||||||
</section>
|
</section>
|
||||||
</Base>
|
</Base>
|
||||||
|
Reference in New Issue
Block a user