-
Notifications
You must be signed in to change notification settings - Fork 9
[DRAFT] UI revamp #41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
1680e93
438dc5f
97d2017
7896836
14dba70
f5221b5
a89ed53
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,53 +1,97 @@ | ||
| import { Moon, Sun } from "lucide-react"; | ||
| import { useTheme } from "@/components/theme-provider"; | ||
| import { Button } from "@/components/ui/button"; | ||
| import { | ||
| DropdownMenu, | ||
| DropdownMenuContent, | ||
| DropdownMenuItem, | ||
| DropdownMenuTrigger, | ||
| } from "@/components/ui/dropdown-menu"; | ||
| "use client"; | ||
|
|
||
| import { Droplet } from "lucide-react"; | ||
| import type React from "react"; | ||
| import { useEffect, useState } from "react"; | ||
| import { type Theme, useTheme } from "@/components/theme-provider"; | ||
|
|
||
| // Explicitly type the array to match the Theme type | ||
| const themes: { | ||
| name: Theme; | ||
| label: string; | ||
| icon: React.ElementType; | ||
| color: string; | ||
| }[] = [ | ||
| { name: "blue", label: "Blue", icon: Droplet, color: "#2563eb" }, | ||
| { name: "green", label: "Green", icon: Droplet, color: "#16a34a" }, | ||
| { name: "purple", label: "Purple", icon: Droplet, color: "#9333ea" }, | ||
| { name: "orange", label: "Orange", icon: Droplet, color: "#ea580c" }, | ||
| { name: "teal", label: "Teal", icon: Droplet, color: "#14b8a6" }, | ||
| ]; | ||
|
|
||
| export function ModeToggle() { | ||
| const { setTheme } = useTheme(); | ||
| const { theme, setTheme } = useTheme(); | ||
| const [mounted, setMounted] = useState(false); | ||
| const [isOpen, setIsOpen] = useState(false); | ||
|
|
||
| useEffect(() => { | ||
| setMounted(true); | ||
| }, []); | ||
|
|
||
| if (!mounted) { | ||
| return ( | ||
| <div className="border-3 border-border bg-card px-4 py-2 font-bold shadow-[4px_4px_0px_0px_var(--shadow-color)]"> | ||
| <Droplet className="h-5 w-5" /> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| const currentTheme = themes.find((t) => t.name === theme) || themes[0]; | ||
| const Icon = currentTheme.icon; | ||
|
|
||
| return ( | ||
| <DropdownMenu> | ||
| <DropdownMenuTrigger asChild> | ||
| <Button | ||
| variant="outline" | ||
| size="icon" | ||
| className="neo-brutal-button bg-[var(--button-red)] border-black hover:opacity-90 focus-visible:ring-0 focus-visible:border-black shadow-[4px_4px_0_0_black] active:shadow-[2px_2px_0_0_black] active:translate-x-[2px] active:translate-y-[2px]" | ||
| style={{ color: "black" }} | ||
| > | ||
| <Sun className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" /> | ||
| <Moon className="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" /> | ||
| <span className="sr-only">Toggle theme</span> | ||
| </Button> | ||
| </DropdownMenuTrigger> | ||
| <DropdownMenuContent | ||
| align="end" | ||
| className="bg-white border-4 border-black shadow-lg m-2" | ||
| <div className="relative"> | ||
| <button | ||
| type="button" | ||
| onClick={() => setIsOpen(!isOpen)} | ||
| className="border-3 flex items-center gap-2 border-border bg-card px-4 py-2 font-bold text-card-foreground shadow-[4px_4px_0px_0px_var(--shadow-color)] transition-all hover:shadow-[2px_2px_0px_0px_var(--shadow-color)] hover:translate-x-[2px] hover:translate-y-[2px]" | ||
| > | ||
| <DropdownMenuItem | ||
| className="mx-4 text-black font-bold" | ||
| onClick={() => setTheme("light")} | ||
| > | ||
| Light | ||
| </DropdownMenuItem> | ||
| <DropdownMenuItem | ||
| className="mx-4 text-black font-bold" | ||
| onClick={() => setTheme("dark")} | ||
| > | ||
| Dark | ||
| </DropdownMenuItem> | ||
| <DropdownMenuItem | ||
| className="mx-4 text-black font-bold" | ||
| onClick={() => setTheme("system")} | ||
| > | ||
| System | ||
| </DropdownMenuItem> | ||
| </DropdownMenuContent> | ||
| </DropdownMenu> | ||
| <Icon className="h-5 w-5" style={{ color: currentTheme.color }} /> | ||
| <span className="hidden sm:inline">{currentTheme.label}</span> | ||
| </button> | ||
|
|
||
| {isOpen && ( | ||
| <> | ||
| <div | ||
| className="fixed inset-0 z-10" | ||
| role="button" | ||
| tabIndex={0} | ||
| onClick={() => setIsOpen(false)} | ||
| onKeyDown={(e) => { | ||
| if (e.key === "Enter" || e.key === " " || e.key === "Escape") | ||
| setIsOpen(false); | ||
| }} | ||
| /> | ||
| <div className="absolute right-0 top-full z-20 mt-2 w-48 border-4 border-border bg-card shadow-[8px_8px_0px_0px_var(--shadow-color)]"> | ||
| <div className="p-2 space-y-1"> | ||
| {themes.map((themeOption) => { | ||
| const ThemeIcon = themeOption.icon; | ||
| return ( | ||
| <button | ||
| type="button" | ||
| key={themeOption.name} | ||
| onClick={() => { | ||
| setTheme(themeOption.name); | ||
| setIsOpen(false); | ||
| }} | ||
| className={`flex w-full items-center gap-3 border-2 border-border px-4 py-2 font-bold transition-all hover:translate-x-[2px] hover:translate-y-[2px] ${ | ||
| theme === themeOption.name | ||
| ? "bg-primary text-primary-foreground" | ||
| : "bg-card text-card-foreground hover:bg-muted" | ||
| }`} | ||
| > | ||
| <ThemeIcon | ||
| className="h-4 w-4" | ||
| style={{ color: themeOption.color }} | ||
| /> | ||
| {themeOption.label} | ||
| </button> | ||
| ); | ||
| })} | ||
| </div> | ||
| </div> | ||
| </> | ||
| )} | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| import { createContext, useContext, useEffect, useState } from "react"; | ||
|
|
||
| type Theme = "dark" | "light" | "system"; | ||
| export type Theme = "blue" | "green" | "purple" | "orange" | "teal"; | ||
|
|
||
| type ThemeProviderProps = { | ||
| children: React.ReactNode; | ||
|
|
@@ -14,15 +14,15 @@ type ThemeProviderState = { | |
| }; | ||
|
|
||
| const initialState: ThemeProviderState = { | ||
| theme: "system", | ||
| theme: "blue", | ||
| setTheme: () => null, | ||
| }; | ||
|
|
||
| const ThemeProviderContext = createContext<ThemeProviderState>(initialState); | ||
|
|
||
| export function ThemeProvider({ | ||
| children, | ||
| defaultTheme = "system", | ||
| defaultTheme = "blue", | ||
| storageKey = "vite-ui-theme", | ||
| ...props | ||
| }: ThemeProviderProps) { | ||
|
|
@@ -32,18 +32,7 @@ export function ThemeProvider({ | |
|
|
||
| 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.remove("blue", "green", "purple", "orange", "teal"); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The list of theme class names to remove is hardcoded. This can lead to maintainability issues if themes are added or removed. It's better to have a single source of truth for the theme names and derive this list from it. You could create a shared file that exports the theme configurations, which would be used here and in |
||
|
|
||
| root.classList.add(theme); | ||
| }, [theme]); | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,9 +1,13 @@ | ||||||
| import { Link } from "react-router"; | ||||||
| import { ModeToggle } from "@/components/mode-toggle"; | ||||||
| import { Button } from "@/components/ui/button"; | ||||||
| import { useAuth } from "@/contexts/AuthContext"; | ||||||
| import { ModeToggle } from "../mode-toggle"; | ||||||
|
|
||||||
| const Header = () => { | ||||||
| interface HeaderProps { | ||||||
| hideThemeToggle?: boolean; | ||||||
| } | ||||||
|
|
||||||
| const Header = ({ hideThemeToggle = false }: HeaderProps) => { | ||||||
| const { user, isLoading, isAuthenticated, login, logout } = useAuth(); | ||||||
|
|
||||||
| return ( | ||||||
|
|
@@ -15,24 +19,19 @@ const Header = () => { | |||||
| </Link> | ||||||
|
|
||||||
| <div className="flex items-center space-x-3"> | ||||||
| <ModeToggle /> | ||||||
| {!hideThemeToggle && <ModeToggle />} | ||||||
|
|
||||||
| {isLoading ? ( | ||||||
| // Show loading state | ||||||
| <div className="animate-pulse text-sm text-muted-foreground"> | ||||||
| Loading... | ||||||
| </div> | ||||||
| ) : isAuthenticated ? ( | ||||||
| // Show authenticated user options | ||||||
| <> | ||||||
| <span className="text-sm text-foreground"> | ||||||
| <span className="text-sm text-foreground hidden md:inline"> | ||||||
| Welcome, {user?.firstName || user?.username || user?.email}! | ||||||
| </span> | ||||||
| <Link | ||||||
| to={ | ||||||
| user?.username ? `/profile/${user.username}` : "/my/profile" | ||||||
| } | ||||||
| > | ||||||
|
|
||||||
| <Link to={`/profile/${user?.username}`}> | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The link to the user's profile page assumes
Suggested change
|
||||||
| <Button | ||||||
| variant="outline" | ||||||
| className="neo-brutal-button border-primary text-primary bg-secondary hover:bg-secondary hover:text-black" | ||||||
|
|
@@ -48,7 +47,6 @@ const Header = () => { | |||||
| </Button> | ||||||
| </> | ||||||
| ) : ( | ||||||
| // Show unauthenticated user options (current state) | ||||||
| <> | ||||||
| <Button | ||||||
| onClick={login} | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For accessibility, the overlay
divused to close the theme selector should have anaria-labelto describe its purpose to screen reader users. Since it has arole="button", an accessible name is expected.