-
-
-
-
-

setIsEditMode(!isEditMode)}
- title={isEditMode ? "خروج از حالت ویرایش" : "ویرایش عنوانها"}
- />
-
عنوان
+ {!isEditMode ? (
+ <>
+ {categories.map((category, index) => (
+
{category}
+ ))}
+ >
+ ) : (
+ <>
+ {categories.map((category, index) => (
+
+ {editingIndex === index ? (
+ <>
+
+
+
+
+
setEditValue(e.target.value)}
+ className="border-2 border-[#bb8f70] rounded-lg px-2 py-1 text-sm focus:outline-none focus:border-[#7f4629] w-[100px] text-center"
+ autoFocus
+ />
+ >
+ ) : (
+ <>
+
+ handleStartEdit(index)}
+ title="ویرایش"
+ />
+ {category}
+ handleDeleteCategory(index)}
+ title="حذف"
+ />
+
+ >
+ )}
+
+ ))}
+ >
+ )}
-
- {!isEditMode ? (
- <>
- {categories.map((category, index) => (
-
{category}
- ))}
- >
- ) : (
- <>
- {categories.map((category, index) => (
-
- {editingIndex === index ? (
- <>
-
-
-
-
-
setEditValue(e.target.value)}
- className="border-2 border-[#bb8f70] rounded-lg px-2 py-1 text-sm focus:outline-none focus:border-[#7f4629] w-[100px] text-center"
- autoFocus
- />
- >
- ) : (
- <>
-
- handleStartEdit(index)}
- title="ویرایش"
- />
- {category}
- handleDeleteCategory(index)}
- title="حذف"
- />
-
- >
- )}
-
- ))}
- >
- )}
-
- {isEditMode && (
-
- {!isAdding ? (
-
- ) : (
-
-
-
-
{
- setIsAdding(false);
- setNewCategory("");
- }}
- title="لغو"
+ {isEditMode && (
+
+ {!isAdding ? (
+
+ ) : (
+
+
+
+ {
+ setIsAdding(false);
+ setNewCategory("");
+ }}
+ title="لغو"
+ />
+
+
setNewCategory(e.target.value)}
+ placeholder="عنوان جدید"
+ className="border-2 border-[#bb8f70] rounded-lg px-2 py-1 text-sm focus:outline-none focus:border-[#7f4629] w-[100px] text-center"
+ autoFocus
/>
-
setNewCategory(e.target.value)}
- placeholder="عنوان جدید"
- className="border-2 border-[#bb8f70] rounded-lg px-2 py-1 text-sm focus:outline-none focus:border-[#7f4629] w-[100px] text-center"
- autoFocus
- />
-
- )}
+ )}
+
+ )}
+
+
+
+
+
+

+
افزودن زیر عنوان
+
+
+
+

+

+
قهوه ها
+
+
+
+
+

+
آیتم
+
+
+
+
+
اسپرسو100%
+

+
+ قیمت
+ 118.000
- )}
-
-
-
-
-
-

-
افزودن زیر عنوان
-
-
-
-

-

-
قهوه ها
-
-
-
-
-

-
آیتم
-
-
-
-
-
اسپرسو100%
-

-
- قیمت
- 118.000
+
+ 45 میلی لیتر، قهوه، 100% عربیکا، دم شده با دستگاه اسپرسو ساز،
+ به همراه یک عدد آب معدنی مینی
+
+
-
- 45 میلی لیتر، قهوه، 100% عربیکا، دم شده با دستگاه اسپرسو ساز،
- به همراه یک عدد آب معدنی مینی
-
-
-
-
-
- کارامل ماکیاتو
-
-

-
-
قیمت
-
149.000
+
+
+ کارامل ماکیاتو
+
+

+
+ قیمت
+ 149.000
+
+
+ 220 میلی لیتر، 2 شات اسپرسو 30% روبوستا، 70% عربیکا، یک لکه
+ فوم شیر، سیروپ کارامل
+
+
-
- 220 میلی لیتر، 2 شات اسپرسو 30% روبوستا، 70% عربیکا، یک لکه
- فوم شیر، سیروپ کارامل
-
-
-
-
-
- اسپرسو آفوگاتو
-
-

-
-
قیمت
-
118.000
+
+
+ اسپرسو آفوگاتو
+
+

