feat: restrict theme changes to authenticated users

This commit is contained in:
2026-10-03 10:27:50 +03:30
parent e758f138ff
commit 7469caa87c
3 changed files with 51 additions and 75 deletions

View File

@@ -1,72 +1,39 @@
import { Button } from "@/components/ui/button";
import { DropdownMenuCheckboxItem } from "@/components/ui/dropdown-menu";
import { useTheme } from "@/components/ThemeProvider";
import { cn } from "@/lib/utils";
import { Moon, Sun } from "lucide-react";
type ModeToggleProps = {
className?: string;
variant?: "icon" | "menu";
};
export default function ModeToggle({ className, variant = "icon" }: ModeToggleProps) {
export default function ModeToggle() {
const { theme, resolvedTheme, setTheme } = useTheme();
const isDark = resolvedTheme === "dark" || theme === "dark";
const handleToggle = () => {
setTheme(isDark ? "light" : "dark");
};
if (variant === "menu") {
return (
<DropdownMenuCheckboxItem
checked={isDark}
onCheckedChange={(checked) => setTheme(checked ? "dark" : "light")}
onSelect={(event) => event.preventDefault()}
className={cn(
"my-1 justify-between gap-3 rounded-xl px-3 py-3 text-foreground focus:bg-secondary focus:text-foreground [&>span:first-child]:hidden",
className,
)}
aria-label="حالت نمایش"
>
<span className="flex items-center gap-2">
{isDark ? <Moon className="h-4 w-4" /> : <Sun className="h-4 w-4" />}
<span>حالت نمایش</span>
</span>
<span
dir="ltr"
aria-hidden="true"
className={cn(
"relative inline-flex h-6 w-11 shrink-0 items-center rounded-full border-2 border-transparent transition-colors",
isDark ? "bg-primary" : "bg-input",
)}
>
<span
className={cn(
"block h-5 w-5 rounded-full bg-background shadow transition-transform",
isDark ? "translate-x-5" : "translate-x-0",
)}
/>
</span>
</DropdownMenuCheckboxItem>
);
}
return (
<Button
variant="ghost"
size="icon"
className={cn(
"rounded-full border-0 bg-transparent text-foreground shadow-none transition hover:bg-secondary hover:text-secondary-foreground hover:shadow-sm dark:hover:bg-secondary dark:hover:text-secondary-foreground",
className,
)}
aria-label="تغییر حالت نمایش"
title="تغییر حالت نمایش"
onClick={handleToggle}
<DropdownMenuCheckboxItem
checked={isDark}
onCheckedChange={(checked) => setTheme(checked ? "dark" : "light")}
onSelect={(event) => event.preventDefault()}
className="my-1 justify-between gap-3 rounded-xl px-3 py-3 text-foreground focus:bg-secondary focus:text-foreground [&>span:first-child]:hidden"
aria-label="حالت نمایش"
>
<Sun className="!size-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute !size-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
<span className="flex items-center gap-2">
{isDark ? <Moon className="h-4 w-4" /> : <Sun className="h-4 w-4" />}
<span>حالت نمایش</span>
</span>
<span
dir="ltr"
aria-hidden="true"
className={cn(
"relative inline-flex h-6 w-11 shrink-0 items-center rounded-full border-2 border-transparent transition-colors",
isDark ? "bg-primary" : "bg-input",
)}
>
<span
className={cn(
"block h-5 w-5 rounded-full bg-background shadow transition-transform",
isDark ? "translate-x-5" : "translate-x-0",
)}
/>
</span>
</DropdownMenuCheckboxItem>
);
}

View File

@@ -102,7 +102,7 @@ function ProfileAvatarMenu() {
</Link>
</DropdownMenuItem>
) : null}
<ModeToggle variant="menu" className="md:hidden" />
<ModeToggle />
<DropdownMenuSeparator />
<DropdownMenuItem asChild className="my-1 flex-row-reverse justify-end gap-2 rounded-xl py-3 text-destructive transition-colors hover:bg-secondary hover:text-destructive focus:bg-secondary focus:text-destructive dark:text-red-300 dark:hover:text-red-200 dark:focus:text-red-200">
<Link to="/logout">
@@ -151,14 +151,12 @@ export default function Navbar({ siteContent }: { siteContent: SiteContent }) {
<NavItem to="/">{siteContent.navigation.home}</NavItem>
<NavItem to="/events">{siteContent.navigation.events}</NavItem>
<NavItem to="/blog">{siteContent.navigation.blog}</NavItem>
<ModeToggle />
{isAuthenticated ? <NotificationsBell /> : null}
<ProfileAvatarMenu />
</div>
<div className="flex items-center gap-2 md:hidden">
{isAuthenticated ? <NotificationsBell /> : null}
{!isAuthenticated ? <ModeToggle /> : null}
<ProfileAvatarMenu />
</div>
</div>

View File

@@ -7,21 +7,32 @@ import { Toaster } from "@/components/ui/toaster";
import { Toaster as Sonner } from "@/components/ui/sonner";
import { ThemeProvider } from "@/components/ThemeProvider";
import MobileVerificationGate from "@/components/MobileVerificationGate";
import { AuthProvider } from "@/contexts/AuthContext";
import { AuthProvider, useAuth } from "@/contexts/AuthContext";
import { NotificationsProvider } from "@/contexts/NotificationsContext";
function AuthAwareThemeProvider({ children }: { children: React.ReactNode }) {
const { isAuthenticated } = useAuth();
return (
<ThemeProvider
attribute="class"
defaultTheme="light"
enableSystem
forcedTheme={isAuthenticated ? undefined : "light"}
storageKey="egce-theme"
>
{children}
</ThemeProvider>
);
}
export default function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = React.useState(() => new QueryClient());
return (
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
storageKey="egce-theme"
>
<QueryClientProvider client={queryClient}>
<AuthProvider>
<QueryClientProvider client={queryClient}>
<AuthProvider>
<AuthAwareThemeProvider>
<NotificationsProvider>
<TooltipProvider>
<Toaster />
@@ -30,8 +41,8 @@ export default function Providers({ children }: { children: React.ReactNode }) {
<MobileVerificationGate />
</TooltipProvider>
</NotificationsProvider>
</AuthProvider>
</QueryClientProvider>
</ThemeProvider>
</AuthAwareThemeProvider>
</AuthProvider>
</QueryClientProvider>
);
}