mirror of
https://github.com/trafficlunar/tomodachi-share.git
synced 2026-05-13 13:17:45 +00:00
Merge 282a1d3938 into af7f1380bc
This commit is contained in:
commit
a4f92f1605
21 changed files with 426 additions and 154 deletions
|
|
@ -7,6 +7,12 @@ datasource db {
|
|||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
enum Theme {
|
||||
LIGHT
|
||||
DARK
|
||||
SYSTEM
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
|
|
@ -14,6 +20,7 @@ model User {
|
|||
emailVerified DateTime?
|
||||
image String?
|
||||
description String? @db.VarChar(512)
|
||||
theme Theme @default(SYSTEM)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
|
|
|||
53
backend/src/app/api/auth/theme/route.ts
Normal file
53
backend/src/app/api/auth/theme/route.ts
Normal 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 });
|
||||
}
|
||||
|
|
@ -39,6 +39,8 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
|||
if (user) {
|
||||
session.user.id = user.id;
|
||||
session.user.email = user.email;
|
||||
// @ts-expect-error - theme is added to User model
|
||||
session.user.theme = user.theme;
|
||||
}
|
||||
return session;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ function RedirectBanner() {
|
|||
if (from !== "old-domain") return null;
|
||||
|
||||
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" />
|
||||
<span>We have moved URLs, welcome to tomodachishare.com!</span>
|
||||
</div>
|
||||
|
|
@ -51,7 +51,7 @@ export default function AdminBanner() {
|
|||
return (
|
||||
<>
|
||||
{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">
|
||||
<Icon icon="humbleicons:exclamation" className="text-2xl min-w-6" />
|
||||
<span>{message}</span>
|
||||
|
|
|
|||
|
|
@ -30,13 +30,13 @@ export default function Dropzone({ onDrop, options, children }: Props) {
|
|||
{...getRootProps()}
|
||||
onDragOver={() => setIsDraggingOver(true)}
|
||||
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"
|
||||
}`}
|
||||
>
|
||||
{/* Used to transition from border-dashed to border-solid */}
|
||||
<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"
|
||||
}`}
|
||||
></div>
|
||||
|
|
|
|||
|
|
@ -7,20 +7,20 @@ export default function Footer() {
|
|||
<div className="max-w-4xl mx-auto px-4 py-4">
|
||||
{/* Main disclaimer */}
|
||||
<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>
|
||||
|
||||
{/* Links section */}
|
||||
<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
|
||||
</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>
|
||||
|
||||
<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
|
||||
</Link>
|
||||
|
||||
|
|
@ -37,22 +37,22 @@ export default function Footer() {
|
|||
Discord
|
||||
</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>
|
||||
|
||||
<Link
|
||||
to="https://trafficlunar.net"
|
||||
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>
|
||||
</div>
|
||||
|
||||
{/* Copyright */}
|
||||
<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>
|
||||
</footer>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Icon } from "@iconify/react";
|
||||
import SearchBar from "./search-bar";
|
||||
import ThemeToggle from "./theme-toggle";
|
||||
import { Link } from "react-router";
|
||||
import { useStore } from "@nanostores/react";
|
||||
import { session } from "../session";
|
||||
|
|
@ -42,30 +43,35 @@ export default function Header() {
|
|||
<Link
|
||||
to={`${import.meta.env.VITE_API_URL}/random`}
|
||||
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"
|
||||
>
|
||||
<Icon icon="mdi:dice-3" fontSize={28} />
|
||||
</Link>
|
||||
</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
|
||||
</Link>
|
||||
</li>
|
||||
{!$session?.user ? (
|
||||
<li>
|
||||
<Link to={"/login"} className="pill button h-full">
|
||||
Login
|
||||
</Link>
|
||||
</li>
|
||||
<>
|
||||
<li>
|
||||
<ThemeToggle size="md" />
|
||||
</li>
|
||||
<li>
|
||||
<Link to={"/login"} className="pill button h-full dark:bg-orange-300 dark:border-orange-400">
|
||||
Login
|
||||
</Link>
|
||||
</li>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<li title="Your profile">
|
||||
<Link
|
||||
to={`/profile/${$session?.user?.id}`}
|
||||
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"
|
||||
>
|
||||
<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>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<ThemeToggle size="md" />
|
||||
</li>
|
||||
<li title="Logout">
|
||||
<Link
|
||||
to={`${import.meta.env.VITE_API_URL}/api/auth/signout`}
|
||||
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"
|
||||
>
|
||||
<Icon icon="ic:round-logout" fontSize={24} />
|
||||
|
|
|
|||
|
|
@ -53,18 +53,18 @@ export default function DeleteAccount() {
|
|||
/>
|
||||
|
||||
<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"
|
||||
}`}
|
||||
>
|
||||
<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">
|
||||
<Icon icon="material-symbols:close-rounded" />
|
||||
</button>
|
||||
</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>}
|
||||
|
||||
|
|
|
|||
|
|
@ -69,27 +69,29 @@ export default function ProfileSettings({ currentDescription }: Props) {
|
|||
};
|
||||
|
||||
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>
|
||||
<h2 className="text-2xl font-bold">Profile Settings</h2>
|
||||
<p className="text-sm text-zinc-500">Update your profile picture, description, name, etc.</p>
|
||||
<h2 className="text-2xl font-bold dark:text-slate-100">Settings</h2>
|
||||
<p className="text-sm text-zinc-500 dark:text-slate-400">Update your account info and username.</p>
|
||||
</div>
|
||||
|
||||
{/* Separator */}
|
||||
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mb-1">
|
||||
<hr className="grow border-zinc-300" />
|
||||
<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 dark:border-slate-600" />
|
||||
<span>Account Info</span>
|
||||
<hr className="grow border-zinc-300" />
|
||||
<hr className="grow border-zinc-300 dark:border-slate-600" />
|
||||
</div>
|
||||
|
||||
{/* Profile Picture */}
|
||||
<ProfilePictureSettings />
|
||||
<div className="dark:border-slate-600">
|
||||
<ProfilePictureSettings />
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="grid grid-cols-5 gap-4 max-lg:grid-cols-1">
|
||||
<div className="col-span-3">
|
||||
<label className="font-semibold">About Me</label>
|
||||
<p className="text-sm text-zinc-500">Write about yourself on your profile</p>
|
||||
<label className="font-semibold dark:text-slate-100">About Me</label>
|
||||
<p className="text-sm text-zinc-500 dark:text-slate-400">Write about yourself on your profile</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-1 h-min col-span-2">
|
||||
|
|
@ -102,7 +104,7 @@ export default function ProfileSettings({ currentDescription }: Props) {
|
|||
value={description || ""}
|
||||
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>
|
||||
|
||||
<SubmitDialogButton
|
||||
|
|
@ -117,8 +119,8 @@ export default function ProfileSettings({ currentDescription }: Props) {
|
|||
{/* Change Name */}
|
||||
<div className="grid grid-cols-5 gap-4 max-lg:grid-cols-1">
|
||||
<div className="col-span-3">
|
||||
<label className="font-semibold">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>
|
||||
<label className="font-semibold dark:text-slate-100">Change Name</label>
|
||||
<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 className="flex justify-end gap-1 h-min col-span-2">
|
||||
|
|
@ -129,26 +131,26 @@ export default function ProfileSettings({ currentDescription }: Props) {
|
|||
error={nameChangeError}
|
||||
onSubmit={handleSubmitNameChange}
|
||||
>
|
||||
<div className="bg-orange-100 rounded-xl border-2 border-amber-500 mt-4 px-2 py-1">
|
||||
<p className="font-semibold">New name:</p>
|
||||
<p className="indent-4">'{name}'</p>
|
||||
<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 dark:text-slate-100">New name:</p>
|
||||
<p className="indent-4 dark:text-slate-300">'{name}'</p>
|
||||
</div>
|
||||
</SubmitDialogButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Separator */}
|
||||
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium my-1">
|
||||
<hr className="grow border-zinc-300" />
|
||||
{/* Separator - Danger Zone */}
|
||||
<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 dark:border-slate-600" />
|
||||
<span>Danger Zone</span>
|
||||
<hr className="grow border-zinc-300" />
|
||||
<hr className="grow border-zinc-300 dark:border-slate-600" />
|
||||
</div>
|
||||
|
||||
{/* Delete Account */}
|
||||
<div className="grid grid-cols-2 gap-4 max-lg:grid-cols-1">
|
||||
<div>
|
||||
<label className="font-semibold">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>
|
||||
<label className="font-semibold dark:text-slate-100">Delete Account</label>
|
||||
<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>
|
||||
|
||||
<DeleteAccount />
|
||||
|
|
|
|||
|
|
@ -43,8 +43,8 @@ export default function ProfilePictureSettings() {
|
|||
return (
|
||||
<div className="grid grid-cols-5 gap-4 max-lg:grid-cols-1">
|
||||
<div className="col-span-3">
|
||||
<label className="font-semibold">Profile Picture</label>
|
||||
<p className="text-sm text-zinc-500">Manage your profile picture. Can only be changed once every 7 days.</p>
|
||||
<label className="font-semibold dark:text-slate-100">Profile Picture</label>
|
||||
<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 className="flex flex-col col-span-2">
|
||||
|
|
@ -61,7 +61,7 @@ export default function ProfilePictureSettings() {
|
|||
alt="new profile picture"
|
||||
width={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>
|
||||
</div>
|
||||
|
|
@ -83,19 +83,19 @@ export default function ProfilePictureSettings() {
|
|||
error={error}
|
||||
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" })}
|
||||
.
|
||||
</p>
|
||||
|
||||
<div className="bg-orange-100 rounded-xl border-2 border-amber-500 mt-4 px-2 py-1 flex items-center">
|
||||
<p className="font-semibold mb-2">New profile picture:</p>
|
||||
<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 dark:text-slate-100">New profile picture:</p>
|
||||
<img
|
||||
src={newPicture ? URL.createObjectURL(newPicture) : "/guest.png"}
|
||||
alt="new profile picture"
|
||||
width={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>
|
||||
</SubmitDialogButton>
|
||||
|
|
|
|||
|
|
@ -50,18 +50,18 @@ export default function SubmitDialogButton({ title, description, onSubmit, error
|
|||
/>
|
||||
|
||||
<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"
|
||||
}`}
|
||||
>
|
||||
<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">
|
||||
<Icon icon="material-symbols:close-rounded" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-zinc-500">{description}</p>
|
||||
<p className="text-sm text-zinc-500 dark:text-slate-400">{description}</p>
|
||||
|
||||
{children}
|
||||
{error && <span className="text-red-400 font-bold mt-2">Error: {error}</span>}
|
||||
|
|
|
|||
|
|
@ -28,20 +28,20 @@ export default function SearchBar() {
|
|||
};
|
||||
|
||||
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
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
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
|
||||
onClick={handleSearch}
|
||||
aria-label="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" />
|
||||
</button>
|
||||
|
|
|
|||
55
frontend/src/components/theme-toggle.tsx
Normal file
55
frontend/src/components/theme-toggle.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
@import "tailwindcss";
|
||||
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
@theme {
|
||||
--animate-like: like 0.5s ease;
|
||||
|
||||
|
|
@ -24,27 +26,33 @@
|
|||
}
|
||||
|
||||
.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 {
|
||||
@apply hover:bg-orange-400 transition cursor-pointer;
|
||||
@apply hover:bg-orange-400 transition cursor-pointer
|
||||
dark:hover:bg-slate-600;
|
||||
}
|
||||
|
||||
.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 {
|
||||
@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 {
|
||||
@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 {
|
||||
@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 {
|
||||
|
|
@ -60,7 +68,8 @@
|
|||
@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: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] {
|
||||
|
|
@ -72,7 +81,8 @@
|
|||
}
|
||||
|
||||
[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,
|
||||
|
|
@ -86,11 +96,13 @@
|
|||
}
|
||||
|
||||
[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 {
|
||||
@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 {
|
||||
|
|
@ -108,6 +120,10 @@
|
|||
background: #ff8903;
|
||||
}
|
||||
|
||||
.dark *::-webkit-scrollbar-track {
|
||||
background: #475569;
|
||||
}
|
||||
|
||||
/* Range input */
|
||||
input[type="range"] {
|
||||
@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;
|
||||
}
|
||||
|
||||
.dark input[type="range"]::-webkit-slider-runnable-track {
|
||||
background: #475569;
|
||||
}
|
||||
|
||||
input[type="range"]::-moz-range-track {
|
||||
@apply h-1 bg-orange-300 rounded-full;
|
||||
}
|
||||
|
||||
.dark input[type="range"]::-moz-range-track {
|
||||
background: #475569;
|
||||
}
|
||||
|
||||
/* Thumb */
|
||||
input[type="range"]::-webkit-slider-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;
|
||||
}
|
||||
|
||||
.dark input[type="range"]::-webkit-slider-thumb,
|
||||
.dark input[type="range"]::-moz-range-thumb {
|
||||
background: #64748b;
|
||||
border-color: #94a3b8;
|
||||
}
|
||||
|
||||
/* Hover */
|
||||
input[type="range"]:hover::-webkit-slider-thumb {
|
||||
@apply not-disabled:bg-orange-500;
|
||||
}
|
||||
|
||||
.dark input[type="range"]:hover::-webkit-slider-thumb {
|
||||
background: #94a3b8;
|
||||
}
|
||||
|
||||
input[type="range"]:hover::-moz-range-thumb {
|
||||
@apply not-disabled:bg-orange-500;
|
||||
}
|
||||
|
||||
.dark input[type="range"]:hover::-moz-range-thumb {
|
||||
background: #94a3b8;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
/* syntax highlighting is a bit broken when it's at the top so it's at the bottom */
|
||||
|
|
@ -151,3 +190,13 @@ body {
|
|||
</svg>');
|
||||
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>');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import Header from "./components/header";
|
|||
import { useEffect } from "react";
|
||||
import { useLocation, useNavigate } from "react-router";
|
||||
import { session } from "./session";
|
||||
import { initializeTheme } from "./lib/theme";
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
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;
|
||||
|
||||
// Initialize theme from session/cookie
|
||||
useEffect(() => {
|
||||
initializeTheme($session?.user?.theme ?? null);
|
||||
}, [$session?.user?.theme]);
|
||||
|
||||
// Calculate header height
|
||||
useEffect(() => {
|
||||
const header = document.querySelector("header");
|
||||
|
|
|
|||
86
frontend/src/lib/theme.ts
Normal file
86
frontend/src/lib/theme.ts
Normal 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");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ export default function IndexPage() {
|
|||
<h1 className="sr-only">
|
||||
{searchParams.get("tags") ? `Miis tagged with '${searchParams.get("tags")}' - TomodachiShare` : "TomodachiShare - index mii list"}
|
||||
</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 />
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -12,13 +12,13 @@ export default function LoginPage() {
|
|||
|
||||
return (
|
||||
<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">
|
||||
<h1 className="text-3xl font-bold mb-4">Welcome to TomodachiShare!</h1>
|
||||
<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 dark:text-slate-100">Welcome to TomodachiShare!</h1>
|
||||
|
||||
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mb-8">
|
||||
<hr className="grow border-zinc-300" />
|
||||
<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 dark:border-slate-600" />
|
||||
<span>Choose your login method</span>
|
||||
<hr className="grow border-zinc-300" />
|
||||
<hr className="grow border-zinc-300 dark:border-slate-600" />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
|
|
@ -48,13 +48,13 @@ export default function LoginPage() {
|
|||
</Link>
|
||||
</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{" "}
|
||||
<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
|
||||
</Link>{" "}
|
||||
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
|
||||
</Link>
|
||||
.
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ export default function MiiPage() {
|
|||
</div>
|
||||
)}
|
||||
{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" />
|
||||
<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.
|
||||
|
|
@ -102,9 +102,9 @@ export default function MiiPage() {
|
|||
</div>
|
||||
)}
|
||||
<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 */}
|
||||
<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
|
||||
src={`${API_URL}/mii/${mii.id}/image?type=mii`}
|
||||
alt="mii headshot"
|
||||
|
|
@ -115,13 +115,13 @@ export default function MiiPage() {
|
|||
</div>
|
||||
{/* QR Code */}
|
||||
{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
|
||||
src={`${API_URL}/mii/${mii.id}/image?type=qr-code`}
|
||||
alt="mii qr code"
|
||||
width={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>
|
||||
) : (
|
||||
|
|
@ -133,11 +133,11 @@ export default function MiiPage() {
|
|||
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.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>
|
||||
Name:{" "}
|
||||
<span className="text-right font-medium">
|
||||
|
|
@ -154,10 +154,10 @@ export default function MiiPage() {
|
|||
)}
|
||||
|
||||
{/* 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"}`}>
|
||||
<hr className="grow border-zinc-300" />
|
||||
<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 dark:border-slate-600" />
|
||||
<span>Platform</span>
|
||||
<hr className="grow border-zinc-300" />
|
||||
<hr className="grow border-zinc-300 dark:border-slate-600" />
|
||||
</div>
|
||||
|
||||
<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
|
||||
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" />
|
||||
|
|
@ -179,7 +179,7 @@ export default function MiiPage() {
|
|||
|
||||
<div
|
||||
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" />
|
||||
|
|
@ -187,10 +187,10 @@ export default function MiiPage() {
|
|||
</div>
|
||||
|
||||
{/* Mii Gender */}
|
||||
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mb-2 w-full">
|
||||
<hr className="grow border-zinc-300" />
|
||||
<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 dark:border-slate-600" />
|
||||
<span>Gender</span>
|
||||
<hr className="grow border-zinc-300" />
|
||||
<hr className="grow border-zinc-300 dark:border-slate-600" />
|
||||
</div>
|
||||
|
||||
<div data-tooltip-span title={mii.gender ?? "NULL"} className="flex gap-1">
|
||||
|
|
@ -208,7 +208,7 @@ export default function MiiPage() {
|
|||
|
||||
<div
|
||||
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" />
|
||||
|
|
@ -216,7 +216,7 @@ export default function MiiPage() {
|
|||
|
||||
<div
|
||||
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" />
|
||||
|
|
@ -225,7 +225,7 @@ export default function MiiPage() {
|
|||
{mii.platform !== "THREE_DS" && (
|
||||
<div
|
||||
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" />
|
||||
|
|
@ -236,10 +236,10 @@ export default function MiiPage() {
|
|||
{/* Makeup */}
|
||||
{mii.platform === "SWITCH" && (
|
||||
<>
|
||||
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mb-2 mt-2 w-full">
|
||||
<hr className="grow border-zinc-300" />
|
||||
<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 dark:border-slate-600" />
|
||||
<span>Makeup</span>
|
||||
<hr className="grow border-zinc-300" />
|
||||
<hr className="grow border-zinc-300 dark:border-slate-600" />
|
||||
</div>
|
||||
|
||||
<div data-tooltip-span title={mii.makeup ?? "NULL"} className="flex gap-1">
|
||||
|
|
@ -259,7 +259,7 @@ export default function MiiPage() {
|
|||
{/* Full Makeup */}
|
||||
<div
|
||||
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" />
|
||||
|
|
@ -268,7 +268,7 @@ export default function MiiPage() {
|
|||
{/* Partial Makeup */}
|
||||
<div
|
||||
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" />
|
||||
|
|
@ -277,10 +277,10 @@ export default function MiiPage() {
|
|||
{/* No Makeup */}
|
||||
<div
|
||||
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>
|
||||
</>
|
||||
|
|
@ -289,10 +289,10 @@ export default function MiiPage() {
|
|||
|
||||
<div className="col-span-2 flex flex-col gap-4 max-md:col-span-1">
|
||||
{/* 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">
|
||||
{/* 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 */}
|
||||
<LikeButton likes={mii.likeCount ?? 0} miiId={mii.id} isLiked={isLiked} big />
|
||||
</div>
|
||||
|
|
@ -306,11 +306,11 @@ export default function MiiPage() {
|
|||
</div>
|
||||
|
||||
{/* Author and Created date */}
|
||||
<div className="mt-2">
|
||||
<Link to={`/profile/${mii.userId}`} className="text-lg wrap-break-word">
|
||||
<div className="mt-2 dark:text-slate-300">
|
||||
<Link to={`/profile/${mii.userId}`} className="text-lg wrap-break-word dark:text-slate-200">
|
||||
By <span className="font-bold">{mii.user.name}</span>
|
||||
</Link>
|
||||
<h4 className="text-sm">
|
||||
<h4 className="text-sm dark:text-slate-400">
|
||||
Created:{" "}
|
||||
{new Date(mii.createdAt).toLocaleString("en-GB", {
|
||||
day: "2-digit",
|
||||
|
|
@ -330,7 +330,7 @@ export default function MiiPage() {
|
|||
</div>
|
||||
|
||||
{/* 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} />
|
||||
|
||||
<ShareMiiButton miiId={mii.id} />
|
||||
|
|
@ -343,8 +343,8 @@ export default function MiiPage() {
|
|||
|
||||
{/* Instructions */}
|
||||
{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">
|
||||
<h2 className="text-xl font-semibold text-amber-700 flex items-center gap-2">
|
||||
<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 dark:text-amber-500">
|
||||
<Icon icon="fa7-solid:list" />
|
||||
Instructions
|
||||
</h2>
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ export default function SubmitPage() {
|
|||
return (
|
||||
<div className="flex justify-center gap-4 w-full max-lg:flex-col max-lg:items-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
|
||||
images={[
|
||||
miiPortraitUri ?? "/loading.svg",
|
||||
|
|
@ -183,7 +183,7 @@ export default function SubmitPage() {
|
|||
/>
|
||||
|
||||
<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"}
|
||||
</h1>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
{/* Separator */}
|
||||
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium my-1">
|
||||
<hr className="grow border-zinc-300" />
|
||||
<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 dark:border-slate-600" />
|
||||
<span>Info</span>
|
||||
<hr className="grow border-zinc-300" />
|
||||
<hr className="grow border-zinc-300 dark:border-slate-600" />
|
||||
</div>
|
||||
|
||||
{/* Platform select */}
|
||||
<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
|
||||
</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">
|
||||
|
|
@ -258,7 +258,7 @@ export default function SubmitPage() {
|
|||
|
||||
{/* Name */}
|
||||
<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
|
||||
</label>
|
||||
<input
|
||||
|
|
@ -274,7 +274,7 @@ export default function SubmitPage() {
|
|||
</div>
|
||||
|
||||
<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
|
||||
</label>
|
||||
<TagSelector tags={tags} setTags={setTags} showTagLimit />
|
||||
|
|
@ -282,7 +282,7 @@ export default function SubmitPage() {
|
|||
|
||||
{/* Description */}
|
||||
<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
|
||||
</label>
|
||||
<textarea
|
||||
|
|
@ -298,7 +298,7 @@ export default function SubmitPage() {
|
|||
|
||||
{/* Gender (switch only) */}
|
||||
<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
|
||||
</label>
|
||||
<div className="col-span-2 flex gap-1">
|
||||
|
|
@ -308,7 +308,7 @@ export default function SubmitPage() {
|
|||
aria-label="Filter for Male Miis"
|
||||
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! ${
|
||||
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" />
|
||||
|
|
@ -320,7 +320,7 @@ export default function SubmitPage() {
|
|||
aria-label="Filter for Female Miis"
|
||||
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! ${
|
||||
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" />
|
||||
|
|
@ -332,7 +332,7 @@ export default function SubmitPage() {
|
|||
aria-label="Filter for Nonbinary Miis"
|
||||
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! ${
|
||||
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" />
|
||||
|
|
@ -342,7 +342,7 @@ export default function SubmitPage() {
|
|||
|
||||
{/* Makeup (switch only) */}
|
||||
<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">
|
||||
{[
|
||||
|
|
@ -355,11 +355,11 @@ export default function SubmitPage() {
|
|||
type="button"
|
||||
onClick={() => setMakeup(value as MiiMakeup)}
|
||||
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="text-xs text-gray-500 mt-0.5">{desc}</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 dark:text-slate-500">{desc}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -368,10 +368,10 @@ export default function SubmitPage() {
|
|||
{/* (Switch Only) Mii Screenshots */}
|
||||
<div className={`${platform === "SWITCH" ? "" : "hidden"}`}>
|
||||
{/* Separator */}
|
||||
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mt-8 mb-2">
|
||||
<hr className="grow border-zinc-300" />
|
||||
<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 dark:border-slate-600" />
|
||||
<span>Mii Screenshots</span>
|
||||
<hr className="grow border-zinc-300" />
|
||||
<hr className="grow border-zinc-300 dark:border-slate-600" />
|
||||
</div>
|
||||
|
||||
<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 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="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 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">
|
||||
|
|
@ -388,7 +388,7 @@ export default function SubmitPage() {
|
|||
alt="Example portrait screenshot"
|
||||
width={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>
|
||||
<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 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="text-sm font-semibold text-zinc-600">
|
||||
Features screenshot <span className="text-orange-500">(the features panel - see example)</span>
|
||||
<span className="text-sm font-semibold text-zinc-600 dark:text-slate-300">
|
||||
Features screenshot <span className="text-orange-500 dark:text-orange-400">(the features panel - see example)</span>
|
||||
</span>
|
||||
</div>
|
||||
<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"
|
||||
width={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>
|
||||
<SwitchFileUpload text="a screenshot of your Mii's features here" image={miiFeaturesUri} setImage={setMiiFeaturesUri} />
|
||||
|
|
@ -420,20 +420,20 @@ export default function SubmitPage() {
|
|||
<SwitchSubmitTutorialButton />
|
||||
</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>
|
||||
|
||||
{/* (3DS only) QR code scanning */}
|
||||
<div className={`${platform === "THREE_DS" ? "" : "hidden"}`}>
|
||||
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mt-8 mb-2">
|
||||
<hr className="grow border-zinc-300" />
|
||||
<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 dark:border-slate-600" />
|
||||
<span>QR Code</span>
|
||||
<hr className="grow border-zinc-300" />
|
||||
<hr className="grow border-zinc-300 dark:border-slate-600" />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<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">
|
||||
<Icon icon="mdi:camera" fontSize={20} />
|
||||
|
|
@ -443,22 +443,22 @@ export default function SubmitPage() {
|
|||
<Camera isOpen={isQrScannerOpen} setIsOpen={setIsQrScannerOpen} setQrBytesRaw={setQrBytesRaw} />
|
||||
<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>
|
||||
|
||||
{/* (Switch only) Mii instructions */}
|
||||
<div className={`${platform === "SWITCH" ? "" : "hidden"}`}>
|
||||
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mt-8 mb-2">
|
||||
<hr className="grow border-zinc-300" />
|
||||
<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 dark:border-slate-600" />
|
||||
<span>Mii Instructions</span>
|
||||
<hr className="grow border-zinc-300" />
|
||||
<hr className="grow border-zinc-300 dark:border-slate-600" />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
{/* YouTube */}
|
||||
<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
|
||||
</label>
|
||||
<input
|
||||
|
|
@ -479,34 +479,34 @@ export default function SubmitPage() {
|
|||
|
||||
<MiiEditor instructions={instructions} />
|
||||
<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.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Custom images selector */}
|
||||
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mt-6 mb-2">
|
||||
<hr className="grow border-zinc-300" />
|
||||
<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 dark:border-slate-600" />
|
||||
<span>Custom images</span>
|
||||
<hr className="grow border-zinc-300" />
|
||||
<hr className="grow border-zinc-300 dark:border-slate-600" />
|
||||
</div>
|
||||
|
||||
<div className="max-w-md w-full self-center flex flex-col items-center">
|
||||
<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
|
||||
<br />
|
||||
or click to open
|
||||
</p>
|
||||
</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>
|
||||
|
||||
<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">
|
||||
{error && <span className="text-red-400 font-bold">Error: {error}</span>}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
import { atom } from "nanostores";
|
||||
|
||||
export type Theme = "LIGHT" | "DARK" | "SYSTEM";
|
||||
|
||||
interface SessionData {
|
||||
user?: {
|
||||
id: string;
|
||||
image: string;
|
||||
name: string;
|
||||
theme?: Theme;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue