mirror of
https://github.com/trafficlunar/tomodachi-share.git
synced 2026-05-13 13:17:45 +00:00
feat: add back likes and admin banners
This commit is contained in:
parent
0bd2d6d565
commit
aa20e931ee
7 changed files with 141 additions and 97 deletions
|
|
@ -2,13 +2,14 @@ import { useState } from "react";
|
||||||
|
|
||||||
export default function BannerForm() {
|
export default function BannerForm() {
|
||||||
const [message, setMessage] = useState("");
|
const [message, setMessage] = useState("");
|
||||||
|
const API_URL = import.meta.env.VITE_API_URL;
|
||||||
|
|
||||||
const onClickClear = async () => {
|
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 () => {
|
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 (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,65 +1,68 @@
|
||||||
// import { useSearchParams } from "next/navigation";
|
import { useEffect, useState } from "react";
|
||||||
// import { Suspense, useEffect, useState } from "react";
|
import { Icon } from "@iconify/react";
|
||||||
|
import { useSearchParams } from "react-router";
|
||||||
// import useSWR from "swr";
|
|
||||||
// import { Icon } from "@iconify/react";
|
interface ApiResponse {
|
||||||
|
message: string;
|
||||||
// interface ApiResponse {
|
}
|
||||||
// message: string;
|
|
||||||
// }
|
function RedirectBanner() {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
// const fetcher = (url: string) => fetch(url).then((res) => res.json());
|
const from = searchParams.get("from");
|
||||||
|
if (from !== "old-domain") return null;
|
||||||
// function RedirectBanner() {
|
|
||||||
// const searchParams = useSearchParams();
|
return (
|
||||||
// const from = searchParams.get("from");
|
<div className="w-full h-10 bg-orange-300 border-y-2 border-y-orange-400 mt-1 pl-2 shadow-md flex justify-center items-center gap-2 text-orange-900 text-nowrap overflow-x-auto font-semibold max-sm:justify-start">
|
||||||
// if (from !== "old-domain") return null;
|
<Icon icon="humbleicons:link" className="text-2xl min-w-6" />
|
||||||
|
<span>We have moved URLs, welcome to tomodachishare.com!</span>
|
||||||
// return (
|
</div>
|
||||||
// <div className="w-full h-10 bg-orange-300 border-y-2 border-y-orange-400 mt-1 pl-2 shadow-md flex justify-center items-center gap-2 text-orange-900 text-nowrap overflow-x-auto font-semibold max-sm:justify-start">
|
);
|
||||||
// <Icon icon="humbleicons:link" className="text-2xl min-w-6" />
|
}
|
||||||
// <span>We have moved URLs, welcome to tomodachishare.com!</span>
|
|
||||||
// </div>
|
export default function AdminBanner() {
|
||||||
// );
|
const [message, setMessage] = useState<string | null>(null);
|
||||||
// }
|
const [shouldShow, setShouldShow] = useState(false);
|
||||||
|
|
||||||
// export default function AdminBanner() {
|
useEffect(() => {
|
||||||
// const { data } = useSWR<ApiResponse>("/api/admin/banner", fetcher);
|
fetch(`${import.meta.env.VITE_API_URL}/api/admin/banner`)
|
||||||
// const [shouldShow, setShouldShow] = useState(true);
|
.then((res) => {
|
||||||
|
if (!res.ok) throw new Error("Failed to get admin banner");
|
||||||
// useEffect(() => {
|
return res.json() as Promise<ApiResponse>;
|
||||||
// if (!data?.message) return;
|
})
|
||||||
|
.then((data) => {
|
||||||
// // Check if the current banner text was closed by the user
|
if (!data.message) return;
|
||||||
// const closedBanner = window.localStorage.getItem("closedBanner");
|
const closedBanner = localStorage.getItem("closedBanner");
|
||||||
// setShouldShow(data.message !== closedBanner);
|
setMessage(data.message);
|
||||||
// }, [data]);
|
setShouldShow(data.message !== closedBanner);
|
||||||
|
})
|
||||||
// const handleClose = () => {
|
.catch((err) => {
|
||||||
// if (!data) return;
|
console.error(err);
|
||||||
|
});
|
||||||
// // Close banner and remember it
|
}, []);
|
||||||
// window.localStorage.setItem("closedBanner", data.message);
|
|
||||||
// setShouldShow(false);
|
const handleClose = () => {
|
||||||
// };
|
if (!message) return;
|
||||||
|
|
||||||
// return (
|
// Close banner and remember it
|
||||||
// <>
|
localStorage.setItem("closedBanner", message);
|
||||||
// {data && data.message && shouldShow && (
|
setShouldShow(false);
|
||||||
// <div className="relative w-full h-10 bg-orange-300 border-y-2 border-y-orange-400 mt-1 pl-2 shadow-md flex justify-center text-orange-900 text-nowrap overflow-x-auto font-semibold max-sm:justify-between">
|
};
|
||||||
// <div className="flex gap-2 h-full items-center w-fit">
|
|
||||||
// <Icon icon="humbleicons:exclamation" className="text-2xl min-w-6" />
|
return (
|
||||||
// <span>{data.message}</span>
|
<>
|
||||||
// </div>
|
{shouldShow && message && (
|
||||||
|
<div className="relative w-full min-h-10 bg-orange-300 border-y-2 border-y-orange-400 mt-1 pl-2 shadow-md flex justify-center text-orange-900 text-nowrap overflow-x-auto font-semibold max-sm:justify-between">
|
||||||
// <button onClick={handleClose} className="min-sm:absolute right-2 cursor-pointer p-1.5">
|
<div className="flex gap-2 h-full items-center w-fit">
|
||||||
// <Icon icon="humbleicons:times" className="text-2xl min-w-6" />
|
<Icon icon="humbleicons:exclamation" className="text-2xl min-w-6" />
|
||||||
// </button>
|
<span>{message}</span>
|
||||||
// </div>
|
</div>
|
||||||
// )}
|
|
||||||
// <Suspense>
|
<button onClick={handleClose} className="sm:absolute right-2 cursor-pointer p-1.5">
|
||||||
// <RedirectBanner />
|
<Icon icon="humbleicons:times" className="text-2xl min-w-6" />
|
||||||
// </Suspense>
|
</button>
|
||||||
// </>
|
</div>
|
||||||
// );
|
)}
|
||||||
// }
|
<RedirectBanner />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Icon, loadIcons } from "@iconify/react";
|
import { Icon, loadIcons } from "@iconify/react";
|
||||||
import { abbreviateNumber } from "../lib/abbreviation";
|
import { abbreviateNumber } from "../lib/abbreviation";
|
||||||
|
import { useStore } from "@nanostores/react";
|
||||||
|
import { session } from "../session";
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
likes: number;
|
likes: number;
|
||||||
|
|
@ -11,33 +14,41 @@ interface Props {
|
||||||
big?: boolean;
|
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 [isLikedState, setIsLikedState] = useState(isLiked);
|
||||||
const [likesState] = useState(likes);
|
const [likesState, setLikesState] = useState(likes);
|
||||||
const [isAnimating] = useState(false);
|
const [isAnimating, setIsAnimating] = useState(false);
|
||||||
|
|
||||||
const onClick = async () => {
|
const onClick = async () => {
|
||||||
// if (disabled) return;
|
if (disabled || !miiId) return;
|
||||||
// if (!session.data?.user) {
|
if ($session === undefined) return;
|
||||||
// router.push("/login");
|
if ($session === null) {
|
||||||
// return;
|
navigate("/login");
|
||||||
// }
|
return;
|
||||||
// setIsLikedState(!isLikedState);
|
}
|
||||||
// setLikesState(isLikedState ? likesState - 1 : likesState + 1);
|
|
||||||
// // Trigger animation
|
const prevLiked = isLikedState;
|
||||||
// if (!isLikedState) {
|
const prevLikes = likesState;
|
||||||
// setIsAnimating(true);
|
setIsLikedState(!prevLiked);
|
||||||
// setTimeout(() => setIsAnimating(false), 1000); // match animation duration
|
setLikesState(prevLiked ? likesState - 1 : likesState + 1);
|
||||||
// }
|
|
||||||
// const response = await fetch(`/api/mii/${miiId}/like`, { method: "POST" });
|
// Trigger animation
|
||||||
// if (response.ok) {
|
if (!prevLiked) {
|
||||||
// const { liked, count } = await response.json();
|
setIsAnimating(true);
|
||||||
// setIsLikedState(liked);
|
setTimeout(() => setIsAnimating(false), 1000);
|
||||||
// setLikesState(count);
|
}
|
||||||
// } else {
|
|
||||||
// setIsLikedState(isLikedState);
|
const response = await fetch(`${import.meta.env.VITE_API_URL}/api/mii/${miiId}/like`, { method: "POST", credentials: "include" });
|
||||||
// setLikesState(likesState);
|
if (response.ok) {
|
||||||
// }
|
const { liked, count } = await response.json();
|
||||||
|
setIsLikedState(liked);
|
||||||
|
setLikesState(count);
|
||||||
|
} else {
|
||||||
|
setIsLikedState(prevLiked);
|
||||||
|
setLikesState(prevLikes);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Preload like button icons
|
// Preload like button icons
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ export default function MiiList({ parentPage, userId }: Props) {
|
||||||
const [data, setData] = useState<ApiResponse | null>(null);
|
const [data, setData] = useState<ApiResponse | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [acceptingAll, setAcceptingAll] = useState(false);
|
const [acceptingAll, setAcceptingAll] = useState(false);
|
||||||
|
const [likedIds, setLikedIds] = useState<Set<number>>(new Set());
|
||||||
|
|
||||||
const $session = useStore(session);
|
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");
|
if (!res.ok) throw new Error("Failed to fetch Miis");
|
||||||
return res.json();
|
return res.json();
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then(async (data) => {
|
||||||
setData(data);
|
setData(data);
|
||||||
setLoading(false);
|
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) => {
|
.catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
}, [searchParams, userId, parentPage]);
|
}, [searchParams, userId, parentPage, $session]);
|
||||||
|
|
||||||
async function handleAcceptAll() {
|
async function handleAcceptAll() {
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
@ -162,7 +172,7 @@ export default function MiiList({ parentPage, userId }: Props) {
|
||||||
{parentPage === "admin" && mii.description && <Description text={mii.description} />}
|
{parentPage === "admin" && mii.description && <Description text={mii.description} />}
|
||||||
|
|
||||||
<div className="mt-auto grid grid-cols-2 items-center">
|
<div className="mt-auto grid grid-cols-2 items-center">
|
||||||
<LikeButton likes={mii._count.likedBy} miiId={mii.id} isLiked={false} abbreviate />
|
<LikeButton likes={mii._count.likedBy} miiId={mii.id} isLiked={likedIds.has(mii.id)} abbreviate />
|
||||||
|
|
||||||
{!userId && (
|
{!userId && (
|
||||||
<Link to={`/profile/${mii.user?.id}`} className="text-sm text-right overflow-hidden text-ellipsis whitespace-nowrap">
|
<Link to={`/profile/${mii.user?.id}`} className="text-sm text-right overflow-hidden text-ellipsis whitespace-nowrap">
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import AdminBanner from "./components/admin/banner";
|
||||||
import Footer from "./components/footer";
|
import Footer from "./components/footer";
|
||||||
import Header from "./components/header";
|
import Header from "./components/header";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
@ -27,7 +28,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header />
|
<Header />
|
||||||
{/* <AdminBanner /> */}
|
<AdminBanner />
|
||||||
<main className="px-4 py-8 max-w-7xl w-full grow flex flex-col">{children}</main>
|
<main className="px-4 py-8 max-w-7xl w-full grow flex flex-col">{children}</main>
|
||||||
<Footer />
|
<Footer />
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,16 @@ import { useStore } from "@nanostores/react";
|
||||||
import MiiList from "../components/mii/list";
|
import MiiList from "../components/mii/list";
|
||||||
import { session } from "../session";
|
import { session } from "../session";
|
||||||
import { Navigate } from "react-router";
|
import { Navigate } from "react-router";
|
||||||
|
import BannerForm from "../components/admin/banner-form";
|
||||||
|
|
||||||
export default function AdminPage() {
|
export default function AdminPage() {
|
||||||
const $session = useStore(session);
|
const $session = useStore(session);
|
||||||
if ($session === undefined) return <div className="p-6 text-center">Loading...</div>;
|
if ($session === undefined) return <div className="p-6 text-center">Loading...</div>;
|
||||||
if ($session === null || Number($session?.user?.id) != import.meta.env.VITE_ADMIN_USER_ID) return <Navigate to="/404" replace />;
|
if ($session === null || Number($session?.user?.id) != import.meta.env.VITE_ADMIN_USER_ID) return <Navigate to="/404" replace />;
|
||||||
return <MiiList parentPage="admin" />;
|
return (
|
||||||
|
<>
|
||||||
|
<BannerForm />
|
||||||
|
<MiiList parentPage="admin" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,16 @@ import { Icon } from "@iconify/react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Link, useNavigate, useParams } from "react-router";
|
import { Link, useNavigate, useParams } from "react-router";
|
||||||
import AuthorButtons from "../components/mii/author-buttons";
|
import AuthorButtons from "../components/mii/author-buttons";
|
||||||
|
import { useStore } from "@nanostores/react";
|
||||||
|
import { session } from "../session";
|
||||||
|
|
||||||
export default function MiiPage() {
|
export default function MiiPage() {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const $session = useStore(session);
|
||||||
const [mii, setMii] = useState<any>(null);
|
const [mii, setMii] = useState<any>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [isLiked, setIsLiked] = useState(false);
|
||||||
|
|
||||||
const API_URL = import.meta.env.VITE_API_URL;
|
const API_URL = import.meta.env.VITE_API_URL;
|
||||||
|
|
||||||
|
|
@ -29,6 +33,14 @@ export default function MiiPage() {
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
setMii(data);
|
setMii(data);
|
||||||
setLoading(false);
|
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) => {
|
.catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
@ -252,7 +264,7 @@ export default function MiiPage() {
|
||||||
{/* Submission name */}
|
{/* Submission name */}
|
||||||
<h1 className="text-4xl font-extrabold wrap-break-word whitespace-break-spaces text-amber-700 flex-1 min-w-0">{mii.name}</h1>
|
<h1 className="text-4xl font-extrabold wrap-break-word whitespace-break-spaces text-amber-700 flex-1 min-w-0">{mii.name}</h1>
|
||||||
{/* Like button */}
|
{/* Like button */}
|
||||||
<LikeButton likes={mii._count?.likedBy ?? 0} miiId={mii.id} isLiked={false} big />
|
<LikeButton likes={mii._count?.likedBy ?? 0} miiId={mii.id} isLiked={isLiked} big />
|
||||||
</div>
|
</div>
|
||||||
{/* Tags */}
|
{/* Tags */}
|
||||||
<div id="tags" className="flex flex-wrap gap-1 mt-1 *:px-2 *:py-1 *:bg-orange-300 *:rounded-full *:text-xs">
|
<div id="tags" className="flex flex-wrap gap-1 mt-1 *:px-2 *:py-1 *:bg-orange-300 *:rounded-full *:text-xs">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue