From eca8c98f9238f5a995bdcd377fe17003ffb29816 Mon Sep 17 00:00:00 2001 From: johnruina Date: Sat, 1 Aug 2026 07:16:40 -0400 Subject: [PATCH] slideshow + multiple images per rating --- client/index.html | 2 +- client/src/App.tsx | 10 +-- client/src/context/ThemeContext.tsx | 5 +- client/src/pages/AdminPage.tsx | 99 +++++++++++++++++++++++------ client/src/pages/PollsPage.tsx | 4 +- client/src/pages/RatingsPage.tsx | 34 ++++++++-- server/prisma/schema.prisma | 2 +- server/src/index.ts | 18 ++++-- 8 files changed, 134 insertions(+), 40 deletions(-) diff --git a/client/index.html b/client/index.html index af8ca3f..d55eecc 100644 --- a/client/index.html +++ b/client/index.html @@ -5,7 +5,7 @@ - JRS - Community Voting Site + JohnRuina Site - Community Rating Site diff --git a/client/src/App.tsx b/client/src/App.tsx index 99dad9d..e856a80 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -6,7 +6,7 @@ import PollsPage from "./pages/PollsPage"; import RatingsPage from "./pages/RatingsPage"; import AdminPage from "./pages/AdminPage"; import Sidebar from "./components/Sidebar"; -import { ThemeProvider } from "./context/ThemeContext"; +import { ThemeProvider, useTheme } from "./context/ThemeContext"; import { VolumeProvider } from "./context/VolumeContext"; type User = { id: number; email: string; username: string }; @@ -17,6 +17,7 @@ function getCookie(name: string) { } function AppInner() { + const { descBg } = useTheme(); const [loading, setLoading] = useState(true); const [user, setUser] = useState(null); const [rateLimited, setRateLimited] = useState(false); @@ -24,8 +25,9 @@ function AppInner() { const [isMobile, setIsMobile] = useState(window.innerWidth <= 768); const sidebarPinned = localStorage.getItem("sidebarPinned") !== "false"; const [sidebarOpen, setSidebarOpen] = useState(false); - const [whatsNewText, setWhatsNewText] = useState(""); const [randomMessage, setRandomMessage] = useState(""); + const [updates, setUpdates] = useState<{ id: number; title: string; text: string; createdAt: string }[]>([]); + const [expandedUpdate, setExpandedUpdate] = useState(null); const navigate = useNavigate(); const location = useLocation(); const page = location.pathname.slice(1) || "home"; @@ -50,7 +52,7 @@ function AppInner() { }, []); useEffect(() => { - fetch("/api/whatsnew").then((r) => r.json()).then((d) => setWhatsNewText(d.text)).catch(() => {}); + fetch("/api/site-updates").then((r) => r.json()).then((d) => { if (Array.isArray(d)) setUpdates(d); }).catch(() => {}); fetch("/api/random-message").then((r) => r.json()).then((d) => setRandomMessage(d.text)).catch(() => {}); }, []); @@ -105,7 +107,7 @@ function AppInner() { )}
-
JOHNRUINA SITE
Simple little Limbus website.{'\n\n'}Currently a Limbus community ranking website, if it gets traction I'll add more rating categories and features.{'\n\n'}Glory to K Corp!
{randomMessage &&
{randomMessage} fetch("/api/random-message").then((r) => r.json()).then((d) => setRandomMessage(d.text)).catch(() => {})} style={{ cursor: "pointer", fontSize: 14, opacity: 0.6, flexShrink: 0 }} title="Reroll">↻
}{whatsNewText &&
What's New
{whatsNewText}
}
} /> +
JOHNRUINA SITE
Simple little Limbus website.{'\n\n'}Currently a Limbus community ranking website, if it gets traction I'll add more rating categories and features.{'\n\n'}Glory to K Corp!
{randomMessage &&
{randomMessage} fetch("/api/random-message").then((r) => r.json()).then((d) => setRandomMessage(d.text)).catch(() => {})} style={{ cursor: "pointer", fontSize: 14, opacity: 0.6, flexShrink: 0 }} title="Reroll">↻
}{updates.length > 0 &&
What's New — {new Date(updates[0].createdAt).toLocaleString("en-US", { timeZone: "America/New_York", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" })}
{updates[0].title}
{updates[0].text}
Site Updates
{updates.map((u) => (
setExpandedUpdate(expandedUpdate === u.id ? null : u.id)} style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", cursor: "pointer", padding: "6px 0" }}>{u.title}{new Date(u.createdAt).toLocaleString("en-US", { timeZone: "America/New_York", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" })}
{expandedUpdate === u.id &&
{u.text}
}
))}
}
} />
} /> } /> } /> diff --git a/client/src/context/ThemeContext.tsx b/client/src/context/ThemeContext.tsx index 73a1385..0015ec2 100644 --- a/client/src/context/ThemeContext.tsx +++ b/client/src/context/ThemeContext.tsx @@ -2,9 +2,10 @@ import { createContext, useContext, useEffect, type ReactNode } from "react"; type ThemeContextType = { dark: boolean; + descBg: string; }; -const ThemeContext = createContext({ dark: true }); +const ThemeContext = createContext({ dark: true, descBg: "#181818" }); export function ThemeProvider({ children }: { children: ReactNode }) { useEffect(() => { @@ -14,7 +15,7 @@ export function ThemeProvider({ children }: { children: ReactNode }) { }, []); return ( - + {children} ); diff --git a/client/src/pages/AdminPage.tsx b/client/src/pages/AdminPage.tsx index 4540162..2734a5a 100644 --- a/client/src/pages/AdminPage.tsx +++ b/client/src/pages/AdminPage.tsx @@ -17,7 +17,7 @@ type Rating = { id: number; ratingTypeId: number; name: string; - picture: string; + picture: string[]; description: string; icon: string; attachments: string; @@ -68,14 +68,14 @@ function AdminPage({ user }: Props) { const adminLoggedIn = user?.email === "invoictoan@gmail.com"; function ua(url: string) { const sep = url.includes("?") ? "&" : "?"; return `${url}${sep}sessionToken=${getCookie("sessionToken")}`; } - const [tab, setTab] = useState<"ratings" | "polls" | "assets" | "whatsnew" | "messages">("ratings"); + const [tab, setTab] = useState<"ratings" | "polls" | "assets" | "updates" | "messages">("ratings"); const [ratings, setRatings] = useState([]); const [ratingTypes, setRatingTypes] = useState<{ id: number; name: string; hasAttachments: boolean; categories: { id: number; name: string }[] }[]>([]); const [polls, setPolls] = useState([]); const [type, setType] = useState(""); const [name, setName] = useState(""); const [description, setDescription] = useState(""); - const [picture, setPicture] = useState(""); + const [picture, setPicture] = useState([]); const [icon, setIcon] = useState(""); const [attachments, setAttachments] = useState(""); @@ -135,7 +135,7 @@ function AdminPage({ user }: Props) { const [selectedRatingIds, setSelectedRatingIds] = useState>(new Set()); const [massTagId, setMassTagId] = useState(""); const [editingRating, setEditingRating] = useState(null); - const [editRatingData, setEditRatingData] = useState({ name: "", description: "", picture: "", icon: "", attachments: "" }); + const [editRatingData, setEditRatingData] = useState({ name: "", description: "", picture: [] as string[], icon: "", attachments: "" }); const [assets, setAssets] = useState<{ name: string; url: string }[]>([]); const [newAssetImage, setNewAssetImage] = useState(""); const [compressImages, setCompressImages] = useState(false); @@ -149,9 +149,11 @@ function AdminPage({ user }: Props) { const [showPicker, setShowPicker] = useState(false); const pickerCbRef = useRef<((url: string) => void) | null>(null); const [dragIdx, setDragIdx] = useState(null); - const [whatsNewText, setWhatsNewText] = useState(""); const [messages, setMessages] = useState<{ id: number; text: string }[]>([]); const [newMessageText, setNewMessageText] = useState(""); + const [updates, setUpdates] = useState<{ id: number; title: string; text: string; createdAt: string }[]>([]); + const [newUpdateTitle, setNewUpdateTitle] = useState(""); + const [newUpdateText, setNewUpdateText] = useState(""); function loadRatingTypes() { fetch("/api/rating-types").then((r) => r.json()).then((types) => { @@ -171,18 +173,18 @@ function AdminPage({ user }: Props) { loadAssets(); loadPollCategories(); loadPollTags(); - loadWhatsNew(); loadMessages(); + loadUpdates(); }, []); - function loadWhatsNew() { - fetch("/api/whatsnew").then((r) => r.json()).then((d) => setWhatsNewText(d.text)).catch(() => {}); - } - function loadMessages() { fetch(ua("/api/admin/messages")).then((r) => r.json()).then((d) => { if (Array.isArray(d)) setMessages(d); }).catch(() => {}); } + function loadUpdates() { + fetch("/api/site-updates").then((r) => r.json()).then((d) => { if (Array.isArray(d)) setUpdates(d); }).catch(() => {}); + } + async function handleRatingSubmit(e: FormEvent) { e.preventDefault(); if (!type) return; @@ -191,7 +193,7 @@ function AdminPage({ user }: Props) { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ratingTypeId: Number(type), name, description, picture, icon, attachments, compress: compressImages }), }); - setName(""); setDescription(""); setPicture(""); setIcon(""); setAttachments(""); + setName(""); setDescription(""); setPicture([]); setIcon(""); setAttachments(""); const res = await fetch("/api/ratings?limit=10000").then((r) => r.json()); setRatings(res.items); } @@ -465,8 +467,8 @@ function AdminPage({ user }: Props) { - +
@@ -488,9 +490,19 @@ function AdminPage({ user }: Props) { setName(e.target.value)} style={{ padding: "8px 12px", fontSize: 14, border: `1px solid ${borderClr}`, background: inputBg, color: textClr }} />