From 8faf5de77f731d31583f2ed9dacd25be3df3964e Mon Sep 17 00:00:00 2001 From: codey Date: Thu, 24 Jul 2025 10:06:36 -0400 Subject: [PATCH] add memes --- src/assets/styles/MemeGrid.css | 24 ++++++ src/components/AppLayout.jsx | 4 +- src/components/AudioPlayer.jsx | 7 ++ src/components/Memes.jsx | 132 +++++++++++++++++++++++++++++++++ src/components/Nav.astro | 1 + src/pages/memes.astro | 13 ++++ 6 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 src/assets/styles/MemeGrid.css create mode 100644 src/components/Memes.jsx create mode 100644 src/pages/memes.astro diff --git a/src/assets/styles/MemeGrid.css b/src/assets/styles/MemeGrid.css new file mode 100644 index 0000000..2ee82a5 --- /dev/null +++ b/src/assets/styles/MemeGrid.css @@ -0,0 +1,24 @@ +.grid-container { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1rem; + padding: 1rem; +} + +.grid-item { + width: 100%; + aspect-ratio: 1/1; + overflow: hidden; +} + +.meme-img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.loading { + text-align: center; + margin: 2rem 0; +} diff --git a/src/components/AppLayout.jsx b/src/components/AppLayout.jsx index 57965be..8c3c074 100644 --- a/src/components/AppLayout.jsx +++ b/src/components/AppLayout.jsx @@ -2,6 +2,7 @@ import { toast } from 'react-toastify'; import { API_URL } from '../config'; import { Player } from './AudioPlayer.jsx'; +import Memes from './Memes.jsx'; import { JoyUIRootIsland } from './Components.jsx'; import { PrimeReactProvider } from "primereact/api"; import CustomToastContainer from '../components/ToastProvider.jsx'; @@ -29,7 +30,8 @@ export default function Root({child}) { Work in progress... bugs are to be expected. */} {child == "LyricSearch" && ()} - {child == "Player" && ()} + {child == "Player" && ()} + {child == "Memes" && } ); diff --git a/src/components/AudioPlayer.jsx b/src/components/AudioPlayer.jsx index ed7f286..3822663 100644 --- a/src/components/AudioPlayer.jsx +++ b/src/components/AudioPlayer.jsx @@ -1,5 +1,6 @@ import { useState, useEffect, useRef } from "react"; import { Howl, Howler } from "howler"; +import { metaData } from "../config"; import Play from "@mui/icons-material/PlayArrow"; import Pause from "@mui/icons-material/Pause"; import "@styles/player.css"; @@ -119,6 +120,10 @@ useEffect(() => { clearGlobalMetadataInterval(); currentStationForInterval = activeStation; + const setPageTitle = (artist, song) => { + document.title = metaData.title + ` - Radio - ${artist} - ${song} [${activeStation}]`; + } + const fetchTrackData = async () => { try { const response = await fetch(`${API_URL}/radio/np?station=${activeStation}`, { @@ -156,6 +161,7 @@ useEffect(() => { setTrackArtist(data.artist); setTrackGenre(data.genre !== "N/A" ? data.genre : ""); setTrackAlbum(data.album); + setPageTitle(data.artist, data.song); setCoverArt(`${API_URL}/radio/album_art?station=${activeStation}&_=${Date.now()}`); setElapsed(data.elapsed); setDuration(data.duration); @@ -182,6 +188,7 @@ const progressColorClass = : "bg-blue-500 dark:bg-blue-400"; + return ( <>
diff --git a/src/components/Memes.jsx b/src/components/Memes.jsx new file mode 100644 index 0000000..e398e1b --- /dev/null +++ b/src/components/Memes.jsx @@ -0,0 +1,132 @@ +import { useEffect, useState, useRef, useCallback } from "react"; +import { ProgressSpinner } from 'primereact/progressspinner'; +import { Dialog } from 'primereact/dialog'; +import { Image } from 'primereact/image'; +import { toast } from 'react-toastify'; +import { API_URL } from '../config'; + +const MEME_API_URL = `${API_URL}/memes/list_memes`; +const BASE_IMAGE_URL = "https://codey.lol/meme"; + +const Memes = () => { + const [images, setImages] = useState([]); + const [page, setPage] = useState(1); + const [loading, setLoading] = useState(false); + const [hasMore, setHasMore] = useState(true); + const [selectedImage, setSelectedImage] = useState(null); + const observerRef = useRef(); + + const loadImages = async (pageNum, attempt = 0) => { + if (loading || !hasMore) return; + setLoading(true); + try { + const res = await fetch(`${MEME_API_URL}?page=${pageNum}`); + + if (res.status === 429) { + const backoff = Math.min(1000 * Math.pow(2, attempt), 10000); // max 10s + console.warn(`Rate limited. Retrying in ${backoff}ms`); + setTimeout(() => { + loadImages(pageNum, attempt + 1); + }, backoff); + return; + } + + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const data = await res.json(); + + const newMemes = data?.memes || []; + + if (newMemes.length === 0 || data.paging.current >= data.paging.of) { + setHasMore(false); + } + + const imageObjects = newMemes.map(m => ({ + id: m.id, + url: `${BASE_IMAGE_URL}/${m.id}.png` + })); + + setImages(prev => [...prev, ...imageObjects]); + setPage(prev => prev + 1); + } catch (e) { + console.error("Failed to load memes", e); + toast.error("Failed to load more memes."); + } finally { + setLoading(false); + } + }; + + const lastImageRef = useCallback(node => { + if (loading) return; + if (observerRef.current) observerRef.current.disconnect(); + observerRef.current = new IntersectionObserver(entries => { + if (entries[0].isIntersecting && hasMore) { + loadImages(page); + } + }); + if (node) observerRef.current.observe(node); + }, [loading, hasMore, page]); + + useEffect(() => { + loadImages(1); + }, []); + + return ( + <> +
+ {images.map((img, i) => { + const isLast = i === images.length - 1; + return ( +
setSelectedImage(img)} + style={{ cursor: 'pointer' }} + > + {`meme-${img.id}`} +
+ ); + })} + {loading && ( +
+ +
+ )} +
+ + {/* Dialog for enlarged image */} + setSelectedImage(null)} + style={{ width: '90vw', maxWidth: '720px' }} + modal + closable + dismissableMask={true} + > + {selectedImage && ( + {`meme-${selectedImage.id}`} + + )} + + + ); +}; + +export default Memes; diff --git a/src/components/Nav.astro b/src/components/Nav.astro index b628364..d1dddb1 100644 --- a/src/components/Nav.astro +++ b/src/components/Nav.astro @@ -6,6 +6,7 @@ import ExitToApp from '@mui/icons-material/ExitToApp'; const navItems = [ { label: "Home", href: "/" }, { label: "Radio", href: "/radio" }, + { label: "Memes", href: "/memes" }, { blockSeparator: true }, { label: "Status", href: "https://status.boatson.boats", icon: ExitToApp }, { label: "Git", href: "https://kode.boatson.boats", icon: ExitToApp }, diff --git a/src/pages/memes.astro b/src/pages/memes.astro new file mode 100644 index 0000000..4472810 --- /dev/null +++ b/src/pages/memes.astro @@ -0,0 +1,13 @@ +--- +import Base from "../layouts/Base.astro"; +import Root from "../components/AppLayout.jsx"; +import "@styles/MemeGrid.css"; +--- + + +
+
+ + +
+