feat(frontend): refine blog and profile experience
Some checks failed
Frontend CI/CD / build (push) Has been cancelled
Frontend CI/CD / deploy (push) Has been cancelled

This commit is contained in:
2026-06-09 08:53:31 +03:30
parent 8b1fc942cf
commit 8e5096d192
13 changed files with 962 additions and 898 deletions

View File

@@ -1,33 +1,37 @@
// src/components/ModeToggle.tsx
import { Button } from '@/components/ui/button';
import { Moon, Sun } from 'lucide-react';
import { useTheme } from '@/components/ThemeProvider';
import { Button } from "@/components/ui/button";
import { useTheme } from "@/components/ThemeProvider";
import { cn } from "@/lib/utils";
import { Moon, Sun } from "lucide-react";
export default function ModeToggle() {
export default function ModeToggle({ className }: { className?: string }) {
const { theme, setTheme } = useTheme();
const handleToggle = () => {
if (theme === 'system' && typeof window !== 'undefined') {
const prefersDark = window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false;
setTheme(prefersDark ? 'light' : 'dark');
if (theme === "system" && typeof window !== "undefined") {
const prefersDark = window.matchMedia?.("(prefers-color-scheme: dark)").matches ?? false;
setTheme(prefersDark ? "light" : "dark");
return;
}
setTheme(theme === 'dark' ? 'light' : 'dark');
setTheme(theme === "dark" ? "light" : "dark");
};
const isDark =
theme === 'dark' ||
(theme === 'system' &&
typeof document !== 'undefined' &&
document.documentElement.classList.contains('dark'));
theme === "dark" ||
(theme === "system" &&
typeof document !== "undefined" &&
document.documentElement.classList.contains("dark"));
const nextThemeLabel = isDark ? 'روشن' : 'تاریک';
const nextThemeLabel = isDark ? "روشن" : "تاریک";
return (
<Button
variant="outline"
variant="ghost"
size="icon"
className={cn(
"rounded-full border-0 bg-transparent shadow-none backdrop-blur transition hover:bg-background/45 hover:shadow-sm",
className,
)}
aria-label={`تغییر تم به حالت ${nextThemeLabel}`}
title={`تغییر تم به حالت ${nextThemeLabel}`}
onClick={handleToggle}

View File

@@ -2,21 +2,24 @@
import type { ReactNode } from "react";
import { useMemo } from "react";
import { LayoutDashboard, LogOut, PencilLine, RotateCcw, UserRound } from "lucide-react";
import { Link, NavLink } from "@/lib/router";
import { useAuth } from "@/contexts/AuthContext";
import ModeToggle from "@/components/ModeToggle";
import NotificationsBell from "@/components/NotificationsBell";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
const NavItem = ({
to,
children,
}: {
to: string;
children: ReactNode;
}) => (
const NavItem = ({ to, children }: { to: string; children: ReactNode }) => (
<NavLink
to={to}
className={({ isActive }) =>
@@ -32,7 +35,7 @@ const NavItem = ({
</NavLink>
);
export default function Navbar() {
function ProfileAvatarMenu() {
const { user, isAuthenticated } = useAuth();
const isAdminUser = isAuthenticated && Boolean(user?.is_staff || user?.is_superuser);
@@ -45,6 +48,82 @@ export default function Navbar() {
[user?.first_name, user?.last_name, user?.username],
);
if (!isAuthenticated) {
return (
<Link to="/auth">
<Button className="rounded-full px-5">ورود / ثبتنام</Button>
</Link>
);
}
return (
<DropdownMenu dir="rtl">
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-11 w-11 rounded-full border-0 bg-transparent p-0 shadow-none transition hover:bg-background/45"
aria-label="منوی حساب کاربری"
>
<Avatar className="h-10 w-10 border border-white/30 shadow-sm">
<AvatarImage
src={
user?.profile_picture_preview_url ||
user?.profile_picture ||
undefined
}
alt={user?.username || "profile"}
/>
<AvatarFallback>{avatarInitials}</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" sideOffset={12} className="w-64 rounded-2xl p-2 text-right">
<DropdownMenuLabel className="text-right">
{[user?.first_name, user?.last_name].filter(Boolean).join(" ") || user?.username || "حساب کاربری"}
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem asChild className="flex-row-reverse justify-start gap-2 rounded-xl">
<Link to="/profile">
<UserRound className="h-4 w-4" />
مشاهده پروفایل
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild className="flex-row-reverse justify-start gap-2 rounded-xl">
<Link to="/profile?edit=1">
<PencilLine className="h-4 w-4" />
ویرایش پروفایل
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild className="flex-row-reverse justify-start gap-2 rounded-xl">
<Link to="/reset-password">
<RotateCcw className="h-4 w-4" />
تغییر یا بازیابی رمز
</Link>
</DropdownMenuItem>
{isAdminUser ? (
<DropdownMenuItem asChild className="flex-row-reverse justify-start gap-2 rounded-xl">
<Link to="/admin">
<LayoutDashboard className="h-4 w-4" />
داشبورد مدیریت
</Link>
</DropdownMenuItem>
) : null}
<DropdownMenuSeparator />
<DropdownMenuItem asChild className="flex-row-reverse justify-start gap-2 rounded-xl text-destructive focus:text-destructive">
<Link to="/logout">
<LogOut className="h-4 w-4" />
خروج از حساب
</Link>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
export default function Navbar() {
const { isAuthenticated } = useAuth();
return (
<nav
className="sticky top-0 z-40 border-b bg-background/75 backdrop-blur-xl supports-[backdrop-filter]:bg-background/55"
@@ -56,12 +135,12 @@ export default function Navbar() {
<div className="hidden h-10 w-10 items-center justify-center rounded-2xl border border-border/70 bg-background/90 shadow-sm sm:flex">
<span className="text-lg font-bold text-primary">گ</span>
</div>
<div className="min-w-0">
<div className="min-w-0 text-right">
<p className="truncate text-sm font-semibold text-foreground sm:text-base">
انجمن علمی کامپیوتر گیلان
انجمن علمی مهندسی کامپیوتر
</p>
<p className="hidden text-xs text-muted-foreground sm:block">
رویدادها، بلاگ و حساب کاربری
دانشکدهی فنی و مهندسی شرق گیلان
</p>
</div>
</Link>
@@ -72,44 +151,13 @@ export default function Navbar() {
<NavItem to="/events">رویدادها</NavItem>
<ModeToggle />
{isAuthenticated ? <NotificationsBell /> : null}
{isAuthenticated ? (
<Link
to="/profile"
className="flex items-center gap-3 rounded-full border border-border/70 bg-background/85 px-2 py-1.5 transition hover:bg-muted/70"
>
<Avatar className="h-9 w-9">
<AvatarImage
src={
user?.profile_picture_preview_url ||
user?.profile_picture ||
undefined
}
alt={user?.username || "profile"}
/>
<AvatarFallback>{avatarInitials}</AvatarFallback>
</Avatar>
<div className="min-w-0 text-right">
<p className="max-w-36 truncate text-sm font-medium text-foreground">
{user?.first_name || user?.last_name
? `${user?.first_name || ""} ${user?.last_name || ""}`.trim()
: user?.username}
</p>
<p className="text-xs text-muted-foreground">
{isAdminUser ? "پروفایل و مدیریت حساب" : "پروفایل من"}
</p>
</div>
</Link>
) : (
<Link to="/auth">
<Button className="rounded-full px-5">ورود / ثبتنام</Button>
</Link>
)}
<ProfileAvatarMenu />
</div>
<div className="flex items-center gap-2 md:hidden">
{isAuthenticated ? <NotificationsBell /> : null}
<ModeToggle />
<ProfileAvatarMenu />
</div>
</div>
</div>

View File

@@ -85,9 +85,9 @@ export default function NotificationsBell() {
<PopoverTrigger asChild>
<Button
type="button"
variant="outline"
variant="ghost"
size="icon"
className="relative h-10 w-10 rounded-full border-border/70 bg-background/85"
className="relative h-10 w-10 rounded-full border-0 bg-transparent shadow-none backdrop-blur transition hover:bg-background/45 hover:shadow-sm"
aria-label="اعلان‌ها"
>
<Bell className="h-4 w-4" />