This commit is contained in:
Landon & Emma 2026-04-24 14:42:41 -04:00 committed by GitHub
commit a4f92f1605
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 426 additions and 154 deletions

View file

@ -7,6 +7,12 @@ datasource db {
url = env("DATABASE_URL") url = env("DATABASE_URL")
} }
enum Theme {
LIGHT
DARK
SYSTEM
}
model User { model User {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
name String name String
@ -14,6 +20,7 @@ model User {
emailVerified DateTime? emailVerified DateTime?
image String? image String?
description String? @db.VarChar(512) description String? @db.VarChar(512)
theme Theme @default(SYSTEM)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt

View file

@ -0,0 +1,53 @@
import { NextRequest, NextResponse } from "next/server";
import z from "zod";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { RateLimit } from "@/lib/rate-limit";
const themeSchema = z.enum(["LIGHT", "DARK", "SYSTEM"]);
export async function GET(request: NextRequest) {
const session = await auth();
if (!session || !session.user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
try {
const user = await prisma.user.findUnique({
where: { id: Number(session.user.id) },
select: { theme: true },
});
if (!user) return NextResponse.json({ error: "User not found" }, { status: 404 });
return NextResponse.json({ theme: user.theme });
} catch (error) {
console.error("Failed to get theme:", error);
return NextResponse.json({ error: "Failed to get theme" }, { status: 500 });
}
}
export async function POST(request: NextRequest) {
const session = await auth();
if (!session || !session.user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const rateLimit = new RateLimit(request, 5);
const check = await rateLimit.handle();
if (check) return check;
const { theme } = await request.json();
const validation = themeSchema.safeParse(theme);
if (!validation.success) return rateLimit.sendResponse({ error: "Invalid theme value" }, 400);
try {
await prisma.user.update({
where: { id: Number(session.user.id) },
data: { theme: validation.data },
});
} catch (error) {
console.error("Failed to update theme:", error);
return rateLimit.sendResponse({ error: "Failed to update theme" }, 500);
}
return rateLimit.sendResponse({ success: true });
}

View file

@ -39,6 +39,8 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
if (user) { if (user) {
session.user.id = user.id; session.user.id = user.id;
session.user.email = user.email; session.user.email = user.email;
// @ts-expect-error - theme is added to User model
session.user.theme = user.theme;
} }
return session; return session;
}, },

View file

@ -12,7 +12,7 @@ function RedirectBanner() {
if (from !== "old-domain") return null; if (from !== "old-domain") return null;
return ( return (
<div className="w-full h-10 bg-orange-300 border-y-2 border-y-orange-400 mt-1 pl-2 shadow-md flex justify-center items-center gap-2 text-orange-900 text-nowrap overflow-x-auto font-semibold max-sm:justify-start"> <div className="w-full h-10 bg-orange-300 border-y-2 border-y-orange-400 mt-1 pl-2 shadow-md flex justify-center items-center gap-2 text-orange-900 text-nowrap overflow-x-auto font-semibold max-sm:justify-start dark:bg-slate-700 dark:border-y-slate-600 dark:text-slate-100">
<Icon icon="humbleicons:link" className="text-2xl min-w-6" /> <Icon icon="humbleicons:link" className="text-2xl min-w-6" />
<span>We have moved URLs, welcome to tomodachishare.com!</span> <span>We have moved URLs, welcome to tomodachishare.com!</span>
</div> </div>
@ -51,7 +51,7 @@ export default function AdminBanner() {
return ( return (
<> <>
{shouldShow && message && ( {shouldShow && message && (
<div className="relative w-full min-h-10 bg-orange-300 border-y-2 border-y-orange-400 mt-1 pl-2 shadow-md flex justify-center text-orange-900 text-nowrap overflow-x-auto font-semibold max-sm:justify-between"> <div className="relative w-full min-h-10 bg-orange-300 border-y-2 border-y-orange-400 mt-1 pl-2 shadow-md flex justify-center text-orange-900 text-nowrap overflow-x-auto font-semibold max-sm:justify-between dark:bg-slate-700 dark:border-y-slate-600 dark:text-slate-100">
<div className="flex gap-2 h-full items-center w-fit"> <div className="flex gap-2 h-full items-center w-fit">
<Icon icon="humbleicons:exclamation" className="text-2xl min-w-6" /> <Icon icon="humbleicons:exclamation" className="text-2xl min-w-6" />
<span>{message}</span> <span>{message}</span>

View file

@ -30,13 +30,13 @@ export default function Dropzone({ onDrop, options, children }: Props) {
{...getRootProps()} {...getRootProps()}
onDragOver={() => setIsDraggingOver(true)} onDragOver={() => setIsDraggingOver(true)}
onDragLeave={() => setIsDraggingOver(false)} onDragLeave={() => setIsDraggingOver(false)}
className={`relative bg-orange-200 flex flex-col justify-center items-center gap-2 p-4 rounded-xl border-2 border-dashed border-amber-500 select-none size-full transition-all duration-200 ${ className={`relative bg-orange-200 flex flex-col justify-center items-center gap-2 p-4 rounded-xl border-2 border-dashed border-amber-500 select-none size-full transition-all duration-200 dark:bg-slate-800 dark:border-slate-600 ${
isDraggingOver && "scale-105 brightness-90 shadow-xl" isDraggingOver && "scale-105 brightness-90 shadow-xl"
}`} }`}
> >
{/* Used to transition from border-dashed to border-solid */} {/* Used to transition from border-dashed to border-solid */}
<div <div
className={`absolute inset-0 rounded-[10px] outline-2 outline-amber-500 transition-opacity duration-300 ${ className={`absolute inset-0 rounded-[10px] outline-2 outline-amber-500 transition-opacity duration-300 dark:outline-slate-500 ${
isDraggingOver ? "opacity-100" : "opacity-0" isDraggingOver ? "opacity-100" : "opacity-0"
}`} }`}
></div> ></div>

View file

@ -7,20 +7,20 @@ export default function Footer() {
<div className="max-w-4xl mx-auto px-4 py-4"> <div className="max-w-4xl mx-auto px-4 py-4">
{/* Main disclaimer */} {/* Main disclaimer */}
<div className="text-center mb-2"> <div className="text-center mb-2">
<p className="text-sm text-zinc-600 font-medium">TomodachiShare is not affiliated with Nintendo</p> <p className="text-sm text-zinc-600 font-medium dark:text-slate-400">TomodachiShare is not affiliated with Nintendo</p>
</div> </div>
{/* Links section */} {/* Links section */}
<div className="flex flex-wrap justify-center items-center gap-x-4 text-sm max-sm:gap-x-12"> <div className="flex flex-wrap justify-center items-center gap-x-4 text-sm max-sm:gap-x-12">
<Link to="/terms-of-service" className="text-zinc-500 hover:text-zinc-700 transition-colors duration-200 hover:underline"> <Link to="/terms-of-service" className="text-zinc-500 hover:text-zinc-700 transition-colors duration-200 hover:underline dark:text-slate-400 dark:hover:text-slate-300">
Terms of Service Terms of Service
</Link> </Link>
<span className="text-zinc-400 hidden sm:inline" aria-hidden="true"> <span className="text-zinc-400 hidden sm:inline dark:text-slate-600" aria-hidden="true">
</span> </span>
<Link to="/privacy" className="text-zinc-500 hover:text-zinc-700 transition-colors duration-200 hover:underline"> <Link to="/privacy" className="text-zinc-500 hover:text-zinc-700 transition-colors duration-200 hover:underline dark:text-slate-400 dark:hover:text-slate-300">
Privacy Policy Privacy Policy
</Link> </Link>
@ -37,22 +37,22 @@ export default function Footer() {
Discord Discord
</Link> </Link>
<span className="text-zinc-400 hidden sm:inline" aria-hidden="true"> <span className="text-zinc-400 hidden sm:inline dark:text-slate-600" aria-hidden="true">
</span> </span>
<Link <Link
to="https://trafficlunar.net" to="https://trafficlunar.net"
target="_blank" target="_blank"
className="text-zinc-500 hover:text-zinc-700 transition-colors duration-200 hover:underline group" className="text-zinc-500 hover:text-zinc-700 transition-colors duration-200 hover:underline group dark:text-slate-400 dark:hover:text-slate-300"
> >
Made by <span className="text-orange-400 group-hover:text-orange-500 font-medium transition-colors duration-200">trafficlunar</span> Made by <span className="text-orange-400 group-hover:text-orange-500 font-medium transition-colors duration-200 dark:text-orange-500 dark:group-hover:text-orange-400">trafficlunar</span>
</Link> </Link>
</div> </div>
{/* Copyright */} {/* Copyright */}
<div className="text-center mt-4 mb-4"> <div className="text-center mt-4 mb-4">
<p className="text-xs text-zinc-400">© {new Date().getFullYear()} TomodachiShare. All rights reserved.</p> <p className="text-xs text-zinc-400 dark:text-slate-500">© {new Date().getFullYear()} TomodachiShare. All rights reserved.</p>
</div> </div>
</div> </div>
</footer> </footer>

View file

@ -1,5 +1,6 @@
import { Icon } from "@iconify/react"; import { Icon } from "@iconify/react";
import SearchBar from "./search-bar"; import SearchBar from "./search-bar";
import ThemeToggle from "./theme-toggle";
import { Link } from "react-router"; import { Link } from "react-router";
import { useStore } from "@nanostores/react"; import { useStore } from "@nanostores/react";
import { session } from "../session"; import { session } from "../session";
@ -42,30 +43,35 @@ export default function Header() {
<Link <Link
to={`${import.meta.env.VITE_API_URL}/random`} to={`${import.meta.env.VITE_API_URL}/random`}
aria-label="Go to Random Link" aria-label="Go to Random Link"
className="pill button p-0! h-full aspect-square" className="pill button p-0! h-full aspect-square dark:bg-orange-300 dark:border-orange-400"
data-tooltip="Go to a Random Mii" data-tooltip="Go to a Random Mii"
> >
<Icon icon="mdi:dice-3" fontSize={28} /> <Icon icon="mdi:dice-3" fontSize={28} />
</Link> </Link>
</li> </li>
<li> <li>
<Link to={"/submit"} className="pill button h-full"> <Link to={"/submit"} className="pill button h-full dark:bg-orange-300 dark:border-orange-400">
Submit Submit
</Link> </Link>
</li> </li>
{!$session?.user ? ( {!$session?.user ? (
<>
<li> <li>
<Link to={"/login"} className="pill button h-full"> <ThemeToggle size="md" />
</li>
<li>
<Link to={"/login"} className="pill button h-full dark:bg-orange-300 dark:border-orange-400">
Login Login
</Link> </Link>
</li> </li>
</>
) : ( ) : (
<> <>
<li title="Your profile"> <li title="Your profile">
<Link <Link
to={`/profile/${$session?.user?.id}`} to={`/profile/${$session?.user?.id}`}
aria-label="Go to profile" aria-label="Go to profile"
className="pill button gap-2! p-0! h-full max-w-64" className="pill button gap-2! p-0! h-full max-w-64 dark:bg-orange-300 dark:border-orange-400"
data-tooltip="Your Profile" data-tooltip="Your Profile"
> >
<img <img
@ -82,11 +88,14 @@ export default function Header() {
<span className="pr-4 overflow-hidden whitespace-nowrap text-ellipsis w-full">{$session?.user?.name ?? "unknown"}</span> <span className="pr-4 overflow-hidden whitespace-nowrap text-ellipsis w-full">{$session?.user?.name ?? "unknown"}</span>
</Link> </Link>
</li> </li>
<li>
<ThemeToggle size="md" />
</li>
<li title="Logout"> <li title="Logout">
<Link <Link
to={`${import.meta.env.VITE_API_URL}/api/auth/signout`} to={`${import.meta.env.VITE_API_URL}/api/auth/signout`}
aria-label="Log Out" aria-label="Log Out"
className="pill button p-2! aspect-square h-full" className="pill button p-2! aspect-square h-full dark:bg-orange-300 dark:border-orange-400"
data-tooltip="Log Out" data-tooltip="Log Out"
> >
<Icon icon="ic:round-logout" fontSize={24} /> <Icon icon="ic:round-logout" fontSize={24} />

View file

@ -53,18 +53,18 @@ export default function DeleteAccount() {
/> />
<div <div
className={`z-50 bg-orange-50 border-2 border-amber-500 rounded-2xl shadow-lg p-6 w-full max-w-md transition-discrete duration-300 flex flex-col ${ className={`z-50 bg-orange-50 border-2 border-amber-500 rounded-2xl shadow-lg p-6 w-full max-w-md transition-discrete duration-300 flex flex-col dark:bg-slate-800 dark:border-slate-600 ${
isVisible ? "scale-100 opacity-100" : "scale-75 opacity-0" isVisible ? "scale-100 opacity-100" : "scale-75 opacity-0"
}`} }`}
> >
<div className="flex justify-between items-center mb-2"> <div className="flex justify-between items-center mb-2">
<h2 className="text-xl font-bold">Delete Account</h2> <h2 className="text-xl font-bold dark:text-slate-100">Delete Account</h2>
<button onClick={close} aria-label="Close" className="text-red-400 hover:text-red-500 text-2xl cursor-pointer"> <button onClick={close} aria-label="Close" className="text-red-400 hover:text-red-500 text-2xl cursor-pointer">
<Icon icon="material-symbols:close-rounded" /> <Icon icon="material-symbols:close-rounded" />
</button> </button>
</div> </div>
<p className="text-sm text-zinc-500">Are you sure? This is permanent and will remove all uploaded Miis. This action cannot be undone.</p> <p className="text-sm text-zinc-500 dark:text-slate-400">Are you sure? This is permanent and will remove all uploaded Miis. This action cannot be undone.</p>
{error && <span className="text-red-400 font-bold mt-2">Error: {error}</span>} {error && <span className="text-red-400 font-bold mt-2">Error: {error}</span>}

View file

@ -69,27 +69,29 @@ export default function ProfileSettings({ currentDescription }: Props) {
}; };
return ( return (
<div className="bg-amber-50 border-2 border-amber-500 rounded-2xl shadow-lg p-4 flex flex-col gap-4"> <div className="bg-amber-50 border-2 border-amber-500 rounded-2xl shadow-lg p-4 flex flex-col gap-4 dark:bg-slate-900 dark:border-slate-700">
<div> <div>
<h2 className="text-2xl font-bold">Profile Settings</h2> <h2 className="text-2xl font-bold dark:text-slate-100">Settings</h2>
<p className="text-sm text-zinc-500">Update your profile picture, description, name, etc.</p> <p className="text-sm text-zinc-500 dark:text-slate-400">Update your account info and username.</p>
</div> </div>
{/* Separator */} {/* Separator */}
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mb-1"> <div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mb-1 dark:text-slate-400">
<hr className="grow border-zinc-300" /> <hr className="grow border-zinc-300 dark:border-slate-600" />
<span>Account Info</span> <span>Account Info</span>
<hr className="grow border-zinc-300" /> <hr className="grow border-zinc-300 dark:border-slate-600" />
</div> </div>
{/* Profile Picture */} {/* Profile Picture */}
<div className="dark:border-slate-600">
<ProfilePictureSettings /> <ProfilePictureSettings />
</div>
{/* Description */} {/* Description */}
<div className="grid grid-cols-5 gap-4 max-lg:grid-cols-1"> <div className="grid grid-cols-5 gap-4 max-lg:grid-cols-1">
<div className="col-span-3"> <div className="col-span-3">
<label className="font-semibold">About Me</label> <label className="font-semibold dark:text-slate-100">About Me</label>
<p className="text-sm text-zinc-500">Write about yourself on your profile</p> <p className="text-sm text-zinc-500 dark:text-slate-400">Write about yourself on your profile</p>
</div> </div>
<div className="flex justify-end gap-1 h-min col-span-2"> <div className="flex justify-end gap-1 h-min col-span-2">
@ -102,7 +104,7 @@ export default function ProfileSettings({ currentDescription }: Props) {
value={description || ""} value={description || ""}
onChange={(e) => setDescription(e.target.value)} onChange={(e) => setDescription(e.target.value)}
/> />
<p className="text-xs text-zinc-400 mt-1 text-right">{(description || "").length}/256</p> <p className="text-xs text-zinc-400 mt-1 text-right dark:text-slate-500">{(description || "").length}/256</p>
</div> </div>
<SubmitDialogButton <SubmitDialogButton
@ -117,8 +119,8 @@ export default function ProfileSettings({ currentDescription }: Props) {
{/* Change Name */} {/* Change Name */}
<div className="grid grid-cols-5 gap-4 max-lg:grid-cols-1"> <div className="grid grid-cols-5 gap-4 max-lg:grid-cols-1">
<div className="col-span-3"> <div className="col-span-3">
<label className="font-semibold">Change Name</label> <label className="font-semibold dark:text-slate-100">Change Name</label>
<p className="text-sm text-zinc-500">This is your name shown on your profile and miis feel free to change it anytime</p> <p className="text-sm text-zinc-500 dark:text-slate-400">This is your name shown on your profile and miis feel free to change it anytime</p>
</div> </div>
<div className="flex justify-end gap-1 h-min col-span-2"> <div className="flex justify-end gap-1 h-min col-span-2">
@ -129,26 +131,26 @@ export default function ProfileSettings({ currentDescription }: Props) {
error={nameChangeError} error={nameChangeError}
onSubmit={handleSubmitNameChange} onSubmit={handleSubmitNameChange}
> >
<div className="bg-orange-100 rounded-xl border-2 border-amber-500 mt-4 px-2 py-1"> <div className="bg-orange-100 rounded-xl border-2 border-amber-500 mt-4 px-2 py-1 dark:bg-slate-800 dark:border-slate-600">
<p className="font-semibold">New name:</p> <p className="font-semibold dark:text-slate-100">New name:</p>
<p className="indent-4">&apos;{name}&apos;</p> <p className="indent-4 dark:text-slate-300">&apos;{name}&apos;</p>
</div> </div>
</SubmitDialogButton> </SubmitDialogButton>
</div> </div>
</div> </div>
{/* Separator */} {/* Separator - Danger Zone */}
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium my-1"> <div className="flex items-center gap-4 text-zinc-500 text-sm font-medium my-1 dark:text-slate-400">
<hr className="grow border-zinc-300" /> <hr className="grow border-zinc-300 dark:border-slate-600" />
<span>Danger Zone</span> <span>Danger Zone</span>
<hr className="grow border-zinc-300" /> <hr className="grow border-zinc-300 dark:border-slate-600" />
</div> </div>
{/* Delete Account */} {/* Delete Account */}
<div className="grid grid-cols-2 gap-4 max-lg:grid-cols-1"> <div className="grid grid-cols-2 gap-4 max-lg:grid-cols-1">
<div> <div>
<label className="font-semibold">Delete Account</label> <label className="font-semibold dark:text-slate-100">Delete Account</label>
<p className="text-sm text-zinc-500">This will permanently remove your account and all uploaded Miis. This action cannot be undone</p> <p className="text-sm text-zinc-500 dark:text-slate-400">This will permanently remove your account and all uploaded Miis. This action cannot be undone</p>
</div> </div>
<DeleteAccount /> <DeleteAccount />

View file

@ -43,8 +43,8 @@ export default function ProfilePictureSettings() {
return ( return (
<div className="grid grid-cols-5 gap-4 max-lg:grid-cols-1"> <div className="grid grid-cols-5 gap-4 max-lg:grid-cols-1">
<div className="col-span-3"> <div className="col-span-3">
<label className="font-semibold">Profile Picture</label> <label className="font-semibold dark:text-slate-100">Profile Picture</label>
<p className="text-sm text-zinc-500">Manage your profile picture. Can only be changed once every 7 days.</p> <p className="text-sm text-zinc-500 dark:text-slate-400">Manage your profile picture. Can only be changed once every 7 days.</p>
</div> </div>
<div className="flex flex-col col-span-2"> <div className="flex flex-col col-span-2">
@ -61,7 +61,7 @@ export default function ProfilePictureSettings() {
alt="new profile picture" alt="new profile picture"
width={96} width={96}
height={96} height={96}
className="rounded-full aspect-square border-2 border-amber-500 object-cover" className="rounded-full aspect-square border-2 border-amber-500 object-cover dark:border-slate-600"
/> />
</Dropzone> </Dropzone>
</div> </div>
@ -83,19 +83,19 @@ export default function ProfilePictureSettings() {
error={error} error={error}
onSubmit={handleSubmit} onSubmit={handleSubmit}
> >
<p className="text-sm text-zinc-500 mt-2"> <p className="text-sm text-zinc-500 mt-2 dark:text-slate-400">
After submitting, you can change it again on {changeDate.toDate().toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" })} After submitting, you can change it again on {changeDate.toDate().toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" })}
. .
</p> </p>
<div className="bg-orange-100 rounded-xl border-2 border-amber-500 mt-4 px-2 py-1 flex items-center"> <div className="bg-orange-100 rounded-xl border-2 border-amber-500 mt-4 px-2 py-1 flex items-center dark:bg-slate-800 dark:border-slate-600">
<p className="font-semibold mb-2">New profile picture:</p> <p className="font-semibold mb-2 dark:text-slate-100">New profile picture:</p>
<img <img
src={newPicture ? URL.createObjectURL(newPicture) : "/guest.png"} src={newPicture ? URL.createObjectURL(newPicture) : "/guest.png"}
alt="new profile picture" alt="new profile picture"
width={128} width={128}
height={128} height={128}
className="rounded-full aspect-square border-2 border-amber-500 ml-auto object-cover" className="rounded-full aspect-square border-2 border-amber-500 ml-auto object-cover dark:border-slate-600"
/> />
</div> </div>
</SubmitDialogButton> </SubmitDialogButton>

View file

@ -50,18 +50,18 @@ export default function SubmitDialogButton({ title, description, onSubmit, error
/> />
<div <div
className={`z-50 bg-orange-50 border-2 border-amber-500 rounded-2xl shadow-lg p-6 w-full max-w-md transition-discrete duration-300 flex flex-col ${ className={`z-50 bg-orange-50 border-2 border-amber-500 rounded-2xl shadow-lg p-6 w-full max-w-md transition-discrete duration-300 flex flex-col dark:bg-slate-800 dark:border-slate-600 ${
isVisible ? "scale-100 opacity-100" : "scale-75 opacity-0" isVisible ? "scale-100 opacity-100" : "scale-75 opacity-0"
}`} }`}
> >
<div className="flex justify-between items-center mb-2"> <div className="flex justify-between items-center mb-2">
<h2 className="text-xl font-bold">{title}</h2> <h2 className="text-xl font-bold dark:text-slate-100">{title}</h2>
<button onClick={close} aria-label="Close" className="text-red-400 hover:text-red-500 text-2xl cursor-pointer"> <button onClick={close} aria-label="Close" className="text-red-400 hover:text-red-500 text-2xl cursor-pointer">
<Icon icon="material-symbols:close-rounded" /> <Icon icon="material-symbols:close-rounded" />
</button> </button>
</div> </div>
<p className="text-sm text-zinc-500">{description}</p> <p className="text-sm text-zinc-500 dark:text-slate-400">{description}</p>
{children} {children}
{error && <span className="text-red-400 font-bold mt-2">Error: {error}</span>} {error && <span className="text-red-400 font-bold mt-2">Error: {error}</span>}

View file

@ -28,20 +28,20 @@ export default function SearchBar() {
}; };
return ( return (
<div className="max-w-md w-full flex rounded-xl focus-within:ring-[3px] ring-orange-400/50 transition shadow-md"> <div className="max-w-md w-full flex rounded-xl focus-within:ring-[3px] ring-orange-400/50 transition shadow-md dark:ring-slate-500/50">
<input <input
type="text" type="text"
placeholder="Search..." placeholder="Search..."
value={query} value={query}
onChange={(e) => setQuery(e.target.value)} onChange={(e) => setQuery(e.target.value)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
className="bg-orange-200 border-2 border-orange-400 py-2 px-3 rounded-l-xl outline-0 w-full placeholder:text-black/40" className="bg-orange-200 border-2 border-orange-400 py-2 px-3 rounded-l-xl outline-0 w-full placeholder:text-black/40 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:placeholder:text-white/40"
/> />
<button <button
onClick={handleSearch} onClick={handleSearch}
aria-label="Search" aria-label="Search"
data-tooltip="Search" data-tooltip="Search"
className="bg-orange-400 p-2 w-12 rounded-r-xl flex justify-center items-center cursor-pointer text-2xl" className="bg-orange-400 p-2 w-12 rounded-r-xl flex justify-center items-center cursor-pointer text-2xl dark:bg-slate-600 dark:hover:bg-slate-500"
> >
<Icon icon="ic:baseline-search" /> <Icon icon="ic:baseline-search" />
</button> </button>

View file

@ -0,0 +1,55 @@
import { Icon } from "@iconify/react";
import { useStore } from "@nanostores/react";
import { themeStore, cycleTheme, applyTheme, type Theme } from "../lib/theme";
interface ThemeToggleProps {
size?: "sm" | "md" | "lg";
className?: string;
}
export default function ThemeToggle({ size = "md", className = "" }: ThemeToggleProps) {
const theme = useStore(themeStore);
const sizeClasses = {
sm: "h-8 w-8",
md: "h-10 w-10",
lg: "h-12 w-12",
};
const iconSizes = {
sm: 16,
md: 20,
lg: 24,
};
const handleClick = () => {
const currentTheme: Theme = theme ?? "SYSTEM";
const nextTheme = cycleTheme(currentTheme);
applyTheme(nextTheme);
};
const getIcon = () => {
if (theme === "DARK") return <Icon icon="mdi:moon" fontSize={iconSizes[size]} />;
if (theme === "LIGHT") return <Icon icon="mdi:white-balance-sunny" fontSize={iconSizes[size]} />;
// SYSTEM or undefined - show computer/monitor icon
return <Icon icon="mdi:monitor" fontSize={iconSizes[size]} />;
};
const getTooltip = () => {
if (theme === "DARK") return "Dark Mode";
if (theme === "LIGHT") return "Light Mode";
return "System Theme";
};
return (
<button
onClick={handleClick}
className={`pill button rounded-full! aspect-square ${sizeClasses[size]} ${className}`}
title={getTooltip()}
aria-label={`Current theme: ${theme ?? "SYSTEM"}. Click to cycle.`}
data-tooltip={getTooltip()}
>
{getIcon()}
</button>
);
}

View file

@ -1,5 +1,7 @@
@import "tailwindcss"; @import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
@theme { @theme {
--animate-like: like 0.5s ease; --animate-like: like 0.5s ease;
@ -24,27 +26,33 @@
} }
.pill { .pill {
@apply flex justify-center items-center px-5 py-2 bg-orange-300 border-2 border-orange-400 rounded-3xl shadow-md; @apply flex justify-center items-center px-5 py-2 bg-orange-300 border-2 border-orange-400 rounded-3xl shadow-md
dark:bg-slate-700 dark:border-slate-600;
} }
.button { .button {
@apply hover:bg-orange-400 transition cursor-pointer; @apply hover:bg-orange-400 transition cursor-pointer
dark:hover:bg-slate-600;
} }
.button:disabled { .button:disabled {
@apply text-zinc-600 bg-zinc-100! border-zinc-300! cursor-auto; @apply text-zinc-600 bg-zinc-100! border-zinc-300! cursor-auto
dark:text-zinc-400 dark:bg-slate-800! dark:border-slate-700!;
} }
.input { .input {
@apply bg-orange-200! outline-0 focus:ring-[3px] ring-orange-400/50 transition placeholder:text-black/40; @apply bg-orange-200! outline-0 focus:ring-[3px] ring-orange-400/50 transition placeholder:text-black/40
dark:bg-slate-800! dark:text-slate-100 dark:placeholder:text-white/40 dark:ring-slate-500/50;
} }
.input:disabled { .input:disabled {
@apply text-zinc-600 bg-zinc-100! border-zinc-300!; @apply text-zinc-600 bg-zinc-100! border-zinc-300!
dark:text-zinc-400 dark:bg-slate-800! dark:border-slate-700!;
} }
.checkbox { .checkbox {
@apply flex items-center justify-center appearance-none size-5 bg-orange-300 border-2 border-orange-400 rounded-md cursor-pointer checked:bg-orange-400; @apply flex items-center justify-center appearance-none size-5 bg-orange-300 border-2 border-orange-400 rounded-md cursor-pointer checked:bg-orange-400
dark:bg-slate-700 dark:border-slate-600 dark:checked:bg-slate-500;
} }
.checkbox::after { .checkbox::after {
@ -60,7 +68,8 @@
@apply relative appearance-none bg-zinc-400 rounded-2xl h-5 w-8.5 cursor-pointer transition-all @apply relative appearance-none bg-zinc-400 rounded-2xl h-5 w-8.5 cursor-pointer transition-all
after:transition-all after:bg-zinc-100 after:rounded-full after:h-3.5 after:absolute after:w-3.5 after:transition-all after:bg-zinc-100 after:rounded-full after:h-3.5 after:absolute after:w-3.5
after:left-[3px] after:top-[3px] hover:bg-zinc-500 checked:bg-orange-400 checked:after:left-[16px] after:left-[3px] after:top-[3px] hover:bg-zinc-500 checked:bg-orange-400 checked:after:left-[16px]
checked:hover:bg-orange-500 ml-auto; checked:hover:bg-orange-500 ml-auto
dark:bg-slate-600 dark:hover:bg-slate-500 dark:checked:bg-slate-500;
} }
[data-tooltip] { [data-tooltip] {
@ -72,7 +81,8 @@
} }
[data-tooltip]::after { [data-tooltip]::after {
@apply content-[attr(data-tooltip)] absolute left-1/2 -translate-x-1/2 top-full mt-2 px-2 py-1 bg-orange-400 border border-orange-400 rounded-md text-sm text-white opacity-0 scale-75 transition-all duration-200 ease-out origin-top shadow-md whitespace-nowrap select-none pointer-events-none; @apply content-[attr(data-tooltip)] absolute left-1/2 -translate-x-1/2 top-full mt-2 px-2 py-1 bg-orange-400 border border-orange-400 rounded-md text-sm text-white opacity-0 scale-75 transition-all duration-200 ease-out origin-top shadow-md whitespace-nowrap select-none pointer-events-none
dark:bg-slate-600 dark:border-slate-600;
} }
[data-tooltip]:hover::before, [data-tooltip]:hover::before,
@ -86,11 +96,13 @@
} }
[data-tooltip-span] > .tooltip { [data-tooltip-span] > .tooltip {
@apply absolute left-1/2 top-full mt-2 px-2 py-1 bg-orange-400 border border-orange-400 rounded-md text-sm text-white whitespace-nowrap select-none pointer-events-none shadow-md opacity-0 scale-75 transition-all duration-200 ease-out origin-top -translate-x-1/2 z-999999; @apply absolute left-1/2 top-full mt-2 px-2 py-1 bg-orange-400 border border-orange-400 rounded-md text-sm text-white whitespace-nowrap select-none pointer-events-none shadow-md opacity-0 scale-75 transition-all duration-200 ease-out origin-top -translate-x-1/2 z-999999
dark:bg-slate-600 dark:border-slate-600;
} }
[data-tooltip-span] > .tooltip::before { [data-tooltip-span] > .tooltip::before {
@apply content-[''] absolute left-1/2 -translate-x-1/2 -top-2 border-4 border-transparent border-b-orange-400; @apply content-[''] absolute left-1/2 -translate-x-1/2 -top-2 border-4 border-transparent border-b-orange-400
dark:border-b-slate-600;
} }
[data-tooltip-span]:hover > .tooltip { [data-tooltip-span]:hover > .tooltip {
@ -108,6 +120,10 @@
background: #ff8903; background: #ff8903;
} }
.dark *::-webkit-scrollbar-track {
background: #475569;
}
/* Range input */ /* Range input */
input[type="range"] { input[type="range"] {
@apply appearance-none bg-transparent not-disabled:cursor-pointer; @apply appearance-none bg-transparent not-disabled:cursor-pointer;
@ -118,27 +134,50 @@ input[type="range"]::-webkit-slider-runnable-track {
@apply h-1 bg-orange-300 rounded-full; @apply h-1 bg-orange-300 rounded-full;
} }
.dark input[type="range"]::-webkit-slider-runnable-track {
background: #475569;
}
input[type="range"]::-moz-range-track { input[type="range"]::-moz-range-track {
@apply h-1 bg-orange-300 rounded-full; @apply h-1 bg-orange-300 rounded-full;
} }
.dark input[type="range"]::-moz-range-track {
background: #475569;
}
/* Thumb */ /* Thumb */
input[type="range"]::-webkit-slider-thumb, input[type="range"]::-webkit-slider-thumb,
input[type="range"]::-moz-range-thumb { input[type="range"]::-moz-range-thumb {
@apply appearance-none size-4.5 bg-orange-400 border-2 border-orange-600 rounded-full shadow-md transition; @apply appearance-none size-4.5 bg-orange-400 border-2 border-orange-600 rounded-full shadow-md transition;
} }
.dark input[type="range"]::-webkit-slider-thumb,
.dark input[type="range"]::-moz-range-thumb {
background: #64748b;
border-color: #94a3b8;
}
/* Hover */ /* Hover */
input[type="range"]:hover::-webkit-slider-thumb { input[type="range"]:hover::-webkit-slider-thumb {
@apply not-disabled:bg-orange-500; @apply not-disabled:bg-orange-500;
} }
.dark input[type="range"]:hover::-webkit-slider-thumb {
background: #94a3b8;
}
input[type="range"]:hover::-moz-range-thumb { input[type="range"]:hover::-moz-range-thumb {
@apply not-disabled:bg-orange-500; @apply not-disabled:bg-orange-500;
} }
.dark input[type="range"]:hover::-moz-range-thumb {
background: #94a3b8;
}
body { body {
@apply bg-amber-50 text-slate-800 min-h-screen; @apply bg-amber-50 text-slate-800 min-h-screen
dark:bg-slate-900 dark:text-slate-100;
font-family: "Lexend Variable", sans-serif; font-family: "Lexend Variable", sans-serif;
/* syntax highlighting is a bit broken when it's at the top so it's at the bottom */ /* syntax highlighting is a bit broken when it's at the top so it's at the bottom */
@ -151,3 +190,13 @@ body {
</svg>'); </svg>');
background-size: 20px 20px; background-size: 20px 20px;
} }
.dark body {
background-image: url('data:image/svg+xml;utf8,\
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20">\
<rect width="10" height="10" fill="%230f172a"/>\
<rect x="10" y="10" width="10" height="10" fill="%230f172a"/>\
<rect x="10" width="10" height="10" fill="%231e293b"/>\
<rect y="10" width="10" height="10" fill="%231e293b"/>\
</svg>');
}

View file

@ -5,6 +5,7 @@ import Header from "./components/header";
import { useEffect } from "react"; import { useEffect } from "react";
import { useLocation, useNavigate } from "react-router"; import { useLocation, useNavigate } from "react-router";
import { session } from "./session"; import { session } from "./session";
import { initializeTheme } from "./lib/theme";
export default function Layout({ children }: { children: React.ReactNode }) { export default function Layout({ children }: { children: React.ReactNode }) {
const $session = useStore(session); const $session = useStore(session);
@ -13,6 +14,11 @@ export default function Layout({ children }: { children: React.ReactNode }) {
const API_URL = import.meta.env.VITE_API_URL; const API_URL = import.meta.env.VITE_API_URL;
// Initialize theme from session/cookie
useEffect(() => {
initializeTheme($session?.user?.theme ?? null);
}, [$session?.user?.theme]);
// Calculate header height // Calculate header height
useEffect(() => { useEffect(() => {
const header = document.querySelector("header"); const header = document.querySelector("header");

86
frontend/src/lib/theme.ts Normal file
View file

@ -0,0 +1,86 @@
import { atom } from "nanostores";
export type Theme = "LIGHT" | "DARK" | "SYSTEM";
const THEME_COOKIE_NAME = "theme";
// Theme store - undefined means not yet initialized
export const themeStore = atom<Theme | undefined>(undefined);
// Get system theme preference
function getSystemTheme(): "light" | "dark" {
if (typeof window === "undefined") return "light";
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
// Get resolved theme (actual light/dark to apply)
export function getResolvedTheme(theme: Theme): "light" | "dark" {
if (theme === "SYSTEM") return getSystemTheme();
return theme === "DARK" ? "dark" : "light";
}
// Get theme from cookie
export function getThemeCookie(): Theme | null {
if (typeof document === "undefined") return null;
const match = document.cookie.match(new RegExp(`(^| )${THEME_COOKIE_NAME}=([^;]+)`));
const value = match?.[2];
if (value === "LIGHT" || value === "DARK" || value === "SYSTEM") return value;
return null;
}
// Set theme cookie
export function setThemeCookie(theme: Theme): void {
if (typeof document === "undefined") return;
// Cookie expires in 1 year
const expires = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toUTCString();
document.cookie = `${THEME_COOKIE_NAME}=${theme};expires=${expires};path=/;SameSite=Lax`;
}
// Apply theme to document
export function applyTheme(theme: Theme): void {
const resolved = getResolvedTheme(theme);
const root = document.documentElement;
if (resolved === "dark") {
root.classList.add("dark");
} else {
root.classList.remove("dark");
}
setThemeCookie(theme);
themeStore.set(theme);
}
// Cycle to next theme
export function cycleTheme(current: Theme): Theme {
const order: Theme[] = ["LIGHT", "DARK", "SYSTEM"];
const currentIndex = order.indexOf(current);
const nextIndex = (currentIndex + 1) % order.length;
return order[nextIndex];
}
// Initialize theme from various sources
export function initializeTheme(serverTheme?: Theme | null): void {
// Priority: cookie > server > system default
const cookieTheme = getThemeCookie();
const initialTheme = cookieTheme ?? serverTheme ?? "SYSTEM";
applyTheme(initialTheme);
// Listen for system theme changes when on SYSTEM
if (typeof window !== "undefined") {
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
mediaQuery.addEventListener("change", () => {
const currentTheme = themeStore.get();
if (currentTheme === "SYSTEM") {
const resolved = getResolvedTheme("SYSTEM");
const root = document.documentElement;
if (resolved === "dark") {
root.classList.add("dark");
} else {
root.classList.remove("dark");
}
}
});
}
}

View file

@ -9,7 +9,7 @@ export default function IndexPage() {
<h1 className="sr-only"> <h1 className="sr-only">
{searchParams.get("tags") ? `Miis tagged with '${searchParams.get("tags")}' - TomodachiShare` : "TomodachiShare - index mii list"} {searchParams.get("tags") ? `Miis tagged with '${searchParams.get("tags")}' - TomodachiShare` : "TomodachiShare - index mii list"}
</h1> </h1>
<p className="text-center mb-4">We're currently going through some major code changes therefore some features won't work.</p> <p className="text-center mb-4 dark:text-slate-300">We're currently going through some major code changes therefore some features won't work.</p>
<MiiList /> <MiiList />
</> </>
); );

View file

@ -12,13 +12,13 @@ export default function LoginPage() {
return ( return (
<div className="grow flex items-center justify-center"> <div className="grow flex items-center justify-center">
<div className="bg-amber-50 border-2 border-amber-500 rounded-2xl shadow-lg px-10 py-12 max-w-md text-center"> <div className="bg-amber-50 border-2 border-amber-500 rounded-2xl shadow-lg px-10 py-12 max-w-md text-center dark:bg-slate-800 dark:border-slate-600">
<h1 className="text-3xl font-bold mb-4">Welcome to TomodachiShare!</h1> <h1 className="text-3xl font-bold mb-4 dark:text-slate-100">Welcome to TomodachiShare!</h1>
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mb-8"> <div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mb-8 dark:text-slate-400">
<hr className="grow border-zinc-300" /> <hr className="grow border-zinc-300 dark:border-slate-600" />
<span>Choose your login method</span> <span>Choose your login method</span>
<hr className="grow border-zinc-300" /> <hr className="grow border-zinc-300 dark:border-slate-600" />
</div> </div>
<div className="flex flex-col items-center gap-2"> <div className="flex flex-col items-center gap-2">
@ -48,13 +48,13 @@ export default function LoginPage() {
</Link> </Link>
</div> </div>
<p className="mt-8 text-xs text-zinc-400"> <p className="mt-8 text-xs text-zinc-400 dark:text-slate-500">
By signing up, you agree to the{" "} By signing up, you agree to the{" "}
<Link to="/terms-of-service" className="underline hover:text-zinc-600"> <Link to="/terms-of-service" className="underline hover:text-zinc-600 dark:hover:text-slate-300">
Terms of Service Terms of Service
</Link>{" "} </Link>{" "}
and{" "} and{" "}
<Link to="/privacy" className="underline hover:text-zinc-600"> <Link to="/privacy" className="underline hover:text-zinc-600 dark:hover:text-slate-300">
Privacy Policy Privacy Policy
</Link> </Link>
. .

View file

@ -92,7 +92,7 @@ export default function MiiPage() {
</div> </div>
)} )}
{mii.in_queue && ( {mii.in_queue && (
<div className="bg-zinc-50 border-2 border-zinc-400 rounded-2xl shadow-lg p-4 flex items-start gap-3 text-zinc-600"> <div className="bg-zinc-50 border-2 border-zinc-400 rounded-2xl shadow-lg p-4 flex items-start gap-3 text-zinc-600 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-300">
<Icon icon="material-symbols:timer" className="text-2xl shrink-0" /> <Icon icon="material-symbols:timer" className="text-2xl shrink-0" />
<p className="font-medium"> <p className="font-medium">
This Mii is waiting to be manually reviewed and is hidden from the main page. The review could take between a few hours and a few days. This Mii is waiting to be manually reviewed and is hidden from the main page. The review could take between a few hours and a few days.
@ -102,9 +102,9 @@ export default function MiiPage() {
</div> </div>
)} )}
<div className="relative grid grid-cols-3 gap-4 max-md:grid-cols-1"> <div className="relative grid grid-cols-3 gap-4 max-md:grid-cols-1">
<div className="bg-amber-50 rounded-3xl border-2 border-amber-500 shadow-lg p-4 h-min flex flex-col items-center max-w-md w-full max-md:place-self-center max-md:row-start-2"> <div className="bg-amber-50 rounded-3xl border-2 border-amber-500 shadow-lg p-4 h-min flex flex-col items-center max-w-md w-full max-md:place-self-center max-md:row-start-2 dark:bg-slate-800 dark:border-slate-600">
{/* Mii Image */} {/* Mii Image */}
<div className="bg-linear-to-b from-amber-100 to-amber-200 overflow-hidden rounded-xl w-full mb-4 flex justify-center"> <div className="bg-linear-to-b from-amber-100 to-amber-200 overflow-hidden rounded-xl w-full mb-4 flex justify-center dark:from-slate-700 dark:to-slate-800">
<ImageViewer <ImageViewer
src={`${API_URL}/mii/${mii.id}/image?type=mii`} src={`${API_URL}/mii/${mii.id}/image?type=mii`}
alt="mii headshot" alt="mii headshot"
@ -115,13 +115,13 @@ export default function MiiPage() {
</div> </div>
{/* QR Code */} {/* QR Code */}
{mii.platform === "THREE_DS" ? ( {mii.platform === "THREE_DS" ? (
<div className="bg-amber-200 overflow-hidden rounded-xl w-full mb-4 flex justify-center p-2"> <div className="bg-amber-200 overflow-hidden rounded-xl w-full mb-4 flex justify-center p-2 dark:bg-slate-700">
<ImageViewer <ImageViewer
src={`${API_URL}/mii/${mii.id}/image?type=qr-code`} src={`${API_URL}/mii/${mii.id}/image?type=qr-code`}
alt="mii qr code" alt="mii qr code"
width={128} width={128}
height={128} height={128}
className="border-2 border-amber-300 rounded-lg hover:brightness-90 transition-all" className="border-2 border-amber-300 rounded-lg hover:brightness-90 transition-all dark:border-slate-600"
/> />
</div> </div>
) : ( ) : (
@ -133,11 +133,11 @@ export default function MiiPage() {
className="rounded-lg hover:brightness-90 mb-4 transition-all" className="rounded-lg hover:brightness-90 mb-4 transition-all"
/> />
)} )}
<hr className="w-full border-t-2 border-t-amber-400" /> <hr className="w-full border-t-2 border-t-amber-400 dark:border-t-slate-600" />
{/* Mii Info */} {/* Mii Info */}
{mii.platform === "THREE_DS" && ( {mii.platform === "THREE_DS" && (
<ul className="text-sm w-full p-2 *:flex *:justify-between *:items-center *:my-1"> <ul className="text-sm w-full p-2 *:flex *:justify-between *:items-center *:my-1 dark:text-slate-300">
<li> <li>
Name:{" "} Name:{" "}
<span className="text-right font-medium"> <span className="text-right font-medium">
@ -154,10 +154,10 @@ export default function MiiPage() {
)} )}
{/* Mii Platform */} {/* Mii Platform */}
<div className={`flex items-center gap-4 text-zinc-500 text-sm font-medium mb-2 w-full ${mii.platform !== "THREE_DS" && "mt-2"}`}> <div className={`flex items-center gap-4 text-zinc-500 text-sm font-medium mb-2 w-full ${mii.platform !== "THREE_DS" && "mt-2"} dark:text-slate-400`}>
<hr className="grow border-zinc-300" /> <hr className="grow border-zinc-300 dark:border-slate-600" />
<span>Platform</span> <span>Platform</span>
<hr className="grow border-zinc-300" /> <hr className="grow border-zinc-300 dark:border-slate-600" />
</div> </div>
<div data-tooltip-span title={mii.platform} className="grid grid-cols-2 gap-2 mb-2"> <div data-tooltip-span title={mii.platform} className="grid grid-cols-2 gap-2 mb-2">
@ -171,7 +171,7 @@ export default function MiiPage() {
<div <div
className={`rounded-xl flex justify-center items-center size-13 text-3xl border-2 shadow-sm ${ className={`rounded-xl flex justify-center items-center size-13 text-3xl border-2 shadow-sm ${
mii.platform === "THREE_DS" ? "bg-sky-100 border-sky-400" : "bg-white border-gray-300" mii.platform === "THREE_DS" ? "bg-sky-100 border-sky-400" : "bg-white border-gray-300 dark:bg-slate-800 dark:border-slate-600"
}`} }`}
> >
<Icon icon="cib:nintendo-3ds" className="text-sky-500" /> <Icon icon="cib:nintendo-3ds" className="text-sky-500" />
@ -179,7 +179,7 @@ export default function MiiPage() {
<div <div
className={`rounded-xl flex justify-center items-center size-13 text-3xl border-2 shadow-sm ${ className={`rounded-xl flex justify-center items-center size-13 text-3xl border-2 shadow-sm ${
mii.platform === "SWITCH" ? "bg-red-100 border-red-400" : "bg-white border-gray-300" mii.platform === "SWITCH" ? "bg-red-100 border-red-400" : "bg-white border-gray-300 dark:bg-slate-800 dark:border-slate-600"
}`} }`}
> >
<Icon icon="cib:nintendo-switch" className="text-red-400" /> <Icon icon="cib:nintendo-switch" className="text-red-400" />
@ -187,10 +187,10 @@ export default function MiiPage() {
</div> </div>
{/* Mii Gender */} {/* Mii Gender */}
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mb-2 w-full"> <div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mb-2 w-full dark:text-slate-400">
<hr className="grow border-zinc-300" /> <hr className="grow border-zinc-300 dark:border-slate-600" />
<span>Gender</span> <span>Gender</span>
<hr className="grow border-zinc-300" /> <hr className="grow border-zinc-300 dark:border-slate-600" />
</div> </div>
<div data-tooltip-span title={mii.gender ?? "NULL"} className="flex gap-1"> <div data-tooltip-span title={mii.gender ?? "NULL"} className="flex gap-1">
@ -208,7 +208,7 @@ export default function MiiPage() {
<div <div
className={`rounded-xl flex justify-center items-center size-13 text-5xl border-2 shadow-sm ${ className={`rounded-xl flex justify-center items-center size-13 text-5xl border-2 shadow-sm ${
mii.gender === "MALE" ? "bg-blue-100 border-blue-400" : "bg-white border-gray-300" mii.gender === "MALE" ? "bg-blue-100 border-blue-400" : "bg-white border-gray-300 dark:bg-slate-800 dark:border-slate-600"
}`} }`}
> >
<Icon icon="foundation:male" className="text-blue-400" /> <Icon icon="foundation:male" className="text-blue-400" />
@ -216,7 +216,7 @@ export default function MiiPage() {
<div <div
className={`rounded-xl flex justify-center items-center size-13 text-5xl border-2 shadow-sm ${ className={`rounded-xl flex justify-center items-center size-13 text-5xl border-2 shadow-sm ${
mii.gender === "FEMALE" ? "bg-pink-100 border-pink-400" : "bg-white border-gray-300" mii.gender === "FEMALE" ? "bg-pink-100 border-pink-400" : "bg-white border-gray-300 dark:bg-slate-800 dark:border-slate-600"
}`} }`}
> >
<Icon icon="foundation:female" className="text-pink-400" /> <Icon icon="foundation:female" className="text-pink-400" />
@ -225,7 +225,7 @@ export default function MiiPage() {
{mii.platform !== "THREE_DS" && ( {mii.platform !== "THREE_DS" && (
<div <div
className={`rounded-xl flex justify-center items-center size-13 text-5xl border-2 shadow-sm ${ className={`rounded-xl flex justify-center items-center size-13 text-5xl border-2 shadow-sm ${
mii.gender === "NONBINARY" ? "bg-purple-100 border-purple-400" : "bg-white border-gray-300" mii.gender === "NONBINARY" ? "bg-purple-100 border-purple-400" : "bg-white border-gray-300 dark:bg-slate-800 dark:border-slate-600"
}`} }`}
> >
<Icon icon="mdi:gender-non-binary" className="text-purple-400" /> <Icon icon="mdi:gender-non-binary" className="text-purple-400" />
@ -236,10 +236,10 @@ export default function MiiPage() {
{/* Makeup */} {/* Makeup */}
{mii.platform === "SWITCH" && ( {mii.platform === "SWITCH" && (
<> <>
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mb-2 mt-2 w-full"> <div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mb-2 mt-2 w-full dark:text-slate-400">
<hr className="grow border-zinc-300" /> <hr className="grow border-zinc-300 dark:border-slate-600" />
<span>Makeup</span> <span>Makeup</span>
<hr className="grow border-zinc-300" /> <hr className="grow border-zinc-300 dark:border-slate-600" />
</div> </div>
<div data-tooltip-span title={mii.makeup ?? "NULL"} className="flex gap-1"> <div data-tooltip-span title={mii.makeup ?? "NULL"} className="flex gap-1">
@ -259,7 +259,7 @@ export default function MiiPage() {
{/* Full Makeup */} {/* Full Makeup */}
<div <div
className={`rounded-xl flex justify-center items-center size-13 text-5xl border-2 shadow-sm ${ className={`rounded-xl flex justify-center items-center size-13 text-5xl border-2 shadow-sm ${
mii.makeup === "FULL" ? "bg-pink-100 border-pink-400" : "bg-white border-gray-300" mii.makeup === "FULL" ? "bg-pink-100 border-pink-400" : "bg-white border-gray-300 dark:bg-slate-800 dark:border-slate-600"
}`} }`}
> >
<Icon icon="mdi:palette" className="text-pink-400" /> <Icon icon="mdi:palette" className="text-pink-400" />
@ -268,7 +268,7 @@ export default function MiiPage() {
{/* Partial Makeup */} {/* Partial Makeup */}
<div <div
className={`rounded-xl flex justify-center items-center size-13 text-5xl border-2 shadow-sm ${ className={`rounded-xl flex justify-center items-center size-13 text-5xl border-2 shadow-sm ${
mii.makeup === "PARTIAL" ? "bg-purple-100 border-purple-400" : "bg-white border-gray-300" mii.makeup === "PARTIAL" ? "bg-purple-100 border-purple-400" : "bg-white border-gray-300 dark:bg-slate-800 dark:border-slate-600"
}`} }`}
> >
<Icon icon="mdi:lipstick" className="text-purple-400" /> <Icon icon="mdi:lipstick" className="text-purple-400" />
@ -277,10 +277,10 @@ export default function MiiPage() {
{/* No Makeup */} {/* No Makeup */}
<div <div
className={`rounded-xl flex justify-center items-center size-13 text-5xl border-2 shadow-sm ${ className={`rounded-xl flex justify-center items-center size-13 text-5xl border-2 shadow-sm ${
mii.makeup === "NONE" ? "bg-gray-200 border-gray-400" : "bg-white border-gray-300" mii.makeup === "NONE" ? "bg-gray-200 border-gray-400 dark:bg-gray-600 dark:border-gray-700" : "bg-white border-gray-300 dark:bg-slate-800 dark:border-slate-600"
}`} }`}
> >
<Icon icon="codex:cross" className="text-gray-400" /> <Icon icon="codex:cross" className="text-gray-400 dark:text-gray-200" />
</div> </div>
</div> </div>
</> </>
@ -289,10 +289,10 @@ export default function MiiPage() {
<div className="col-span-2 flex flex-col gap-4 max-md:col-span-1"> <div className="col-span-2 flex flex-col gap-4 max-md:col-span-1">
{/* Information */} {/* Information */}
<div className="bg-amber-50 border-2 border-amber-500 rounded-2xl shadow-lg p-4 flex flex-col gap-1"> <div className="bg-amber-50 border-2 border-amber-500 rounded-2xl shadow-lg p-4 flex flex-col gap-1 dark:bg-slate-800 dark:border-slate-600">
<div className="flex justify-between items-start"> <div className="flex justify-between items-start">
{/* Submission name */} {/* Submission name */}
<h1 className="text-4xl font-extrabold wrap-break-word whitespace-break-spaces text-amber-700 flex-1 min-w-0">{mii.name}</h1> <h1 className="text-4xl font-extrabold wrap-break-word whitespace-break-spaces text-amber-700 flex-1 min-w-0 dark:text-amber-500">{mii.name}</h1>
{/* Like button */} {/* Like button */}
<LikeButton likes={mii.likeCount ?? 0} miiId={mii.id} isLiked={isLiked} big /> <LikeButton likes={mii.likeCount ?? 0} miiId={mii.id} isLiked={isLiked} big />
</div> </div>
@ -306,11 +306,11 @@ export default function MiiPage() {
</div> </div>
{/* Author and Created date */} {/* Author and Created date */}
<div className="mt-2"> <div className="mt-2 dark:text-slate-300">
<Link to={`/profile/${mii.userId}`} className="text-lg wrap-break-word"> <Link to={`/profile/${mii.userId}`} className="text-lg wrap-break-word dark:text-slate-200">
By <span className="font-bold">{mii.user.name}</span> By <span className="font-bold">{mii.user.name}</span>
</Link> </Link>
<h4 className="text-sm"> <h4 className="text-sm dark:text-slate-400">
Created:{" "} Created:{" "}
{new Date(mii.createdAt).toLocaleString("en-GB", { {new Date(mii.createdAt).toLocaleString("en-GB", {
day: "2-digit", day: "2-digit",
@ -330,7 +330,7 @@ export default function MiiPage() {
</div> </div>
{/* Buttons */} {/* Buttons */}
<div className="flex gap-3 w-fit bg-amber-50 border-2 border-amber-500 rounded-2xl shadow-lg p-4 text-3xl text-orange-400 max-md:place-self-center *:size-12 *:flex *:flex-col *:items-center *:gap-1 **:transition-discrete **:duration-150 *:hover:brightness-75 *:hover:scale-[1.08] *:[&_span]:text-xs"> <div className="flex gap-3 w-fit bg-amber-50 border-2 border-amber-500 rounded-2xl shadow-lg p-4 text-3xl text-orange-400 max-md:place-self-center dark:bg-slate-800 dark:border-slate-600 *:size-12 *:flex *:flex-col *:items-center *:gap-1 **:transition-discrete **:duration-150 *:hover:brightness-75 *:hover:scale-[1.08] *:[&_span]:text-xs">
<AuthorButtons mii={mii} /> <AuthorButtons mii={mii} />
<ShareMiiButton miiId={mii.id} /> <ShareMiiButton miiId={mii.id} />
@ -343,8 +343,8 @@ export default function MiiPage() {
{/* Instructions */} {/* Instructions */}
{mii.platform === "SWITCH" && ( {mii.platform === "SWITCH" && (
<div className="bg-amber-50 border-2 border-amber-500 rounded-2xl shadow-lg p-4 flex flex-col gap-3 max-h-96 overflow-y-auto"> <div className="bg-amber-50 border-2 border-amber-500 rounded-2xl shadow-lg p-4 flex flex-col gap-3 max-h-96 overflow-y-auto dark:bg-slate-800 dark:border-slate-600">
<h2 className="text-xl font-semibold text-amber-700 flex items-center gap-2"> <h2 className="text-xl font-semibold text-amber-700 flex items-center gap-2 dark:text-amber-500">
<Icon icon="fa7-solid:list" /> <Icon icon="fa7-solid:list" />
Instructions Instructions
</h2> </h2>

View file

@ -173,7 +173,7 @@ export default function SubmitPage() {
return ( return (
<div className="flex justify-center gap-4 w-full max-lg:flex-col max-lg:items-center"> <div className="flex justify-center gap-4 w-full max-lg:flex-col max-lg:items-center">
<div className="flex justify-center"> <div className="flex justify-center">
<div className="w-75 h-min flex flex-col bg-zinc-50 rounded-3xl border-2 border-zinc-300 shadow-lg p-3"> <div className="w-75 h-min flex flex-col bg-zinc-50 rounded-3xl border-2 border-zinc-300 shadow-lg p-3 dark:bg-slate-800 dark:border-slate-600">
<Carousel <Carousel
images={[ images={[
miiPortraitUri ?? "/loading.svg", miiPortraitUri ?? "/loading.svg",
@ -183,7 +183,7 @@ export default function SubmitPage() {
/> />
<div className="p-4 flex flex-col gap-1 h-full"> <div className="p-4 flex flex-col gap-1 h-full">
<h1 className="font-bold text-2xl line-clamp-1" title={name}> <h1 className="font-bold text-2xl line-clamp-1 dark:text-slate-100" title={name}>
{name || "Mii name"} {name || "Mii name"}
</h1> </h1>
<div id="tags" className="flex flex-wrap gap-1"> <div id="tags" className="flex flex-wrap gap-1">
@ -206,19 +206,19 @@ export default function SubmitPage() {
<div className="bg-amber-50 border-2 border-amber-500 rounded-2xl shadow-lg p-4 flex flex-col gap-2 w-full"> <div className="bg-amber-50 border-2 border-amber-500 rounded-2xl shadow-lg p-4 flex flex-col gap-2 w-full">
<div> <div>
<h2 className="text-2xl font-bold">Submit your Mii</h2> <h2 className="text-2xl font-bold">Submit your Mii</h2>
<p className="text-sm text-zinc-500">Share your creation for others to see.</p> <p className="text-sm text-zinc-500 dark:text-slate-400">Share your creation for others to see.</p>
</div> </div>
{/* Separator */} {/* Separator */}
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium my-1"> <div className="flex items-center gap-4 text-zinc-500 text-sm font-medium my-1 dark:text-slate-400">
<hr className="grow border-zinc-300" /> <hr className="grow border-zinc-300 dark:border-slate-600" />
<span>Info</span> <span>Info</span>
<hr className="grow border-zinc-300" /> <hr className="grow border-zinc-300 dark:border-slate-600" />
</div> </div>
{/* Platform select */} {/* Platform select */}
<div className="w-full grid grid-cols-3 items-center"> <div className="w-full grid grid-cols-3 items-center">
<label htmlFor="name" className="font-semibold"> <label htmlFor="name" className="font-semibold dark:text-slate-100">
Platform Platform
</label> </label>
<div className="relative col-span-2 grid grid-cols-2 bg-orange-300 border-2 border-orange-400 rounded-4xl shadow-md inset-shadow-sm/10"> <div className="relative col-span-2 grid grid-cols-2 bg-orange-300 border-2 border-orange-400 rounded-4xl shadow-md inset-shadow-sm/10">
@ -258,7 +258,7 @@ export default function SubmitPage() {
{/* Name */} {/* Name */}
<div className="w-full grid grid-cols-3 items-center"> <div className="w-full grid grid-cols-3 items-center">
<label htmlFor="name" className="font-semibold"> <label htmlFor="name" className="font-semibold dark:text-slate-100">
Name Name
</label> </label>
<input <input
@ -274,7 +274,7 @@ export default function SubmitPage() {
</div> </div>
<div className="w-full grid grid-cols-3 items-center"> <div className="w-full grid grid-cols-3 items-center">
<label htmlFor="tags" className="font-semibold"> <label htmlFor="tags" className="font-semibold dark:text-slate-100">
Tags Tags
</label> </label>
<TagSelector tags={tags} setTags={setTags} showTagLimit /> <TagSelector tags={tags} setTags={setTags} showTagLimit />
@ -282,7 +282,7 @@ export default function SubmitPage() {
{/* Description */} {/* Description */}
<div className="w-full grid grid-cols-3 items-start"> <div className="w-full grid grid-cols-3 items-start">
<label htmlFor="description" className="font-semibold py-2"> <label htmlFor="description" className="font-semibold py-2 dark:text-slate-100">
Description Description
</label> </label>
<textarea <textarea
@ -298,7 +298,7 @@ export default function SubmitPage() {
{/* Gender (switch only) */} {/* Gender (switch only) */}
<div className={`w-full grid grid-cols-3 items-start z-20 ${platform === "SWITCH" ? "" : "hidden"}`}> <div className={`w-full grid grid-cols-3 items-start z-20 ${platform === "SWITCH" ? "" : "hidden"}`}>
<label htmlFor="gender" className="font-semibold py-2"> <label htmlFor="gender" className="font-semibold py-2 dark:text-slate-100">
Gender Gender
</label> </label>
<div className="col-span-2 flex gap-1"> <div className="col-span-2 flex gap-1">
@ -308,7 +308,7 @@ export default function SubmitPage() {
aria-label="Filter for Male Miis" aria-label="Filter for Male Miis"
data-tooltip="Male" data-tooltip="Male"
className={`cursor-pointer rounded-xl flex justify-center items-center size-11 text-4xl border-2 transition-all after:bg-blue-400! after:border-blue-400! before:border-b-blue-400! ${ className={`cursor-pointer rounded-xl flex justify-center items-center size-11 text-4xl border-2 transition-all after:bg-blue-400! after:border-blue-400! before:border-b-blue-400! ${
gender === "MALE" ? "bg-blue-100 border-blue-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400" gender === "MALE" ? "bg-blue-100 border-blue-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400 dark:bg-slate-800 dark:border-slate-600 dark:hover:border-slate-500"
}`} }`}
> >
<Icon icon="foundation:male" className="text-blue-400" /> <Icon icon="foundation:male" className="text-blue-400" />
@ -320,7 +320,7 @@ export default function SubmitPage() {
aria-label="Filter for Female Miis" aria-label="Filter for Female Miis"
data-tooltip="Female" data-tooltip="Female"
className={`cursor-pointer rounded-xl flex justify-center items-center size-11 text-4xl border-2 transition-all after:bg-pink-400! after:border-pink-400! before:border-b-pink-400! ${ className={`cursor-pointer rounded-xl flex justify-center items-center size-11 text-4xl border-2 transition-all after:bg-pink-400! after:border-pink-400! before:border-b-pink-400! ${
gender === "FEMALE" ? "bg-pink-100 border-pink-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400" gender === "FEMALE" ? "bg-pink-100 border-pink-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400 dark:bg-slate-800 dark:border-slate-600 dark:hover:border-slate-500"
}`} }`}
> >
<Icon icon="foundation:female" className="text-pink-400" /> <Icon icon="foundation:female" className="text-pink-400" />
@ -332,7 +332,7 @@ export default function SubmitPage() {
aria-label="Filter for Nonbinary Miis" aria-label="Filter for Nonbinary Miis"
data-tooltip="Nonbinary" data-tooltip="Nonbinary"
className={`cursor-pointer rounded-xl flex justify-center items-center size-11 text-4xl border-2 transition-all after:bg-purple-400! after:border-purple-400! before:border-b-purple-400! ${ className={`cursor-pointer rounded-xl flex justify-center items-center size-11 text-4xl border-2 transition-all after:bg-purple-400! after:border-purple-400! before:border-b-purple-400! ${
gender === "NONBINARY" ? "bg-purple-100 border-purple-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400" gender === "NONBINARY" ? "bg-purple-100 border-purple-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400 dark:bg-slate-800 dark:border-slate-600 dark:hover:border-slate-500"
}`} }`}
> >
<Icon icon="mdi:gender-non-binary" className="text-purple-400" /> <Icon icon="mdi:gender-non-binary" className="text-purple-400" />
@ -342,7 +342,7 @@ export default function SubmitPage() {
{/* Makeup (switch only) */} {/* Makeup (switch only) */}
<div className={`w-full grid grid-cols-3 items-start ${platform === "SWITCH" ? "" : "hidden"}`}> <div className={`w-full grid grid-cols-3 items-start ${platform === "SWITCH" ? "" : "hidden"}`}>
<label className="font-semibold py-2">Face Paint</label> <label className="font-semibold py-2 dark:text-slate-100">Face Paint</label>
<div className="col-span-2 flex flex-col gap-1.5"> <div className="col-span-2 flex flex-col gap-1.5">
{[ {[
@ -355,11 +355,11 @@ export default function SubmitPage() {
type="button" type="button"
onClick={() => setMakeup(value as MiiMakeup)} onClick={() => setMakeup(value as MiiMakeup)}
className={`cursor-pointer rounded-xl text-left px-3 py-2 border-2 transition-all ${ className={`cursor-pointer rounded-xl text-left px-3 py-2 border-2 transition-all ${
makeup === value ? `bg-${color}-100 border-${color}-400 shadow-md` : "bg-white border-gray-300 hover:border-gray-400" makeup === value ? `bg-${color}-100 border-${color}-400 shadow-md` : "bg-white border-gray-300 hover:border-gray-400 dark:bg-slate-800 dark:border-slate-600 dark:hover:border-slate-500"
}`} }`}
> >
<div className={`font-medium text-sm ${makeup === value ? `text-${color}-500` : "text-gray-500"}`}>{label}</div> <div className={`font-medium text-sm ${makeup === value ? `text-${color}-500` : "text-gray-500 dark:text-slate-400"}`}>{label}</div>
<div className="text-xs text-gray-500 mt-0.5">{desc}</div> <div className="text-xs text-gray-500 mt-0.5 dark:text-slate-500">{desc}</div>
</button> </button>
))} ))}
</div> </div>
@ -368,10 +368,10 @@ export default function SubmitPage() {
{/* (Switch Only) Mii Screenshots */} {/* (Switch Only) Mii Screenshots */}
<div className={`${platform === "SWITCH" ? "" : "hidden"}`}> <div className={`${platform === "SWITCH" ? "" : "hidden"}`}>
{/* Separator */} {/* Separator */}
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mt-8 mb-2"> <div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mt-8 mb-2 dark:text-slate-400">
<hr className="grow border-zinc-300" /> <hr className="grow border-zinc-300 dark:border-slate-600" />
<span>Mii Screenshots</span> <span>Mii Screenshots</span>
<hr className="grow border-zinc-300" /> <hr className="grow border-zinc-300 dark:border-slate-600" />
</div> </div>
<div className="flex flex-col items-center gap-4 w-full"> <div className="flex flex-col items-center gap-4 w-full">
@ -379,7 +379,7 @@ export default function SubmitPage() {
<div className="flex flex-col items-center gap-2 w-full"> <div className="flex flex-col items-center gap-2 w-full">
<div className="flex items-center gap-2 self-start"> <div className="flex items-center gap-2 self-start">
<span className="bg-orange-400 text-white text-xs font-bold rounded-full size-5 flex items-center justify-center shrink-0">1</span> <span className="bg-orange-400 text-white text-xs font-bold rounded-full size-5 flex items-center justify-center shrink-0">1</span>
<span className="text-sm font-semibold text-zinc-600">Portrait screenshot</span> <span className="text-sm font-semibold text-zinc-600 dark:text-slate-300">Portrait screenshot</span>
</div> </div>
<div className="flex gap-3 w-full items-start max-sm:flex-col max-sm:items-center"> <div className="flex gap-3 w-full items-start max-sm:flex-col max-sm:items-center">
<div data-tooltip="Your screenshot should look like this"> <div data-tooltip="Your screenshot should look like this">
@ -388,7 +388,7 @@ export default function SubmitPage() {
alt="Example portrait screenshot" alt="Example portrait screenshot"
width={80} width={80}
height={80} height={80}
className="size-20 object-cover rounded-xl border-2 border-orange-300 shrink-0 opacity-70" className="size-20 object-cover rounded-xl border-2 border-orange-300 shrink-0 opacity-70 dark:border-slate-600"
/> />
</div> </div>
<SwitchFileUpload text="a screenshot of your Mii here" image={miiPortraitUri} setImage={setMiiPortraitUri} forceCrop /> <SwitchFileUpload text="a screenshot of your Mii here" image={miiPortraitUri} setImage={setMiiPortraitUri} forceCrop />
@ -399,8 +399,8 @@ export default function SubmitPage() {
<div className="flex flex-col items-center gap-2 w-full"> <div className="flex flex-col items-center gap-2 w-full">
<div className="flex items-center gap-2 self-start"> <div className="flex items-center gap-2 self-start">
<span className="bg-orange-400 text-white text-xs font-bold rounded-full size-5 flex items-center justify-center shrink-0">2</span> <span className="bg-orange-400 text-white text-xs font-bold rounded-full size-5 flex items-center justify-center shrink-0">2</span>
<span className="text-sm font-semibold text-zinc-600"> <span className="text-sm font-semibold text-zinc-600 dark:text-slate-300">
Features screenshot <span className="text-orange-500">(the features panel - see example)</span> Features screenshot <span className="text-orange-500 dark:text-orange-400">(the features panel - see example)</span>
</span> </span>
</div> </div>
<div className="flex gap-3 w-full items-start max-sm:flex-col max-sm:items-center"> <div className="flex gap-3 w-full items-start max-sm:flex-col max-sm:items-center">
@ -410,7 +410,7 @@ export default function SubmitPage() {
alt="Example features screenshot showing the parts panel" alt="Example features screenshot showing the parts panel"
width={80} width={80}
height={80} height={80}
className="size-20 object-cover rounded-xl border-2 border-orange-300 shrink-0 opacity-70" className="size-20 object-cover rounded-xl border-2 border-orange-300 shrink-0 opacity-70 dark:border-slate-600"
/> />
</div> </div>
<SwitchFileUpload text="a screenshot of your Mii's features here" image={miiFeaturesUri} setImage={setMiiFeaturesUri} /> <SwitchFileUpload text="a screenshot of your Mii's features here" image={miiFeaturesUri} setImage={setMiiFeaturesUri} />
@ -420,20 +420,20 @@ export default function SubmitPage() {
<SwitchSubmitTutorialButton /> <SwitchSubmitTutorialButton />
</div> </div>
<p className="text-xs text-zinc-400 text-center mt-2">A tutorial on how to screenshot the features is above.</p> <p className="text-xs text-zinc-400 text-center mt-2 dark:text-slate-500">A tutorial on how to screenshot the features is above.</p>
</div> </div>
{/* (3DS only) QR code scanning */} {/* (3DS only) QR code scanning */}
<div className={`${platform === "THREE_DS" ? "" : "hidden"}`}> <div className={`${platform === "THREE_DS" ? "" : "hidden"}`}>
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mt-8 mb-2"> <div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mt-8 mb-2 dark:text-slate-400">
<hr className="grow border-zinc-300" /> <hr className="grow border-zinc-300 dark:border-slate-600" />
<span>QR Code</span> <span>QR Code</span>
<hr className="grow border-zinc-300" /> <hr className="grow border-zinc-300 dark:border-slate-600" />
</div> </div>
<div className="flex flex-col items-center gap-2"> <div className="flex flex-col items-center gap-2">
<QrUpload setQrBytesRaw={setQrBytesRaw} /> <QrUpload setQrBytesRaw={setQrBytesRaw} />
<span>or</span> <span className="dark:text-slate-300">or</span>
<button type="button" aria-label="Use your camera" onClick={() => setIsQrScannerOpen(true)} className="pill button gap-2"> <button type="button" aria-label="Use your camera" onClick={() => setIsQrScannerOpen(true)} className="pill button gap-2">
<Icon icon="mdi:camera" fontSize={20} /> <Icon icon="mdi:camera" fontSize={20} />
@ -443,22 +443,22 @@ export default function SubmitPage() {
<Camera isOpen={isQrScannerOpen} setIsOpen={setIsQrScannerOpen} setQrBytesRaw={setQrBytesRaw} /> <Camera isOpen={isQrScannerOpen} setIsOpen={setIsQrScannerOpen} setQrBytesRaw={setQrBytesRaw} />
<ThreeDsScanTutorialButton /> <ThreeDsScanTutorialButton />
<span className="text-xs text-zinc-400">For emulators, aes_keys.txt is required.</span> <span className="text-xs text-zinc-400 dark:text-slate-500">For emulators, aes_keys.txt is required.</span>
</div> </div>
</div> </div>
{/* (Switch only) Mii instructions */} {/* (Switch only) Mii instructions */}
<div className={`${platform === "SWITCH" ? "" : "hidden"}`}> <div className={`${platform === "SWITCH" ? "" : "hidden"}`}>
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mt-8 mb-2"> <div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mt-8 mb-2 dark:text-slate-400">
<hr className="grow border-zinc-300" /> <hr className="grow border-zinc-300 dark:border-slate-600" />
<span>Mii Instructions</span> <span>Mii Instructions</span>
<hr className="grow border-zinc-300" /> <hr className="grow border-zinc-300 dark:border-slate-600" />
</div> </div>
<div className="flex flex-col items-center gap-2"> <div className="flex flex-col items-center gap-2">
{/* YouTube */} {/* YouTube */}
<div className="w-full grid grid-cols-3 items-center"> <div className="w-full grid grid-cols-3 items-center">
<label htmlFor="youtube" className="font-semibold"> <label htmlFor="youtube" className="font-semibold dark:text-slate-100">
YouTube Video YouTube Video
</label> </label>
<input <input
@ -479,34 +479,34 @@ export default function SubmitPage() {
<MiiEditor instructions={instructions} /> <MiiEditor instructions={instructions} />
<SwitchSubmitTutorialButton /> <SwitchSubmitTutorialButton />
<span className="text-xs text-zinc-400 text-center px-32 max-sm:px-8"> <span className="text-xs text-zinc-400 text-center px-32 max-sm:px-8 dark:text-slate-500">
Mii editor may be inaccurate. Instructions are REALLY recommended, but you do not have to add every instruction. Mii editor may be inaccurate. Instructions are REALLY recommended, but you do not have to add every instruction.
</span> </span>
</div> </div>
</div> </div>
{/* Custom images selector */} {/* Custom images selector */}
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mt-6 mb-2"> <div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mt-6 mb-2 dark:text-slate-400">
<hr className="grow border-zinc-300" /> <hr className="grow border-zinc-300 dark:border-slate-600" />
<span>Custom images</span> <span>Custom images</span>
<hr className="grow border-zinc-300" /> <hr className="grow border-zinc-300 dark:border-slate-600" />
</div> </div>
<div className="max-w-md w-full self-center flex flex-col items-center"> <div className="max-w-md w-full self-center flex flex-col items-center">
<Dropzone onDrop={handleDrop}> <Dropzone onDrop={handleDrop}>
<p className="text-center text-sm"> <p className="text-center text-sm dark:text-slate-300">
Drag and drop your images here Drag and drop your images here
<br /> <br />
or click to open or click to open
</p> </p>
</Dropzone> </Dropzone>
<span className="text-xs text-zinc-400 mt-2">Animated images currently not supported.</span> <span className="text-xs text-zinc-400 mt-2 dark:text-slate-500">Animated images currently not supported.</span>
</div> </div>
<ImageList files={files} setFiles={setFiles} /> <ImageList files={files} setFiles={setFiles} />
<hr className="border-zinc-300 my-2" /> <hr className="border-zinc-300 my-2 dark:border-slate-600" />
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
{error && <span className="text-red-400 font-bold">Error: {error}</span>} {error && <span className="text-red-400 font-bold">Error: {error}</span>}

View file

@ -1,10 +1,13 @@
import { atom } from "nanostores"; import { atom } from "nanostores";
export type Theme = "LIGHT" | "DARK" | "SYSTEM";
interface SessionData { interface SessionData {
user?: { user?: {
id: string; id: string;
image: string; image: string;
name: string; name: string;
theme?: Theme;
}; };
} }