Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 33 additions & 28 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,39 @@
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
}

/* Dark mode variables */
.dark {
--background: #0a0a0a;
--foreground: #ededed;
--muted: #1a1a1a;
--muted-foreground: #a1a1aa;
--popover: #0a0a0a;
--popover-foreground: #ededed;
--card: #0a0a0a;
--card-foreground: #ededed;
--border: #27272a;
--input: #27272a;
--primary: #ededed;
--primary-foreground: #0a0a0a;
--secondary: #1a1a1a;
--secondary-foreground: #ededed;
--accent: #1a1a1a;
--accent-foreground: #ededed;
--destructive: #ef4444;
--destructive-foreground: #ffffff;
--success: #10b981;
--success-foreground: #ffffff;
--warning: #f59e0b;
--warning-foreground: #ffffff;
--ring: #ededed;

/* Dark mode shadows */
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.4), 0 2px 4px -2px rgb(0 0 0 / 0.3);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.4), 0 4px 6px -4px rgb(0 0 0 / 0.3);
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.4), 0 8px 10px -6px rgb(0 0 0 / 0.3);
}

@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
Expand All @@ -106,34 +139,6 @@
--border-radius: var(--radius);
}

@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
--muted: #1a1a1a;
--muted-foreground: #a1a1aa;
--popover: #0a0a0a;
--popover-foreground: #ededed;
--card: #0a0a0a;
--card-foreground: #ededed;
--border: #27272a;
--input: #27272a;
--primary: #ededed;
--primary-foreground: #0a0a0a;
--secondary: #1a1a1a;
--secondary-foreground: #ededed;
--accent: #1a1a1a;
--accent-foreground: #ededed;
--destructive: #ef4444;
--destructive-foreground: #ffffff;
--success: #10b981;
--success-foreground: #ffffff;
--warning: #f59e0b;
--warning-foreground: #ffffff;
--ring: #ededed;
}
}

body {
background: var(--background);
color: var(--foreground);
Expand Down
15 changes: 9 additions & 6 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import "./globals.css";
import { Header } from "@/components/layout/header";
import { Footer } from "@/components/layout/footer";
import { SkipLinks } from "@/components/ui/skip-links";
import { ThemeProvider } from "@/lib/theme-context";

const geistSans = Geist({
variable: "--font-geist-sans",
Expand Down Expand Up @@ -55,12 +56,14 @@ export default function RootLayout({
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<SkipLinks />
<div className="flex min-h-screen flex-col">
<Header />
<main id="main-content" className="flex-1">{children}</main>
<Footer />
</div>
<ThemeProvider>
<SkipLinks />
<div className="flex min-h-screen flex-col">
<Header />
<main id="main-content" className="flex-1">{children}</main>
<Footer />
</div>
</ThemeProvider>
</body>
</html>
);
Expand Down
48 changes: 3 additions & 45 deletions src/hooks/use-theme.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,9 @@
"use client"

import { useState, useEffect } from 'react'
import { useThemeContext } from '@/lib/theme-context'

export type Theme = 'light' | 'dark'
export type { Theme } from '@/lib/theme-context'

export function useTheme() {
const [theme, setTheme] = useState<Theme>('light')
const [mounted, setMounted] = useState(false)

useEffect(() => {
setMounted(true)

// Check for stored theme preference or default to system preference
const stored = localStorage.getItem('theme') as Theme | null
if (stored) {
setTheme(stored)
document.documentElement.classList.toggle('dark', stored === 'dark')
} else {
// Check system preference
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
const systemTheme: Theme = prefersDark ? 'dark' : 'light'
setTheme(systemTheme)
document.documentElement.classList.toggle('dark', systemTheme === 'dark')
}
}, [])

const toggleTheme = () => {
if (!mounted) return

const newTheme: Theme = theme === 'light' ? 'dark' : 'light'
setTheme(newTheme)
localStorage.setItem('theme', newTheme)
document.documentElement.classList.toggle('dark', newTheme === 'dark')
}

const setThemeValue = (newTheme: Theme) => {
if (!mounted) return

setTheme(newTheme)
localStorage.setItem('theme', newTheme)
document.documentElement.classList.toggle('dark', newTheme === 'dark')
}

return {
theme,
toggleTheme,
setTheme: setThemeValue,
mounted,
}
return useThemeContext()
}
88 changes: 88 additions & 0 deletions src/lib/theme-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"use client"

import React, { createContext, useContext, useState, useEffect } from 'react'

export type Theme = 'light' | 'dark'

interface ThemeContextType {
theme: Theme
toggleTheme: () => void
setTheme: (theme: Theme) => void
mounted: boolean
}

const ThemeContext = createContext<ThemeContextType | undefined>(undefined)

interface ThemeProviderProps {
children: React.ReactNode
}

export function ThemeProvider({ children }: ThemeProviderProps) {
const [theme, setThemeState] = useState<Theme>('light')
const [mounted, setMounted] = useState(false)

// Initialize theme on mount
useEffect(() => {
setMounted(true)

// Check for stored theme preference or default to system preference
const stored = localStorage.getItem('theme') as Theme | null
Copy link

Copilot AI Sep 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type assertion as Theme | null is unsafe. If localStorage contains an invalid theme value, the type assertion will succeed but the runtime check will fail. Consider using type guards or removing the assertion and relying only on the runtime validation.

Suggested change
const stored = localStorage.getItem('theme') as Theme | null
const stored = localStorage.getItem('theme')

Copilot uses AI. Check for mistakes.
if (stored && (stored === 'light' || stored === 'dark')) {
setThemeState(stored)
applyTheme(stored)
} else {
// Check system preference
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
const systemTheme: Theme = prefersDark ? 'dark' : 'light'
setThemeState(systemTheme)
applyTheme(systemTheme)
}
}, [])
Comment on lines +25 to +40
Copy link

Copilot AI Sep 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The localStorage.getItem() and window.matchMedia() calls should be wrapped in try-catch blocks to handle potential SecurityError exceptions in environments where these APIs are restricted (e.g., sandboxed iframes, private browsing modes).

Copilot uses AI. Check for mistakes.

const applyTheme = (newTheme: Theme) => {
const root = document.documentElement
if (newTheme === 'dark') {
root.classList.add('dark')
} else {
root.classList.remove('dark')
}
}

const toggleTheme = () => {
if (!mounted) return

const newTheme: Theme = theme === 'light' ? 'dark' : 'light'
setThemeState(newTheme)
localStorage.setItem('theme', newTheme)
applyTheme(newTheme)
}

const setTheme = (newTheme: Theme) => {
if (!mounted) return

setThemeState(newTheme)
localStorage.setItem('theme', newTheme)
applyTheme(newTheme)
}

const value: ThemeContextType = {
theme,
toggleTheme,
setTheme,
mounted,
}

return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
)
}

export function useThemeContext(): ThemeContextType {
const context = useContext(ThemeContext)
if (context === undefined) {
throw new Error('useThemeContext must be used within a ThemeProvider')
}
return context
}