From 4bdfefc1c602f8d073d7fc757301280a61e7b7c7 Mon Sep 17 00:00:00 2001 From: Landon & Emma <80786423+LandonAndEmma@users.noreply.github.com> Date: Wed, 22 Apr 2026 13:16:54 -0400 Subject: [PATCH] Add dark mode Fix #8 --- backend/prisma/schema.prisma | 7 ++ backend/src/app/api/auth/theme/route.ts | 53 +++++++++ backend/src/lib/auth.ts | 2 + frontend/src/components/admin/banner.tsx | 4 +- frontend/src/components/dropzone.tsx | 4 +- frontend/src/components/footer.tsx | 16 +-- frontend/src/components/header.tsx | 19 ++- .../profile-settings/delete-account.tsx | 6 +- .../src/components/profile-settings/index.tsx | 112 ++++++++++++++---- .../profile-settings/profile-picture.tsx | 14 +-- .../profile-settings/submit-dialog-button.tsx | 6 +- frontend/src/components/search-bar.tsx | 6 +- frontend/src/components/theme-toggle.tsx | 61 ++++++++++ frontend/src/index.css | 71 +++++++++-- frontend/src/layout.tsx | 6 + frontend/src/lib/theme.ts | 86 ++++++++++++++ frontend/src/session.ts | 3 + 17 files changed, 410 insertions(+), 66 deletions(-) create mode 100644 backend/src/app/api/auth/theme/route.ts create mode 100644 frontend/src/components/theme-toggle.tsx create mode 100644 frontend/src/lib/theme.ts diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index a5ccbae..a55c585 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -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 diff --git a/backend/src/app/api/auth/theme/route.ts b/backend/src/app/api/auth/theme/route.ts new file mode 100644 index 0000000..2d497c7 --- /dev/null +++ b/backend/src/app/api/auth/theme/route.ts @@ -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 }); +} diff --git a/backend/src/lib/auth.ts b/backend/src/lib/auth.ts index 56b515e..a442028 100644 --- a/backend/src/lib/auth.ts +++ b/backend/src/lib/auth.ts @@ -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; }, diff --git a/frontend/src/components/admin/banner.tsx b/frontend/src/components/admin/banner.tsx index 4ac02b5..8175e19 100644 --- a/frontend/src/components/admin/banner.tsx +++ b/frontend/src/components/admin/banner.tsx @@ -12,7 +12,7 @@ function RedirectBanner() { if (from !== "old-domain") return null; return ( -
TomodachiShare is not affiliated with Nintendo
+TomodachiShare is not affiliated with Nintendo
© {new Date().getFullYear()} TomodachiShare. All rights reserved.
+© {new Date().getFullYear()} TomodachiShare. All rights reserved.
Are you sure? This is permanent and will remove all uploaded Miis. This action cannot be undone.
+Are you sure? This is permanent and will remove all uploaded Miis. This action cannot be undone.
{error && Error: {error}} diff --git a/frontend/src/components/profile-settings/index.tsx b/frontend/src/components/profile-settings/index.tsx index 6e6fa57..07d1289 100644 --- a/frontend/src/components/profile-settings/index.tsx +++ b/frontend/src/components/profile-settings/index.tsx @@ -1,4 +1,5 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; +import { useStore } from "@nanostores/react"; import { userNameSchema } from "@tomodachi-share/shared/schemas"; @@ -7,6 +8,8 @@ import SubmitDialogButton from "./submit-dialog-button"; import DeleteAccount from "./delete-account"; import z from "zod"; import { useNavigate } from "react-router"; +import { session } from "../../session"; +import { type Theme, applyTheme } from "../../lib/theme"; interface Props { currentDescription: string | null | undefined; @@ -14,12 +17,22 @@ interface Props { export default function ProfileSettings({ currentDescription }: Props) { const navigate = useNavigate(); + const $session = useStore(session); const [description, setDescription] = useState(currentDescription); const [name, setName] = useState(""); + const [selectedTheme, setSelectedTheme] = useStateUpdate your profile picture, description, name, etc.
+Update your account info, username, and site-wide theme.
Write about yourself on your profile
+ +Write about yourself on your profile
{(description || "").length}/256
+{(description || "").length}/256
This is your name shown on your profile and miis — feel free to change it anytime
+ +This is your name shown on your profile and miis — feel free to change it anytime
New name:
-'{name}'
+New name:
+'{name}'
Choose your preferred color theme for the site
+This will permanently remove your account and all uploaded Miis. This action cannot be undone
+ +This will permanently remove your account and all uploaded Miis. This action cannot be undone
Manage your profile picture. Can only be changed once every 7 days.
+ +Manage your profile picture. Can only be changed once every 7 days.
+
After submitting, you can change it again on {changeDate.toDate().toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" })} .
-New profile picture:
+New profile picture:
{description}
+{description}
{children} {error && Error: {error}} diff --git a/frontend/src/components/search-bar.tsx b/frontend/src/components/search-bar.tsx index 73d5b1a..c3849b1 100644 --- a/frontend/src/components/search-bar.tsx +++ b/frontend/src/components/search-bar.tsx @@ -28,20 +28,20 @@ export default function SearchBar() { }; return ( -