feat: Add persistent Dark Mode with ThemeToggle component and layout script
All checks were successful
Automated Container Build / build-and-push (push) Successful in 58s
All checks were successful
Automated Container Build / build-and-push (push) Successful in 58s
This commit is contained in:
parent
ec8e601332
commit
87fc071961
5 changed files with 117 additions and 16 deletions
|
|
@ -36,6 +36,36 @@
|
|||
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
.dark {
|
||||
--color-bg-base: #0f1614;
|
||||
--color-bg-surface: #17221f;
|
||||
--color-bg-surface-alt: #1e2d28;
|
||||
--color-bg-callout: #1a332a;
|
||||
--color-primary: #4ab2b2;
|
||||
--color-primary-hover: #5ec7c7;
|
||||
--color-primary-light: #6cd1d1;
|
||||
--color-text-heading: #e4f0ec;
|
||||
--color-text-body: #c3d9d3;
|
||||
--color-text-secondary: #9cb8b0;
|
||||
--color-text-muted: #6b8a81;
|
||||
--color-border: #38524a;
|
||||
--color-border-light: #253832;
|
||||
--color-success: #4ade80;
|
||||
--color-success-bg: #143621;
|
||||
--color-error: #f87171;
|
||||
--color-error-bg: #3b1c1c;
|
||||
--color-badge-bg: #214035;
|
||||
--color-badge-text: #6fd6b8;
|
||||
--color-danger: #ef4444;
|
||||
--color-danger-hover: #f87171;
|
||||
|
||||
--shadow-card: 0 1px 3px rgba(0, 0, 0, 0.4), 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||
--shadow-card-hover: 0 4px 12px rgba(0, 0, 0, 0.6), 0 2px 4px rgba(0, 0, 0, 0.4);
|
||||
--shadow-modal: 0 20px 60px rgba(0, 0, 0, 0.5), 0 8px 20px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Base Styles ── */
|
||||
body {
|
||||
background: var(--color-bg-base);
|
||||
|
|
|
|||
|
|
@ -20,6 +20,21 @@ export default function RootLayout({
|
|||
}>) {
|
||||
return (
|
||||
<html lang="en" className={`${inter.variable} h-full`} suppressHydrationWarning>
|
||||
<head>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
try {
|
||||
if (localStorage.theme === 'dark') {
|
||||
document.documentElement.classList.add('dark')
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark')
|
||||
}
|
||||
} catch (_) {}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</head>
|
||||
<body className="min-h-full flex flex-col font-sans antialiased">
|
||||
{children}
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { getShareLink } from "@/services/shareService";
|
|||
import { notFound } from "next/navigation";
|
||||
import { FlashcardViewer } from "@/components/flashcards/FlashcardViewer";
|
||||
import { QuizViewer } from "@/components/quizzes/QuizViewer";
|
||||
import { ThemeToggle } from "@/components/ui/ThemeToggle";
|
||||
import Link from "next/link";
|
||||
|
||||
interface SharedPageProps {
|
||||
|
|
@ -48,6 +49,8 @@ export default async function SharedPage(props: SharedPageProps) {
|
|||
Shared {type === "flashcards" ? "Deck" : "Quiz"}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<ThemeToggle />
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm font-medium px-3 py-1 bg-badge-bg text-badge-text rounded-full">
|
||||
Read Only
|
||||
|
|
@ -61,6 +64,7 @@ export default async function SharedPage(props: SharedPageProps) {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main Content */}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { ThemeToggle } from "./ThemeToggle";
|
||||
|
||||
export function Navbar() {
|
||||
const router = useRouter();
|
||||
|
|
@ -35,6 +36,8 @@ export function Navbar() {
|
|||
</svg>
|
||||
Study
|
||||
</Link>
|
||||
<div className="flex items-center gap-4">
|
||||
<ThemeToggle />
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="text-sm text-text-muted hover:text-text-heading transition-colors cursor-pointer"
|
||||
|
|
@ -43,6 +46,7 @@ export function Navbar() {
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
48
src/components/ui/ThemeToggle.tsx
Normal file
48
src/components/ui/ThemeToggle.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function ThemeToggle() {
|
||||
const [isDark, setIsDark] = useState(false);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
setIsDark(document.documentElement.classList.contains("dark"));
|
||||
}, []);
|
||||
|
||||
if (!mounted) {
|
||||
return <div className="w-8 h-8" />; // placeholder to prevent layout shift
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
const nextDark = !isDark;
|
||||
setIsDark(nextDark);
|
||||
if (nextDark) {
|
||||
document.documentElement.classList.add("dark");
|
||||
localStorage.theme = "dark";
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark");
|
||||
localStorage.theme = "light";
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="p-1.5 text-text-muted hover:text-text-heading transition-colors cursor-pointer rounded-lg hover:bg-bg-surface-alt flex items-center justify-center"
|
||||
title={isDark ? "Switch to Light Mode" : "Switch to Dark Mode"}
|
||||
aria-label="Toggle Dark Mode"
|
||||
>
|
||||
{isDark ? (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue