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 + +
  • + ) : ( + <> +
  • + + profile picture + {$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" ? ( - - mii image - - ) : ( -
    - {[ - `${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) => ( - mii image - ))} -
    - )} + {parentPage !== "admin" ? + mii image + : `${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" && (