change base

This commit is contained in:
Mahdi Rahimi 2025-12-25 11:18:47 +03:30
parent c2ef1976e3
commit ee89fb1407
93 changed files with 5394 additions and 3194 deletions

55
.gitignore vendored
View File

@ -1,24 +1,41 @@
# Logs
logs
*.log
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
.pnpm-debug.log*
node_modules
dist
dist-ssr
*.local
# env files (can opt-in for committing if needed)
.env*
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@ -1,16 +0,0 @@
# React + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.

8
app/(auth)/layout.jsx Normal file
View File

@ -0,0 +1,8 @@
export default function PublicLayout({ children }) {
return (
<>
{children}
</>
);
}

200
app/(auth)/login/page.js Normal file
View File

@ -0,0 +1,200 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { useDispatch } from "react-redux";
import { setProfile } from "@/redux/slices/profileSlice";
import { showToast } from "@/redux/slices/toastSlice";
import Link from "next/link";
export default function LoginPage() {
const router = useRouter();
const dispatch = useDispatch();
const [formData, setFormData] = useState({
email: "",
password: "",
});
const [errors, setErrors] = useState({});
const [isLoading, setIsLoading] = useState(false);
const validateForm = () => {
const newErrors = {};
if (!formData.email.trim()) {
newErrors.email = "ایمیل الزامی است";
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
newErrors.email = "ایمیل معتبر نیست";
}
if (!formData.password.trim()) {
newErrors.password = "رمز عبور الزامی است";
} else if (formData.password.length < 6) {
newErrors.password = "رمز عبور باید حداقل 6 کاراکتر باشد";
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleChange = (e) => {
const { name, value } = e.target;
setFormData((prev) => ({
...prev,
[name]: value,
}));
if (errors[name]) {
setErrors((prev) => ({
...prev,
[name]: "",
}));
}
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!validateForm()) {
return;
}
setIsLoading(true);
try {
// فراخوانی API (توکن خودکار در HTTP-only cookie ذخیره می‌شود)
const response = await fetch("/api/auth/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email: formData.email,
password: formData.password,
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || "خطا در ورود");
}
const data = await response.json();
// ذخیره user info در Redux (فقط برای UI)
dispatch(
setProfile(
data.user || { email: formData.email }
)
);
dispatch(
showToast({
message: "خوش آمدید! شما با موفقیت وارد شدید",
type: "success",
})
);
// Redirect به صفحه اصلی
// (middleware خودکار بررسی توکن را انجام می‌دهد)
router.push("/");
} catch (error) {
dispatch(
showToast({
message: error.message || "خطایی در حین ورود رخ داد",
type: "error",
})
);
setErrors((prev) => ({
...prev,
submit: error.message,
}));
} finally {
setIsLoading(false);
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-linear-to-br from-blue-50 to-indigo-100 px-4">
<div className="w-full max-w-md bg-white rounded-lg shadow-lg p-8">
<h1 className="text-3xl font-bold text-center text-gray-800 mb-2">
خوش آمدید
</h1>
<p className="text-center text-gray-600 mb-6">
لطفاً وارد حساب خود شوید
</p>
{errors.submit && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
{errors.submit}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-5">
{/* Email Field */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
ایمیل
</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder="ایمیل خود را وارد کنید"
className={`w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 transition ${
errors.email ? "border-red-500" : "border-gray-300"
}`}
disabled={isLoading}
/>
{errors.email && (
<p className="mt-1 text-red-600 text-xs">{errors.email}</p>
)}
</div>
{/* Password Field */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
رمز عبور
</label>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
placeholder="رمز عبور خود را وارد کنید"
className={`w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 transition ${
errors.password ? "border-red-500" : "border-gray-300"
}`}
disabled={isLoading}
/>
{errors.password && (
<p className="mt-1 text-red-600 text-xs">{errors.password}</p>
)}
</div>
{/* Submit Button */}
<button
type="submit"
disabled={isLoading}
className="w-full bg-indigo-600 text-white font-semibold py-2 rounded-lg hover:bg-indigo-700 transition disabled:bg-gray-400 disabled:cursor-not-allowed"
>
{isLoading ? "درحال ورود..." : "ورود"}
</button>
</form>
{/* Register Link */}
<div className="mt-6 text-center text-gray-600">
حساب کاربری ندارید؟{" "}
<Link
href="/register"
className="text-indigo-600 font-semibold hover:underline"
>
ثبتنام کنید
</Link>
</div>
</div>
</div>
);
}

258
app/(auth)/register/page.js Normal file
View File

@ -0,0 +1,258 @@
"use client";
import { useState } from "react";
import { useDispatch } from "react-redux";
import { useRouter } from "next/navigation";
import { setToken } from "@/redux/slices/authSlice";
import { setProfile } from "@/redux/slices/profileSlice";
import { showToast } from "@/redux/slices/toastSlice";
import authService from "@/services/auth";
import Link from "next/link";
export default function RegisterPage() {
const dispatch = useDispatch();
const router = useRouter();
const [formData, setFormData] = useState({
name: "",
email: "",
password: "",
confirmPassword: "",
});
const [errors, setErrors] = useState({});
const [isLoading, setIsLoading] = useState(false);
const validateForm = () => {
const newErrors = {};
if (!formData.name.trim()) {
newErrors.name = "نام الزامی است";
} else if (formData.name.trim().length < 3) {
newErrors.name = "نام باید حداقل 3 کاراکتر باشد";
}
if (!formData.email.trim()) {
newErrors.email = "ایمیل الزامی است";
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
newErrors.email = "ایمیل معتبر نیست";
}
if (!formData.password.trim()) {
newErrors.password = "رمز عبور الزامی است";
} else if (formData.password.length < 6) {
newErrors.password = "رمز عبور باید حداقل 6 کاراکتر باشد";
}
if (!formData.confirmPassword.trim()) {
newErrors.confirmPassword = "تأیید رمز عبور الزامی است";
} else if (formData.password !== formData.confirmPassword) {
newErrors.confirmPassword = "رمز عبور و تأیید آن یکسان نیستند";
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleChange = (e) => {
const { name, value } = e.target;
setFormData((prev) => ({
...prev,
[name]: value,
}));
// پاک کردن error هنگام تایپ
if (errors[name]) {
setErrors((prev) => ({
...prev,
[name]: "",
}));
}
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!validateForm()) {
return;
}
setIsLoading(true);
try {
const response = await fetch("/api/auth/register", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: formData.name,
email: formData.email,
password: formData.password,
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || "خطا در ثبت‌نام");
}
const data = await response.json();
dispatch(
setProfile(
data.user || { name: formData.name, email: formData.email }
)
);
dispatch(
showToast({
message: "ثبت‌نام شما موفق بود! خوش آمدید",
type: "success",
})
);
router.push("/");
} catch (error) {
dispatch(
showToast({
message: error.message || "خطایی در حین ثبت‌نام رخ داد",
type: "error",
})
);
setErrors((prev) => ({
...prev,
submit: error.message,
}));
} finally {
setIsLoading(false);
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-linear-to-br from-green-50 to-teal-100 px-4">
<div className="w-full max-w-md bg-white rounded-lg shadow-lg p-8">
<h1 className="text-3xl font-bold text-center text-gray-800 mb-2">
ثبتنام
</h1>
<p className="text-center text-gray-600 mb-6">
حساب کاربری جدید ایجاد کنید
</p>
{errors.submit && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
{errors.submit}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-5">
{/* Name Field */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
نام
</label>
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
placeholder="نام خود را وارد کنید"
className={`w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-teal-500 transition ${
errors.name ? "border-red-500" : "border-gray-300"
}`}
disabled={isLoading}
/>
{errors.name && (
<p className="mt-1 text-red-600 text-xs">{errors.name}</p>
)}
</div>
{/* Email Field */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
ایمیل
</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder="ایمیل خود را وارد کنید"
className={`w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-teal-500 transition ${
errors.email ? "border-red-500" : "border-gray-300"
}`}
disabled={isLoading}
/>
{errors.email && (
<p className="mt-1 text-red-600 text-xs">{errors.email}</p>
)}
</div>
{/* Password Field */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
رمز عبور
</label>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
placeholder="رمز عبور خود را وارد کنید"
className={`w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-teal-500 transition ${
errors.password ? "border-red-500" : "border-gray-300"
}`}
disabled={isLoading}
/>
{errors.password && (
<p className="mt-1 text-red-600 text-xs">{errors.password}</p>
)}
</div>
{/* Confirm Password Field */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
تأیید رمز عبور
</label>
<input
type="password"
name="confirmPassword"
value={formData.confirmPassword}
onChange={handleChange}
placeholder="رمز عبور را دوباره وارد کنید"
className={`w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-teal-500 transition ${
errors.confirmPassword ? "border-red-500" : "border-gray-300"
}`}
disabled={isLoading}
/>
{errors.confirmPassword && (
<p className="mt-1 text-red-600 text-xs">
{errors.confirmPassword}
</p>
)}
</div>
{/* Submit Button */}
<button
type="submit"
disabled={isLoading}
className="w-full bg-teal-600 text-white font-semibold py-2 rounded-lg hover:bg-teal-700 transition disabled:bg-gray-400 disabled:cursor-not-allowed"
>
{isLoading ? "درحال ثبت‌نام..." : "ثبت‌نام"}
</button>
</form>
{/* Login Link */}
<div className="mt-6 text-center text-gray-600">
قبلاً حساب دارید؟{" "}
<Link
href="/login"
className="text-teal-600 font-semibold hover:underline"
>
وارد شوید
</Link>
</div>
</div>
</div>
);
}

13
app/(protected)/layout.js Normal file
View File

@ -0,0 +1,13 @@
// app/(protected)/layout.tsx
import Header from "@/components/layout/Header";
import Footer from "@/components/layout/Footer";
export default function ProtectedLayout({ children }) {
return (
<div className="w-full min-h-screen flex flex-col">
<Header />
<main className="flex-1 mt-20">{children}</main>
<Footer />
</div>
);
}

15
app/(protected)/page.js Normal file
View File

@ -0,0 +1,15 @@
"use client";
import { useSelector } from "react-redux";
import Link from "next/link";
export default function HomePage() {
const { isAuthenticated } = useSelector((state) => state.auth);
const { user } = useSelector((state) => state.profile);
return (
<>
</>
);
}

BIN
app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

83
app/globals.css Normal file
View File

@ -0,0 +1,83 @@
@import "tailwindcss";
/* @custom-variant dark (&:where(.dark, .dark *)); */
:root {
--color-text1: #F3EFE7;
--color-text2: #878687;
--color-text3: #402E32;
--color-button1: #99582A;
--color-button2: #7F4629;
--color-button3: #996B54;
--color-background: #FDF9F6;
--font-sans: "Vazirmatn", system-ui, sans-serif;
}
html {
font-family: var(--font-sans);
}
body {
background-color: var(--background);
color: var(--foreground);
line-height: 1.6;
/* فاصله مناسب خطوط متن */
-webkit-font-smoothing: antialiased;
/* نرم‌تر شدن فونت */
text-rendering: optimizeLegibility;
/* خوانایی بهتر متن */
}
/* Tailwind Configuration */
@theme {
--color-text1: var(--color-text1);
--color-text2: var(--color-text2);
--color-text3: var(--color-text3);
--color-btn1: var(--color-button1);
--color-btn2: var(--color-button2);
--color-btn3: var(--color-button3);
--color-bg1: var(--color-background);
}
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* دکمه‌ها فونت پروژه رو بگیرن */
button {
font-family: inherit;
cursor: pointer;
}
/* تصاویر و مدیاها ریسپانسیو باشن */
img,
picture,
video,
canvas,
svg {
display: block;
max-width: 100%;
}
/* رنگ انتخاب متن (drag select) */
::selection {
background-color: var(--primary-200);
/* بک‌گراند انتخاب */
color: var(--primary-foreground);
/* رنگ متن انتخاب */
}

31
app/layout.js Normal file
View File

@ -0,0 +1,31 @@
import { Geist, Geist_Mono } from "next/font/google";
import "@/app/globals.css";
import Providers from "@/app/providers";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata = {
title: "ProKit",
description: "پلتفرم مدیریت پروژه",
};
export default function RootLayout({ children }) {
return (
<html dir="rtl" lang="fa">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<Providers>{children}</Providers>
</body>
</html>
);
}

7
app/loading.jsx Normal file
View File

@ -0,0 +1,7 @@
export default function loading() {
return (
<div className="flex justify-center items-center w-full h-screen">
<p>Loading...</p>
</div>
);
}

28
app/not-found.jsx Normal file
View File

@ -0,0 +1,28 @@
'use client';
import { useRouter } from 'next/navigation';
export default function NotFound() {
const router = useRouter();
return (
<main className="h-screen w-full flex flex-col justify-center items-center bg-gray-900">
<h1 className="text-9xl font-extrabold text-white-500 tracking-widest">404</h1>
<div className="bg-primary-500 px-2 text-sm rounded rotate-12 absolute text-white-500 font-medium">
صفحه پیدا نشد
</div>
<button
onClick={() => router.push('/')}
className="mt-5 relative inline-block rounded-full text-sm font-medium text-primary-500 group active:text-primary-600 focus:outline-none focus:ring focus:ring-primary-400 transition-all"
>
<span
className="absolute inset-0 transition-transform rounded-full translate-x-0.5 translate-y-0.5 bg-primary-500 group-hover:translate-y-0 group-hover:translate-x-0"
></span>
<span className="relative text-white font-bold text-lg rounded-full block px-8 py-3 bg-gray-900 border-2 border-primary-500 group-hover:bg-primary-500 transition-colors">
بازگشت
</span>
</button>
</main>
);
}

14
app/providers.jsx Normal file
View File

@ -0,0 +1,14 @@
"use client";
import { Provider } from "react-redux";
import { store } from "@/redux/store";
import Toast from "@/components/ui/Toast";
export default function Providers({ children }) {
return (
<Provider store={store}>
<Toast />
{children}
</Provider>
);
}

View File

@ -0,0 +1,69 @@
import React from 'react'
import Image from 'next/image';
import Link from 'next/link';
import { FaLinkedin, FaInstagram, FaFacebook } from "react-icons/fa";
import { CiHeart } from "react-icons/ci";
export default function Footer({ className = "" }) {
return (
<>
<footer className={`bg-gray-100 w-full p-6 md:p-8 lg:p-12 text-black-500 text-sm md:text-base transition-all ${className}`}>
<div>
{/* نیمه بالا */}
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 md:gap-12 border-b-2 border-gray-300 pb-8 md:pb-12'>
{/* بخش لوگو */}
<div className='space-y-4'>
<Image
src="/KafehJoo.svg"
alt="KafehJoo Logo Default"
width={57}
height={49.34}
className="hover:scale-110 transition-transform"
/>
<p className='text-sm md:text-base leading-relaxed'>ما عاشق کافهگردیایم! اینجا رو ساختیم تا هر کسی بتونه راحت کافههای شهرشو پیدا کنه، منو ببینه و تجربههاشو با بقیه به اشتراک بذاره</p>
</div>
{/* بخش لینک‌ها */}
<div className='hidden md:block'>
<p className='font-bold mb-4 text-primary-500'>دسترسی سریع</p>
<ul className='space-y-2'>
<li><Link href="/" className='hover:text-primary-500 transition-colors'>خانه</Link></li>
<li><Link href="/blog" className='hover:text-primary-500 transition-colors'>بلاگ</Link></li>
<li><Link href="/contact_us" className='hover:text-primary-500 transition-colors'>تماس با ما</Link></li>
<li><Link href="/about_us" className='hover:text-primary-500 transition-colors'>دربارهی ما</Link></li>
</ul>
</div>
{/* بخش خبرنامه و شبکه‌های اجتماعی */}
<div className='space-y-4'>
<p className='font-bold text-primary-500'>خبرنامه</p>
<input
type="email"
placeholder='example@gmail.com'
className='rounded-lg bg-white border-2 border-gray-300 w-full text-right px-3 py-2 focus:border-primary-500 focus:outline-none transition-colors'
/>
<div className='flex flex-row-reverse gap-4 pt-2'>
<FaInstagram className='text-lg cursor-pointer hover:text-primary-500 hover:scale-125 transition-all' />
<FaFacebook className='text-lg cursor-pointer hover:text-primary-500 hover:scale-125 transition-all' />
<FaLinkedin className='text-lg cursor-pointer hover:text-primary-500 hover:scale-125 transition-all' />
</div>
</div>
</div>
{/* نیمه پایین */}
<div className='flex flex-col md:flex-row justify-between items-center gap-4 pt-6 md:pt-8 text-xs md:text-sm'>
<div className='flex flex-col md:flex-row justify-center items-center gap-4 md:gap-6'>
<Link href="/privacy" className='hover:text-primary-500 transition-colors'>حریم خصوصی</Link>
<p className='hidden md:block text-gray-400'>|</p>
<Link href="/terms" className='hover:text-primary-500 transition-colors'>قوانین و مقررات</Link>
</div>
<div className='flex justify-center items-center gap-1'>
<p>همه حقوق محفوظ است 2025 CafeFinder</p>
<CiHeart className='text-primary-500' />
</div>
</div>
</div>
</footer>
</>
)
}

View File

@ -0,0 +1,48 @@
import React from 'react';
import Link from 'next/link';
import Image from 'next/image';
import { IoIosArrowDown } from "react-icons/io";
export default function Header({ className = "" }) {
return (
<>
<header className={`flex justify-between items-center sticky top-0 left-0 right-0 px-4 md:px-8 lg:px-16 z-50 bg-white-500 shadow-md h-16 md:h-20 transition-all ${className}`}>
{/* لوگو و منوی دراپ داون */}
<div className="flex justify-center items-center gap-4 md:gap-8 shrink-0">
<Image
src="/KafehJoo.svg"
alt="KafehJoo Logo Default"
width={50}
height={45}
className="hover:scale-110 transition-transform" />
<div className="hidden md:block relative">
<select className="h-9 md:h-10 w-24 pr-2 bg-white border-2 border-primary-300 rounded-lg appearance-none text-sm cursor-pointer hover:border-primary-500 focus:border-primary-500 focus:outline-none transition-colors">
<option value="شهر">شهر</option>
<option value="کشور">کشور</option>
<option value="استان">استان</option>
</select>
<IoIosArrowDown className="absolute left-2 top-1/2 transform -translate-y-1/2 text-primary-500 pointer-events-none" />
</div>
</div>
{/* لیست آیتم‌ها */}
<nav aria-label="main menu" className="hidden lg:flex items-center justify-between px-4 py-2">
<div className="flex items-center gap-8 text-black-500 font-semibold text-sm lg:text-base">
<Link href="/" className="hover:text-primary-500 transition-colors pb-1 border-b-2 border-transparent hover:border-primary-500">خانه</Link>
<Link href="/categories" className="hover:text-primary-500 transition-colors pb-1 border-b-2 border-transparent hover:border-primary-500">دستهبندی</Link>
<Link href="/contact_us" className="hover:text-primary-500 transition-colors pb-1 border-b-2 border-transparent hover:border-primary-500">تماس با ما</Link>
<Link href="/about_us" className="hover:text-primary-500 transition-colors pb-1 border-b-2 border-transparent hover:border-primary-500">درباره ما</Link>
</div>
</nav>
{/* دکمه ثبت نام */}
<Link href="/register"
className="flex justify-center items-center text-xs md:text-sm
font-bold h-9 md:h-10 px-4 md:px-6 text-white-100 bg-primary-500
hover:bg-primary-600 rounded-lg md:rounded-xl transition-all hover:shadow-lg
hover:-translate-y-0.5 active:translate-y-0 active:shadow-none shrink-0">
ثبت نام
</Link>
</header>
</>
)
}

View File

@ -0,0 +1,14 @@
export default function Sidebar({ className="" }) {
return (
<aside className={`w-64 bg-gray-200 p-4 ${className}`}>
<nav>
<ul>
<li className="mb-2"><a href="#" className="text-gray-700 hover:text-gray-900">Home</a></li>
<li className="mb-2"><a href="#" className="text-gray-700 hover:text-gray-900">About</a></li>
<li className="mb-2"><a href="#" className="text-gray-700 hover:text-gray-900">Services</a></li>
<li className="mb-2"><a href="#" className="text-gray-700 hover:text-gray-900">Contact</a></li>
</ul>
</nav>
</aside>
);
}

View File

View File

48
components/ui/Toast.jsx Normal file
View File

@ -0,0 +1,48 @@
"use client";
import { useSelector, useDispatch } from "react-redux";
import { useEffect } from "react";
import { hideToast } from "@/redux/slices/toastSlice";
export default function Toast() {
const dispatch = useDispatch();
const { message, type, visible } = useSelector((state) => state.toast);
useEffect(() => {
if (visible) {
const timer = setTimeout(() => {
dispatch(hideToast());
}, 3000);
return () => clearTimeout(timer);
}
}, [visible, dispatch]);
if (!visible) return null;
const bgColor = {
success: "bg-green-50 border-green-200 text-green-700",
error: "bg-red-50 border-red-200 text-red-700",
warning: "bg-yellow-50 border-yellow-200 text-yellow-700",
info: "bg-blue-50 border-blue-200 text-blue-700",
}[type];
const progressColor = {
success: "bg-green-500",
error: "bg-red-500",
warning: "bg-yellow-500",
info: "bg-blue-500",
}[type];
return (
<div className="fixed top-5 left-5 z-50 max-w-sm animate-in slide-in-from-top">
<div
className={`border p-4 rounded-lg shadow-lg ${bgColor}`}
role="alert"
>
<p className="font-medium text-sm">{message}</p>
<div className={`h-1 w-full ${progressColor} mt-2 rounded-full`}></div>
</div>
</div>
);
}

View File

@ -1,29 +0,0 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{js,jsx}'],
extends: [
js.configs.recommended,
reactHooks.configs['recommended-latest'],
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
rules: {
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
},
},
])

16
eslint.config.mjs Normal file
View File

@ -0,0 +1,16 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
const eslintConfig = defineConfig([
...nextVitals,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;

View File

@ -1,13 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>my-project</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

7
jsconfig.json Normal file
View File

@ -0,0 +1,7 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./*"]
}
}
}

58
middleware.ts Normal file
View File

@ -0,0 +1,58 @@
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
/**
* Middleware برای احراز هویت و حفاظت routes
*
* این middleware:
* 1. بررسی توکن از HTTP-only cookie
* 2. کاربران بدون توکن را به /login تغییر مسیر میدهد
* 3. کاربران لاگینشده که به /login میروند را به / تغییر مسیر میدهد
*/
const publicRoutes = ["/login", "/register", "/"];
const protectedRoutes = ["/(protected)", "/dashboard", "/profile"];
export function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname;
const token = request.cookies.get("token")?.value;
// بررسی اینکه آیا route protected است
const isProtectedRoute = protectedRoutes.some(
(route) => pathname.startsWith(route) || pathname === route
);
// بررسی اینکه آیا route public است
const isPublicRoute = publicRoutes.some(
(route) => pathname.startsWith(route) || pathname === route
);
// اگر کاربر토کن ندارد و سعی می‌کند به protected route برود
if (isProtectedRoute && !token) {
return NextResponse.redirect(new URL("/login", request.url));
}
// اگر کاربر توکن دارد و سعی می‌کند به auth routes برود
if (token && (pathname === "/login" || pathname === "/register")) {
return NextResponse.redirect(new URL("/", request.url));
}
// ادامه رفتن درخواست
return NextResponse.next();
}
/**
* Matcher برای مسیرهایی که middleware برای آنها اجرا شود
*/
export const config = {
matcher: [
// Protected routes
"/(protected)/:path*",
"/dashboard/:path*",
"/profile/:path*",
// Auth routes
"/login",
"/register",
],
};

7
next.config.mjs Normal file
View File

@ -0,0 +1,7 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
/* config options here */
reactCompiler: true,
};
export default nextConfig;

5835
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,40 +1,30 @@
{
"name": "my-project",
"name": "nextprokit",
"version": "0.1.0",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"@reduxjs/toolkit": "^2.11.2",
"@tailwindcss/vite": "^4.1.14",
"axios": "^1.13.2",
"framer-motion": "^12.23.26",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"js-cookie": "^3.0.5",
"jsonwebtoken": "^9.0.3",
"next": "16.1.1",
"postcss": "^8.5.6",
"react": "19.2.3",
"react-dom": "19.2.3",
"react-icons": "^5.5.0",
"react-redux": "^9.2.0",
"react-router-dom": "^7.9.4",
"tailwind-scrollbar-hide": "^4.0.0",
"tailwindcss": "^4.1.14"
"react-redux": "^9.2.0"
},
"devDependencies": {
"@eslint/js": "^9.36.0",
"@types/react": "^19.1.16",
"@types/react-dom": "^19.1.9",
"@vitejs/plugin-react": "^5.0.4",
"eslint": "^9.36.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.22",
"globals": "^16.4.0",
"vite": "npm:rolldown-vite@7.1.14",
"vite-plugin-svgr": "^4.5.0"
},
"overrides": {
"vite": "npm:rolldown-vite@7.1.14"
"@tailwindcss/postcss": "^4.1.18",
"babel-plugin-react-compiler": "1.0.0",
"eslint": "^9",
"eslint-config-next": "16.1.1",
"tailwindcss": "^4.1.18"
}
}

7
postcss.config.mjs Normal file
View File

@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 725 KiB

After

Width:  |  Height:  |  Size: 725 KiB

Binary file not shown.

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

36
redux/slices/authSlice.js Normal file
View File

@ -0,0 +1,36 @@
import { createSlice } from "@reduxjs/toolkit";
const getInitialState = () => {
if (typeof window === "undefined") {
return { token: null, isAuthenticated: false };
}
const token = localStorage.getItem("token");
return {
token: token || null,
isAuthenticated: !!token,
};
};
const authSlice = createSlice({
name: "auth",
initialState: getInitialState(),
reducers: {
setToken(state, action) {
state.token = action.payload;
state.isAuthenticated = true;
if (typeof window !== "undefined") {
localStorage.setItem("token", action.payload);
}
},
clearToken(state) {
state.token = null;
state.isAuthenticated = false;
if (typeof window !== "undefined") {
localStorage.removeItem("token");
}
},
},
});
export const { setToken, clearToken } = authSlice.actions;
export default authSlice.reducer;

View File

@ -2,10 +2,16 @@ import { createSlice } from "@reduxjs/toolkit";
const loadingSlice = createSlice({
name: "loading",
initialState: false,
initialState: {
isLoading: false,
},
reducers: {
showLoading: () => true,
hideLoading: () => false,
showLoading: (state) => {
state.isLoading = true;
},
hideLoading: (state) => {
state.isLoading = false;
},
},
});

View File

@ -0,0 +1,36 @@
import { createSlice } from "@reduxjs/toolkit";
const getInitialState = () => {
if (typeof window === "undefined") {
return { user: null, isLoaded: false };
}
const user = JSON.parse(localStorage.getItem("user") || "null");
return {
user: user,
isLoaded: !!user,
};
};
const profileSlice = createSlice({
name: "profile",
initialState: getInitialState(),
reducers: {
setProfile(state, action) {
state.user = action.payload;
state.isLoaded = true;
if (typeof window !== "undefined") {
localStorage.setItem("user", JSON.stringify(action.payload));
}
},
clearProfile(state) {
state.user = null;
state.isLoaded = false;
if (typeof window !== "undefined") {
localStorage.removeItem("user");
}
},
},
});
export const { setProfile, clearProfile } = profileSlice.actions;
export default profileSlice.reducer;

View File

@ -4,7 +4,7 @@ const toastSlice = createSlice({
name: "toast",
initialState: {
message: "",
type: "success", // success | error | info
type: "success", // success | error | warning | info
visible: false,
},
reducers: {
@ -16,11 +16,14 @@ const toastSlice = createSlice({
},
hideToast(state) {
state.visible = false;
},
clearToast(state) {
state.message = "";
state.type = "success";
state.visible = false;
},
},
});
export const { showToast, hideToast } = toastSlice.actions;
export const { showToast, hideToast, clearToast } = toastSlice.actions;
export default toastSlice.reducer;

View File

@ -2,6 +2,7 @@ import axios from "axios";
import { store } from "../../redux/store";
import { showLoading, hideLoading } from "../../redux/slices/loadingSlice";
import { clearToken } from "../../redux/slices/authSlice";
import { clearProfile } from "../../redux/slices/profileSlice";
import { showToast } from "../../redux/slices/toastSlice";
import { toastIgnore } from "./toastIgnore";
@ -87,13 +88,28 @@ axios.interceptors.response.use(
const { status, data } = error.response;
const url = error.config?.url || "";
// برای خطاهای 401 و 403: فورا توکن پاک شود
if ([401, 403].includes(status)) {
store.dispatch(clearToken());
store.dispatch(clearProfile());
// redirect به login اگر در protected route هستیم
if (typeof window !== "undefined") {
const currentPath = window.location.pathname;
const publicRoutes = ["/login", "/register", "/"];
const isPublicRoute = publicRoutes.some(route => currentPath.startsWith(route));
if (!isPublicRoute) {
window.location.href = "/login";
}
}
}
if (!toastIgnore.some(u => url.includes(u))) {
const message = getErrorMessageByStatus(status, data);
store.dispatch(showToast({ type: "error", message }));
}
if ([401, 403].includes(status)) store.dispatch(clearToken());
return Promise.reject(error);
}
);

10
services/auth.js Normal file
View File

@ -0,0 +1,10 @@
import requests from "./api/base-api";
const authService = {
login: (data) => requests.post("/users/user-login", data),
register: (userData) => requests.post("/users/user-register", userData),
sendOTP: (data) => requests.post("/otp/send", data),
verifyOTP: (data) => requests.post("/otp/verify", data),
};
export default authService;

11
services/cafe.js Normal file
View File

@ -0,0 +1,11 @@
import requests from "./api/base-api";
const cafeService = {
likeCafe: (cafeId) => requests.post("/users/user-cafe-like", { cafeId }),
getLikedCafes: () => requests.get("/users/user-cafe-liked"),
visitCafe: (cafeId) => requests.post("/users/user-cafe-score", { cafeId }),
getVisitedCafes: () => requests.get("/users/user-cafe-visited"),
};
export default cafeService;

8
services/profile.js Normal file
View File

@ -0,0 +1,8 @@
import requests from "./api/base-api";
const profileService = {
getProfile: () => requests.get("/users/user-profile"),
updateProfile: (profileData) => requests.put("/users/user-edit-profile", profileData),
};
export default profileService;

View File

@ -1,9 +0,0 @@
import AppRoutes from "./routes/routes";
function App() {
return (
<AppRoutes />
);
}
export default App;

View File

@ -1,3 +0,0 @@
<svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.9947 0.75H15.7947C16.9531 0.75 17.8929 1.69791 17.8929 2.8673V5.69066C17.8929 6.85916 16.9531 7.80796 15.7947 7.80796H12.9947C11.8354 7.80796 10.8956 6.85916 10.8956 5.69066V2.8673C10.8956 1.69791 11.8354 0.75 12.9947 0.75ZM2.84907 0.75H5.64813C6.80746 0.75 7.74721 1.69791 7.74721 2.8673V5.69066C7.74721 6.85916 6.80746 7.80796 5.64813 7.80796H2.84907C1.68975 7.80796 0.75 6.85916 0.75 5.69066V2.8673C0.75 1.69791 1.68975 0.75 2.84907 0.75ZM2.84907 10.8349H5.64813C6.80746 10.8349 7.74721 11.7828 7.74721 12.9531V15.7756C7.74721 16.9449 6.80746 17.8929 5.64813 17.8929H2.84907C1.68975 17.8929 0.75 16.9449 0.75 15.7756V12.9531C0.75 11.7828 1.68975 10.8349 2.84907 10.8349ZM12.9947 10.8349H15.7947C16.9531 10.8349 17.8929 11.7828 17.8929 12.9531V15.7756C17.8929 16.9449 16.9531 17.8929 15.7947 17.8929H12.9947C11.8354 17.8929 10.8956 16.9449 10.8956 15.7756V12.9531C10.8956 11.7828 11.8354 10.8349 12.9947 10.8349Z" stroke="#402E32" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

View File

@ -1,3 +0,0 @@
<svg width="20" height="18" viewBox="0 0 20 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.6003 1.22078C10.2416 0.593073 9.27402 0.593073 8.91537 1.22078L6.75685 4.9986C6.61897 5.23991 6.36911 5.40866 6.07962 5.45598L1.54741 6.19684C0.794363 6.31994 0.495365 7.17535 1.02676 7.68639L4.22492 10.7621C4.42921 10.9585 4.52464 11.2316 4.4836 11.5021L3.84106 15.7378C3.7343 16.4416 4.5171 16.9703 5.20417 16.6584L9.33926 14.7815C9.60339 14.6616 9.91223 14.6616 10.1764 14.7815L14.3115 16.6584C14.9985 16.9703 15.7813 16.4416 15.6746 15.7378L15.032 11.5021C14.991 11.2316 15.0864 10.9585 15.2907 10.7621L18.4889 7.68639C19.0203 7.17535 18.7213 6.31994 17.9682 6.19684L13.436 5.45598C13.1465 5.40866 12.8967 5.23991 12.7588 4.9986L10.6003 1.22078Z" stroke="#402E32" stroke-width="1.5"/>
</svg>

Before

Width:  |  Height:  |  Size: 805 B

View File

@ -1,3 +0,0 @@
<svg width="20" height="19" viewBox="0 0 20 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.75 17.75V8.75H2.75C1.64543 8.75 0.75 9.64543 0.75 10.75V15.75C0.75 16.8546 1.64543 17.75 2.75 17.75H6.75ZM6.75 17.75H12.75M6.75 17.75V2.75C6.75 1.64543 7.64543 0.75 8.75 0.75H10.75C11.8546 0.75 12.75 1.64543 12.75 2.75V17.75M12.75 17.75V5.75H16.75C17.8546 5.75 18.75 6.64543 18.75 7.75V15.75C18.75 16.8546 17.8546 17.75 16.75 17.75H12.75Z" stroke="#402E32" stroke-width="1.5" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 517 B

View File

@ -1,3 +0,0 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.75 0.807191H2.08333C1.34695 0.807191 0.75 1.40414 0.75 2.14052V11.4739C0.75 12.2102 1.34695 12.8072 2.08333 12.8072H11.4167C12.153 12.8072 12.75 12.2102 12.75 11.4739V6.80719M9.47673 2.20075L11.3567 4.08075M9.41667 2.14052L11.4167 4.14052M10.6119 0.945262L3.61193 7.94526C3.4869 8.07029 3.41667 8.23986 3.41667 8.41667V9.47386C3.41667 9.84205 3.71514 10.1405 4.08333 10.1405H5.14052C5.31734 10.1405 5.4869 10.0703 5.61193 9.94526L12.6119 2.94526C12.8723 2.68491 12.8723 2.2628 12.6119 2.00245L11.5547 0.945262C11.2944 0.684913 10.8723 0.684912 10.6119 0.945262Z" stroke="#402E32" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 763 B

View File

@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.505 14.8317C10.505 15.8556 9.67492 16.6857 8.65101 16.6857C7.6271 16.6857 6.79705 15.8556 6.79705 14.8317C6.79705 13.8078 7.6271 12.9778 8.65101 12.9778C9.67492 12.9778 10.505 13.8078 10.505 14.8317ZM10.505 14.8317V7.41589L16.6848 5.56193V13.5958M16.6848 13.5958C16.6848 14.6197 15.8548 15.4497 14.8309 15.4497C13.807 15.4497 12.9769 14.6197 12.9769 13.5958C12.9769 12.5718 13.807 11.7418 14.8309 11.7418C15.8548 11.7418 16.6848 12.5718 16.6848 13.5958ZM5.56108 22.8655H17.9208C20.6512 22.8655 22.8647 20.6521 22.8647 17.9217V5.56193C22.8647 2.8315 20.6512 0.618042 17.9208 0.618042H5.56108C2.83064 0.618042 0.617188 2.8315 0.617188 5.56193V17.9217C0.617188 20.6521 2.83064 22.8655 5.56108 22.8655Z" stroke="#402E32" stroke-width="1.23597" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 905 B

View File

@ -1,3 +0,0 @@
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.617188 0.618042H3.08913C4.45435 0.618042 5.56108 1.72477 5.56108 3.08999V9.26985M14.2129 14.2137H20.3927M21.0107 17.9217V12.9778C21.0107 11.6125 19.904 10.5058 18.5388 10.5058H11.7409M21.0107 17.9217C19.6455 17.9217 18.5388 19.0284 18.5388 20.3936C18.5388 21.7588 19.6455 22.8655 21.0107 22.8655C22.376 22.8655 23.4827 21.7588 23.4827 20.3936C23.4827 19.0284 22.376 17.9217 21.0107 17.9217ZM16.0668 10.5058V8.03388C16.0668 6.66866 14.9601 5.56193 13.5949 5.56193H5.56108M14.2129 16.0677C14.2129 19.822 11.1694 22.8655 7.41504 22.8655C3.66069 22.8655 0.617188 19.822 0.617188 16.0677C0.617188 12.3134 3.66069 9.26985 7.41504 9.26985C11.1694 9.26985 14.2129 12.3134 14.2129 16.0677ZM10.505 16.0677C10.505 17.7742 9.12156 19.1576 7.41504 19.1576C5.70851 19.1576 4.32511 17.7742 4.32511 16.0677C4.32511 14.3612 5.70851 12.9778 7.41504 12.9778C9.12156 12.9778 10.505 14.3612 10.505 16.0677Z" stroke="#402E32" stroke-width="1.23597" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,3 +0,0 @@
<svg width="24" height="23" viewBox="0 0 24 23" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.32511 13.5958H6.79705M9.88698 13.5958H13.5949M16.6848 13.5958H19.1568M2.47115 9.26985L0.617188 6.7979M2.47115 9.26985H21.0107M2.47115 9.26985L3.90639 2.57204C4.15062 1.4323 5.15785 0.618042 6.32346 0.618042H17.1584C18.324 0.618042 19.3313 1.4323 19.5755 2.57204L21.0107 9.26985M21.0107 9.26985L22.8647 6.7979M3.08913 17.9217H20.3927M3.08913 17.9217C1.72391 17.9217 0.617188 16.8149 0.617188 15.4497V11.7418C0.617188 10.3766 1.72392 9.26985 3.08913 9.26985H20.3927C21.758 9.26985 22.8647 10.3766 22.8647 11.7418V15.4497C22.8647 16.8149 21.758 17.9217 20.3927 17.9217M3.08913 17.9217H8.03302V19.1576C8.03302 20.5228 6.9263 21.6296 5.56108 21.6296C4.19586 21.6296 3.08913 20.5228 3.08913 19.1576L3.08913 17.9217ZM20.3927 17.9217H15.4489V19.1576C15.4489 20.5228 16.5556 21.6296 17.9208 21.6296C19.286 21.6296 20.3927 20.5228 20.3927 19.1576V17.9217Z" stroke="#402E32" stroke-width="1.23597" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.79705 0.618042V5.56193M16.6848 0.618042V5.56193M1.23517 9.26985H22.2467M10.5049 14.2137L11.7409 13.5957L11.7409 18.5396M10.5049 18.5396H12.9769M4.32511 22.8655H19.1568C21.2046 22.8655 22.8647 21.2055 22.8647 19.1576V6.7979C22.8647 4.75008 21.2046 3.08999 19.1568 3.08999H4.32511C2.27728 3.08999 0.617188 4.75008 0.617188 6.7979V19.1576C0.617188 21.2055 2.27728 22.8655 4.32511 22.8655Z" stroke="#402E32" stroke-width="1.23597" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 591 B

View File

@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.3924 6.77985V12.5284L15.9111 15.2833M7.48537 1.46768C13.1597 -0.883137 19.6646 1.81252 22.0155 7.48687C24.3663 13.1612 21.6706 19.6661 15.9963 22.017C10.3219 24.3678 3.81701 21.6721 1.46619 15.9978C-0.883396 10.3234 1.81102 3.8185 7.48537 1.46768Z" stroke="#402E32" stroke-width="1.23597" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 454 B

View File

@ -1,3 +0,0 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.75 9C6.33579 9 6 9.33579 6 9.75C6 10.1642 6.33579 10.5 6.75 10.5V9.75V9ZM12.75 10.5C13.1642 10.5 13.5 10.1642 13.5 9.75C13.5 9.33579 13.1642 9 12.75 9V9.75V10.5ZM9 12.75C9 13.1642 9.33579 13.5 9.75 13.5C10.1642 13.5 10.5 13.1642 10.5 12.75H9.75H9ZM10.5 6.75C10.5 6.33579 10.1642 6 9.75 6C9.33579 6 9 6.33579 9 6.75H9.75H10.5ZM6.75 9.75V10.5H9.75V9.75V9H6.75V9.75ZM9.75 9.75V10.5H12.75V9.75V9H9.75V9.75ZM9.75 12.75H10.5V9.75H9.75H9V12.75H9.75ZM9.75 9.75H10.5V6.75H9.75H9V9.75H9.75ZM4.75 0.75V1.5H14.75V0.75V0H4.75V0.75ZM18.75 4.75H18V14.75H18.75H19.5V4.75H18.75ZM14.75 18.75V18H4.75V18.75V19.5H14.75V18.75ZM0.75 14.75H1.5V4.75H0.75H0V14.75H0.75ZM4.75 18.75V18C2.95507 18 1.5 16.5449 1.5 14.75H0.75H0C0 17.3734 2.12665 19.5 4.75 19.5V18.75ZM18.75 14.75H18C18 16.5449 16.5449 18 14.75 18V18.75V19.5C17.3734 19.5 19.5 17.3734 19.5 14.75H18.75ZM14.75 0.75V1.5C16.5449 1.5 18 2.95507 18 4.75H18.75H19.5C19.5 2.12665 17.3734 0 14.75 0V0.75ZM4.75 0.75V0C2.12665 0 0 2.12665 0 4.75H0.75H1.5C1.5 2.95507 2.95507 1.5 4.75 1.5V0.75Z" fill="#402E32"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,3 +0,0 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.75 18.75H14.75M4.75 18.75C2.54086 18.75 0.75 16.9591 0.75 14.75V4.75C0.75 2.54086 2.54086 0.75 4.75 0.75H14.75C16.9591 0.75 18.75 2.54086 18.75 4.75V14.75C18.75 16.9591 16.9591 18.75 14.75 18.75M4.75 18.75C4.58067 18.75 4.4138 18.7395 4.25 18.7191V17.9643C4.25 17.2139 4.39226 16.4708 4.66866 15.7775C4.94506 15.0842 5.35019 14.4543 5.86091 13.9237C6.37164 13.3931 6.97795 12.9721 7.64524 12.685C8.31253 12.3978 9.02773 12.25 9.75 12.25C10.4723 12.25 11.1875 12.3978 11.8548 12.685C12.5221 12.9721 13.1284 13.3931 13.6391 13.9237C14.1498 14.4543 14.5549 15.0842 14.8313 15.7775C15.1077 16.4708 15.25 17.2139 15.25 17.9643V18.7191C15.0862 18.7395 14.9193 18.75 14.75 18.75M12.75 6.75C12.75 8.40685 11.4069 9.75 9.75 9.75C8.09315 9.75 6.75 8.40685 6.75 6.75C6.75 5.09315 8.09315 3.75 9.75 3.75C11.4069 3.75 12.75 5.09315 12.75 6.75Z" stroke="#402E32" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1,4 +0,0 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.75 10.75C16.75 12.3323 16.2808 13.879 15.4018 15.1946C14.5227 16.5102 13.2733 17.5355 11.8115 18.141C10.3497 18.7465 8.74113 18.905 7.18928 18.5963C5.63743 18.2876 4.21197 17.5257 3.09315 16.4069C1.97433 15.288 1.2124 13.8626 0.903721 12.3107C0.59504 10.7589 0.753466 9.15034 1.35897 7.68853C1.96447 6.22672 2.98985 4.97729 4.30544 4.09824C5.62104 3.21919 7.16775 2.75 8.75 2.75V10.75H16.75Z" stroke="#402E32" stroke-width="1.5" stroke-linejoin="round"/>
<path d="M18.75 8.25C18.75 7.26509 18.556 6.28982 18.1791 5.37987C17.8022 4.46993 17.2497 3.64314 16.5533 2.9467C15.8569 2.25026 15.0301 1.69781 14.1201 1.3209C13.2102 0.943993 12.2349 0.75 11.25 0.75L11.25 8.25H18.75Z" stroke="#402E32" stroke-width="1.5" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 853 B

View File

@ -1,3 +0,0 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.25 6.25H4.75C2.54086 6.25 0.75 8.04086 0.75 10.25L0.75 14.75C0.75 16.9591 2.54086 18.75 4.75 18.75H14.75C16.9591 18.75 18.75 16.9591 18.75 14.75V10.25C18.75 8.04086 16.9591 6.25 14.75 6.25H14.25M5.75 14.75H13.75M9.75 0.75V11.75M9.75 0.75L12.75 3.75M9.75 0.75L6.75 3.75" stroke="#402E32" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 470 B

View File

@ -1,6 +0,0 @@
<svg width="20" height="22" viewBox="0 0 20 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.31 1.24891C5.58554 0.939636 5.55819 0.46555 5.24891 0.190013C4.93964 -0.0855245 4.46555 -0.0581734 4.19001 0.251103C3.66325 0.842363 3.40066 1.43351 3.31078 2.02505C3.22425 2.59457 3.30732 3.11644 3.38015 3.52547L3.39022 3.58196C3.53804 4.41075 3.60445 4.78309 3.18851 5.25279C2.91391 5.56289 2.94269 6.0369 3.25279 6.3115C3.56289 6.58611 4.0369 6.55733 4.3115 6.24723C5.22085 5.22032 5.02338 4.15916 4.88515 3.41635C4.8753 3.36341 4.86575 3.3121 4.85693 3.26252C4.78608 2.86462 4.74739 2.55557 4.79376 2.25037C4.83679 1.96718 4.96176 1.6398 5.31 1.24891Z" fill="#FDF8F4"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.75 8C0.783503 8 0 8.7835 0 9.75V13.25C0 16.3184 1.90616 18.9416 4.59893 20H1.25C0.835787 20 0.5 20.3358 0.5 20.75C0.5 21.1642 0.835787 21.5 1.25 21.5H13C13.4142 21.5 13.75 21.1642 13.75 20.75C13.75 20.3358 13.4142 20 13 20H9.90107C11.8478 19.2348 13.3835 17.6517 14.0849 15.6739C14.6597 16.1876 15.4184 16.5 16.25 16.5C18.0449 16.5 19.5 15.0449 19.5 13.25C19.5 11.4551 18.0449 10 16.25 10C15.6057 10 15.0051 10.1875 14.5 10.5109V9.75C14.5 8.7835 13.7165 8 12.75 8H1.75ZM14.5 13.25C14.5 12.2835 15.2835 11.5 16.25 11.5C17.2165 11.5 18 12.2835 18 13.25C18 14.2165 17.2165 15 16.25 15C15.2835 15 14.5 14.2165 14.5 13.25Z" fill="#FDF8F4"/>
<path d="M8.24891 0.190013C8.55819 0.46555 8.58554 0.939636 8.31 1.24891C7.96176 1.6398 7.83679 1.96718 7.79376 2.25037C7.74739 2.55557 7.78608 2.86462 7.85693 3.26252C7.86575 3.3121 7.8753 3.36341 7.88515 3.41635C8.02338 4.15916 8.22085 5.22032 7.3115 6.24723C7.0369 6.55733 6.56289 6.58611 6.25279 6.3115C5.94269 6.0369 5.91391 5.56289 6.18852 5.25279C6.60445 4.78309 6.53804 4.41075 6.39022 3.58196L6.38015 3.52547C6.30732 3.11644 6.22425 2.59457 6.31078 2.02505C6.40066 1.43351 6.66325 0.842363 7.19001 0.251103C7.46555 -0.0581734 7.93964 -0.0855245 8.24891 0.190013Z" fill="#FDF8F4"/>
<path d="M11.31 1.24891C11.5855 0.939636 11.5582 0.46555 11.2489 0.190013C10.9396 -0.0855245 10.4656 -0.0581734 10.19 0.251103C9.66325 0.842363 9.40066 1.43351 9.31078 2.02505C9.22425 2.59457 9.30732 3.11644 9.38015 3.52547L9.39022 3.58196C9.53804 4.41075 9.60445 4.78309 9.18852 5.25279C8.91391 5.56289 8.94269 6.0369 9.25279 6.3115C9.56289 6.58611 10.0369 6.55733 10.3115 6.24723C11.2209 5.22032 11.0234 4.15916 10.8852 3.41635C10.8753 3.36341 10.8658 3.3121 10.8569 3.26252C10.7861 2.86462 10.7474 2.55557 10.7938 2.25037C10.8368 1.96718 10.9618 1.6398 11.31 1.24891Z" fill="#FDF8F4"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -1,3 +0,0 @@
<svg width="20" height="22" viewBox="0 0 20 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.75 13.25C13.75 14.6307 14.8693 15.75 16.25 15.75C17.6307 15.75 18.75 14.6307 18.75 13.25C18.75 11.8693 17.6307 10.75 16.25 10.75C14.8693 10.75 13.75 11.8693 13.75 13.25ZM13.75 13.25V9.75C13.75 9.19772 13.3023 8.75 12.75 8.75H1.75C1.19772 8.75 0.75 9.19772 0.75 9.75V13.25C0.75 16.8399 3.66015 19.75 7.25 19.75C10.8399 19.75 13.75 16.8399 13.75 13.25ZM13 20.75H1.25M4.75 0.75C3 2.71429 5.14942 4.16968 3.75 5.75M7.75 0.75C6 2.71429 8.14942 4.16968 6.75 5.75M10.75 0.75C9 2.71429 11.1494 4.16968 9.75 5.75" stroke="#402E32" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 706 B

View File

@ -1,4 +0,0 @@
<svg width="30" height="34" viewBox="0 0 30 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.9513 25.5C20.1447 26.0474 20.25 26.6364 20.25 27.25C20.25 30.1495 17.8995 32.5 15 32.5C12.1005 32.5 9.75 30.1495 9.75 27.25C9.75 26.6364 9.85527 26.0474 10.0487 25.5H19.9513Z" stroke="#402E32" stroke-width="2" stroke-linejoin="round"/>
<path d="M25 14.0925V11C25 5.47715 20.5228 1 15 1C9.47715 1 5 5.47715 5 11V14.0925C5 15.7813 4.27097 17.3879 3 18.5L2.23324 19.1709C1.44954 19.8567 1 20.8473 1 21.8887C1 23.8832 2.61684 25.5 4.6113 25.5H25.3887C27.3832 25.5 29 23.8832 29 21.8887C29 20.8473 28.5505 19.8567 27.7668 19.1709L27 18.5C25.729 17.3879 25 15.7813 25 14.0925Z" stroke="#402E32" stroke-width="2" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 748 B

View File

@ -1,3 +0,0 @@
<svg width="26" height="27" viewBox="0 0 26 27" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M24.0833 6.75H1.41667M0.75 3.41667V5.64543C0.75 6.35267 1.03095 7.03095 1.53105 7.53105L7.96895 13.969C8.46905 14.469 8.75 15.1473 8.75 15.8546V23.926C8.75 24.9171 9.79308 25.5618 10.6796 25.1185L16.013 22.4519C16.4647 22.226 16.75 21.7643 16.75 21.2593V15.2564C16.75 14.5089 17.0637 13.7958 17.6147 13.2907L23.8853 7.54267C24.4363 7.03758 24.75 6.32441 24.75 5.57693V3.41667C24.75 1.94391 23.5561 0.75 22.0833 0.75H3.41667C1.94391 0.75 0.75 1.94391 0.75 3.41667Z" stroke="#402E32" stroke-width="1.5" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 638 B

View File

@ -1,3 +0,0 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.75 9C13.1642 9 13.5 9.33579 13.5 9.75C13.5 10.1642 13.1642 10.5 12.75 10.5V9.75V9ZM6.75 10.5C6.33579 10.5 6 10.1642 6 9.75C6 9.33579 6.33579 9 6.75 9V9.75V10.5ZM10.5 12.75C10.5 13.1642 10.1642 13.5 9.75 13.5C9.33579 13.5 9 13.1642 9 12.75H9.75H10.5ZM9 6.75C9 6.33579 9.33579 6 9.75 6C10.1642 6 10.5 6.33579 10.5 6.75H9.75H9ZM12.75 9.75V10.5H9.75V9.75V9H12.75V9.75ZM9.75 9.75V10.5H6.75V9.75V9H9.75V9.75ZM9.75 12.75H9V9.75H9.75H10.5V12.75H9.75ZM9.75 9.75H9V6.75H9.75H10.5V9.75H9.75ZM14.75 0.75V1.5H4.75V0.75V0H14.75V0.75ZM0.75 4.75H1.5V14.75H0.75H0V4.75H0.75ZM4.75 18.75V18H14.75V18.75V19.5H4.75V18.75ZM18.75 14.75H18V4.75H18.75H19.5V14.75H18.75ZM14.75 18.75V18C16.5449 18 18 16.5449 18 14.75H18.75H19.5C19.5 17.3734 17.3734 19.5 14.75 19.5V18.75ZM0.75 14.75H1.5C1.5 16.5449 2.95507 18 4.75 18V18.75V19.5C2.12665 19.5 0 17.3734 0 14.75H0.75ZM4.75 0.75V1.5C2.95507 1.5 1.5 2.95507 1.5 4.75H0.75H0C0 2.12665 2.12665 0 4.75 0V0.75ZM14.75 0.75V0C17.3734 0 19.5 2.12665 19.5 4.75H18.75H18C18 2.95507 16.5449 1.5 14.75 1.5V0.75Z" fill="#FDF8F4"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,3 +0,0 @@
<svg width="14" height="8" viewBox="0 0 14 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.75 0.75L6.75 6.75L12.75 0.75" stroke="#402E32" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 228 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 10 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 143 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 139 KiB

View File

@ -1,3 +0,0 @@
<svg width="30" height="34" viewBox="0 0 30 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.3333 20.5417C21.3333 22.7278 23.1055 24.5 25.2917 24.5C27.4778 24.5 29.25 22.7278 29.25 20.5417C29.25 18.3555 27.4778 16.5833 25.2917 16.5833C23.1055 16.5833 21.3333 18.3555 21.3333 20.5417ZM21.3333 20.5417V15C21.3333 14.1255 20.6245 13.4167 19.75 13.4167H2.33333C1.45888 13.4167 0.75 14.1255 0.75 15V20.5417C0.75 26.2256 5.35774 30.8333 11.0417 30.8333C16.7256 30.8333 21.3333 26.2256 21.3333 20.5417ZM20.1458 32.4167H1.54167M7.08333 0.75C4.3125 3.86012 7.71574 6.1645 5.5 8.66667M11.8333 0.75C9.0625 3.86012 12.4657 6.1645 10.25 8.66667M16.5833 0.75C13.8125 3.86012 17.2157 6.1645 15 8.66667" stroke="#402E32" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 796 B

View File

@ -1,4 +0,0 @@
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="36" height="36" rx="6" fill="#E6E2DE"/>
<path d="M18 8.99997H11C9.89543 8.99997 9 9.8954 9 11V25C9 26.1045 9.89543 27 11 27H25C26.1046 27 27 26.1045 27 25V18M22.0901 11.0903L24.9101 13.9103M22 11L25 14M23.7929 9.20708L13.2929 19.7071C13.1054 19.8946 13 20.149 13 20.4142V22C13 22.5523 13.4477 23 14 23H15.5858C15.851 23 16.1054 22.8946 16.2929 22.7071L26.7929 12.2071C27.1834 11.8166 27.1834 11.1834 26.7929 10.7929L25.2071 9.20708C24.8166 8.81655 24.1834 8.81655 23.7929 9.20708Z" stroke="#402E32" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 683 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 3.3 MiB

View File

@ -1,3 +0,0 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.75 18.75L14.925 14.925M10.2476 3.9C13.1241 4.67075 14.8366 7.6378 14.066 10.5136M9.07551 0.75C4.48481 0.75 0.75 4.38288 0.75 8.84951C0.75 13.3161 4.48481 16.95 9.07551 16.95C13.6652 16.95 17.4 13.3161 17.4 8.84951C17.4 4.38288 13.6652 0.75 9.07551 0.75Z" stroke="#402E32" stroke-width="1.5" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 432 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

View File

@ -1,71 +0,0 @@
import { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { AnimatePresence, motion } from "framer-motion";
// Redux
import { hideToast } from "../redux/slices/toastSlice";
// Animation Variants
const ANIMATION_VARIANTS = {
mobile: {
initial: { y: 100, opacity: 0 },
animate: { y: 0, opacity: 1 },
exit: { y: 100, opacity: 0 },
},
desktop: {
initial: { x: -100, opacity: 0 },
animate: { x: 0, opacity: 1 },
exit: { x: -100, opacity: 0 },
},
};
const TOAST_COLORS = {
success: "bg-green-500",
error: "bg-red-500",
info: "bg-blue-500",
};
const TOAST_DURATION = 3000;
export default function Toast() {
const { message, type, visible } = useSelector((state) => state.toast);
const dispatch = useDispatch();
const isMobile = typeof window !== "undefined" && window.innerWidth < 768;
// Auto-hide toast after duration
useEffect(() => {
if (visible) {
const timer = setTimeout(() => dispatch(hideToast()), TOAST_DURATION);
return () => clearTimeout(timer);
}
}, [visible, dispatch]);
const bgColor = TOAST_COLORS[type] || TOAST_COLORS.info;
const variants = isMobile ? ANIMATION_VARIANTS.mobile : ANIMATION_VARIANTS.desktop;
return (
<AnimatePresence>
{visible && (
<motion.div
variants={variants}
initial="initial"
animate="animate"
exit="exit"
transition={{
type: "spring",
stiffness: 260,
damping: 20,
}}
className={`${bgColor} fixed z-50 text-white px-6 py-3 rounded-lg shadow-2xl max-w-xs md:max-w-sm ${
isMobile
? "left-1/2 bottom-6 -translate-x-1/2"
: "top-6 left-6"
}`}
role="alert"
>
<p className="text-sm md:text-base font-medium">{message}</p>
</motion.div>
)}
</AnimatePresence>
);
}

View File

@ -1,55 +0,0 @@
// import React from "react";
// import Search from "../assets/icons/search.svg";
// import Arrow from "../assets/icons/arrow.svg";
// import Vector7 from "../assets/icons/Vector7.svg";
// import Pic from "../assets/icons/pic.png";
// const Header = ({ title }) => {
// return (
// <header className="flex items-center gap-4 mb-6">
// <h3 className="text-[#402E32] w-[90%] font-bold">{title}</h3>
// <div className="w-12 h-10 border-2 border-[#8B8886] flex justify-center items-center rounded-2xl bg-[#E6DBCC]">
// <img src={Search} alt="لوگو" />
// </div>
// <div className="w-35 h-10 border-2 border-[#8B8886] flex justify-center items-center rounded-2xl bg-[#E6DBCC]">
// <img src={Pic} className="w-10 h-10 shadow-2xl ml-1" alt="لوگو" />
// <p className="text-[#402E32] -mt-1.5 pl-1.5">سارا راد</p>
// <img src={Arrow} alt="لوگو" />
// </div>
// <img src={Vector7} alt="لوگو" />
// </header>
// );
// };
// export default Header;
// src/components/Header.jsx
import React from "react";
import Search from "../../../assets/icons/search.svg";
import Arrow from "../../../assets/icons/arrow.svg";
import Vector7 from "../../../assets/icons/Vector7.svg";
import Pic from "../../../assets/icons/pic.png";
const Header = ({ title }) => {
return (
<header className="flex items-center justify-end gap-4 p-3 ml-5 mr-2">
{/* <h3 className="text-[#402E32] font-bold text-lg">{title}</h3> */}
<div className="flex items-center gap-2">
<div className="w-12 h-10 border-2 border-[#8B8886] flex justify-center items-center rounded-2xl bg-[#F4EADF]">
<img src={Search} alt="search" />
</div>
<div className="flex items-center w-40 md:w-60 justify-between border-2 border-[#8B8886] rounded-2xl px-3 py-1 bg-[#F4EADF]">
<img src={Pic} className="w-8 h-8 rounded-full ml-1" alt="user" />
<p className="text-[#402E32] text-sm">سارا راد</p>
<img src={Arrow} className="w-4 h-4 ml-1" alt="arrow" />
</div>
<img src={Vector7} alt="vector" />
</div>
</header>
);
};
export default Header;

View File

@ -1,163 +0,0 @@
import { Link, useLocation, useNavigate } from "react-router-dom";
import LogoDM from "../../../assets/icons/LogoDM.svg";
import { BiBarChartAlt2 } from "react-icons/bi";
import { AiOutlinePieChart } from "react-icons/ai";
import { PiCoffee } from "react-icons/pi";
import { HiOutlineLogout } from "react-icons/hi";
import { FiMenu, FiX } from "react-icons/fi";
export default function Sidebar({ className, isOpen, setIsOpen }) {
const location = useLocation();
const navigate = useNavigate();
const isActive = (path) => location.pathname === path;
const handleLogout = () => {
try {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
localStorage.removeItem('token');
localStorage.removeItem('adminInfo');
console.log('Tokens cleared from localStorage');
console.log('Current token:', localStorage.getItem('token'));
navigate('/login');
} catch (error) {
console.error('Logout error:', error);
navigate('/login');
}
};
const closeSidebar = () => setIsOpen(false);
return (
<>
<button
onClick={() => setIsOpen(!isOpen)}
className="lg:hidden fixed top-4 right-4 z-[70] bg-[#7F4629] text-white p-2.5 rounded-lg shadow-lg"
aria-label="Toggle menu"
>
{isOpen ? <FiX className="w-5 h-5" /> : <FiMenu className="w-5 h-5" />}
</button>
{isOpen && (
<div
className="lg:hidden fixed inset-0 bg-black/50 z-[60]"
onClick={closeSidebar}
/>
)}
<aside
dir="rtl"
className={`${className}
transition-transform duration-300 ease-in-out
${isOpen ? 'translate-x-0' : 'translate-x-full'}
lg:translate-x-0 lg:w-[220px]
`}
>
<div className="flex justify-center mt-5 mb-2">
<img src={LogoDM} className="h-10 w-10 lg:h-12 lg:w-12" alt="Logo" />
</div>
<nav className="flex-1 px-3 mt-6 space-y-3 overflow-y-auto">
<Link
to="/dashboard"
onClick={closeSidebar}
className={`group flex items-center gap-2.5 rounded-lg p-2.5 transition-all duration-200 ${
isActive("/dashboard")
? "bg-[#7F4629] shadow-md"
: "hover:bg-[#7F4629]"
}`}
>
<BiBarChartAlt2
className={`w-5 h-5 flex-shrink-0 transition-colors ${
isActive("/dashboard")
? "text-white"
: "text-[#402E32] group-hover:text-white"
}`}
/>
<span
className={`text-sm font-medium transition-colors ${
isActive("/dashboard")
? "text-white"
: "text-[#402E32] group-hover:text-white"
}`}
>
داشبورد
</span>
</Link>
<Link
to="/cafe-management"
onClick={closeSidebar}
className={`group flex items-center gap-2.5 rounded-lg p-2.5 transition-all duration-200 ${
isActive("/cafe-management")
? "bg-[#7F4629] shadow-md"
: "hover:bg-[#7F4629]"
}`}
>
<PiCoffee
className={`w-5 h-5 flex-shrink-0 transition-colors ${
isActive("/cafe-management")
? "text-white"
: "text-[#402E32] group-hover:text-white"
}`}
/>
<span
className={`text-sm font-medium transition-colors ${
isActive("/cafe-management")
? "text-white"
: "text-[#402E32] group-hover:text-white"
}`}
>
مدیریت کافه ها
</span>
</Link>
<Link
to="/stats"
onClick={closeSidebar}
className={`group flex items-center gap-2.5 rounded-lg p-2.5 transition-all duration-200 ${
isActive("/stats")
? "bg-[#7F4629] shadow-md"
: "hover:bg-[#7F4629]"
}`}
>
<AiOutlinePieChart
className={`w-5 h-5 flex-shrink-0 transition-colors ${
isActive("/stats")
? "text-white"
: "text-[#402E32] group-hover:text-white"
}`}
/>
<span
className={`text-sm font-medium transition-colors ${
isActive("/stats")
? "text-white"
: "text-[#402E32] group-hover:text-white"
}`}
>
آمار و تحلیل
</span>
</Link>
</nav>
<div className="p-3 border-t border-[#D9CAB3]">
<button
onClick={() => {
handleLogout();
closeSidebar();
}}
className="group flex items-center gap-2.5 rounded-lg p-2.5 transition-all duration-200 hover:bg-[#7F4629] w-full"
>
<HiOutlineLogout className="w-5 h-5 flex-shrink-0 text-[#402E32] transition-colors group-hover:text-white" />
<span className="text-sm text-[#402E32] font-medium transition-colors group-hover:text-white">
خروج
</span>
</button>
</div>
</aside>
</>
);
};

View File

@ -1,54 +0,0 @@
import React, { useState } from "react";
import { Outlet, useLocation } from "react-router-dom";
import Sidebar from "./Sidebar/sidebar";
import Header from "./Header/header";
const HEADER_PATHS = [
"/management",
"/management/",
"/management/cafes",
"/edit-cafe",
"/edit-cafe/",
"/cafe",
"/cafes",
"/admin",
];
export default function Layout() {
const [isOpen, setIsOpen] = useState(false);
const location = useLocation();
const path = location.pathname;
// Check if current path should show header
const showHeader = HEADER_PATHS.some((p) => path.startsWith(p) || path.includes(p));
return (
<div className="flex min-h-screen w-full bg-[#FDF8F4]">
{/* Sidebar */}
<Sidebar
className="fixed top-0 right-0 bottom-0 z-[65] w-[260px] flex flex-col bg-[#EFEEEE] rounded-l-2xl shadow-xl border-l border-[#D9CAB3]"
isOpen={isOpen}
setIsOpen={setIsOpen}
/>
{/* Main Content */}
<div className="flex-1 flex flex-col min-h-screen w-full lg:mr-[220px]">
{/* Header */}
{showHeader && (
<div className="fixed top-0 right-0 left-0 lg:right-[220px] z-30 bg-[#FDF8F4] border-b border-[#e6e2de]">
<Header title="مدیریت کافه‌ها" />
</div>
)}
{/* Main Content Area */}
<main
className={`flex-1 w-full overflow-x-hidden bg-[#FDF8F4] transition-all p-3 lg:p-6`}
>
<div className="max-w-full mx-auto">
<Outlet />
</div>
</main>
</div>
</div>
);
}

View File

@ -1,29 +0,0 @@
import { useEffect } from "react";
import { useDispatch } from "react-redux";
import { setProfile, clearProfile } from "../redux/slices/profileSlice";
import profileService from "../services/profile";
export const useProfile = (shouldFetch = true) => {
const dispatch = useDispatch();
useEffect(() => {
if (!shouldFetch) return;
const fetchProfileData = async () => {
try {
const res = await profileService.getProfile();
if (res?.data?.data) {
dispatch(setProfile(res.data.data));
console.log("Profile data fetched:", res.data.data);
} else {
dispatch(clearProfile());
}
} catch (error) {
console.error("Error fetching profile:", error);
dispatch(clearProfile());
}
};
fetchProfileData();
}, [shouldFetch, dispatch]);
};

View File

@ -1,19 +0,0 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import { store } from './redux/store.js';
import './styles/index.css';
import App from './App.jsx';
import Toast from './components/Toast.jsx';
createRoot(document.getElementById('root')).render(
<StrictMode>
<Provider store={store}>
<BrowserRouter>
<App />
<Toast />
</BrowserRouter>
</Provider>
</StrictMode>,
)

View File

@ -1,187 +0,0 @@
import React, { useState, useEffect } from "react";
import { BiEdit } from "react-icons/bi";
import Vector9 from "../../assets/icons/Vector9.svg";
import Star1 from "../../assets/icons/Star1.svg";
import Group from "../../assets/icons/Group.svg";
import Pic1 from "../../assets/icons/pic1.svg";
import { Link } from "react-router-dom";
import cafeService from "../../services/cafe";
const CafeManagement = () => {
const [cafes, setCafes] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
useEffect(() => {
const fetchCafes = async () => {
setLoading(true);
setError("");
try {
const res = await cafeService.getCafeList();
if (res.data.success && res.data.data) {
setCafes(res.data.data);
console.log("✅ Cafes loaded successfully:", res.data);
} else {
setError("داده‌های دریافتی معتبر نیست");
}
} catch (error) {
console.error("❌ Error loading cafes:", error);
const errorMessage =
error.response?.data?.message ||
(error.request ? "خطا در برقراری ارتباط با سرور" : "خطای نامشخص رخ داده است");
setError(errorMessage);
} finally {
setLoading(false);
}
};
fetchCafes();
}, []);
const renderContent = () => {
if (loading) {
return (
<div className="flex justify-center items-center h-screen">
<div className="text-[#7F4629] text-xl font-bold">در حال بارگذاری...</div>
</div>
);
}
if (error) {
return (
<div className="bg-red-50 border-2 border-red-300 text-red-700 px-4 py-3 rounded-xl mt-4">
{error}
</div>
);
}
if (cafes.length === 0) {
return (
<div className="text-center mt-20 text-gray-500">
هیچ کافهای یافت نشد
</div>
);
}
return (
<>
{/* جدول دسکتاپ */}
<div className="hidden lg:block mt-10 overflow-x-auto">
<table className="w-full border-collapse">
<thead>
<tr className="bg-[#EFEEEE]">
<th className="px-4 py-3 text-right text-[#402E32] font-medium text-sm">لوگو</th>
<th className="px-4 py-3 text-right text-[#402E32] font-medium text-sm">اسم</th>
<th className="px-4 py-3 text-right text-[#402E32] font-medium text-sm">آدرس</th>
<th className="px-4 py-3 text-right text-[#402E32] font-medium text-sm">ریتینگ</th>
<th className="px-4 py-3 text-right text-[#402E32] font-medium text-sm">ساعت کاری</th>
<th className="px-4 py-3 text-right text-[#402E32] font-medium text-sm">ادیت</th>
</tr>
</thead>
<tbody>
{cafes.map((cafe) => (
<tr key={cafe._id} className="border-b border-[#EFEEEE] hover:bg-gray-50 transition-colors">
<td className="px-4 py-4 text-right">
<img
src={cafe.photo || Pic1}
alt={cafe.Name}
className="w-10 h-10 rounded-full object-cover"
/>
</td>
<td className="px-4 py-4 text-right text-[#402E32] font-medium text-sm whitespace-nowrap">
{cafe.Name}
</td>
<td className="px-4 py-4 text-right text-[#402E32] text-sm max-w-xs overflow-hidden text-ellipsis">
{cafe.address}
</td>
<td className="px-4 py-4 text-right">
<div className="flex items-center gap-2">
<img src={Star1} alt="rating" className="w-5 h-5" />
<span className="text-[#402E32] text-sm">{cafe.rating || 0}</span>
</div>
</td>
<td className="px-4 py-4 text-right">
<div className="flex items-center gap-2">
<img src={Group} alt="time" className="w-5 h-5" />
<span className="text-[#402E32] text-sm whitespace-nowrap">
{cafe.openinghour || "نامشخص"}
</span>
</div>
</td>
<td className="px-4 py-4 text-center">
<Link
to={`/edit-cafe/${cafe._id}`}
className="inline-flex items-center justify-center gap-2 border-2 border-[#BB8F70] px-6 py-2 rounded-3xl text-sm font-light text-[#402E32] hover:bg-[#7F4629] hover:text-white hover:border-[#7F4629] transition-all duration-300"
>
<span>ادیت</span>
<BiEdit className="w-4 h-4" />
</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* کارت موبایل */}
<div className="lg:hidden mt-6 space-y-4">
{cafes.map((cafe) => (
<div key={cafe._id} className="bg-white border-2 border-[#8b8886] rounded-xl p-4">
<div className="flex items-start gap-4 mb-4">
<img
src={cafe.photo || Pic1}
alt={cafe.Name}
className="w-16 h-16 rounded-full object-cover flex-shrink-0"
/>
<div className="flex-1 min-w-0">
<h3 className="font-bold text-[#402E32] mb-1 text-sm">{cafe.Name}</h3>
<p className="text-xs text-gray-600 truncate">{cafe.address}</p>
</div>
</div>
<div className="flex items-center justify-between mb-4 text-xs gap-4">
<div className="flex items-center gap-1">
<img src={Star1} alt="rating" className="w-4 h-4" />
<span className="text-[#402E32]">{cafe.rating || 0}</span>
</div>
<div className="flex items-center gap-1">
<img src={Group} alt="time" className="w-4 h-4" />
<span className="text-[#402E32]">{cafe.openinghour || "نامشخص"}</span>
</div>
</div>
<Link
to={`/edit-cafe/${cafe._id}`}
className="flex justify-center items-center gap-2 w-full border-2 border-[#BB8F70] py-2 rounded-full text-sm font-medium text-[#402E32] hover:bg-[#7F4629] hover:text-white hover:border-[#7F4629] transition-all duration-300"
>
<span>ادیت کافه</span>
<BiEdit className="w-4 h-4" />
</Link>
</div>
))}
</div>
</>
);
};
return (
<section dir="rtl" className="w-full pt-24 max-w-full overflow-x-hidden">
{/* بخش دکمه اضافه کردن */}
<div className="flex items-center justify-between mb-8">
<button className="flex items-center justify-center gap-3 px-6 py-3 bg-[#7F4629] text-white rounded-3xl text-sm lg:text-base font-medium hover:bg-amber-950 transition-all duration-300 cursor-pointer">
<span>افزودن شعبه جدید</span>
<img src={Vector9} alt="افزودن" className="w-5 h-5" />
</button>
</div>
{/* عنوان */}
<h1 className="text-[#402E32] font-bold text-lg lg:text-xl mb-6">کافه های شما</h1>
{/* محتوای اصلی */}
{renderContent()}
</section>
);
};
export default CafeManagement;

View File

@ -1,405 +0,0 @@
import React, { useState, useEffect } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { GrLocation } from "react-icons/gr";
import { BiEdit } from "react-icons/bi";
import { FaRegStar } from "react-icons/fa";
import { IoMdCheckmark, IoMdClose } from "react-icons/io";
// Assets
import Bg1 from "../../assets/icons/bg1.svg";
import Vector9 from "../../assets/icons/Vector9.svg";
import Vector11 from "../../assets/icons/Vector11.svg";
import Vector12 from "../../assets/icons/Vector12.svg";
import Vector13 from "../../assets/icons/Vector13.svg";
import Vector14 from "../../assets/icons/Vector14.svg";
import Vector15 from "../../assets/icons/Vector15.svg";
import Vector16 from "../../assets/icons/Vector16.svg";
import Coffee1 from "../../assets/icons/coffee1.svg";
import Coffee2 from "../../assets/icons/coffee2.svg";
import Coffee3 from "../../assets/icons/coffee3.svg";
import Sperso from "../../assets/icons/sperso.svg";
import Edit from "../../assets/icons/edit.svg";
// Services
import cafeService from "../../services/cafe";
// Constants
const DEFAULT_CATEGORIES = [
"نوشیدنی سرد",
"نوشیدنی گرم",
"کیک و دسر",
"صبحانه",
"ساندویچ و برگر",
"سالاد و پیش غذا",
];
const CAFE_FEATURES = [
{ icon: Coffee3, label: "منو کافه:", width: "lg:w-[140px]" },
{ icon: Vector15, label: "ساعت کاری:", value: "23 - 8" },
{ icon: Vector14, label: "رزرو :", value: "رزرو آنلاین" },
{ icon: Vector11, label: "موسیقی :", value: "موسیقی زنده آخر هفته" },
{ icon: Vector13, label: "پارکینگ :", value: "عمومی" },
{ icon: Vector12, label: "دسترسی آسان :", value: "مناسب افراد ناتوان" },
];
const CAFE_PRODUCTS = [
{
name: "اسپرسو100%",
price: "118.000",
image: Sperso,
description: "45 میلی لیتر، قهوه، 100% عربیکا، دم شده با دستگاه اسپرسو ساز، به همراه یک عدد آب معدنی مینی",
},
{
name: "کارامل ماکیاتو",
price: "149.000",
image: Coffee1,
description: "220 میلی لیتر، 2 شات اسپرسو 30% روبوستا، 70% عربیکا، یک لکه فوم شیر، سیروپ کارامل",
},
{
name: "اسپرسو آفوگاتو",
price: "118.000",
image: Coffee2,
description: "اسپرسو، یک اسکوپ بستنی وانیلی",
},
];
export default function EditCafe() {
const { id } = useParams();
const navigate = useNavigate();
// State Management
const [cafeData, setCafeData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [categories, setCategories] = useState(() => {
const saved = localStorage.getItem("cafeCategories");
return saved ? JSON.parse(saved) : DEFAULT_CATEGORIES;
});
const [isEditMode, setIsEditMode] = useState(false);
const [isAddingCategory, setIsAddingCategory] = useState(false);
const [newCategory, setNewCategory] = useState("");
const [editingIndex, setEditingIndex] = useState(null);
const [editValue, setEditValue] = useState("");
// Effects
useEffect(() => {
fetchCafeData();
}, [id]);
useEffect(() => {
localStorage.setItem("cafeCategories", JSON.stringify(categories));
}, [categories]);
// API Calls
const fetchCafeData = async () => {
setLoading(true);
setError("");
try {
const res = await cafeService.getCafeList();
if (res.data.success && res.data.data) {
const cafe = res.data.data.find((c) => c._id === id);
if (cafe) {
setCafeData(cafe);
console.log("✅ Cafe data loaded:", cafe);
} else {
setError("کافه مورد نظر یافت نشد");
}
} else {
setError("داده‌های کافه معتبر نیست");
}
} catch (error) {
console.error("❌ Error loading cafe:", error);
const errorMessage =
error.response?.data?.message ||
(error.request ? "خطا در برقراری ارتباط با سرور" : "خطای نامشخص رخ داده است");
setError(errorMessage);
} finally {
setLoading(false);
}
};
// Category Handlers
const handleAddCategory = () => {
if (newCategory.trim()) {
setCategories([...categories, newCategory.trim()]);
setNewCategory("");
setIsAddingCategory(false);
setTimeout(() => {
const scrollContainer = document.querySelector(".categories-scroll");
if (scrollContainer) {
scrollContainer.scrollLeft = scrollContainer.scrollWidth;
}
}, 0);
}
};
const handleDeleteCategory = (index) => {
setCategories(categories.filter((_, i) => i !== index));
};
const handleEditCategory = (index) => {
setEditingIndex(index);
setEditValue(categories[index]);
};
const handleSaveCategory = () => {
if (editValue.trim()) {
const newCategories = [...categories];
newCategories[editingIndex] = editValue.trim();
setCategories(newCategories);
setEditingIndex(null);
setEditValue("");
}
};
const handleCancelEdit = () => {
setEditingIndex(null);
setEditValue("");
};
// Render States
if (loading) {
return (
<div className="flex justify-center items-center h-screen">
<div className="text-[#7F4629] text-2xl font-bold">در حال بارگذاری...</div>
</div>
);
}
if (error) {
return (
<div className="p-6">
<div className="bg-red-50 border-2 border-red-300 text-red-700 px-4 py-3 rounded-xl">
{error}
</div>
</div>
);
}
if (!cafeData) {
return (
<div className="p-6 text-center text-gray-500">
اطلاعات کافه یافت نشد
</div>
);
}
return (
<section dir="rtl" className="w-full overflow-x-hidden">
<style>{`
.scrollbar-hide::-webkit-scrollbar { display: none; }
.scrollbar-hide { -ms-overflow-style: none; scrollbar-width: none; }
`}</style>
{/* Header */}
<h1 className="text-[#402E32] font-bold text-lg lg:text-xl mb-6">ادیت {cafeData.Name}</h1>
{/* Main Container */}
<div className="border-2 mt-1 border-[#8b8886] p-4 md:p-6 lg:p-10 rounded-2xl">
{/* Cafe Info Section */}
<div className="flex flex-col lg:flex-row gap-6 mb-8">
{/* Image */}
<div className="flex-shrink-0">
<img
src={cafeData.photo || Bg1}
alt={cafeData.Name}
className="w-full lg:w-[300px] xl:w-[400px] rounded-lg object-cover"
/>
</div>
{/* Info */}
<div className="flex-1">
{/* Action Buttons */}
<div className="flex gap-3 justify-end mb-6">
<button
onClick={() => navigate("/cafe-management")}
className="px-4 md:px-6 py-2 border-2 border-[#bb8f70] text-[#402e32] rounded-3xl hover:bg-[#7f4629] hover:text-white hover:border-[#7f4629] transition-all duration-300 text-sm md:text-base font-medium"
>
انصراف
</button>
<button className="px-4 md:px-6 py-2 bg-[#7f4629] text-white rounded-3xl hover:bg-[#5f494f] transition-all duration-300 text-sm md:text-base font-medium">
تایید
</button>
</div>
{/* Basic Info */}
<div className="space-y-4">
<div>
<h2 className="font-bold text-lg text-[#402E32] mb-2">{cafeData.Name}</h2>
<div className="w-16 h-1 bg-[#80931e] rounded-full"></div>
</div>
<div className="flex items-center gap-3 text-[#402E32]">
<GrLocation className="text-lg flex-shrink-0" />
<span className="text-sm md:text-base">{cafeData.address || "آدرس موجود نیست"}</span>
</div>
<div className="flex items-center gap-3 text-[#402E32]">
<FaRegStar className="text-lg flex-shrink-0" />
<span className="text-sm md:text-base">{cafeData.rating || 0}</span>
</div>
<div>
<h3 className="font-bold text-[#402E32] mb-2">درباره کافه</h3>
<p className="text-sm md:text-base text-[#555]">
{cafeData.description || "توضیحاتی برای این کافه وجود ندارد."}
</p>
</div>
</div>
</div>
</div>
{/* Features Section */}
<div className="mb-8">
<h2 className="font-bold text-[#402E32] mb-4 text-base md:text-lg">ویژگی ها</h2>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
{CAFE_FEATURES.map((feature, idx) => (
<div
key={idx}
className="bg-[#e1d5c2] p-4 rounded-2xl flex flex-col items-center gap-3 text-center transform hover:-translate-y-1 transition-all duration-300 hover:shadow-md"
>
<img src={feature.icon} alt={feature.label} className="w-6 h-6" />
<span className="text-[#402E32] font-medium text-xs md:text-sm whitespace-normal">
{feature.label}
</span>
{feature.value && (
<span className="text-[#402E32] text-xs">{feature.value}</span>
)}
</div>
))}
<button className="bg-[#5e5450] p-4 rounded-2xl flex items-center justify-center transform hover:-translate-y-1 transition-all duration-300 hover:shadow-md">
<img src={Vector9} alt="افزودن" className="w-6 h-6" />
</button>
</div>
</div>
{/* Categories Section */}
<div className="mb-8">
<div className="flex items-center justify-between mb-4">
<h2 className="font-bold text-[#402E32] text-base md:text-lg">دستهبندیها</h2>
<button
onClick={() => setIsEditMode(!isEditMode)}
className="flex items-center gap-2 text-[#7f4629] hover:text-[#5f494f] transition-colors"
title={isEditMode ? "خروج از حالت ویرایش" : "ویرایش دسته‌بندی‌ها"}
>
<img src={Vector16} alt="ویرایش" className="w-5 h-5" />
<span className="text-sm md:text-base">{isEditMode ? "تمام" : "ویرایش"}</span>
</button>
</div>
<div className="categories-scroll flex gap-3 md:gap-4 overflow-x-auto scrollbar-hide pb-2">
{categories.map((category, idx) => (
<div key={idx} className="flex-shrink-0">
{isEditMode && editingIndex === idx ? (
<div className="flex flex-col gap-2">
<div className="flex gap-1">
<IoMdCheckmark
onClick={handleSaveCategory}
className="text-green-600 cursor-pointer hover:text-green-800 w-5 h-5"
title="ذخیره"
/>
<IoMdClose
onClick={handleCancelEdit}
className="text-red-600 cursor-pointer hover:text-red-800 w-5 h-5"
title="لغو"
/>
</div>
<input
type="text"
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
className="border-2 border-[#bb8f70] rounded-lg px-2 py-1 text-xs md:text-sm focus:outline-none focus:border-[#7f4629] min-w-[100px]"
autoFocus
/>
</div>
) : (
<div className={`flex items-center gap-2 px-3 py-2 rounded-lg text-[#402E32] text-xs md:text-sm whitespace-nowrap ${
isEditMode
? "bg-gray-100 hover:bg-gray-200"
: "bg-[#e1d5c2]"
} transition-colors`}>
<span>{category}</span>
{isEditMode && (
<>
<BiEdit
onClick={() => handleEditCategory(idx)}
className="text-[#7f4629] cursor-pointer hover:text-[#5f494f] w-4 h-4 ml-1"
title="ویرایش"
/>
<IoMdClose
onClick={() => handleDeleteCategory(idx)}
className="text-white bg-[#a79fa1] rounded cursor-pointer hover:bg-[#9a8f91] w-4 h-4"
title="حذف"
/>
</>
)}
</div>
)}
</div>
))}
{isEditMode && !isAddingCategory && (
<button
onClick={() => setIsAddingCategory(true)}
className="flex-shrink-0 text-[#7f4629] hover:text-[#5f494f] font-bold text-sm md:text-base transition-colors whitespace-nowrap"
>
+ افزودن
</button>
)}
{isEditMode && isAddingCategory && (
<div className="flex-shrink-0 flex flex-col gap-2">
<div className="flex gap-1">
<IoMdCheckmark
onClick={handleAddCategory}
className="text-green-600 cursor-pointer hover:text-green-800 w-5 h-5"
title="ذخیره"
/>
<IoMdClose
onClick={() => {
setIsAddingCategory(false);
setNewCategory("");
}}
className="text-red-600 cursor-pointer hover:text-red-800 w-5 h-5"
title="لغو"
/>
</div>
<input
type="text"
value={newCategory}
onChange={(e) => setNewCategory(e.target.value)}
placeholder="دسته جدید"
className="border-2 border-[#bb8f70] rounded-lg px-2 py-1 text-xs md:text-sm focus:outline-none focus:border-[#7f4629] min-w-[100px]"
autoFocus
/>
</div>
)}
</div>
</div>
{/* Products Section */}
<div>
<h2 className="font-bold text-[#402E32] mb-4 text-base md:text-lg">محصولات</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-6">
{CAFE_PRODUCTS.map((product, idx) => (
<div
key={idx}
className="bg-white border border-gray-200 rounded-xl p-4 transform hover:-translate-y-1 transition-all duration-300 hover:shadow-md"
>
<h3 className="font-bold text-[#402E32] text-sm md:text-base mb-3">{product.name}</h3>
<img src={product.image} alt={product.name} className="w-full h-40 object-cover rounded-lg mb-3" />
<div className="flex justify-between items-center mb-3 text-sm">
<span className="text-[#66585b]">قیمت:</span>
<span className="font-medium text-[#402E32]">{product.price}</span>
</div>
<p className="text-xs md:text-sm text-[#66585b] line-clamp-3">
{product.description}
</p>
</div>
))}
</div>
</div>
</div>
</section>
);
}

View File

@ -1,15 +0,0 @@
import React from "react";
import { useProfile } from "../../hooks/useProfile";
export default function Dashboard() {
useProfile();
return (
<section dir="rtl" className="w-full">
<h1 className="text-2xl font-bold text-[#402E32] mb-6">صفحه داشبورد</h1>
<div className="bg-white border-2 border-[#8b8886] rounded-2xl p-6 md:p-8">
<p className="text-gray-600">محتوای داشبورد بهزودی اضافه خواهد شد</p>
</div>
</section>
);
}

View File

@ -1,179 +0,0 @@
import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
import { useDispatch } from "react-redux";
import { FiLock } from "react-icons/fi";
import { FaRegUser } from "react-icons/fa6";
// Assets
import Loginpic from "../../assets/image/loginpic.jpg";
import LogoDM from "../../assets/icons/LogoDM.svg";
// Redux
import { setToken } from "../../redux/slices/authSlice";
import { setProfile } from "../../redux/slices/profileSlice";
// Services
import authService from "../../services/auth";
export default function Login() {
const navigate = useNavigate();
const dispatch = useDispatch();
// State Management
const [userName, setUserName] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
// Handle Login
const handleLogin = async (e) => {
e.preventDefault();
setError("");
setLoading(true);
try {
const res = await authService.login({ userName, password });
if (res.data.success && res.data.data?.tokens?.accessToken) {
dispatch(setToken(res.data.data.tokens.accessToken));
dispatch(setProfile(res.data.data.admin));
console.log("✅ Login successful:", res.data.data.admin);
navigate("/dashboard");
} else {
setError("اطلاعات دریافتی معتبر نیست");
}
} catch (error) {
console.error("❌ Login error:", error);
const errorMessage =
error.response?.data?.message ||
(error.request ? "خطا در برقراری ارتباط با سرور" : "خطای نامشخص رخ داده است");
setError(errorMessage);
} finally {
setLoading(false);
}
};
return (
<div className="relative min-h-screen" dir="rtl">
{/* Header */}
<header className="fixed top-0 left-0 right-0 bg-white z-10 shadow-sm">
<div className="flex items-center justify-between px-6 md:px-8 lg:px-12 py-4">
{/* Left Section */}
<div className="flex items-center gap-4">
<img src={LogoDM} alt="Logo" className="h-10 w-10 md:h-12 md:w-12" />
<div className="relative">
<select className="appearance-none px-4 md:px-6 py-2 pr-4 pl-10 bg-white border-2 border-gray-300 rounded-xl text-sm focus:outline-none focus:border-[#7f4629] transition-colors cursor-pointer">
<option>شهر</option>
<option>تهران</option>
<option>اصفهان</option>
<option>شیراز</option>
<option>مشهد</option>
</select>
<svg className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500 pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</div>
</div>
{/* Center Links (Hidden on Mobile) */}
<nav className="hidden md:flex items-center gap-8 font-bold text-[#402E32]">
<a href="#" className="hover:text-[#7f4629] transition-colors text-sm">خانه</a>
<a href="#" className="hover:text-[#7f4629] transition-colors text-sm">دسته بندی</a>
<a href="#" className="hover:text-[#7f4629] transition-colors text-sm">تماس با ما</a>
<a href="#" className="hover:text-[#7f4629] transition-colors text-sm">درباره ما</a>
</nav>
{/* Right Section */}
<button className="px-6 md:px-10 py-2 bg-[#7f4629] text-white rounded-full text-sm md:text-base font-medium hover:bg-[#5f494f] transition-colors">
ثبت نام
</button>
</div>
</header>
{/* Main Content */}
<div className="flex min-h-screen pt-20">
{/* Left Side - Login Form */}
<div className="w-full md:w-1/2 flex items-start justify-center md:justify-start bg-[#f5f0e8] px-4 md:px-12 lg:px-24 py-8 md:py-16">
<div className="w-full max-w-md">
{/* Breadcrumb */}
<div className="flex items-center gap-2 text-xs md:text-sm text-gray-500 mb-8">
<span>ورود</span>
<span className="font-bold text-lg">&gt;</span>
<span>خانه</span>
</div>
{/* Header */}
<div className="mb-8">
<h1 className="text-2xl md:text-3xl font-bold text-[#402E32] mb-2">ورود ادمین</h1>
<p className="text-sm md:text-base text-gray-700">دسترسی ویژه برای مدیریت و گزارشها</p>
</div>
{/* Login Form */}
<form onSubmit={handleLogin} className="space-y-6">
{/* Error Message */}
{error && (
<div className="bg-red-50 border-2 border-red-300 text-red-700 px-4 py-3 rounded-xl text-sm">
{error}
</div>
)}
{/* Username Field */}
<div className="relative">
<label className="absolute -top-2.5 right-4 bg-[#f5f0e8] px-2 text-sm text-gray-600">نام کاربری</label>
<div className="relative">
<input
type="text"
value={userName}
onChange={(e) => setUserName(e.target.value)}
placeholder="userName"
className="w-full pl-12 pr-4 py-3 border-2 border-gray-300 rounded-xl focus:outline-none focus:border-[#7f4629] transition-colors text-sm"
dir="ltr"
required
/>
<FaRegUser className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
</div>
</div>
{/* Password Field */}
<div className="relative">
<label className="absolute -top-2.5 right-4 bg-[#f5f0e8] px-2 text-sm text-gray-600">رمز عبور</label>
<div className="relative">
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="password"
className="w-full pl-12 pr-4 py-3 border-2 border-gray-300 rounded-xl focus:outline-none focus:border-[#7f4629] transition-colors text-sm"
dir="ltr"
required
/>
<FiLock className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
</div>
</div>
{/* Forgot Password */}
<div className="text-right">
<a href="#" className="text-sm text-gray-500 hover:text-[#7f4629] transition-colors">
فراموشی رمز عبور؟
</a>
</div>
{/* Submit Button */}
<button
type="submit"
disabled={loading || !userName || !password}
className="w-full bg-[#7f4629] text-white py-3 rounded-full font-bold text-sm md:text-base hover:bg-[#5f494f] transition-all duration-300 shadow-lg hover:shadow-xl disabled:bg-gray-400 disabled:cursor-not-allowed"
>
{loading ? "در حال ورود..." : "ورود"}
</button>
</form>
</div>
</div>
{/* Right Side - Image (Hidden on Mobile) */}
<div className="hidden md:block w-1/2 relative overflow-hidden">
<img src={Loginpic} alt="Coffee workspace" className="w-full h-full object-cover" />
</div>
</div>
</div>
);
}

View File

@ -1,12 +0,0 @@
import React from "react";
export default function Stats() {
return (
<section dir="rtl" className="w-full">
<h1 className="text-2xl font-bold text-[#402E32] mb-6">آمار و تحلیل</h1>
<div className="bg-white border-2 border-[#8b8886] rounded-2xl p-6 md:p-8">
<p className="text-gray-600">محتوای آمار و تحلیل بهزودی اضافه خواهد شد</p>
</div>
</section>
);
}

View File

@ -1,26 +0,0 @@
import { createSlice } from "@reduxjs/toolkit";
const token = localStorage.getItem("token");
const authSlice = createSlice({
name: "auth",
initialState: {
token: token,
isAuthenticated: !!token,
},
reducers: {
setToken(state, action) {
state.token = action.payload;
state.isAuthenticated = true;
localStorage.setItem("token", action.payload);
},
clearToken(state) {
state.token = null;
state.isAuthenticated = false;
localStorage.removeItem("token");
},
},
});
export const { setToken, clearToken } = authSlice.actions;
export default authSlice.reducer;

View File

@ -1,20 +0,0 @@
import { createSlice } from "@reduxjs/toolkit";
const profileSlice = createSlice({
name: "profile",
initialState: {
profile: null,
},
reducers: {
setProfile(state, action) {
state.profile = action.payload;
},
clearProfile(state) {
state.profile = null;
},
},
});
export const { setProfile, clearProfile } = profileSlice.actions;
export default profileSlice.reducer;

View File

@ -1,15 +0,0 @@
import { Navigate } from "react-router-dom";
import { useSelector } from "react-redux";
import { useProfile } from "../../hooks/useProfile";
export default function ProtectedRoute({ children }) {
const isAuth = useSelector((state) => state.auth.isAuthenticated);
const token = localStorage.getItem("token");
useProfile(isAuth && token);
if (!isAuth || !token) {
return <Navigate to="/login" replace />;
}
return children;
}

View File

@ -1,28 +0,0 @@
import { Routes, Route, Navigate } from 'react-router-dom';
import ProtectedRoute from './ProtectedRoute/ProtectedRoute';
import Login from '../pages/Login/Login';
import Layout from '../components/Layout/Layout';
import Dashboard from '../pages/Dashboard/Dashboard';
import CafeManagement from '../pages/CafeManagement/CafeManagement';
import EditCafe from '../pages/CafeManagement/EditCafe';
import Stats from '../pages/Stats/Stats';
export default function AppRoutes() {
return (
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/" element={<ProtectedRoute> <Layout /> </ProtectedRoute>}>
{/* <Route path="/" element={ <Layout />}> */}
<Route path="dashboard" element={<Dashboard />} />
<Route path="cafe-management" element={<CafeManagement />} />
<Route path="edit-cafe/:id" element={<EditCafe />} />
<Route path="stats" element={<Stats />} />
<Route index element={<Navigate to="/dashboard" replace />} />
</Route>
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
);
}

View File

@ -1,15 +0,0 @@
import requests from "./api/base-api";
const authService = {
// register: (userData) => requests.post("/auth/register", userData),
// verifyOTP: (data) => requests.post("/auth/verify-otp", data),
login: (data) => requests.post("/admin/v1/login", data),
// forgotPassword: (data) => requests.post("/auth/forgot-password", data),
// resetPassword: (data) => requests.post("/auth/reset-password", data),
logout: () => requests.post("/auth/logout"),
};
export default authService;

View File

@ -1,8 +0,0 @@
import requests from "./api/base-api";
const cafeService = {
getCafeList: () => requests.get("/cafe/v1/get-cafe-list"),
editCafe: (id, cafeData) => requests.put(`/cafe/v1/get-cafe-profile-by-cafe/${id}`, cafeData),
};
export default cafeService;

View File

@ -1,7 +0,0 @@
import requests from "./api/base-api";
const dashboardService = {
getCafeList: () => requests.get("/cafe/v1/get-cafe-list"),
};
export default dashboardService;

View File

@ -1,10 +0,0 @@
import requests from "./api/base-api";
const profileService = {
getProfile: () => requests.get("/admin/v1/get-my-profile"),
deleteProfile: () => requests.delete("/admin/v1/delete-my-profile"),
// updateProfile: (profileData) => requests.put("/admin/v1/update-profile", profileData),
// changePassword: (passwordData) => requests.post("/admin/v1/change-password", passwordData),
};
export default profileService;

View File

@ -1,25 +0,0 @@
@import "tailwindcss";
@font-face {
font-family: "MyEstedad";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("/fonts/Estedad.ttf") format("truetype");
}
@theme {
--font-MyEstedad: "MyEstedad", sans-serif;
}
@layer base {
html {
direction: rtl;
}
body {
/* @apple font-MyEstedad; */
font-family: var(--font-MyEstedad);
direction: rtl;
}
}

View File

@ -1,9 +0,0 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
import svgr from "vite-plugin-svgr";
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss(), svgr()],
});