Add ThemeProvider

This commit is contained in:
Alex Gleason 2025-05-30 22:37:13 +02:00
parent e0d94fae71
commit a6eeb05d6e
No known key found for this signature in database
GPG Key ID: 7211D1F99744FBB7
6 changed files with 163 additions and 11 deletions

View File

@ -75,6 +75,23 @@ The project uses shadcn/ui components located in `@/components/ui`. These are un
These components follow a consistent pattern using React's `forwardRef` and use the `cn()` utility for class name merging. Many are built on Radix UI primitives for accessibility and customized with Tailwind CSS. These components follow a consistent pattern using React's `forwardRef` and use the `cn()` utility for class name merging. Many are built on Radix UI primitives for accessibility and customized with Tailwind CSS.
## Theme Toggle Component
A pre-built theme toggle component is available for easy integration:
```tsx
import { ThemeToggle } from "@/components/theme-toggle";
function Header() {
return (
<header>
<h1>My App</h1>
<ThemeToggle />
</header>
);
}
```
## Nostr Protocol Integration ## Nostr Protocol Integration
This project comes with custom hooks for querying and publishing events on the Nostr network. This project comes with custom hooks for querying and publishing events on the Nostr network.

View File

@ -2,6 +2,7 @@
// To add new routes, edit the AppRouter.tsx file. // To add new routes, edit the AppRouter.tsx file.
import NostrProvider from '@/components/NostrProvider' import NostrProvider from '@/components/NostrProvider'
import { ThemeProvider } from "@/components/ThemeProvider";
import { Toaster } from "@/components/ui/toaster"; import { Toaster } from "@/components/ui/toaster";
import { Toaster as Sonner } from "@/components/ui/sonner"; import { Toaster as Sonner } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip"; import { TooltipProvider } from "@/components/ui/tooltip";
@ -28,17 +29,19 @@ const queryClient = new QueryClient({
export function App() { export function App() {
return ( return (
<NostrLoginProvider storageKey='nostr:login'> <ThemeProvider defaultTheme="system">
<NostrProvider relays={defaultRelays}> <NostrLoginProvider storageKey='nostr:login'>
<QueryClientProvider client={queryClient}> <NostrProvider relays={defaultRelays}>
<TooltipProvider> <QueryClientProvider client={queryClient}>
<Toaster /> <TooltipProvider>
<Sonner /> <Toaster />
<AppRouter /> <Sonner />
</TooltipProvider> <AppRouter />
</QueryClientProvider> </TooltipProvider>
</NostrProvider> </QueryClientProvider>
</NostrLoginProvider> </NostrProvider>
</NostrLoginProvider>
</ThemeProvider>
); );
} }

View File

@ -0,0 +1,69 @@
import { useEffect, useState } from "react"
import { Theme, ThemeProviderContext } from "@/lib/theme-context"
type ThemeProviderProps = {
children: React.ReactNode
defaultTheme?: Theme
storageKey?: string
}
export function ThemeProvider({
children,
defaultTheme = "system",
storageKey = "theme",
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
)
useEffect(() => {
const root = window.document.documentElement
root.classList.remove("light", "dark")
if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light"
root.classList.add(systemTheme)
return
}
root.classList.add(theme)
}, [theme])
useEffect(() => {
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
const handleChange = () => {
if (theme === "system") {
const root = window.document.documentElement
root.classList.remove("light", "dark")
const systemTheme = mediaQuery.matches ? "dark" : "light"
root.classList.add(systemTheme)
}
}
mediaQuery.addEventListener("change", handleChange)
return () => mediaQuery.removeEventListener("change", handleChange)
}, [theme])
const value = {
theme,
setTheme: (theme: Theme) => {
localStorage.setItem(storageKey, theme)
setTheme(theme)
},
}
return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
)
}

View File

@ -0,0 +1,37 @@
import { Moon, Sun } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { useTheme } from "@/hooks/useTheme"
export function ThemeToggle() {
const { setTheme } = useTheme()
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}

11
src/hooks/useTheme.ts Normal file
View File

@ -0,0 +1,11 @@
import { useContext } from "react"
import { ThemeProviderContext } from "@/lib/theme-context"
export const useTheme = () => {
const context = useContext(ThemeProviderContext)
if (context === undefined)
throw new Error("useTheme must be used within a ThemeProvider")
return context
}

15
src/lib/theme-context.ts Normal file
View File

@ -0,0 +1,15 @@
import { createContext } from "react";
export type Theme = "dark" | "light" | "system";
export type ThemeProviderState = {
theme: Theme;
setTheme: (theme: Theme) => void;
};
const initialState: ThemeProviderState = {
theme: "system",
setTheme: () => null,
};
export const ThemeProviderContext = createContext<ThemeProviderState>(initialState);