From aa20e931ee6aff66103235c98a48de2297456ac1 Mon Sep 17 00:00:00 2001 From: trafficlunar Date: Sun, 19 Apr 2026 00:45:24 +0100 Subject: [PATCH] feat: add back likes and admin banners --- frontend/src/components/admin/banner-form.tsx | 5 +- frontend/src/components/admin/banner.tsx | 133 +++++++++--------- frontend/src/components/like-button.tsx | 59 ++++---- frontend/src/components/mii/list/index.tsx | 16 ++- frontend/src/layout.tsx | 3 +- frontend/src/pages/admin.tsx | 8 +- frontend/src/pages/mii.tsx | 14 +- 7 files changed, 141 insertions(+), 97 deletions(-) diff --git a/frontend/src/components/admin/banner-form.tsx b/frontend/src/components/admin/banner-form.tsx index 8a4c5ba..d9c02b0 100644 --- a/frontend/src/components/admin/banner-form.tsx +++ b/frontend/src/components/admin/banner-form.tsx @@ -2,13 +2,14 @@ import { useState } from "react"; export default function BannerForm() { const [message, setMessage] = useState(""); + const API_URL = import.meta.env.VITE_API_URL; const onClickClear = async () => { - await fetch("/api/admin/banner", { method: "DELETE" }); + await fetch(`${API_URL}/api/admin/banner`, { method: "DELETE", credentials: "include" }); // TODO }; const onClickSet = async () => { - await fetch("/api/admin/banner", { method: "POST", body: message }); + await fetch(`${API_URL}/api/admin/banner`, { method: "POST", body: message, credentials: "include" }); }; return ( diff --git a/frontend/src/components/admin/banner.tsx b/frontend/src/components/admin/banner.tsx index 0b00c63..4ac02b5 100644 --- a/frontend/src/components/admin/banner.tsx +++ b/frontend/src/components/admin/banner.tsx @@ -1,65 +1,68 @@ -// import { useSearchParams } from "next/navigation"; -// import { Suspense, useEffect, useState } from "react"; - -// import useSWR from "swr"; -// import { Icon } from "@iconify/react"; - -// interface ApiResponse { -// message: string; -// } - -// const fetcher = (url: string) => fetch(url).then((res) => res.json()); - -// function RedirectBanner() { -// const searchParams = useSearchParams(); -// const from = searchParams.get("from"); -// if (from !== "old-domain") return null; - -// return ( -//
-// -// We have moved URLs, welcome to tomodachishare.com! -//
-// ); -// } - -// export default function AdminBanner() { -// const { data } = useSWR("/api/admin/banner", fetcher); -// const [shouldShow, setShouldShow] = useState(true); - -// useEffect(() => { -// if (!data?.message) return; - -// // Check if the current banner text was closed by the user -// const closedBanner = window.localStorage.getItem("closedBanner"); -// setShouldShow(data.message !== closedBanner); -// }, [data]); - -// const handleClose = () => { -// if (!data) return; - -// // Close banner and remember it -// window.localStorage.setItem("closedBanner", data.message); -// setShouldShow(false); -// }; - -// return ( -// <> -// {data && data.message && shouldShow && ( -//
-//
-// -// {data.message} -//
- -// -//
-// )} -// -// -// -// -// ); -// } +import { useEffect, useState } from "react"; +import { Icon } from "@iconify/react"; +import { useSearchParams } from "react-router"; + +interface ApiResponse { + message: string; +} + +function RedirectBanner() { + const [searchParams] = useSearchParams(); + const from = searchParams.get("from"); + if (from !== "old-domain") return null; + + return ( +
+ + We have moved URLs, welcome to tomodachishare.com! +
+ ); +} + +export default function AdminBanner() { + const [message, setMessage] = useState(null); + const [shouldShow, setShouldShow] = useState(false); + + useEffect(() => { + fetch(`${import.meta.env.VITE_API_URL}/api/admin/banner`) + .then((res) => { + if (!res.ok) throw new Error("Failed to get admin banner"); + return res.json() as Promise; + }) + .then((data) => { + if (!data.message) return; + const closedBanner = localStorage.getItem("closedBanner"); + setMessage(data.message); + setShouldShow(data.message !== closedBanner); + }) + .catch((err) => { + console.error(err); + }); + }, []); + + const handleClose = () => { + if (!message) return; + + // Close banner and remember it + localStorage.setItem("closedBanner", message); + setShouldShow(false); + }; + + return ( + <> + {shouldShow && message && ( +
+
+ + {message} +
+ + +
+ )} + + + ); +} diff --git a/frontend/src/components/like-button.tsx b/frontend/src/components/like-button.tsx index e764f24..410d100 100644 --- a/frontend/src/components/like-button.tsx +++ b/frontend/src/components/like-button.tsx @@ -1,6 +1,9 @@ import { useEffect, useState } from "react"; import { Icon, loadIcons } from "@iconify/react"; import { abbreviateNumber } from "../lib/abbreviation"; +import { useStore } from "@nanostores/react"; +import { session } from "../session"; +import { useNavigate } from "react-router"; interface Props { likes: number; @@ -11,33 +14,41 @@ interface Props { big?: boolean; } -export default function LikeButton({ likes, isLiked, disabled, abbreviate, big }: Props) { +export default function LikeButton({ likes, miiId, isLiked, disabled, abbreviate, big }: Props) { + const $session = useStore(session); + const navigate = useNavigate(); const [isLikedState, setIsLikedState] = useState(isLiked); - const [likesState] = useState(likes); - const [isAnimating] = useState(false); + const [likesState, setLikesState] = useState(likes); + const [isAnimating, setIsAnimating] = useState(false); const onClick = async () => { - // if (disabled) return; - // if (!session.data?.user) { - // router.push("/login"); - // return; - // } - // setIsLikedState(!isLikedState); - // setLikesState(isLikedState ? likesState - 1 : likesState + 1); - // // Trigger animation - // if (!isLikedState) { - // setIsAnimating(true); - // setTimeout(() => setIsAnimating(false), 1000); // match animation duration - // } - // const response = await fetch(`/api/mii/${miiId}/like`, { method: "POST" }); - // if (response.ok) { - // const { liked, count } = await response.json(); - // setIsLikedState(liked); - // setLikesState(count); - // } else { - // setIsLikedState(isLikedState); - // setLikesState(likesState); - // } + if (disabled || !miiId) return; + if ($session === undefined) return; + if ($session === null) { + navigate("/login"); + return; + } + + const prevLiked = isLikedState; + const prevLikes = likesState; + setIsLikedState(!prevLiked); + setLikesState(prevLiked ? likesState - 1 : likesState + 1); + + // Trigger animation + if (!prevLiked) { + setIsAnimating(true); + setTimeout(() => setIsAnimating(false), 1000); + } + + const response = await fetch(`${import.meta.env.VITE_API_URL}/api/mii/${miiId}/like`, { method: "POST", credentials: "include" }); + if (response.ok) { + const { liked, count } = await response.json(); + setIsLikedState(liked); + setLikesState(count); + } else { + setIsLikedState(prevLiked); + setLikesState(prevLikes); + } }; // Preload like button icons diff --git a/frontend/src/components/mii/list/index.tsx b/frontend/src/components/mii/list/index.tsx index 1d15b35..4c9f84e 100644 --- a/frontend/src/components/mii/list/index.tsx +++ b/frontend/src/components/mii/list/index.tsx @@ -27,6 +27,7 @@ export default function MiiList({ parentPage, userId }: Props) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [acceptingAll, setAcceptingAll] = useState(false); + const [likedIds, setLikedIds] = useState>(new Set()); const $session = useStore(session); @@ -40,15 +41,24 @@ export default function MiiList({ parentPage, userId }: Props) { if (!res.ok) throw new Error("Failed to fetch Miis"); return res.json(); }) - .then((data) => { + .then(async (data) => { setData(data); setLoading(false); + + if ($session === undefined || $session === null || !data.miis.length) return; + const ids = data.miis.map((m: any) => m.id).join(","); + fetch(`${import.meta.env.VITE_API_URL}/api/mii/has-liked?ids=${ids}`, { credentials: "include" }) + .then((res) => (res.ok ? res.json() : [])) + .then((likedIds: number[]) => setLikedIds(new Set(likedIds))) + .catch((err) => { + console.error("Error fetching likes:", err); + }); }) .catch((err) => { console.error(err); setLoading(false); }); - }, [searchParams, userId, parentPage]); + }, [searchParams, userId, parentPage, $session]); async function handleAcceptAll() { if (!data) return; @@ -162,7 +172,7 @@ export default function MiiList({ parentPage, userId }: Props) { {parentPage === "admin" && mii.description && }
- + {!userId && ( diff --git a/frontend/src/layout.tsx b/frontend/src/layout.tsx index 509927f..3c3d060 100644 --- a/frontend/src/layout.tsx +++ b/frontend/src/layout.tsx @@ -1,3 +1,4 @@ +import AdminBanner from "./components/admin/banner"; import Footer from "./components/footer"; import Header from "./components/header"; import { useEffect } from "react"; @@ -27,7 +28,7 @@ export default function Layout({ children }: { children: React.ReactNode }) { return ( <>
- {/* */} +
{children}
diff --git a/frontend/src/pages/admin.tsx b/frontend/src/pages/admin.tsx index aeef771..08254f1 100644 --- a/frontend/src/pages/admin.tsx +++ b/frontend/src/pages/admin.tsx @@ -2,10 +2,16 @@ import { useStore } from "@nanostores/react"; import MiiList from "../components/mii/list"; import { session } from "../session"; import { Navigate } from "react-router"; +import BannerForm from "../components/admin/banner-form"; export default function AdminPage() { const $session = useStore(session); if ($session === undefined) return
Loading...
; if ($session === null || Number($session?.user?.id) != import.meta.env.VITE_ADMIN_USER_ID) return ; - return ; + return ( + <> + + + + ); } diff --git a/frontend/src/pages/mii.tsx b/frontend/src/pages/mii.tsx index 5733251..cf73c47 100644 --- a/frontend/src/pages/mii.tsx +++ b/frontend/src/pages/mii.tsx @@ -11,12 +11,16 @@ import { Icon } from "@iconify/react"; import { useEffect, useState } from "react"; import { Link, useNavigate, useParams } from "react-router"; import AuthorButtons from "../components/mii/author-buttons"; +import { useStore } from "@nanostores/react"; +import { session } from "../session"; export default function MiiPage() { const { id } = useParams(); const navigate = useNavigate(); + const $session = useStore(session); const [mii, setMii] = useState(null); const [loading, setLoading] = useState(true); + const [isLiked, setIsLiked] = useState(false); const API_URL = import.meta.env.VITE_API_URL; @@ -29,6 +33,14 @@ export default function MiiPage() { .then((data) => { setMii(data); setLoading(false); + + if ($session === null || $session === undefined) return; + fetch(`${API_URL}/api/mii/has-liked?ids=${data.id}`, { credentials: "include" }) + .then((res) => (res.ok ? res.json() : [])) + .then((likedIds: number[]) => setIsLiked(likedIds.length > 0)) + .catch((err) => { + console.error("Error liking:", err); + }); }) .catch((err) => { console.error(err); @@ -252,7 +264,7 @@ export default function MiiPage() { {/* Submission name */}

{mii.name}

{/* Like button */} - +
{/* Tags */}