refactor(all): migrate from React to Next.js

This commit is contained in:
2026-05-20 09:46:17 +03:30
parent dacbd3a328
commit f23108cda3
86 changed files with 2831 additions and 2679 deletions

View File

@@ -1,75 +1,55 @@
import * as React from "react";
import { Link } from "react-router-dom";
"use client";
import { Link } from "@/lib/router";
import { Button } from "@/components/ui/button";
import { useToast } from "@/hooks/use-toast";
import { Instagram, Send, Twitter, Linkedin } from "lucide-react";
import { api } from "@/lib/api"; // متد subscribeNewsletter را پایین توضیح داده‌ام
import { Instagram, Linkedin, Send, Twitter } from "lucide-react";
export default function Footer() {
// const { toast } = useToast();
// const [email, setEmail] = React.useState("");
// const [loading, setLoading] = React.useState(false);
const year = new Date().getFullYear();
// const validateEmail = (v: string) =>
// /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v.trim());
// const onSubmit = async (e: React.FormEvent) => {
// e.preventDefault();
// const em = email.trim();
// if (!validateEmail(em)) {
// toast({ title: "ایمیل نامعتبر است", description: "لطفاً یک ایمیل صحیح وارد کنید.", variant: "destructive" });
// return;
// }
// try {
// setLoading(true);
// const response = await api.subscribeNewsletter(em);
// if (response.success) {
// toast({ title: "عضویت موفق", description: response.message });
// setEmail("");
// } else {
// toast({ title: "عضویت ناموفق", description: response.message, variant: "destructive" });
// }
// } catch (err: any) {
// toast({ title: "خطا", description: err?.message || "مشکلی رخ داد.", variant: "destructive" });
// } finally {
// setLoading(false);
// }
// };
return (
<footer className="border-t bg-background/60 backdrop-blur supports-[backdrop-filter]:bg-background/40" dir="rtl">
<footer
className="border-t bg-background/60 backdrop-blur supports-[backdrop-filter]:bg-background/40"
dir="rtl"
>
<div className="container mx-auto px-4 py-10">
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-4">
{/* برند + درباره + اینماد */}
<div className="space-y-4">
<div className="flex items-center gap-2">
<img src="/favicon.ico" alt="لوگوی انجمن" className="h-9 w-9 rounded" />
<span className="text-xl font-bold">انجمن علمی کامپیوتر گیلان</span>
</div>
<p className="text-sm text-muted-foreground leading-7">
ترویج علم کامپیوتر، برگزاری رویدادهای تخصصی، تقویت شبکهٔ دانشجویی و پیوند با صنعت.
ترویج علم کامپیوتر، برگزاری رویدادهای تخصصی، تقویت شبکهی دانشجویی و پیوند با صنعت.
</p>
</div>
{/* لینک‌های سریع */}
<div>
<h4 className="mb-3 text-base font-semibold">لینکهای مفید</h4>
<ul className="space-y-2 text-sm">
<li><Link to="/" className="text-muted-foreground hover:text-foreground">خانه</Link></li>
<li><Link to="/events" className="text-muted-foreground hover:text-foreground">رویدادها</Link></li>
<li><Link to="/blog" className="text-muted-foreground hover:text-foreground">بلاگ</Link></li>
<li><Link to="/about" className="text-muted-foreground hover:text-foreground">دربارهٔ انجمن</Link></li>
{/* <li><Link to="/contact" className="text-muted-foreground hover:text-foreground">تماس با ما</Link></li> */}
{/* <li><Link to="/rules" className="text-muted-foreground hover:text-foreground">قوانین و حریم خصوصی</Link></li> */}
<li>
<Link to="/" className="text-muted-foreground hover:text-foreground">
خانه
</Link>
</li>
<li>
<Link to="/events" className="text-muted-foreground hover:text-foreground">
رویدادها
</Link>
</li>
<li>
<Link to="/blog" className="text-muted-foreground hover:text-foreground">
بلاگ
</Link>
</li>
<li>
<Link to="/about" className="text-muted-foreground hover:text-foreground">
دربارهی انجمن
</Link>
</li>
</ul>
</div>
{/* اطلاعات تماس / شبکه‌های اجتماعی */}
<div>
<h4 className="mb-3 text-base font-semibold">ارتباط با ما</h4>
<ul className="space-y-2 text-sm text-muted-foreground">
@@ -78,53 +58,49 @@ export default function Footer() {
</ul>
<div className="mt-4 flex items-center gap-2">
<a href="https://Instagram.com/guilance.ir" target="_blank" rel="noreferrer" className="inline-flex">
<a
href="https://Instagram.com/guilance.ir"
target="_blank"
rel="noreferrer"
className="inline-flex"
>
<Button variant="outline" size="icon" className="h-9 w-9" aria-label="اینستاگرام">
<Instagram className="h-4 w-4" />
</Button>
</a>
<a href="https://t.me/guilance" target="_blank" rel="noreferrer" className="inline-flex">
<a
href="https://t.me/guilance"
target="_blank"
rel="noreferrer"
className="inline-flex"
>
<Button variant="outline" size="icon" className="h-9 w-9" aria-label="تلگرام">
<Send className="h-4 w-4" />
</Button>
</a>
<a href="https://www.linkedin.com/in/amiirkhl/" target="_blank" rel="noreferrer" className="inline-flex">
<a
href="https://www.linkedin.com/in/amiirkhl/"
target="_blank"
rel="noreferrer"
className="inline-flex"
>
<Button variant="outline" size="icon" className="h-9 w-9" aria-label="لینکدین">
<Linkedin className="h-4 w-4" />
</Button>
</a>
<a href="https://x.com" target="_blank" rel="noreferrer" className="inline-flex">
<Button variant="outline" size="icon" className="h-9 w-9" aria-label="ایکس (توییتر)">
<Button
variant="outline"
size="icon"
className="h-9 w-9"
aria-label="ایکس (توییتر)"
>
<Twitter className="h-4 w-4" />
</Button>
</a>
</div>
</div>
{/* خبرنامه */}
{/* <div>
<h4 className="mb-3 text-base font-semibold">عضویت در خبرنامه</h4>
<p className="mb-3 text-sm text-muted-foreground">
برای اطلاع از رویدادها و اخبار انجمن، ایمیل خود را وارد کنید.
</p>
<form onSubmit={onSubmit} className="flex flex-col sm:flex-row gap-2">
<Input
type="email"
inputMode="email"
placeholder="ایمیل شما"
dir="ltr"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="sm:flex-1 text-left"
/>
<Button type="submit" disabled={loading}>
{loading ? "در حال عضویت..." : "عضویت"}
</Button>
</form>
<p className="mt-2 text-xs text-muted-foreground">
با عضویت، با <Link to="/rules" className="underline underline-offset-4">قوانین و حریم خصوصی</Link> موافقم.
</p>
</div> */}
<div className="justify-self-end">
<a
href="https://trustseal.enamad.ir/?id=649977&Code=m0wWM1DFYqd4fLEnjyMU3o2pupfuqDVW"
@@ -134,7 +110,7 @@ export default function Footer() {
>
<img
src="/enamad.png"
width="125px"
width="125"
alt="نماد اعتماد الکترونیکی"
referrerPolicy="origin"
style={{ cursor: "pointer" }}
@@ -144,12 +120,10 @@ export default function Footer() {
</div>
</div>
{/* خط جداکننده */}
<div className="my-8 h-px w-full bg-border" />
{/* کپی‌رایت */}
<div className="flex gap-2 items-center justify-center text-sm text-muted-foreground md:flex-row">
<div>© {year} انجمن علمی کامپیوتر گیلان تمامی حقوق محفوظ است.</div>
<div>© {year} انجمن علمی کامپیوتر گیلان - تمامی حقوق محفوظ است.</div>
</div>
</div>
</footer>

View File

@@ -1,15 +0,0 @@
import { Outlet } from 'react-router-dom';
import Navbar from './Navbar';
import Footer from './Footer';
import ScrollToTop from './ScrollToTop';
export default function Layout() {
return (
<div className="min-h-screen">
<ScrollToTop onlyOnPush={false} smooth={false} />
<Navbar />
<Outlet />
<Footer />
</div>
);
}

View File

@@ -1,5 +1,7 @@
"use client";
import { useMemo, useState } from 'react';
import { Link, NavLink, useNavigate } from 'react-router-dom';
import { Link, NavLink, useNavigate } from '@/lib/router';
import { Menu, ChevronDown } from 'lucide-react';
import { useAuth } from '@/contexts/AuthContext';
import { Button } from '@/components/ui/button';
@@ -63,7 +65,7 @@ export default function Navbar() {
<ChevronDown className="h-4 w-4 text-muted-foreground" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56" dir="rtl">
<DropdownMenuContent align="end" className="w-56 text-right">
<DropdownMenuLabel className="text-xs text-muted-foreground">
{user?.first_name || user?.last_name ? `${user?.first_name || ''} ${user?.last_name || ''}`.trim() : user?.username}
</DropdownMenuLabel>

View File

@@ -1,36 +0,0 @@
import * as React from "react";
import { useLocation, useNavigationType } from "react-router-dom";
export default function ScrollToTop({
onlyOnPush = false, // if true, keeps scroll on back/forward (POP)
smooth = false, // smooth animation
}: { onlyOnPush?: boolean; smooth?: boolean }) {
const { pathname, hash } = useLocation();
const navType = useNavigationType(); // 'PUSH' | 'POP' | 'REPLACE'
React.useLayoutEffect(() => {
// If URL has a hash (#id), scroll to that element
if (hash) {
const el = document.getElementById(hash.slice(1));
if (el) {
el.scrollIntoView({ behavior: smooth ? "smooth" : "auto", block: "start" });
return;
}
}
// If you want to keep scroll when user hits back/forward:
if (onlyOnPush && navType === "POP") return;
window.scrollTo({ top: 0, left: 0, behavior: smooth ? "smooth" : "auto" });
}, [pathname, hash, navType, onlyOnPush, smooth]);
// Disable native restoration if you always want to control it
React.useEffect(() => {
if (!onlyOnPush && "scrollRestoration" in window.history) {
const prev = window.history.scrollRestoration;
window.history.scrollRestoration = "manual";
return () => { window.history.scrollRestoration = prev as "auto" | "manual"; };
}
}, [onlyOnPush]);
return null;
}

View File

@@ -1,56 +1,3 @@
/* eslint-disable react-refresh/only-export-components */
import * as React from 'react';
"use client";
type Theme = 'light' | 'dark' | 'system';
type Ctx = { theme: Theme; setTheme: (t: Theme) => void };
const ThemeContext = React.createContext<Ctx | null>(null);
export function ThemeProvider({
children,
defaultTheme = 'system',
storageKey = 'egce-theme',
}: {
children: React.ReactNode;
defaultTheme?: Theme;
storageKey?: string;
}) {
const [theme, setTheme] = React.useState<Theme>(() => {
try { return (localStorage.getItem(storageKey) as Theme) || defaultTheme; }
catch { return defaultTheme; }
});
React.useEffect(() => {
const root = document.documentElement;
const mql = window.matchMedia('(prefers-color-scheme: dark)');
const apply = (t: Theme) => {
const isDark = t === 'system' ? mql.matches : t === 'dark';
root.classList.toggle('dark', isDark);
};
apply(theme);
const onChange = () => theme === 'system' && apply('system');
mql.addEventListener('change', onChange);
return () => mql.removeEventListener('change', onChange);
}, [theme]);
React.useEffect(() => {
try {
localStorage.setItem(storageKey, theme);
} catch (error) {
console.warn('Unable to persist theme preference', error);
}
}, [theme, storageKey]);
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const ctx = React.useContext(ThemeContext);
if (!ctx) throw new Error('useTheme must be used within ThemeProvider');
return ctx;
}
export { ThemeProvider, useTheme } from "next-themes";

View File

@@ -0,0 +1,32 @@
"use client";
import * as React from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { TooltipProvider } from "@/components/ui/tooltip";
import { Toaster } from "@/components/ui/toaster";
import { Toaster as Sonner } from "@/components/ui/sonner";
import { ThemeProvider } from "@/components/ThemeProvider";
import { AuthProvider } from "@/contexts/AuthContext";
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>
<TooltipProvider>
<Toaster />
<Sonner />
{children}
</TooltipProvider>
</AuthProvider>
</QueryClientProvider>
</ThemeProvider>
);
}

View File

@@ -1,4 +1,3 @@
/* eslint-disable react-refresh/only-export-components */
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";

View File

@@ -1,4 +1,3 @@
/* eslint-disable react-refresh/only-export-components */
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";

View File

@@ -1,4 +1,3 @@
/* eslint-disable react-refresh/only-export-components */
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { Slot } from "@radix-ui/react-slot";

View File

@@ -1,4 +1,3 @@
/* eslint-disable react-refresh/only-export-components */
import * as React from "react";
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
import { cva } from "class-variance-authority";

View File

@@ -1,4 +1,3 @@
/* eslint-disable react-refresh/only-export-components */
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { VariantProps, cva } from "class-variance-authority";

View File

@@ -1,4 +1,3 @@
/* eslint-disable react-refresh/only-export-components */
import { useTheme } from "next-themes";
import { Toaster as Sonner, toast } from "sonner";

View File

@@ -1,4 +1,3 @@
/* eslint-disable react-refresh/only-export-components */
import * as React from "react";
import * as TogglePrimitive from "@radix-ui/react-toggle";
import { cva, type VariantProps } from "class-variance-authority";