From a6eeb05d6ed5428a6e82a30f651f51f4b5f9d7b3 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 30 May 2025 22:37:13 +0200 Subject: [PATCH] Add ThemeProvider --- CONTEXT.md | 17 ++++++++ src/App.tsx | 25 +++++++----- src/components/ThemeProvider.tsx | 69 ++++++++++++++++++++++++++++++++ src/components/ThemeToggle.tsx | 37 +++++++++++++++++ src/hooks/useTheme.ts | 11 +++++ src/lib/theme-context.ts | 15 +++++++ 6 files changed, 163 insertions(+), 11 deletions(-) create mode 100644 src/components/ThemeProvider.tsx create mode 100644 src/components/ThemeToggle.tsx create mode 100644 src/hooks/useTheme.ts create mode 100644 src/lib/theme-context.ts diff --git a/CONTEXT.md b/CONTEXT.md index a90fe07..2da08ac 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -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. +## Theme Toggle Component + +A pre-built theme toggle component is available for easy integration: + +```tsx +import { ThemeToggle } from "@/components/theme-toggle"; + +function Header() { + return ( +
+

My App

+ +
+ ); +} +``` + ## Nostr Protocol Integration This project comes with custom hooks for querying and publishing events on the Nostr network. diff --git a/src/App.tsx b/src/App.tsx index a7afb31..2f89457 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,6 +2,7 @@ // To add new routes, edit the AppRouter.tsx file. import NostrProvider from '@/components/NostrProvider' +import { ThemeProvider } from "@/components/ThemeProvider"; import { Toaster } from "@/components/ui/toaster"; import { Toaster as Sonner } from "@/components/ui/sonner"; import { TooltipProvider } from "@/components/ui/tooltip"; @@ -28,17 +29,19 @@ const queryClient = new QueryClient({ export function App() { return ( - - - - - - - - - - - + + + + + + + + + + + + + ); } diff --git a/src/components/ThemeProvider.tsx b/src/components/ThemeProvider.tsx new file mode 100644 index 0000000..8fdf742 --- /dev/null +++ b/src/components/ThemeProvider.tsx @@ -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( + () => (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 ( + + {children} + + ) +} + diff --git a/src/components/ThemeToggle.tsx b/src/components/ThemeToggle.tsx new file mode 100644 index 0000000..54bf602 --- /dev/null +++ b/src/components/ThemeToggle.tsx @@ -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 ( + + + + + + setTheme("light")}> + Light + + setTheme("dark")}> + Dark + + setTheme("system")}> + System + + + + ) +} \ No newline at end of file diff --git a/src/hooks/useTheme.ts b/src/hooks/useTheme.ts new file mode 100644 index 0000000..937d68c --- /dev/null +++ b/src/hooks/useTheme.ts @@ -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 +} \ No newline at end of file diff --git a/src/lib/theme-context.ts b/src/lib/theme-context.ts new file mode 100644 index 0000000..638a1cc --- /dev/null +++ b/src/lib/theme-context.ts @@ -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(initialState); \ No newline at end of file