diff --git a/backend/next.config.ts b/backend/next.config.ts
index f9ef87e..9253312 100644
--- a/backend/next.config.ts
+++ b/backend/next.config.ts
@@ -9,8 +9,7 @@ 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,DELETE,OPTIONS" },
- { key: "Access-Control-Allow-Headers", value: "Content-Type" },
+ { key: "Access-Control-Allow-Methods", value: "GET,POST,PATCH,DELETE,OPTIONS" },
],
},
];
diff --git a/backend/src/app/api/admin/accept-mii/route.ts b/backend/src/app/api/admin/accept-mii/route.ts
index 06b966b..941f9df 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 POST(request: NextRequest) {
+export async function PATCH(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 f164e7d..c5459b3 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 POST(request: NextRequest) {
+export async function PATCH(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 2f23490..422fd7d 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 POST(request: NextRequest) {
+export async function PATCH(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 10237d5..69cb2d3 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 POST() {
+export async function PATCH() {
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 587daf5..5269d0c 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 POST(request: NextRequest) {
+export async function PATCH(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 ccf92f4..c772e28 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 POST(request: NextRequest) {
+export async function DELETE(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 8a9af28..2e79541 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 POST(request: NextRequest) {
+export async function PATCH(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 c5f5775..7f5d5fa 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 POST(request: NextRequest) {
+export async function PATCH(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 6c6127b..1a96c99 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 POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
+export async function DELETE(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 d840838..930bc8e 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 POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
+export async function PATCH(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 332f5bb..9464bad 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 POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
+export async function PATCH(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 7a4caef..ba90a2a 100644
--- a/backend/src/app/api/mii/list/route.ts
+++ b/backend/src/app/api/mii/list/route.ts
@@ -89,10 +89,6 @@ 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 a83887c..6da81b6 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 POST(request: NextRequest) {
+export async function DELETE(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 178d776..c062842 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
new file mode 100644
index 0000000..d72c91b
--- /dev/null
+++ b/backend/src/app/out/page.tsx
@@ -0,0 +1,72 @@
+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 71f2774..9a04006 100644
--- a/backend/src/lib/auth.ts
+++ b/backend/src/lib/auth.ts
@@ -18,7 +18,6 @@ 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 61b8ac7..373d1e3 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/shared/src/utils.ts b/backend/src/lib/utils.ts
similarity index 100%
rename from shared/src/utils.ts
rename to backend/src/lib/utils.ts
diff --git a/frontend/public/logo.svg b/frontend/public/logo.svg
deleted file mode 100644
index 58309aa..0000000
--- a/frontend/public/logo.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ 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 d9c02b0..f6a93ea 100644
--- a/frontend/src/components/admin/banner-form.tsx
+++ b/frontend/src/components/admin/banner-form.tsx
@@ -1,26 +1,25 @@
-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)} />
-
-
-
- );
-}
+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)} />
+
+
+
+ );
+}
diff --git a/frontend/src/components/admin/banner.tsx b/frontend/src/components/admin/banner.tsx
index 4ac02b5..0b00c63 100644
--- a/frontend/src/components/admin/banner.tsx
+++ b/frontend/src/components/admin/banner.tsx
@@ -1,68 +1,65 @@
-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}
-
-
-
-
- )}
-
- >
- );
-}
+// 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}
+//
+
+//
+//
+// )}
+//
+//
+//
+// >
+// );
+// }
diff --git a/frontend/src/components/admin/control-center.tsx b/frontend/src/components/admin/control-center.tsx
index 320690d..4616a07 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: "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)}
-// />
-//
-//
-
-//
-//
-//
-//
-// );
-// }
+// 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)}
+// />
+//
+//
+
+//
+//
+//
+//
+// );
+// }
diff --git a/frontend/src/components/admin/regenerate-images.tsx b/frontend/src/components/admin/regenerate-images.tsx
index 8e15372..fda6a53 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: "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,
- )}
- >
- );
-}
+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,
+ )}
+ >
+ );
+}
diff --git a/frontend/src/components/admin/return-to-island.tsx b/frontend/src/components/admin/return-to-island.tsx
index 868d2ee..528f8d2 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: "POST" });
+// const response = await fetch("/api/return", { method: "DELETE" });
// if (!response.ok) {
// const data = await response.json();
diff --git a/frontend/src/components/header-profile.tsx b/frontend/src/components/header-profile.tsx
new file mode 100644
index 0000000..4853946
--- /dev/null
+++ b/frontend/src/components/header-profile.tsx
@@ -0,0 +1,61 @@
+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 c3d8493..0c515bf 100644
--- a/frontend/src/components/header.tsx
+++ b/frontend/src/components/header.tsx
@@ -1,27 +1,9 @@
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 410d100..9082918 100644
--- a/frontend/src/components/like-button.tsx
+++ b/frontend/src/components/like-button.tsx
@@ -1,9 +1,6 @@
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;
@@ -14,41 +11,33 @@ interface Props {
big?: boolean;
}
-export default function LikeButton({ likes, miiId, isLiked, disabled, abbreviate, big }: Props) {
- const $session = useStore(session);
- const navigate = useNavigate();
+export default function LikeButton({ likes, isLiked, disabled, abbreviate, big }: Props) {
const [isLikedState, setIsLikedState] = useState(isLiked);
- const [likesState, setLikesState] = useState(likes);
- const [isAnimating, setIsAnimating] = useState(false);
+ const [likesState] = useState(likes);
+ const [isAnimating] = useState(false);
const onClick = async () => {
- 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);
- }
+ // 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);
+ // }
};
// Preload like button icons
diff --git a/frontend/src/components/mii/author-buttons.tsx b/frontend/src/components/mii/author-buttons.tsx
index 7af8efc..c7f4840 100644
--- a/frontend/src/components/mii/author-buttons.tsx
+++ b/frontend/src/components/mii/author-buttons.tsx
@@ -1,18 +1,16 @@
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 = 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;
+ // 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;
return (
<>
diff --git a/frontend/src/components/mii/delete-mii-button.tsx b/frontend/src/components/mii/delete-mii-button.tsx
index 199ae3b..3820466 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: "POST", credentials: "include" });
+ const response = await fetch(`${import.meta.env.VITE_API_URL}/api/mii/${miiId}/delete`, { method: "DELETE", 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 4c9f84e..5cfaa97 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 Description from "../../description";
+import Carousel from "../../carousel";
interface ApiResponse {
totalCount: number;
@@ -26,8 +26,6 @@ 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);
@@ -41,46 +39,15 @@ export default function MiiList({ parentPage, userId }: Props) {
if (!res.ok) throw new Error("Failed to fetch Miis");
return res.json();
})
- .then(async (data) => {
+ .then((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, $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);
- }
- }
+ }, [searchParams, userId, parentPage]);
return (
<>
@@ -95,16 +62,6 @@ export default function MiiList({ parentPage, userId }: Props) {
- {parentPage === "admin" && data.miis.length > 0 && (
-
- )}
@@ -123,29 +80,21 @@ export default function MiiList({ parentPage, userId }: Props) {
)}
- {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) => (
-

- ))}
-
- )}
+ {parentPage !== "admin" ?
+
+ : `${import.meta.env.VITE_API_URL}/mii/${mii.id}/image?type=image${index}`),
+ ]}
+ />}
@@ -160,7 +109,6 @@ export default function MiiList({ parentPage, userId }: Props) {
)}
-
{mii.tags.map((tag: string) => (
@@ -169,10 +117,8 @@ export default function MiiList({ parentPage, userId }: Props) {
))}
- {parentPage === "admin" && mii.description && }
-
-
+
{!userId && (
@@ -189,12 +135,13 @@ export default function MiiList({ parentPage, userId }: Props) {
)}
+ {/* Admin Controls */}
{parentPage === "admin" && (
{/* Tags */}