From cf70aa4a67266fc14f79b9f528cb4288e5683005 Mon Sep 17 00:00:00 2001 From: imnyang Date: Tue, 21 Apr 2026 22:26:20 +0900 Subject: [PATCH] feat: implement dark mode support with theme provider and toggle component --- apps/frontend/package.json | 1 + apps/frontend/src/app/add/page.tsx | 2 +- apps/frontend/src/app/dashboard/page.tsx | 2 +- apps/frontend/src/app/detail/[id]/page.tsx | 12 +-- apps/frontend/src/app/edit/[id]/page.tsx | 2 +- apps/frontend/src/app/globals.css | 69 ++++++++++++++-- apps/frontend/src/app/layout.tsx | 9 ++- apps/frontend/src/app/page.tsx | 30 ++++--- apps/frontend/src/components/header.tsx | 14 +++- .../src/components/theme-provider.tsx | 52 ++++++++++++ apps/frontend/src/components/theme-toggle.tsx | 81 +++++++++++++++++++ bun.lock | 5 +- 12 files changed, 248 insertions(+), 31 deletions(-) create mode 100644 apps/frontend/src/components/theme-provider.tsx create mode 100644 apps/frontend/src/components/theme-toggle.tsx diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 68d0e2e..c52eff2 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -10,6 +10,7 @@ "format": "biome format --write" }, "dependencies": { + "lucide-react": "^1.8.0", "next": "16.2.3", "react": "19.2.4", "react-dom": "19.2.4", diff --git a/apps/frontend/src/app/add/page.tsx b/apps/frontend/src/app/add/page.tsx index ca5087c..75e2f2f 100644 --- a/apps/frontend/src/app/add/page.tsx +++ b/apps/frontend/src/app/add/page.tsx @@ -321,7 +321,7 @@ export default function AddPage() { } return ( -
+
diff --git a/apps/frontend/src/app/dashboard/page.tsx b/apps/frontend/src/app/dashboard/page.tsx index 3ce16a6..87b81a8 100644 --- a/apps/frontend/src/app/dashboard/page.tsx +++ b/apps/frontend/src/app/dashboard/page.tsx @@ -301,7 +301,7 @@ export default function DashboardPage() { } return ( -
+
diff --git a/apps/frontend/src/app/detail/[id]/page.tsx b/apps/frontend/src/app/detail/[id]/page.tsx index 5c4f550..73c868b 100644 --- a/apps/frontend/src/app/detail/[id]/page.tsx +++ b/apps/frontend/src/app/detail/[id]/page.tsx @@ -190,7 +190,7 @@ export default async function DetailPage({ params }: { params: Promise<{ id: str post = await postPromise; } catch (loadError) { return ( -
+
@@ -226,7 +226,7 @@ export default async function DetailPage({ params }: { params: Promise<{ id: str const tags = post.tags ?? []; return ( -
+
@@ -249,10 +249,10 @@ export default async function DetailPage({ params }: { params: Promise<{ id: str alt={post.author ?? post._id} loading="eager" decoding="async" - className="w-full border border-border bg-white object-contain image-scale" + className="w-full border border-border bg-card object-contain image-scale" /> ) : ( -
+
)}
@@ -294,7 +294,7 @@ export default async function DetailPage({ params }: { params: Promise<{ id: str href={post.url} target="_blank" rel="noopener noreferrer" - className="border border-border px-4 py-2 text-sm text-foreground/80 transition hover:bg-black/5" + className="border border-border px-4 py-2 text-sm text-foreground/80 transition hover:bg-accent" > 원본 보기 @@ -302,7 +302,7 @@ export default async function DetailPage({ params }: { params: Promise<{ id: str
돌아가기 diff --git a/apps/frontend/src/app/edit/[id]/page.tsx b/apps/frontend/src/app/edit/[id]/page.tsx index 920dd5c..7dab258 100644 --- a/apps/frontend/src/app/edit/[id]/page.tsx +++ b/apps/frontend/src/app/edit/[id]/page.tsx @@ -168,7 +168,7 @@ export default function EditPage() { } return ( -
+
diff --git a/apps/frontend/src/app/globals.css b/apps/frontend/src/app/globals.css index ca3a235..a2dfe34 100644 --- a/apps/frontend/src/app/globals.css +++ b/apps/frontend/src/app/globals.css @@ -5,7 +5,10 @@ src: url("/fonts/Orbit-Regular.woff2") format("truetype"); } -@theme inline { +/* Custom dark variant for class-based dark mode in Tailwind v4 */ +@custom-variant dark (&:where(.dark, .dark *)); + +@theme { --color-background: hsl(340 40% 98%); --color-foreground: hsl(315 21% 8%); --color-card: hsl(340 40% 98%); @@ -45,10 +48,52 @@ --radius-xl: calc(var(--radius) + 4px); } +@layer theme { + .dark { + --color-background: hsl(240 10% 4%); + --color-foreground: hsl(0 0% 95%); + --color-card: hsl(240 10% 5%); + --color-card-foreground: hsl(0 0% 95%); + --color-popover: hsl(240 10% 4%); + --color-popover-foreground: hsl(0 0% 95%); + --color-primary: hsl(340 50% 70%); + --color-primary-foreground: hsl(240 10% 4%); + --color-secondary: hsl(240 5% 15%); + --color-secondary-foreground: hsl(0 0% 95%); + --color-muted: hsl(240 5% 12%); + --color-muted-foreground: hsl(240 5% 65%); + --color-accent: hsl(240 5% 15%); + --color-accent-foreground: hsl(0 0% 95%); + --color-destructive: hsl(0 62.8% 30.6%); + --color-destructive-foreground: hsl(0 0% 95%); + --color-border: hsl(240 5% 18%); + --color-input: hsl(240 5% 18%); + --color-ring: hsl(340 50% 70%); + --color-chart-1: hsl(220 70% 50%); + --color-chart-2: hsl(160 60% 45%); + --color-chart-3: hsl(30 80% 55%); + --color-chart-4: hsl(280 65% 60%); + --color-chart-5: hsl(340 75% 55%); + --color-sidebar: hsl(240 10% 5%); + --color-sidebar-foreground: hsl(240 5% 90%); + --color-sidebar-primary: hsl(340 50% 70%); + --color-sidebar-primary-foreground: hsl(240 10% 4%); + --color-sidebar-accent: hsl(240 5% 15%); + --color-sidebar-accent-foreground: hsl(0 0% 95%); + --color-sidebar-border: hsl(240 5% 18%); + --color-sidebar-ring: hsl(340 50% 70%); + + --scrollbar: hsla(240 5% 40% / 0.5); + --scrollbar-hover: hsla(240 5% 40% / 0.8); + --selection: #ff1493; + } +} + :root { --scrollbar: hsla(340 10% 60% / 0.5); --scrollbar-hover: hsla(340 10% 60% / 0.8); --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); + --selection: #ff69b4; } .image-scale { @@ -87,21 +132,31 @@ background: var(--scrollbar-hover); } -/* ::-webkit-scrollbar:not(.highlighttable, .highlight table, .gist .highlight) { - background: var(--theme); -} -*/ /* reset */ ::-webkit-scrollbar { width: 19px; height: 11px; } -/* from PaperMod https://github.com/adityatelange/hugo-PaperMod/blob/c98a924842fc7ee0c14212c316c69ede3ad76ca3/assets/css/includes/scroll-bar.css */ - @layer base { body { @apply bg-background text-foreground min-h-screen font-sans antialiased; font-family: "Orbit", sans-serif; } } + +@layer utilities { + .bg-main-gradient { + background-image: radial-gradient(circle at top, rgba(255, 255, 255, 0.9), rgba(247, 229, 236, 0.85) 45%, rgba(244, 240, 243, 1) 100%); + } + + .dark .bg-main-gradient { + background-image: + radial-gradient(circle at top left, rgba(255, 105, 180, 0.03), transparent 40%), + radial-gradient(circle at top, rgba(80, 80, 100, 0.08), rgba(10, 10, 15, 0.95) 40%, rgba(5, 5, 8, 1) 100%); + } + + .dark img { + filter: invert(1) hue-rotate(180deg); + } +} diff --git a/apps/frontend/src/app/layout.tsx b/apps/frontend/src/app/layout.tsx index 1ee5ad6..a7f0706 100644 --- a/apps/frontend/src/app/layout.tsx +++ b/apps/frontend/src/app/layout.tsx @@ -3,6 +3,8 @@ import "./globals.css"; import "react-photo-album/masonry.css"; +import { ThemeProvider } from "../components/theme-provider"; + export const metadata: Metadata = { title: "Akiyama Mizuki", description: "Gallery" @@ -17,8 +19,13 @@ export default function RootLayout({ - {children} + + + {children} + + ); } diff --git a/apps/frontend/src/app/page.tsx b/apps/frontend/src/app/page.tsx index 2661c8d..7581b12 100644 --- a/apps/frontend/src/app/page.tsx +++ b/apps/frontend/src/app/page.tsx @@ -29,6 +29,7 @@ type GalleryPhoto = { key: string; alt: string; author: string; + source: string; }; type Me = { @@ -414,6 +415,7 @@ export default function App() { key: upload._id, alt: `tweet ${upload.tweetId} media ${upload.mediaIndex + 1}`, author: upload.author?.trim() || "unknown", + source: upload.tweet?.url || "", }); }; image.onerror = () => { @@ -424,6 +426,7 @@ export default function App() { key: upload._id, alt: `tweet ${upload.tweetId} media ${upload.mediaIndex + 1}`, author: upload.author?.trim() || "unknown", + source: upload.tweet?.url || "", }); }; }), @@ -443,7 +446,7 @@ export default function App() { }, [items]); return ( -
+
@@ -482,7 +485,7 @@ export default function App() { {Array.from({ length: 12 }).map((_, index) => (
))}
@@ -515,10 +518,19 @@ export default function App() { decoding="async" className="block w-full" /> -
-
+
+
+
), @@ -564,7 +576,7 @@ export default function App() { ) : null} + +
+
+
+
, + document.body + ) : null; + + return ( + <> + + + {modalUI} + + ); +} diff --git a/bun.lock b/bun.lock index 4500140..d75cc4b 100644 --- a/bun.lock +++ b/bun.lock @@ -24,6 +24,7 @@ "name": "akiyama.mizuki.guru", "version": "0.1.0", "dependencies": { + "lucide-react": "^1.8.0", "next": "16.2.3", "react": "19.2.4", "react-dom": "19.2.4", @@ -397,7 +398,7 @@ "bson": ["bson@7.2.0", "", {}, "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ=="], - "bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="], + "bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], "caniuse-lite": ["caniuse-lite@1.0.30001788", "", {}, "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ=="], @@ -459,6 +460,8 @@ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + "lucide-react": ["lucide-react@1.8.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-WuvlsjngSk7TnTBJ1hsCy3ql9V9VOdcPkd3PKcSmM34vJD8KG6molxz7m7zbYFgICwsanQWmJ13JlYs4Zp7Arw=="], + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], "memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="],