+
+ قیمت
+ 118.000
+
+
+ اسپرسو، یک اسکوپ بستنی وانیلی
+
+
-
- اسپرسو، یک اسکوپ بستنی وانیلی
-
-
-
-
- >
- );
-};
-
-export default EditCafe;
\ No newline at end of file
+
+ >
+ );
+ }
diff --git a/src/pages/Dashboard/Dashboard.jsx b/src/pages/Dashboard/Dashboard.jsx
index 809df27..5be2154 100644
--- a/src/pages/Dashboard/Dashboard.jsx
+++ b/src/pages/Dashboard/Dashboard.jsx
@@ -1,4 +1,9 @@
+import {useProfile} from "../../hooks/useProfile";
+
const Dashboard = () => {
+ useProfile();
+
+
return (
صفحه داشبورد
diff --git a/src/pages/Login/Login.jsx b/src/pages/Login/Login.jsx
index 0c12f8d..cba36af 100644
--- a/src/pages/Login/Login.jsx
+++ b/src/pages/Login/Login.jsx
@@ -1,13 +1,14 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState } from 'react';
import Loginpic from '../../assets/image/loginpic.jpg';
-import { FiLock } from 'react-icons/fi';
+import { FiLock } from 'react-icons/fi';
import { FaRegUser } from "react-icons/fa6";
-import axios from 'axios';
import LogoDM from "../../assets/icons/LogoDM.svg";
import { useNavigate, Navigate } from 'react-router-dom';
+import authService from '../../services/auth';
+import { useDispatch } from 'react-redux';
+import { setToken } from '../../redux/slices/authSlice';
+import { setProfile } from '../../redux/slices/profileSlice';
-// تنظیم base URL برای axios
-axios.defaults.baseURL = 'https://cafeju.maksiran.ir';
const Login = () => {
const [userName, setUserName] = useState('');
@@ -15,17 +16,10 @@ const Login = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const navigate = useNavigate();
+ const dispatch = useDispatch();
- // چک کردن token موقع لود شدن صفحه
- useEffect(() => {
- const token = localStorage.getItem('token');
- // اگر قبلاً لاگین کرده، به داشبورد هدایت میشه
- if (token) {
- navigate('/dashboard');
- }
- }, [navigate]);
- const handleLogin = (e) => {
+ const handleLogin = async (e) => {
e.preventDefault();
setError('');
setLoading(true);
@@ -35,50 +29,31 @@ const Login = () => {
password: password
};
- console.log('Sending login request with:', loginData);
+ try {
+ const res = await authService.login(loginData);
+ if (res.data.success && res.data.data && res.data.data.tokens.accessToken) {
+ dispatch(setToken(res.data.data.tokens.accessToken));
+ navigate('/dashboard');
+ dispatch(setProfile(res.data.data.admin));
+ console.log('login:', res.data.data.admin);
+ }
- axios.post('/api/admin/v1/login', loginData)
- .then((response) => {
- setLoading(false);
- console.log('✅ Login successful!');
- console.log('Response:', response.data);
+ console.log('Response:', res);
+ } catch (error) {
+ setLoading(false);
+ console.error('❌ Login error:', error);
+ // if (error.response) {
+ // setError(error.response.data.message || error.response.data.en_message || 'نام کاربری یا رمز عبور اشتباه است');
+ // } else if (error.request) {
+ // setError('خطا در برقراری ارتباط با سرور');
+ // } else {
+ // console.error('Error setting up request');
+ // setError('خطای نامشخص رخ داده است');
+ // }
+ } finally {
+ setLoading(false);
+ }
- if (response.data.success && response.data.data && response.data.data.tokens) {
- const accessToken = response.data.data.tokens.accessToken;
- const refreshToken = response.data.data.tokens.refreshToken;
-
- localStorage.setItem('accessToken', accessToken);
- localStorage.setItem('refreshToken', refreshToken);
- localStorage.setItem('token', accessToken);
-
- localStorage.setItem('adminInfo', JSON.stringify(response.data.data.admin));
-
- console.log('Tokens saved to localStorage');
- console.log('Admin info:', response.data.data.admin);
-
- console.log('Navigating to dashboard...');
- navigate('/dashboard');
- } else {
- console.error('❌ Invalid response format');
- setError('پاسخ سرور معتبر نیست');
- }
- })
- .catch((error) => {
- setLoading(false);
- console.error('❌ Login error:', error);
-
- if (error.response) {
- console.error('Response status:', error.response.status);
- console.error('Response data:', error.response.data);
- setError(error.response.data.message || error.response.data.en_message || 'نام کاربری یا رمز عبور اشتباه است');
- } else if (error.request) {
- console.error('No response received from server');
- setError('خطا در برقراری ارتباط با سرور');
- } else {
- console.error('Error setting up request');
- setError('خطای نامشخص رخ داده است');
- }
- });
};
return (
@@ -222,18 +197,6 @@ const Login = () => {
);
};
-export const ProtectedRoute = ({ children }) => {
- const token = localStorage.getItem('token');
- console.log('ProtectedRoute - Token check:', token ? 'Token exists' : 'No token');
-
- if (!token) {
- console.log('ProtectedRoute - Redirecting to login');
- return ;
- }
-
- console.log('ProtectedRoute - Rendering protected content');
- return children;
-};
export default Login;
\ No newline at end of file
diff --git a/src/redux/slices/authSlice.js b/src/redux/slices/authSlice.js
new file mode 100644
index 0000000..b0beeb5
--- /dev/null
+++ b/src/redux/slices/authSlice.js
@@ -0,0 +1,26 @@
+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;
diff --git a/src/redux/slices/loadingSlice.js b/src/redux/slices/loadingSlice.js
new file mode 100644
index 0000000..42d5cbe
--- /dev/null
+++ b/src/redux/slices/loadingSlice.js
@@ -0,0 +1,13 @@
+import { createSlice } from "@reduxjs/toolkit";
+
+const loadingSlice = createSlice({
+ name: "loading",
+ initialState: false,
+ reducers: {
+ showLoading: () => true,
+ hideLoading: () => false,
+ },
+});
+
+export const { showLoading, hideLoading } = loadingSlice.actions;
+export default loadingSlice.reducer;
diff --git a/src/redux/slices/profileSlice.js b/src/redux/slices/profileSlice.js
new file mode 100644
index 0000000..0647fba
--- /dev/null
+++ b/src/redux/slices/profileSlice.js
@@ -0,0 +1,20 @@
+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;
\ No newline at end of file
diff --git a/src/redux/slices/toastSlice.js b/src/redux/slices/toastSlice.js
new file mode 100644
index 0000000..3d8336d
--- /dev/null
+++ b/src/redux/slices/toastSlice.js
@@ -0,0 +1,26 @@
+import { createSlice } from "@reduxjs/toolkit";
+
+const toastSlice = createSlice({
+ name: "toast",
+ initialState: {
+ message: "",
+ type: "success", // success | error | info
+ visible: false,
+ },
+ reducers: {
+ showToast(state, action) {
+ const { message, type } = action.payload;
+ state.message = message;
+ state.type = type || "success";
+ state.visible = true;
+ },
+ hideToast(state) {
+ state.visible = false;
+ state.message = "";
+ state.type = "success";
+ },
+ },
+});
+
+export const { showToast, hideToast } = toastSlice.actions;
+export default toastSlice.reducer;
diff --git a/src/redux/store.js b/src/redux/store.js
new file mode 100644
index 0000000..805b369
--- /dev/null
+++ b/src/redux/store.js
@@ -0,0 +1,14 @@
+import { configureStore } from "@reduxjs/toolkit";
+import authReducer from "./slices/authSlice";
+import loadingReducer from "./slices/loadingSlice";
+import toastReducer from "./slices/toastSlice";
+import profileReducer from "./slices/profileSlice";
+
+export const store = configureStore({
+ reducer: {
+ auth: authReducer,
+ loading: loadingReducer,
+ toast: toastReducer,
+ profile: profileReducer,
+ },
+});
diff --git a/src/routes/ProtectedRoute/ProtectedRoute.jsx b/src/routes/ProtectedRoute/ProtectedRoute.jsx
new file mode 100644
index 0000000..513a6b6
--- /dev/null
+++ b/src/routes/ProtectedRoute/ProtectedRoute.jsx
@@ -0,0 +1,15 @@
+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 ;
+ }
+ return children;
+}
diff --git a/src/routes/Routes.jsx b/src/routes/Routes.jsx
new file mode 100644
index 0000000..6a4e07d
--- /dev/null
+++ b/src/routes/Routes.jsx
@@ -0,0 +1,28 @@
+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 (
+
+ } />
+
+ }>
+ {/* }> */}
+ } />
+ } />
+ } />
+ } />
+
+ } />
+
+
+ } />
+
+ );
+}
\ No newline at end of file
diff --git a/src/services/api/base-api.js b/src/services/api/base-api.js
new file mode 100644
index 0000000..a3af15e
--- /dev/null
+++ b/src/services/api/base-api.js
@@ -0,0 +1,115 @@
+import axios from "axios";
+import { store } from "../../redux/store";
+import { showLoading, hideLoading } from "../../redux/slices/loadingSlice";
+import { clearToken } from "../../redux/slices/authSlice";
+import { showToast } from "../../redux/slices/toastSlice";
+import { toastIgnore } from "./toastIgnore";
+
+const BASE_URL = 'https://cafeju.maksiran.ir/api';
+const TIMEOUT = 600000;
+
+axios.defaults.baseURL = BASE_URL;
+axios.defaults.timeout = TIMEOUT;
+
+// Helpers
+const isMutatingMethod = (method) => ["post", "put", "delete", "patch"].includes(method?.toLowerCase());
+
+const extractMessage = (obj) =>
+ obj?.response?.data?.data?.message ||
+ obj?.data?.data?.message ||
+ obj?.message ||
+ obj?.data?.message ||
+ obj?.response?.data?.message ||
+ obj?.response?.message ||
+ "خطایی رخ داده است!";
+
+const getErrorMessageByStatus = (status, data) => {
+ switch (status) {
+ case 400: return data?.message || "درخواست نامعتبر است";
+ case 401: return data?.message || "نشست شما منقضی شده، مجدد وارد شوید";
+ case 403: return data?.message || "دسترسی شما محدود شده است";
+ case 404: return data?.message || "صفحه یا اطلاعات درخواستی یافت نشد";
+ case 422: return data?.message || "اطلاعات ارسالی نامعتبر است";
+ case 429: return data?.message || "تعداد درخواستها زیاد است، کمی صبر کنید";
+ case 500: return data?.message || "خطای داخلی سرور، لطفاً بعداً تلاش کنید";
+ case 502:
+ case 503: return data?.message || "سرور در حال حاضر در دسترس نیست";
+ case 504: return data?.message || "سرور پاسخ نداد، دوباره تلاش کنید";
+ default: return data?.message || `خطای غیرمنتظره (کد: ${status})`;
+ }
+};
+
+// Request Interceptor
+axios.interceptors.request.use(
+ (config) => {
+ const token = localStorage.getItem("token");
+ if (token) config.headers.Authorization = `Bearer ${token}`;
+ store.dispatch(showLoading());
+ return config;
+ },
+ (error) => {
+ store.dispatch(hideLoading());
+ return Promise.reject(error);
+ }
+);
+
+// Response Interceptor
+axios.interceptors.response.use(
+ (response) => {
+ try {
+ const method = response.config.method;
+ const url = response.config.url;
+ if (!toastIgnore.some(u => url.includes(u)) && isMutatingMethod(method)) {
+ const message = extractMessage(response) || "عملیات با موفقیت انجام شد";
+ store.dispatch(showToast({ type: "success", message }));
+ }
+ } catch (e) {
+ console.error("toast response error:", e);
+ } finally {
+ store.dispatch(hideLoading());
+ }
+ return response;
+ },
+ (error) => {
+ store.dispatch(hideLoading());
+
+ if (!error.response) {
+ const networkMessage = error.message.includes("Network Error")
+ ? "لطفا اتصال اینترنت خود را بررسی کنید"
+ : error.message.includes("timeout")
+ ? "مهلت زمانی درخواست تمام شد، دوباره تلاش کنید"
+ : "خطا در برقراری ارتباط با سرور";
+
+ store.dispatch(showToast({ message: networkMessage, type: "error" }));
+ return Promise.reject(error);
+ }
+
+ const { status, data } = error.response;
+ const url = error.config?.url || "";
+
+ 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);
+ }
+);
+
+// Request Wrappers
+const requests = {
+ get: (url) => axios.get(url),
+ getByParams: (url, params) => axios.get(url, { params }),
+ post: (url, body) => axios.post(url, body),
+ put: (url, body) => axios.put(url, body),
+ patch: (url, body) => axios.patch(url, body),
+ delete: (url) => axios.delete(url),
+ postFormData: (url, formData) =>
+ axios.post(url, formData, { headers: { "Content-Type": "multipart/form-data" } }),
+ putMedia: (url, body) =>
+ axios.put(url, body, { headers: { "Content-Type": "application/x-www-form-urlencoded" } }),
+};
+
+export default requests;
\ No newline at end of file
diff --git a/src/services/api/toastIgnore.js b/src/services/api/toastIgnore.js
new file mode 100644
index 0000000..8daf49c
--- /dev/null
+++ b/src/services/api/toastIgnore.js
@@ -0,0 +1,3 @@
+export const toastIgnore = [
+ // "/posts/like",
+];
\ No newline at end of file
diff --git a/src/services/auth.js b/src/services/auth.js
new file mode 100644
index 0000000..0165c1a
--- /dev/null
+++ b/src/services/auth.js
@@ -0,0 +1,15 @@
+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;
\ No newline at end of file
diff --git a/src/services/cafe.js b/src/services/cafe.js
new file mode 100644
index 0000000..ecde669
--- /dev/null
+++ b/src/services/cafe.js
@@ -0,0 +1,8 @@
+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;
\ No newline at end of file
diff --git a/src/services/dashboard.js b/src/services/dashboard.js
new file mode 100644
index 0000000..e03ca82
--- /dev/null
+++ b/src/services/dashboard.js
@@ -0,0 +1,7 @@
+import requests from "./api/base-api";
+
+const dashboardService = {
+ getCafeList: () => requests.get("/cafe/v1/get-cafe-list"),
+};
+
+export default dashboardService;
\ No newline at end of file
diff --git a/src/services/profile.js b/src/services/profile.js
new file mode 100644
index 0000000..0769d4a
--- /dev/null
+++ b/src/services/profile.js
@@ -0,0 +1,10 @@
+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;
\ No newline at end of file
diff --git a/src/styles/App.css b/src/styles/App.css
deleted file mode 100644
index 36813f5..0000000
--- a/src/styles/App.css
+++ /dev/null
@@ -1,112 +0,0 @@
-@tailwind utilities;
-
-/* Custom Breakpoints for Mobile-First Design */
-@layer base {
- /* Base styles - Mobile First (320px+) */
- * {
- box-sizing: border-box;
- margin: 0;
- padding: 0;
- }
-
- body {
- overflow-x: hidden;
- width: 100%;
- max-width: 100vw;
- }
-
- #root {
- width: 100%;
- min-height: 100vh;
- overflow-x: hidden;
- }
-}
-
-/* Custom Responsive Classes */
-@layer utilities {
- /* Hide scrollbar globally */
- .scrollbar-hide::-webkit-scrollbar {
- display: none;
- }
-
- .scrollbar-hide {
- -ms-overflow-style: none;
- scrollbar-width: none;
- }
-
- /* Prevent horizontal scroll */
- .no-scroll-x {
- overflow-x: hidden;
- max-width: 100vw;
- }
-}
-
-/* Responsive Container Sizes */
-@layer components {
- .container-responsive {
- width: 100%;
- margin: 0 auto;
- padding-left: 1rem;
- padding-right: 1rem;
- }
-
- /* iPhone SE and smaller (320px - 374px) */
- @media (min-width: 320px) {
- .container-responsive {
- max-width: 320px;
- }
- }
-
- /* iPhone 12/13 Mini (375px - 389px) */
- @media (min-width: 375px) {
- .container-responsive {
- max-width: 375px;
- }
- }
-
- /* iPhone 14 (390px - 429px) */
- @media (min-width: 390px) {
- .container-responsive {
- max-width: 390px;
- }
- }
-
- /* iPhone 14 Pro Max (430px - 639px) */
- @media (min-width: 430px) {
- .container-responsive {
- max-width: 430px;
- }
- }
-
- /* Small devices (640px - 767px) - Tailwind sm */
- @media (min-width: 640px) {
- .container-responsive {
- max-width: 640px;
- padding-left: 1.5rem;
- padding-right: 1.5rem;
- }
- }
-
- /* Medium devices (768px - 1023px) - Tailwind md */
- @media (min-width: 768px) {
- .container-responsive {
- max-width: 768px;
- padding-left: 2rem;
- padding-right: 2rem;
- }
- }
-
- /* Large devices (1024px+) - Tailwind lg */
- @media (min-width: 1024px) {
- .container-responsive {
- max-width: 1024px;
- }
- }
-
- /* Extra Large devices (1280px+) - Tailwind xl */
- @media (min-width: 1280px) {
- .container-responsive {
- max-width: 1280px;
- }
- }
-}
\ No newline at end of file