Compare commits
3 Commits
e709c35bc1
...
d85d8697bc
Author | SHA1 | Date | |
---|---|---|---|
d85d8697bc | |||
8faf5de77f | |||
0ee80f4b49 |
24
src/assets/styles/MemeGrid.css
Normal file
24
src/assets/styles/MemeGrid.css
Normal file
@@ -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;
|
||||
}
|
@@ -242,3 +242,4 @@ Custom
|
||||
overflow-y: auto !important;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
|
@@ -100,7 +100,7 @@ body {
|
||||
}
|
||||
|
||||
.station-tabs {
|
||||
padding-top: 2%;
|
||||
padding-top: 4%;
|
||||
}
|
||||
|
||||
/* Music bar and progress */
|
||||
|
@@ -2,10 +2,9 @@
|
||||
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 Alert from '@mui/joy/Alert';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
import CustomToastContainer from '../components/ToastProvider.jsx';
|
||||
import LyricSearch from './LyricSearch.jsx';
|
||||
import 'primereact/resources/themes/bootstrap4-light-blue/theme.css';
|
||||
@@ -23,15 +22,16 @@ export default function Root({child}) {
|
||||
newestOnTop={true}
|
||||
closeOnClick={true}/>
|
||||
<JoyUIRootIsland>
|
||||
<Alert
|
||||
{/* <Alert
|
||||
className="alert"
|
||||
startDecorator={<WarningIcon />}
|
||||
variant="soft"
|
||||
color="danger">
|
||||
Work in progress... bugs are to be expected.
|
||||
</Alert>
|
||||
</Alert> */}
|
||||
{child == "LyricSearch" && (<LyricSearch client:only="react" />)}
|
||||
{child == "Player" && (<Player client:only="react"></Player>)}
|
||||
{child == "Player" && (<Player client:only="react" />)}
|
||||
{child == "Memes" && <Memes client:only="react" />}
|
||||
</JoyUIRootIsland>
|
||||
</PrimeReactProvider>
|
||||
);
|
||||
|
@@ -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 (
|
||||
<>
|
||||
<div className="station-tabs flex gap-2 justify-center mb-4 flex-wrap z-10 relative">
|
||||
@@ -224,10 +231,6 @@ const progressColorClass =
|
||||
style={{ width: `${progress}%` }}
|
||||
></div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<div className="music-control">
|
||||
<div
|
||||
className="music-control__play"
|
||||
|
132
src/components/Memes.jsx
Normal file
132
src/components/Memes.jsx
Normal file
@@ -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 (
|
||||
<>
|
||||
<div className="grid-container">
|
||||
{images.map((img, i) => {
|
||||
const isLast = i === images.length - 1;
|
||||
return (
|
||||
<div
|
||||
key={img.id}
|
||||
className="grid-item"
|
||||
ref={isLast ? lastImageRef : null}
|
||||
onClick={() => setSelectedImage(img)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<Image
|
||||
src={img.url}
|
||||
alt={`meme-${img.id}`}
|
||||
imageClassName="meme-img"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{loading && (
|
||||
<div className="loading">
|
||||
<ProgressSpinner style={{ width: '50px', height: '50px' }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Dialog for enlarged image */}
|
||||
<Dialog
|
||||
header={`Meme #${selectedImage?.id}`}
|
||||
visible={!!selectedImage}
|
||||
onHide={() => setSelectedImage(null)}
|
||||
style={{ width: '90vw', maxWidth: '720px' }}
|
||||
modal
|
||||
closable
|
||||
dismissableMask={true}
|
||||
>
|
||||
{selectedImage && (
|
||||
<img
|
||||
src={selectedImage.url}
|
||||
alt={`meme-${selectedImage.id}`}
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
maxHeight: '70vh', // restrict height to viewport height
|
||||
objectFit: 'contain',
|
||||
display: 'block',
|
||||
margin: '0 auto',
|
||||
borderRadius: '6px',
|
||||
}}
|
||||
/>
|
||||
|
||||
)}
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Memes;
|
@@ -9,7 +9,7 @@ import Themes from "astro-themes";
|
||||
import { ViewTransitions } from "astro:transitions";
|
||||
|
||||
import BaseHead from "../components/BaseHead.astro";
|
||||
import Navbar from "../components/Nav.astro";
|
||||
import Navbar from "./Nav.astro";
|
||||
import Footer from "../components/Footer.astro";
|
||||
|
||||
import "@fontsource/geist-sans/400.css";
|
||||
|
13
src/pages/memes.astro
Normal file
13
src/pages/memes.astro
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
import Base from "../layouts/Base.astro";
|
||||
import Root from "../components/AppLayout.jsx";
|
||||
import "@styles/MemeGrid.css";
|
||||
---
|
||||
|
||||
<Base>
|
||||
<section>
|
||||
<div class="prose prose-neutral dark:prose-invert">
|
||||
<Root child="Memes" client:only="react">
|
||||
</Root>
|
||||
</section>
|
||||
</Base>
|
Reference in New Issue
Block a user