diff --git a/backend/next.config.ts b/backend/next.config.ts
index 9253312..f9ef87e 100644
--- a/backend/next.config.ts
+++ b/backend/next.config.ts
@@ -9,7 +9,8 @@ const nextConfig: NextConfig = {
headers: [
{ key: "Access-Control-Allow-Origin", value: process.env.NEXT_PUBLIC_FRONTEND_URL || "http://localhost:4321" },
{ key: "Access-Control-Allow-Credentials", value: "true" },
- { key: "Access-Control-Allow-Methods", value: "GET,POST,PATCH,DELETE,OPTIONS" },
+ { key: "Access-Control-Allow-Methods", value: "GET,POST,DELETE,OPTIONS" },
+ { key: "Access-Control-Allow-Headers", value: "Content-Type" },
],
},
];
diff --git a/backend/src/app/api/admin/accept-mii/route.ts b/backend/src/app/api/admin/accept-mii/route.ts
index 941f9df..06b966b 100644
--- a/backend/src/app/api/admin/accept-mii/route.ts
+++ b/backend/src/app/api/admin/accept-mii/route.ts
@@ -4,7 +4,7 @@ import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { idSchema } from "@tomodachi-share/shared/schemas";
-export async function PATCH(request: NextRequest) {
+export async function POST(request: NextRequest) {
const session = await auth();
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
diff --git a/backend/src/app/api/admin/can-submit/route.ts b/backend/src/app/api/admin/can-submit/route.ts
index c5459b3..f164e7d 100644
--- a/backend/src/app/api/admin/can-submit/route.ts
+++ b/backend/src/app/api/admin/can-submit/route.ts
@@ -1,13 +1,13 @@
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { auth } from "@/lib/auth";
-import { settings } from "@/lib/settings";
+import { settings } from "../../../../lib/settings";
export async function GET() {
return NextResponse.json({ success: true, value: settings.canSubmit });
}
-export async function PATCH(request: NextRequest) {
+export async function POST(request: NextRequest) {
const session = await auth();
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
diff --git a/backend/src/app/api/admin/queue/route.ts b/backend/src/app/api/admin/queue/route.ts
index 422fd7d..2f23490 100644
--- a/backend/src/app/api/admin/queue/route.ts
+++ b/backend/src/app/api/admin/queue/route.ts
@@ -1,13 +1,13 @@
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { auth } from "@/lib/auth";
-import { settings } from "@/lib/settings";
+import { settings } from "../../../../lib/settings";
export async function GET() {
return NextResponse.json({ success: true, value: settings.queueEnabled });
}
-export async function PATCH(request: NextRequest) {
+export async function POST(request: NextRequest) {
const session = await auth();
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
diff --git a/backend/src/app/api/admin/regenerate-metadata-images/route.ts b/backend/src/app/api/admin/regenerate-metadata-images/route.ts
index 69cb2d3..10237d5 100644
--- a/backend/src/app/api/admin/regenerate-metadata-images/route.ts
+++ b/backend/src/app/api/admin/regenerate-metadata-images/route.ts
@@ -3,7 +3,7 @@ import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { generateMetadataImage } from "@/lib/images";
-export async function PATCH() {
+export async function POST() {
const session = await auth();
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
diff --git a/backend/src/app/api/auth/about-me/route.ts b/backend/src/app/api/auth/about-me/route.ts
index 5269d0c..587daf5 100644
--- a/backend/src/app/api/auth/about-me/route.ts
+++ b/backend/src/app/api/auth/about-me/route.ts
@@ -6,7 +6,7 @@ import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { RateLimit } from "@/lib/rate-limit";
-export async function PATCH(request: NextRequest) {
+export async function POST(request: NextRequest) {
const session = await auth();
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
diff --git a/backend/src/app/api/auth/delete/route.ts b/backend/src/app/api/auth/delete/route.ts
index c772e28..ccf92f4 100644
--- a/backend/src/app/api/auth/delete/route.ts
+++ b/backend/src/app/api/auth/delete/route.ts
@@ -4,7 +4,7 @@ import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { RateLimit } from "@/lib/rate-limit";
-export async function DELETE(request: NextRequest) {
+export async function POST(request: NextRequest) {
const session = await auth();
if (!session || !session.user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
diff --git a/backend/src/app/api/auth/name/route.ts b/backend/src/app/api/auth/name/route.ts
index 2e79541..8a9af28 100644
--- a/backend/src/app/api/auth/name/route.ts
+++ b/backend/src/app/api/auth/name/route.ts
@@ -6,7 +6,7 @@ import { prisma } from "@/lib/prisma";
import { userNameSchema } from "@tomodachi-share/shared/schemas";
import { RateLimit } from "@/lib/rate-limit";
-export async function PATCH(request: NextRequest) {
+export async function POST(request: NextRequest) {
const session = await auth();
if (!session || !session.user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
diff --git a/backend/src/app/api/auth/picture/route.ts b/backend/src/app/api/auth/picture/route.ts
index 7f5d5fa..c5f5775 100644
--- a/backend/src/app/api/auth/picture/route.ts
+++ b/backend/src/app/api/auth/picture/route.ts
@@ -17,7 +17,7 @@ const formDataSchema = z.object({
image: z.union([z.instanceof(File), z.any()]).optional(),
});
-export async function PATCH(request: NextRequest) {
+export async function POST(request: NextRequest) {
const session = await auth();
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
diff --git a/backend/src/app/api/mii/[id]/delete/route.ts b/backend/src/app/api/mii/[id]/delete/route.ts
index 1a96c99..6c6127b 100644
--- a/backend/src/app/api/mii/[id]/delete/route.ts
+++ b/backend/src/app/api/mii/[id]/delete/route.ts
@@ -10,7 +10,7 @@ import { RateLimit } from "@/lib/rate-limit";
const uploadsDirectory = path.join(process.cwd(), "uploads", "mii");
-export async function DELETE(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
+export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const session = await auth();
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
diff --git a/backend/src/app/api/mii/[id]/edit/route.ts b/backend/src/app/api/mii/[id]/edit/route.ts
index 930bc8e..d840838 100644
--- a/backend/src/app/api/mii/[id]/edit/route.ts
+++ b/backend/src/app/api/mii/[id]/edit/route.ts
@@ -14,7 +14,7 @@ import { idSchema, nameSchema, switchMiiInstructionsSchema, tagsSchema } from "@
import { generateMetadataImage, validateImage } from "@/lib/images";
import { RateLimit } from "@/lib/rate-limit";
import { minifyInstructions, SwitchMiiInstructions } from "@tomodachi-share/shared";
-import { settings } from "@/lib/settings";
+import { settings } from "../../../../../lib/settings";
const uploadsDirectory = path.join(process.cwd(), "uploads", "mii");
@@ -41,7 +41,7 @@ const editSchema = z.object({
image3: z.union([z.instanceof(File), z.any()]).optional(),
});
-export async function PATCH(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
+export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const session = await auth();
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
diff --git a/backend/src/app/api/mii/[id]/like/route.ts b/backend/src/app/api/mii/[id]/like/route.ts
index 9464bad..332f5bb 100644
--- a/backend/src/app/api/mii/[id]/like/route.ts
+++ b/backend/src/app/api/mii/[id]/like/route.ts
@@ -5,7 +5,7 @@ import { prisma } from "@/lib/prisma";
import { idSchema } from "@tomodachi-share/shared/schemas";
import { RateLimit } from "@/lib/rate-limit";
-export async function PATCH(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
+export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const session = await auth();
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
diff --git a/backend/src/app/api/mii/list/route.ts b/backend/src/app/api/mii/list/route.ts
index ba90a2a..7a4caef 100644
--- a/backend/src/app/api/mii/list/route.ts
+++ b/backend/src/app/api/mii/list/route.ts
@@ -89,6 +89,10 @@ export async function GET(request: NextRequest) {
_count: {
select: { likedBy: true },
},
+ // Admin
+ ...(parentPage === "admin" && {
+ description: true,
+ }),
};
let totalCount: number;
diff --git a/backend/src/app/api/return/route.ts b/backend/src/app/api/return/route.ts
index 6da81b6..a83887c 100644
--- a/backend/src/app/api/return/route.ts
+++ b/backend/src/app/api/return/route.ts
@@ -4,7 +4,7 @@ import { auth } from "@/lib/auth";
import { RateLimit } from "@/lib/rate-limit";
import { prisma } from "@/lib/prisma";
-export async function DELETE(request: NextRequest) {
+export async function POST(request: NextRequest) {
const session = await auth();
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
diff --git a/backend/src/app/api/submit/route.ts b/backend/src/app/api/submit/route.ts
index c062842..178d776 100644
--- a/backend/src/app/api/submit/route.ts
+++ b/backend/src/app/api/submit/route.ts
@@ -18,7 +18,7 @@ import Mii from "../../../../../shared/src/mii.js/mii";
import { convertQrCode, minifyInstructions, ThreeDsTomodachiLifeMii } from "@tomodachi-share/shared";
import { SwitchMiiInstructions } from "@tomodachi-share/shared";
-import { settings } from "@/lib/settings";
+import { settings } from "../../../lib/settings";
const uploadsDirectory = path.join(process.cwd(), "uploads", "mii");
diff --git a/backend/src/app/out/page.tsx b/backend/src/app/out/page.tsx
deleted file mode 100644
index d72c91b..0000000
--- a/backend/src/app/out/page.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import { Metadata } from "next";
-import Link from "next/link";
-import { redirect } from "next/navigation";
-import { Icon } from "@iconify/react";
-
-export const metadata: Metadata = {
- title: "Leaving TomodachiShare",
- description: "Warning: You are leaving TomodachiShare, proceed with caution",
-};
-
-interface Props {
- searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
-}
-
-export default async function LinkOutPage({ searchParams }: Props) {
- const url = (await searchParams).url;
- if (!url || Array.isArray(url)) redirect("/");
-
- let parsed: URL;
- try {
- parsed = new URL(url);
- } catch {
- redirect("/"); // redirect if URL is invalid
- }
-
- // Next.js doesn't allow attacks like these but you can never be too safe
- if (!["http:", "https:"].includes(parsed.protocol)) redirect("/");
-
- const isSafe = Array.from(SAFE_LINKS).some((domain) => parsed.hostname === domain || parsed.hostname.endsWith(`.${domain}`));
- if (isSafe) redirect(url);
-
- return (
-
-
-
-
- Warning
-
-
You're attempting to leave TomodachiShare island! The destination website is potentially dangerous.
-
-
- {url}
-
-
-
-
-
- Travel Back
-
-
-
- Continue
-
-
-
-
- );
-}
-
-const SAFE_LINKS = new Set([
- "tomodachishare.com",
- "trafficlunar.net",
- "youtube.com",
- "youtu.be",
- "twitter.com",
- "x.com",
- "reddit.com",
- "tiktok.com",
- "tumblr.com",
- "instagram.com",
- "wikipedia.org",
-]);
diff --git a/backend/src/lib/auth.ts b/backend/src/lib/auth.ts
index 9a04006..71f2774 100644
--- a/backend/src/lib/auth.ts
+++ b/backend/src/lib/auth.ts
@@ -18,6 +18,7 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
sameSite: "none",
path: "/",
secure: true,
+ domain: process.env.NODE_ENV === "production" ? ".tomodachishare.com" : "localhost",
},
},
},
diff --git a/backend/src/lib/images.tsx b/backend/src/lib/images.tsx
index 373d1e3..61b8ac7 100644
--- a/backend/src/lib/images.tsx
+++ b/backend/src/lib/images.tsx
@@ -190,7 +190,7 @@ export async function generateMetadataImage(mii: Mii, author: string): Promise<{
{/* Watermark */}
-

+

{/* I tried using text-orange-400 but it wasn't correct..? */}
TomodachiShare
diff --git a/frontend/public/logo.svg b/frontend/public/logo.svg
new file mode 100644
index 0000000..58309aa
--- /dev/null
+++ b/frontend/public/logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/src/components/admin/banner-form.tsx b/frontend/src/components/admin/banner-form.tsx
index f6a93ea..d9c02b0 100644
--- a/frontend/src/components/admin/banner-form.tsx
+++ b/frontend/src/components/admin/banner-form.tsx
@@ -1,25 +1,26 @@
-import { useState } from "react";
-
-export default function BannerForm() {
- const [message, setMessage] = useState("");
-
- const onClickClear = async () => {
- await fetch("/api/admin/banner", { method: "DELETE" });
- };
-
- const onClickSet = async () => {
- await fetch("/api/admin/banner", { method: "POST", body: message });
- };
-
- return (
-
- setMessage(e.target.value)} />
-
-
-
- );
-}
+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_URL}/api/admin/banner`, { method: "DELETE", credentials: "include" }); // TODO
+ };
+
+ const onClickSet = async () => {
+ await fetch(`${API_URL}/api/admin/banner`, { method: "POST", body: message, credentials: "include" });
+ };
+
+ return (
+
+ setMessage(e.target.value)} />
+
+
+
+ );
+}
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/admin/control-center.tsx b/frontend/src/components/admin/control-center.tsx
index 4616a07..320690d 100644
--- a/frontend/src/components/admin/control-center.tsx
+++ b/frontend/src/components/admin/control-center.tsx
@@ -1,45 +1,45 @@
-// import { settings } from "@/lib/settings";
-// import { useState } from "react";
-
-// export default function ControlCenter() {
-// const [canSubmit, setCanSubmit] = useState(settings.canSubmit);
-// const [isQueueEnabled, setIsQeueueEnabled] = useState(settings.queueEnabled);
-
-// const onClickSet = async () => {
-// await fetch("/api/admin/can-submit", { method: "PATCH", body: JSON.stringify(canSubmit) });
-// await fetch("/api/admin/queue", { method: "PATCH", body: JSON.stringify(isQueueEnabled) });
-// };
-
-// return (
-//
-//
-// setCanSubmit(e.target.checked)}
-// />
-//
-//
-//
-// setIsQeueueEnabled(e.target.checked)}
-// />
-//
-//
-
-//
-//
-//
-//
-// );
-// }
+// import { settings } from "@/lib/settings";
+// import { useState } from "react";
+
+// export default function ControlCenter() {
+// const [canSubmit, setCanSubmit] = useState(settings.canSubmit);
+// const [isQueueEnabled, setIsQeueueEnabled] = useState(settings.queueEnabled);
+
+// const onClickSet = async () => {
+// await fetch("/api/admin/can-submit", { method: "POST", body: JSON.stringify(canSubmit) });
+// await fetch("/api/admin/queue", { method: "POST", body: JSON.stringify(isQueueEnabled) });
+// };
+
+// return (
+//
+//
+// setCanSubmit(e.target.checked)}
+// />
+//
+//
+//
+// setIsQeueueEnabled(e.target.checked)}
+// />
+//
+//
+
+//
+//
+//
+//
+// );
+// }
diff --git a/frontend/src/components/admin/regenerate-images.tsx b/frontend/src/components/admin/regenerate-images.tsx
index fda6a53..8e15372 100644
--- a/frontend/src/components/admin/regenerate-images.tsx
+++ b/frontend/src/components/admin/regenerate-images.tsx
@@ -1,84 +1,84 @@
-import { useEffect, useState } from "react";
-import { createPortal } from "react-dom";
-
-import { Icon } from "@iconify/react";
-import SubmitButton from "../submit-button";
-
-export default function RegenerateImagesButton() {
- const [isOpen, setIsOpen] = useState(false);
- const [isVisible, setIsVisible] = useState(false);
-
- const [error, setError] = useState(undefined);
-
- const handleSubmit = async () => {
- const response = await fetch("/api/admin/regenerate-metadata-images", { method: "PATCH" });
-
- if (!response.ok) {
- const data = await response.json();
- setError(data.error);
-
- return;
- }
-
- close();
- };
-
- const close = () => {
- setIsVisible(false);
- setTimeout(() => {
- setIsOpen(false);
- }, 300);
- };
-
- useEffect(() => {
- if (isOpen) {
- // slight delay to trigger animation
- setTimeout(() => setIsVisible(true), 10);
- }
- }, [isOpen]);
-
- return (
- <>
-
-
- {isOpen &&
- createPortal(
-
-
-
-
-
-
Regenerate Images
-
-
-
-
Are you sure? This will delete and regenerate every metadata image.
-
- {error &&
Error: {error}}
-
-
-
-
-
-
-
,
- document.body,
- )}
- >
- );
-}
+import { useEffect, useState } from "react";
+import { createPortal } from "react-dom";
+
+import { Icon } from "@iconify/react";
+import SubmitButton from "../submit-button";
+
+export default function RegenerateImagesButton() {
+ const [isOpen, setIsOpen] = useState(false);
+ const [isVisible, setIsVisible] = useState(false);
+
+ const [error, setError] = useState(undefined);
+
+ const handleSubmit = async () => {
+ const response = await fetch("/api/admin/regenerate-metadata-images", { method: "POST" });
+
+ if (!response.ok) {
+ const data = await response.json();
+ setError(data.error);
+
+ return;
+ }
+
+ close();
+ };
+
+ const close = () => {
+ setIsVisible(false);
+ setTimeout(() => {
+ setIsOpen(false);
+ }, 300);
+ };
+
+ useEffect(() => {
+ if (isOpen) {
+ // slight delay to trigger animation
+ setTimeout(() => setIsVisible(true), 10);
+ }
+ }, [isOpen]);
+
+ return (
+ <>
+
+
+ {isOpen &&
+ createPortal(
+
+
+
+
+
+
Regenerate Images
+
+
+
+
Are you sure? This will delete and regenerate every metadata image.
+
+ {error &&
Error: {error}}
+
+
+
+
+
+
+
,
+ document.body,
+ )}
+ >
+ );
+}
diff --git a/frontend/src/components/admin/return-to-island.tsx b/frontend/src/components/admin/return-to-island.tsx
index 528f8d2..868d2ee 100644
--- a/frontend/src/components/admin/return-to-island.tsx
+++ b/frontend/src/components/admin/return-to-island.tsx
@@ -11,7 +11,7 @@
// const [error, setError] = useState(undefined);
// const handleClick = async () => {
-// const response = await fetch("/api/return", { method: "DELETE" });
+// const response = await fetch("/api/return", { method: "POST" });
// if (!response.ok) {
// const data = await response.json();
diff --git a/frontend/src/components/header-profile.tsx b/frontend/src/components/header-profile.tsx
deleted file mode 100644
index 4853946..0000000
--- a/frontend/src/components/header-profile.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-import { Icon } from "@iconify/react";
-import { useEffect } from "react";
-import { useStore } from "@nanostores/react";
-import { session } from "../session";
-import { Link } from "react-router";
-
-export default function HeaderProfile() {
- const API_BASE_URL = import.meta.env.VITE_API_URL;
- const $session = useStore(session);
-
- useEffect(() => {
- fetch(`${API_BASE_URL}/api/auth/session`, { credentials: "include" })
- .then((res) => {
- if (!res.ok) throw new Error("Failed to get session");
- return res.json();
- })
- .then((data) => {
- session.set(data);
- })
- .catch((err) => {
- console.error(err);
- });
- }, []);
-
- return (
- <>
- {!$session?.user ? (
-
-
- Login
-
-
- ) : (
- <>
-
-
-
- {$session?.user?.name ?? "unknown"}
-
-
-
-
-
-
-
- >
- )}
- >
- );
-}
diff --git a/frontend/src/components/header.tsx b/frontend/src/components/header.tsx
index 0c515bf..c3d8493 100644
--- a/frontend/src/components/header.tsx
+++ b/frontend/src/components/header.tsx
@@ -1,9 +1,27 @@
import { Icon } from "@iconify/react";
import SearchBar from "./search-bar";
-import HeaderProfile from "./header-profile";
import { Link } from "react-router";
+import { useStore } from "@nanostores/react";
+import { session } from "../session";
+import { useEffect } from "react";
export default function Header() {
+ const $session = useStore(session);
+
+ useEffect(() => {
+ fetch(`${import.meta.env.VITE_API_URL}/api/auth/session`, { credentials: "include" })
+ .then((res) => {
+ if (!res.ok) throw new Error("Failed to get session");
+ return res.json();
+ })
+ .then((data) => {
+ session.set(data);
+ })
+ .catch((err) => {
+ console.error(err);
+ });
+ }, []);
+
return (
-
+ {!$session?.user ? (
+
+
+ Login
+
+
+ ) : (
+ <>
+
+
+
{
+ e.currentTarget.onerror = null; // Prevent infinite loops
+ e.currentTarget.src = "/guest.png";
+ }}
+ alt="profile picture"
+ width={40}
+ height={40}
+ className="rounded-full aspect-square object-cover h-full bg-white outline-2 outline-orange-400"
+ />
+ {$session?.user?.name ?? "unknown"}
+
+
+
+
+
+
+
+ >
+ )}
);
diff --git a/frontend/src/components/like-button.tsx b/frontend/src/components/like-button.tsx
index 9082918..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: "PATCH" });
- // 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/author-buttons.tsx b/frontend/src/components/mii/author-buttons.tsx
index c7f4840..7af8efc 100644
--- a/frontend/src/components/mii/author-buttons.tsx
+++ b/frontend/src/components/mii/author-buttons.tsx
@@ -1,16 +1,18 @@
import { Icon } from "@iconify/react";
import DeleteMiiButton from "./delete-mii-button";
import { Link } from "react-router";
+import { useStore } from "@nanostores/react";
+import { session } from "../../session";
interface Props {
mii: any;
}
export default function AuthorButtons({ mii }: Props) {
- // const session = useSession();
-
- // if (!session.data || (Number(session.data.user?.id) !== mii.userId && Number(session.data.user?.id) !== Number(import.meta.env.NEXT_PUBLIC_ADMIN_USER_ID)))
- // return null;
+ const $session = useStore(session);
+ if ($session === undefined) return null;
+ if ($session === null) return null;
+ if (Number($session?.user?.id) !== mii.userId && Number($session?.user?.id) !== Number(import.meta.env.VITE_ADMIN_USER_ID)) return null;
return (
<>
diff --git a/frontend/src/components/mii/delete-mii-button.tsx b/frontend/src/components/mii/delete-mii-button.tsx
index 3820466..199ae3b 100644
--- a/frontend/src/components/mii/delete-mii-button.tsx
+++ b/frontend/src/components/mii/delete-mii-button.tsx
@@ -22,7 +22,7 @@ export default function DeleteMiiButton({ miiId, miiName, likes, inMiiPage }: Pr
const [inputMiiName, setInputMiiName] = useState("");
const handleSubmit = async () => {
- const response = await fetch(`${import.meta.env.VITE_API_URL}/api/mii/${miiId}/delete`, { method: "DELETE", credentials: "include" });
+ const response = await fetch(`${import.meta.env.VITE_API_URL}/api/mii/${miiId}/delete`, { method: "POST", credentials: "include" });
if (!response.ok) {
const { error } = await response.json();
setError(error);
diff --git a/frontend/src/components/mii/list/index.tsx b/frontend/src/components/mii/list/index.tsx
index 5cfaa97..4c9f84e 100644
--- a/frontend/src/components/mii/list/index.tsx
+++ b/frontend/src/components/mii/list/index.tsx
@@ -9,7 +9,7 @@ import { Icon } from "@iconify/react";
import LikeButton from "../../like-button";
import { useStore } from "@nanostores/react";
import { session } from "../../../session";
-import Carousel from "../../carousel";
+import Description from "../../description";
interface ApiResponse {
totalCount: number;
@@ -26,6 +26,8 @@ export default function MiiList({ parentPage, userId }: Props) {
const [searchParams] = useSearchParams();
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);
@@ -39,15 +41,46 @@ 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;
+ setAcceptingAll(true);
+ try {
+ await Promise.all(
+ data.miis.map((mii) =>
+ fetch(`${import.meta.env.VITE_API_URL}/api/admin/accept-mii?id=${mii.id}`, {
+ method: "POST",
+ credentials: "include",
+ }),
+ ),
+ );
+ const params = new URLSearchParams(searchParams.toString());
+ if (userId) params.append("userId", userId.toString());
+ if (parentPage) params.append("parentPage", parentPage);
+ const res = await fetch(`${import.meta.env.VITE_API_URL}/api/mii/list?${params.toString()}`, { credentials: "include" });
+ if (res.ok) setData(await res.json());
+ } finally {
+ setAcceptingAll(false);
+ }
+ }
return (
<>
@@ -62,6 +95,16 @@ export default function MiiList({ parentPage, userId }: Props) {
+ {parentPage === "admin" && data.miis.length > 0 && (
+
+ )}
@@ -80,21 +123,29 @@ export default function MiiList({ parentPage, userId }: Props) {
)}
- {parentPage !== "admin" ?
-
- : `${import.meta.env.VITE_API_URL}/mii/${mii.id}/image?type=image${index}`),
- ]}
- />}
+ {parentPage !== "admin" ? (
+
+
+
+ ) : (
+
+ {[
+ `${import.meta.env.VITE_API_URL}/mii/${mii.id}/image?type=mii`,
+ mii.platform === "THREE_DS"
+ ? `${import.meta.env.VITE_API_URL}/mii/${mii.id}/image?type=qr-code`
+ : `${import.meta.env.VITE_API_URL}/mii/${mii.id}/image?type=features`,
+ ...Array.from({ length: mii.imageCount }, (_, i) => `${import.meta.env.VITE_API_URL}/mii/${mii.id}/image?type=image${i}`),
+ ].map((src, i) => (
+

+ ))}
+
+ )}
@@ -109,6 +160,7 @@ export default function MiiList({ parentPage, userId }: Props) {
)}
+
{mii.tags.map((tag: string) => (
@@ -117,8 +169,10 @@ export default function MiiList({ parentPage, userId }: Props) {
))}
+ {parentPage === "admin" && mii.description && }
+
-
+
{!userId && (
@@ -135,13 +189,12 @@ export default function MiiList({ parentPage, userId }: Props) {
)}
- {/* Admin Controls */}
{parentPage === "admin" && (
{/* Tags */}