change project base
This commit is contained in:
parent
e72cd844f5
commit
5b494855c8
|
|
@ -8,11 +8,14 @@
|
||||||
"name": "my-project",
|
"name": "my-project",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@reduxjs/toolkit": "^2.11.2",
|
||||||
"@tailwindcss/vite": "^4.1.14",
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
|
"framer-motion": "^12.23.26",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
|
"react-redux": "^9.2.0",
|
||||||
"react-router-dom": "^7.9.4",
|
"react-router-dom": "^7.9.4",
|
||||||
"tailwind-scrollbar-hide": "^4.0.0",
|
"tailwind-scrollbar-hide": "^4.0.0",
|
||||||
"tailwindcss": "^4.1.14"
|
"tailwindcss": "^4.1.14"
|
||||||
|
|
@ -639,6 +642,32 @@
|
||||||
"url": "https://github.com/sponsors/Boshen"
|
"url": "https://github.com/sponsors/Boshen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@reduxjs/toolkit": {
|
||||||
|
"version": "2.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
|
||||||
|
"integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@standard-schema/spec": "^1.0.0",
|
||||||
|
"@standard-schema/utils": "^0.3.0",
|
||||||
|
"immer": "^11.0.0",
|
||||||
|
"redux": "^5.0.1",
|
||||||
|
"redux-thunk": "^3.1.0",
|
||||||
|
"reselect": "^5.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
|
||||||
|
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-redux": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rolldown/binding-android-arm64": {
|
"node_modules/@rolldown/binding-android-arm64": {
|
||||||
"version": "1.0.0-beta.41",
|
"version": "1.0.0-beta.41",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.41.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.41.tgz",
|
||||||
|
|
@ -893,6 +922,18 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@standard-schema/spec": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@standard-schema/utils": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@svgr/babel-plugin-add-jsx-attribute": {
|
"node_modules/@svgr/babel-plugin-add-jsx-attribute": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz",
|
||||||
|
|
@ -1681,7 +1722,7 @@
|
||||||
"version": "19.2.2",
|
"version": "19.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
|
||||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
|
|
@ -1697,6 +1738,12 @@
|
||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/use-sync-external-store": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||||
|
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@vitejs/plugin-react": {
|
"node_modules/@vitejs/plugin-react": {
|
||||||
"version": "5.0.4",
|
"version": "5.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.0.4.tgz",
|
||||||
|
|
@ -2053,7 +2100,7 @@
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
|
|
@ -2545,6 +2592,33 @@
|
||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/framer-motion": {
|
||||||
|
"version": "12.23.26",
|
||||||
|
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.26.tgz",
|
||||||
|
"integrity": "sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"motion-dom": "^12.23.23",
|
||||||
|
"motion-utils": "^12.23.6",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@emotion/is-prop-valid": "*",
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@emotion/is-prop-valid": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fsevents": {
|
"node_modules/fsevents": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
|
|
@ -2718,6 +2792,16 @@
|
||||||
"node": ">= 4"
|
"node": ">= 4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/immer": {
|
||||||
|
"version": "11.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/immer/-/immer-11.1.0.tgz",
|
||||||
|
"integrity": "sha512-dlzb07f5LDY+tzs+iLCSXV2yuhaYfezqyZQc+n6baLECWkOMEWxkECAOnXL0ba7lsA25fM9b2jtzpu/uxo1a7g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/immer"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/import-fresh": {
|
"node_modules/import-fresh": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||||
|
|
@ -3261,6 +3345,21 @@
|
||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/motion-dom": {
|
||||||
|
"version": "12.23.23",
|
||||||
|
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz",
|
||||||
|
"integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"motion-utils": "^12.23.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/motion-utils": {
|
||||||
|
"version": "12.23.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz",
|
||||||
|
"integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
|
@ -3525,6 +3624,29 @@
|
||||||
"react": "*"
|
"react": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-redux": {
|
||||||
|
"version": "9.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||||
|
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/use-sync-external-store": "^0.0.6",
|
||||||
|
"use-sync-external-store": "^1.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^18.2.25 || ^19",
|
||||||
|
"react": "^18.0 || ^19",
|
||||||
|
"redux": "^5.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"redux": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-refresh": {
|
"node_modules/react-refresh": {
|
||||||
"version": "0.17.0",
|
"version": "0.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
||||||
|
|
@ -3573,6 +3695,27 @@
|
||||||
"react-dom": ">=18"
|
"react-dom": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/redux": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/redux-thunk": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"redux": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/reselect": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/resolve-from": {
|
"node_modules/resolve-from": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||||
|
|
@ -3793,7 +3936,6 @@
|
||||||
"version": "2.8.1",
|
"version": "2.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
"node_modules/type-check": {
|
"node_modules/type-check": {
|
||||||
|
|
@ -3850,6 +3992,15 @@
|
||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/use-sync-external-store": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"name": "rolldown-vite",
|
"name": "rolldown-vite",
|
||||||
"version": "7.1.14",
|
"version": "7.1.14",
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,14 @@
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@reduxjs/toolkit": "^2.11.2",
|
||||||
"@tailwindcss/vite": "^4.1.14",
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
|
"framer-motion": "^12.23.26",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
|
"react-redux": "^9.2.0",
|
||||||
"react-router-dom": "^7.9.4",
|
"react-router-dom": "^7.9.4",
|
||||||
"tailwind-scrollbar-hide": "^4.0.0",
|
"tailwind-scrollbar-hide": "^4.0.0",
|
||||||
"tailwindcss": "^4.1.14"
|
"tailwindcss": "^4.1.14"
|
||||||
|
|
|
||||||
31
src/App.jsx
31
src/App.jsx
|
|
@ -1,35 +1,8 @@
|
||||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
import AppRoutes from "./routes/routes";
|
||||||
import Login, { ProtectedRoute } 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';
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<AppRoutes />
|
||||||
<Routes>
|
|
||||||
<Route path="/login" element={<Login />} />
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/"
|
|
||||||
element={
|
|
||||||
<ProtectedRoute>
|
|
||||||
<Layout />
|
|
||||||
</ProtectedRoute>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Route path="dashboard" element={<Dashboard />} />
|
|
||||||
<Route path="cafe-management" element={<CafeManagement />} />
|
|
||||||
<Route path="edit-cafe/:id" element={<EditCafe />} />
|
|
||||||
<Route path="stats" element={<div>صفحه آمار و تحلیل</div>} />
|
|
||||||
|
|
||||||
<Route index element={<Navigate to="/dashboard" replace />} />
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
|
||||||
</Routes>
|
|
||||||
</BrowserRouter>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useSelector, useDispatch } from "react-redux";
|
||||||
|
import { hideToast } from "../redux/slices/toastSlice";
|
||||||
|
import { motion, AnimatePresence } from "framer-motion"; //eslint-disable-line no-unused-vars
|
||||||
|
|
||||||
|
export default function Toast() {
|
||||||
|
const { message, type, visible } = useSelector((state) => state.toast);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (visible) {
|
||||||
|
const timer = setTimeout(() => dispatch(hideToast()), 3000);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [visible, dispatch]);
|
||||||
|
|
||||||
|
const bgColor =
|
||||||
|
type === "success"
|
||||||
|
? "bg-green-500"
|
||||||
|
: type === "error"
|
||||||
|
? "bg-red-500"
|
||||||
|
: "bg-blue-500";
|
||||||
|
|
||||||
|
// موبایل: از پایین بیا و برگرد پایین
|
||||||
|
const mobileVariants = {
|
||||||
|
initial: { y: 100, opacity: 0 },
|
||||||
|
animate: { y: 0, opacity: 1 },
|
||||||
|
exit: { y: 100, opacity: 0 }
|
||||||
|
};
|
||||||
|
|
||||||
|
// دسکتاپ: از چپ بیا
|
||||||
|
const desktopVariants = {
|
||||||
|
initial: { x: -100, opacity: 0 },
|
||||||
|
animate: { x: 0, opacity: 1 },
|
||||||
|
exit: { x: -100, opacity: 0 }
|
||||||
|
};
|
||||||
|
|
||||||
|
// تعیین حالت بر اساس اندازه صفحه
|
||||||
|
const isMobile = typeof window !== "undefined" && window.innerWidth < 768;
|
||||||
|
const variants = isMobile ? mobileVariants : desktopVariants;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{visible && (
|
||||||
|
<motion.div
|
||||||
|
variants={variants}
|
||||||
|
initial="initial"
|
||||||
|
animate="animate"
|
||||||
|
exit="exit"
|
||||||
|
transition={{
|
||||||
|
type: "spring",
|
||||||
|
stiffness: 260,
|
||||||
|
damping: 20,
|
||||||
|
duration: 0.9
|
||||||
|
}}
|
||||||
|
className={`${bgColor} fixed left-1/2 md:left-6 bottom-20 md:bottom-auto md:top-20 text-white px-6 py-3 rounded-lg shadow-2xl z-60 transform -translate-x-1/2 md:translate-x-0 max-w-xs md:max-w-sm`}
|
||||||
|
>
|
||||||
|
{message}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -25,10 +25,10 @@
|
||||||
|
|
||||||
// src/components/Header.jsx
|
// src/components/Header.jsx
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import Search from "../../assets/icons/search.svg";
|
import Search from "../../../assets/icons/search.svg";
|
||||||
import Arrow from "../../assets/icons/arrow.svg";
|
import Arrow from "../../../assets/icons/arrow.svg";
|
||||||
import Vector7 from "../../assets/icons/Vector7.svg";
|
import Vector7 from "../../../assets/icons/Vector7.svg";
|
||||||
import Pic from "../../assets/icons/pic.png";
|
import Pic from "../../../assets/icons/pic.png";
|
||||||
|
|
||||||
const Header = ({ title }) => {
|
const Header = ({ title }) => {
|
||||||
return (
|
return (
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||||
import LogoDM from "../../assets/icons/LogoDM.svg";
|
import LogoDM from "../../../assets/icons/LogoDM.svg";
|
||||||
import { BiBarChartAlt2 } from "react-icons/bi";
|
import { BiBarChartAlt2 } from "react-icons/bi";
|
||||||
import { AiOutlinePieChart } from "react-icons/ai";
|
import { AiOutlinePieChart } from "react-icons/ai";
|
||||||
import { PiCoffee } from "react-icons/pi";
|
import { PiCoffee } from "react-icons/pi";
|
||||||
|
|
@ -24,10 +24,10 @@ const Sidebar = () => {
|
||||||
console.log('Tokens cleared from localStorage');
|
console.log('Tokens cleared from localStorage');
|
||||||
console.log('Current token:', localStorage.getItem('token'));
|
console.log('Current token:', localStorage.getItem('token'));
|
||||||
|
|
||||||
window.location.href = '/login';
|
navigate('/login');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Logout error:', error);
|
console.error('Logout error:', error);
|
||||||
window.location.href = '/login';
|
navigate('/login');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Outlet, useLocation } from "react-router-dom";
|
import { Outlet, useLocation } from "react-router-dom";
|
||||||
import Sidebar from "./Sidebar";
|
import Sidebar from "./Sidebar/sidebar";
|
||||||
import Header from "./Header";
|
import Header from "./Header/header";
|
||||||
|
|
||||||
export default function Layout() {
|
export default function Layout() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
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]);
|
||||||
|
};
|
||||||
17
src/main.jsx
17
src/main.jsx
|
|
@ -1,10 +1,19 @@
|
||||||
import { StrictMode } from 'react'
|
import { StrictMode } from 'react';
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client';
|
||||||
import './styles/index.css'
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import App from './App.jsx'
|
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(
|
createRoot(document.getElementById('root')).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
<Provider store={store}>
|
||||||
|
<BrowserRouter>
|
||||||
<App />
|
<App />
|
||||||
|
<Toast />
|
||||||
|
</BrowserRouter>
|
||||||
|
</Provider>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
)
|
)
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { BiEdit } from "react-icons/bi";
|
import { BiEdit } from "react-icons/bi";
|
||||||
import axios from "axios";
|
|
||||||
import Vector9 from "../../assets/icons/Vector9.svg";
|
import Vector9 from "../../assets/icons/Vector9.svg";
|
||||||
import Star1 from "../../assets/icons/Star1.svg";
|
import Star1 from "../../assets/icons/Star1.svg";
|
||||||
import Group from "../../assets/icons/Group.svg";
|
import Group from "../../assets/icons/Group.svg";
|
||||||
import Pic1 from "../../assets/icons/pic1.svg";
|
import Pic1 from "../../assets/icons/pic1.svg";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import cafeService from "../../services/cafe";
|
||||||
|
|
||||||
const CafeManagement = () => {
|
const CafeManagement = () => {
|
||||||
const [cafes, setCafes] = useState([]);
|
const [cafes, setCafes] = useState([]);
|
||||||
|
|
@ -14,26 +14,19 @@ const CafeManagement = () => {
|
||||||
|
|
||||||
// دریافت لیست کافهها از API
|
// دریافت لیست کافهها از API
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchCafes = () => {
|
const fetchCafes = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError("");
|
setError("");
|
||||||
|
|
||||||
axios
|
try {
|
||||||
.get("https://cafeju.maksiran.ir/api/cafe/v1/get-cafe-list")
|
const res = await cafeService.getCafeList();
|
||||||
.then((response) => {
|
if (res.data.success && res.data.data) {
|
||||||
console.log("✅ Cafes loaded successfully:", response.data);
|
setCafes(res.data.data);
|
||||||
|
|
||||||
if (response.data.success && response.data.data) {
|
|
||||||
setCafes(response.data.data);
|
|
||||||
} else {
|
} else {
|
||||||
setError("دادههای دریافتی معتبر نیست");
|
setError("دادههای دریافتی معتبر نیست");
|
||||||
}
|
}
|
||||||
setLoading(false);
|
console.log("✅ Cafes loaded successfully:", res.data);
|
||||||
})
|
} catch (error) {
|
||||||
.catch((error) => {
|
|
||||||
console.error("❌ Error loading cafes:", error);
|
|
||||||
setLoading(false);
|
|
||||||
|
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
setError(error.response.data.message || "خطا در دریافت لیست کافهها");
|
setError(error.response.data.message || "خطا در دریافت لیست کافهها");
|
||||||
} else if (error.request) {
|
} else if (error.request) {
|
||||||
|
|
@ -41,7 +34,10 @@ const CafeManagement = () => {
|
||||||
} else {
|
} else {
|
||||||
setError("خطای نامشخص رخ داده است");
|
setError("خطای نامشخص رخ داده است");
|
||||||
}
|
}
|
||||||
});
|
console.error("❌ Error loading cafes:", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchCafes();
|
fetchCafes();
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { useParams, useNavigate } from "react-router-dom";
|
import { useParams, useNavigate } from "react-router-dom";
|
||||||
import axios from "axios";
|
|
||||||
import Bg1 from "../../assets/icons/bg1.svg";
|
import Bg1 from "../../assets/icons/bg1.svg";
|
||||||
import {GrLocation} from "react-icons/gr";
|
import { GrLocation } from "react-icons/gr";
|
||||||
import {BiEdit} from "react-icons/bi";
|
import { BiEdit } from "react-icons/bi";
|
||||||
import {FaRegStar} from "react-icons/fa";
|
import { FaRegStar } from "react-icons/fa";
|
||||||
import Vector11 from "../../assets/icons/Vector11.svg";
|
import Vector11 from "../../assets/icons/Vector11.svg";
|
||||||
import Vector12 from "../../assets/icons/Vector12.svg";
|
import Vector12 from "../../assets/icons/Vector12.svg";
|
||||||
import Vector13 from "../../assets/icons/Vector13.svg";
|
import Vector13 from "../../assets/icons/Vector13.svg";
|
||||||
|
|
@ -18,45 +17,15 @@ import Coffee1 from "../../assets/icons/coffee1.svg";
|
||||||
import Coffee3 from "../../assets/icons/coffee3.svg";
|
import Coffee3 from "../../assets/icons/coffee3.svg";
|
||||||
import Edit from "../../assets/icons/edit.svg";
|
import Edit from "../../assets/icons/edit.svg";
|
||||||
import { IoMdCheckmark, IoMdClose } from "react-icons/io";
|
import { IoMdCheckmark, IoMdClose } from "react-icons/io";
|
||||||
|
import cafeService from "../../services/cafe";
|
||||||
|
|
||||||
const EditCafe = () => {
|
export default function EditCafe() {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [cafeData, setCafeData] = useState(null);
|
const [cafeData, setCafeData] = useState(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (id) {
|
|
||||||
setLoading(true);
|
|
||||||
setError("");
|
|
||||||
|
|
||||||
axios
|
|
||||||
.get(`https://cafeju.maksiran.ir/api/cafe/v1/get-cafe-profile-by-cafe/${id}`)
|
|
||||||
.then((response) => {
|
|
||||||
console.log("✅ Cafe data loaded:", response.data);
|
|
||||||
if (response.data.success && response.data.data) {
|
|
||||||
setCafeData(response.data.data);
|
|
||||||
} else {
|
|
||||||
setError("دادههای کافه معتبر نیست");
|
|
||||||
}
|
|
||||||
setLoading(false);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("❌ Error loading cafe:", error);
|
|
||||||
setLoading(false);
|
|
||||||
if (error.response) {
|
|
||||||
setError(error.response.data.message || "خطا در دریافت اطلاعات کافه");
|
|
||||||
} else if (error.request) {
|
|
||||||
setError("خطا در برقراری ارتباط با سرور");
|
|
||||||
} else {
|
|
||||||
setError("خطای نامشخص رخ داده است");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [id]);
|
|
||||||
|
|
||||||
const [categories, setCategories] = useState(() => {
|
const [categories, setCategories] = useState(() => {
|
||||||
const saved = localStorage.getItem('cafeCategories');
|
const saved = localStorage.getItem('cafeCategories');
|
||||||
return saved ? JSON.parse(saved) : [
|
return saved ? JSON.parse(saved) : [
|
||||||
|
|
@ -69,16 +38,55 @@ const EditCafe = () => {
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
localStorage.setItem('cafeCategories', JSON.stringify(categories));
|
|
||||||
}, [categories]);
|
|
||||||
|
|
||||||
const [isEditMode, setIsEditMode] = useState(false);
|
const [isEditMode, setIsEditMode] = useState(false);
|
||||||
const [isAdding, setIsAdding] = useState(false);
|
const [isAdding, setIsAdding] = useState(false);
|
||||||
const [newCategory, setNewCategory] = useState("");
|
const [newCategory, setNewCategory] = useState("");
|
||||||
const [editingIndex, setEditingIndex] = useState(null);
|
const [editingIndex, setEditingIndex] = useState(null);
|
||||||
const [editValue, setEditValue] = useState("");
|
const [editValue, setEditValue] = useState("");
|
||||||
|
|
||||||
|
// دریافت اطلاعات کافه
|
||||||
|
useEffect(() => {
|
||||||
|
const getCafeData = 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);
|
||||||
|
if (error.response) {
|
||||||
|
setError(error.response.data.message || "خطا در دریافت اطلاعات کافه");
|
||||||
|
} else if (error.request) {
|
||||||
|
setError("خطا در برقراری ارتباط با سرور");
|
||||||
|
} else {
|
||||||
|
setError("خطای نامشخص رخ داده است");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (id) {
|
||||||
|
getCafeData();
|
||||||
|
}
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
// ذخیره دستهبندیها در localStorage
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem('cafeCategories', JSON.stringify(categories));
|
||||||
|
}, [categories]);
|
||||||
|
|
||||||
|
// Handlers
|
||||||
const handleAddCategory = () => {
|
const handleAddCategory = () => {
|
||||||
if (newCategory.trim()) {
|
if (newCategory.trim()) {
|
||||||
setCategories([...categories, newCategory.trim()]);
|
setCategories([...categories, newCategory.trim()]);
|
||||||
|
|
@ -169,7 +177,7 @@ const EditCafe = () => {
|
||||||
|
|
||||||
<div className="mt-4 lg:mt-9 border-2 border-[#8b8886] p-4 lg:p-10 rounded-2xl flex flex-col lg:flex-row gap-4">
|
<div className="mt-4 lg:mt-9 border-2 border-[#8b8886] p-4 lg:p-10 rounded-2xl flex flex-col lg:flex-row gap-4">
|
||||||
<div className="w-full lg:w-auto">
|
<div className="w-full lg:w-auto">
|
||||||
<img className="w-full lg:w-[461px] rounded-lg" src={cafeData.photo || Bg1} alt="Logo"/>
|
<img className="w-full lg:w-[461px] rounded-lg" src={cafeData.photo || Bg1} alt="Logo" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative flex flex-col gap-4 lg:gap-4.5 w-full">
|
<div className="relative flex flex-col gap-4 lg:gap-4.5 w-full">
|
||||||
|
|
@ -185,18 +193,18 @@ const EditCafe = () => {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<BiEdit className="w-5 h-5 mt-0 lg:-mt-1"/>
|
<BiEdit className="w-5 h-5 mt-0 lg:-mt-1" />
|
||||||
<h3 className="font-bold text-base">{cafeData.Name}</h3>
|
<h3 className="font-bold text-base">{cafeData.Name}</h3>
|
||||||
<hr className="w-full lg:w-38 -mt-3 border-1 text-[#80931e]"/>
|
<hr className="w-full lg:w-38 -mt-3 border-1 text-[#80931e]" />
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<GrLocation/>
|
<GrLocation />
|
||||||
<span className="text-sm lg:text-base">{cafeData.address || "آدرس موجود نیست"}</span>
|
<span className="text-sm lg:text-base">{cafeData.address || "آدرس موجود نیست"}</span>
|
||||||
</div>
|
</div>
|
||||||
<hr className="w-full lg:w-38 -mt-3 border-1 text-[#80931e]"/>
|
<hr className="w-full lg:w-38 -mt-3 border-1 text-[#80931e]" />
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<FaRegStar/>
|
<FaRegStar />
|
||||||
<span className="text-sm lg:text-base">{cafeData.rating || 0}</span>
|
<span className="text-sm lg:text-base">{cafeData.rating || 0}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -204,7 +212,7 @@ const EditCafe = () => {
|
||||||
<span className="text-sm lg:text-base">
|
<span className="text-sm lg:text-base">
|
||||||
{cafeData.description || "توضیحاتی برای این کافه وجود ندارد."}
|
{cafeData.description || "توضیحاتی برای این کافه وجود ندارد."}
|
||||||
</span>
|
</span>
|
||||||
<hr className="w-full lg:w-[104%] -mt-3 border-1 text-[#80931e]"/>
|
<hr className="w-full lg:w-[104%] -mt-3 border-1 text-[#80931e]" />
|
||||||
|
|
||||||
<h3 className="text-[#402E32] font-bold mt-6 lg:mt-10 lg:relative lg:-right-64">
|
<h3 className="text-[#402E32] font-bold mt-6 lg:mt-10 lg:relative lg:-right-64">
|
||||||
ویژگی ها
|
ویژگی ها
|
||||||
|
|
@ -213,7 +221,7 @@ const EditCafe = () => {
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:flex lg:relative lg:-mr-71 lg:-ml-7 mt-5 gap-3">
|
<div className="grid grid-cols-2 sm:grid-cols-3 lg:flex lg:relative lg:-mr-71 lg:-ml-7 mt-5 gap-3">
|
||||||
<div
|
<div
|
||||||
className="bg-[#e1d5c2] w-full lg:w-[140px] h-[137.6px] flex flex-col items-center gap-4 lg:gap-8 text-center rounded-2xl lg:rounded-4xl transform hover:-translate-y-1 transition-all duration-300 hover:shadow-md">
|
className="bg-[#e1d5c2] w-full lg:w-[140px] h-[137.6px] flex flex-col items-center gap-4 lg:gap-8 text-center rounded-2xl lg:rounded-4xl transform hover:-translate-y-1 transition-all duration-300 hover:shadow-md">
|
||||||
<img src={Coffee3} className="w-6 h-6 text-[#402E32] mt-5"/>
|
<img src={Coffee3} className="w-6 h-6 text-[#402E32] mt-5" />
|
||||||
<span className="text-[#402E32] font-medium text-sm lg:text-base whitespace-nowrap px-2">
|
<span className="text-[#402E32] font-medium text-sm lg:text-base whitespace-nowrap px-2">
|
||||||
منو کافه:
|
منو کافه:
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -221,42 +229,42 @@ const EditCafe = () => {
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="flex flex-col gap-3 lg:gap-4 text-[#402E32] font-medium w-full lg:w-[161.5px] h-[137.6px] bg-[#e1d5c2] rounded-2xl lg:rounded-4xl pr-3 text-sm lg:text-base transform hover:-translate-y-1 transition-all duration-300 hover:shadow-md">
|
className="flex flex-col gap-3 lg:gap-4 text-[#402E32] font-medium w-full lg:w-[161.5px] h-[137.6px] bg-[#e1d5c2] rounded-2xl lg:rounded-4xl pr-3 text-sm lg:text-base transform hover:-translate-y-1 transition-all duration-300 hover:shadow-md">
|
||||||
<img src={Vector15} className="w-6 h-6 text-[#402E32] mt-5"/>
|
<img src={Vector15} className="w-6 h-6 text-[#402E32] mt-5" />
|
||||||
<span>ساعت کاری:</span>
|
<span>ساعت کاری:</span>
|
||||||
<span> 23 - 8</span>
|
<span> 23 - 8</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="flex flex-col gap-3 lg:gap-4 text-[#402E32] font-medium w-full lg:w-[161.5px] h-[137.6px] bg-[#e1d5c2] rounded-2xl lg:rounded-4xl pr-3 text-sm lg:text-base transform hover:-translate-y-1 transition-all duration-300 hover:shadow-md">
|
className="flex flex-col gap-3 lg:gap-4 text-[#402E32] font-medium w-full lg:w-[161.5px] h-[137.6px] bg-[#e1d5c2] rounded-2xl lg:rounded-4xl pr-3 text-sm lg:text-base transform hover:-translate-y-1 transition-all duration-300 hover:shadow-md">
|
||||||
<img src={Vector14} className="w-6 h-6 text-[#402E32] mt-5"/>
|
<img src={Vector14} className="w-6 h-6 text-[#402E32] mt-5" />
|
||||||
<span>رزرو :</span>
|
<span>رزرو :</span>
|
||||||
<span>رزرو آنلاین</span>
|
<span>رزرو آنلاین</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="flex flex-col gap-3 lg:gap-4 text-[#402E32] font-medium w-full lg:w-[170.5px] h-[137.6px] bg-[#e1d5c2] rounded-2xl lg:rounded-4xl pr-3 text-sm lg:text-base transform hover:-translate-y-1 transition-all duration-300 hover:shadow-md">
|
className="flex flex-col gap-3 lg:gap-4 text-[#402E32] font-medium w-full lg:w-[170.5px] h-[137.6px] bg-[#e1d5c2] rounded-2xl lg:rounded-4xl pr-3 text-sm lg:text-base transform hover:-translate-y-1 transition-all duration-300 hover:shadow-md">
|
||||||
<img src={Vector11} className="w-6 h-6 text-[#402E32] mt-5"/>
|
<img src={Vector11} className="w-6 h-6 text-[#402E32] mt-5" />
|
||||||
<span>موسیقی :</span>
|
<span>موسیقی :</span>
|
||||||
<span>موسیقی زنده آخر هفته</span>
|
<span>موسیقی زنده آخر هفته</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="flex flex-col gap-3 lg:gap-4 text-[#402E32] font-medium w-full lg:w-[161.5px] h-[137.6px] bg-[#e1d5c2] rounded-2xl lg:rounded-4xl pr-3 text-sm lg:text-base transform hover:-translate-y-1 transition-all duration-300 hover:shadow-md">
|
className="flex flex-col gap-3 lg:gap-4 text-[#402E32] font-medium w-full lg:w-[161.5px] h-[137.6px] bg-[#e1d5c2] rounded-2xl lg:rounded-4xl pr-3 text-sm lg:text-base transform hover:-translate-y-1 transition-all duration-300 hover:shadow-md">
|
||||||
<img src={Vector13} className="w-6 h-6 text-[#402E32] mt-5"/>
|
<img src={Vector13} className="w-6 h-6 text-[#402E32] mt-5" />
|
||||||
<span>پارکینگ :</span>
|
<span>پارکینگ :</span>
|
||||||
<span>عمومی</span>
|
<span>عمومی</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="flex flex-col gap-3 lg:gap-4 text-[#402E32] font-medium w-full lg:w-[161.5px] h-[137.6px] bg-[#e1d5c2] rounded-2xl lg:rounded-4xl pr-3 text-sm lg:text-base transform hover:-translate-y-1 transition-all duration-300 hover:shadow-md">
|
className="flex flex-col gap-3 lg:gap-4 text-[#402E32] font-medium w-full lg:w-[161.5px] h-[137.6px] bg-[#e1d5c2] rounded-2xl lg:rounded-4xl pr-3 text-sm lg:text-base transform hover:-translate-y-1 transition-all duration-300 hover:shadow-md">
|
||||||
<img src={Vector12} className="w-6 h-6 text-[#402E32] mt-5"/>
|
<img src={Vector12} className="w-6 h-6 text-[#402E32] mt-5" />
|
||||||
<span>دسترسی آسان :</span>
|
<span>دسترسی آسان :</span>
|
||||||
<span>مناسب افراد ناتوان</span>
|
<span>مناسب افراد ناتوان</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="flex justify-center items-center w-full lg:w-[150.5px] h-[137.6px] bg-[#5e5450] rounded-2xl lg:rounded-4xl transform hover:-translate-y-1 transition-all duration-300 hover:shadow-md">
|
className="flex justify-center items-center w-full lg:w-[150.5px] h-[137.6px] bg-[#5e5450] rounded-2xl lg:rounded-4xl transform hover:-translate-y-1 transition-all duration-300 hover:shadow-md">
|
||||||
<img src={Vector9} className="w-6 h-6 text-[#402E32]"/>
|
<img src={Vector9} className="w-6 h-6 text-[#402E32]" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -333,7 +341,7 @@ const EditCafe = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isEditMode && (
|
{isEditMode && (
|
||||||
<div className="flex-shrink-0 w-full sm:w-auto" style={{minWidth: '120px'}}>
|
<div className="flex-shrink-0 w-full sm:w-auto" style={{ minWidth: '120px' }}>
|
||||||
{!isAdding ? (
|
{!isAdding ? (
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsAdding(true)}
|
onClick={() => setIsAdding(true)}
|
||||||
|
|
@ -373,22 +381,22 @@ const EditCafe = () => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr className="w-full lg:w-[132%] lg:-mr-66 mt-3 border-2 rounded-3xl text-[#939393]"/>
|
<hr className="w-full lg:w-[132%] lg:-mr-66 mt-3 border-2 rounded-3xl text-[#939393]" />
|
||||||
|
|
||||||
<div className="mt-9 flex items-center gap-2 lg:-mr-64 text-[#a79fa1] font-bold text-sm lg:text-base">
|
<div className="mt-9 flex items-center gap-2 lg:-mr-64 text-[#a79fa1] font-bold text-sm lg:text-base">
|
||||||
<img src={Vector16} alt="Logo"/>
|
<img src={Vector16} alt="Logo" />
|
||||||
<h3>افزودن زیر عنوان</h3>
|
<h3>افزودن زیر عنوان</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-9 flex items-center gap-2 lg:-mr-64 text-[#66585b] font-bold text-sm lg:text-base">
|
<div className="mt-9 flex items-center gap-2 lg:-mr-64 text-[#66585b] font-bold text-sm lg:text-base">
|
||||||
<img className="w-5.5 h-5.5" src={Edit} alt="Logo"/>
|
<img className="w-5.5 h-5.5" src={Edit} alt="Logo" />
|
||||||
<img className="w-6 h-6" src={Coffee3} alt="Logo"/>
|
<img className="w-6 h-6" src={Coffee3} alt="Logo" />
|
||||||
<h3>قهوه ها</h3>
|
<h3>قهوه ها</h3>
|
||||||
<hr className="mt-9 -mr-15 w-[5%] border-1 text-[#80931e] hidden lg:block"/>
|
<hr className="mt-9 -mr-15 w-[5%] border-1 text-[#80931e] hidden lg:block" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-9 flex items-center gap-2 lg:-mr-64 text-[#66585b] font-bold text-sm lg:text-base">
|
<div className="mt-9 flex items-center gap-2 lg:-mr-64 text-[#66585b] font-bold text-sm lg:text-base">
|
||||||
<img src={Vector16} alt="Logo"/>
|
<img src={Vector16} alt="Logo" />
|
||||||
<h3>آیتم</h3>
|
<h3>آیتم</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -396,7 +404,7 @@ const EditCafe = () => {
|
||||||
<div
|
<div
|
||||||
className="mt-8 lg:mt-13 flex items-center flex-col justify-center w-full lg:w-[320px] h-[391px] transform hover:-translate-y-1 transition-all duration-300 hover:shadow-md">
|
className="mt-8 lg:mt-13 flex items-center flex-col justify-center w-full lg:w-[320px] h-[391px] transform hover:-translate-y-1 transition-all duration-300 hover:shadow-md">
|
||||||
<h1 className="text-[#402E32] font-bold text-sm lg:text-base lg:-mr-50">اسپرسو100%</h1>
|
<h1 className="text-[#402E32] font-bold text-sm lg:text-base lg:-mr-50">اسپرسو100%</h1>
|
||||||
<img src={Sperso} alt="Logo" className="mt-5"/>
|
<img src={Sperso} alt="Logo" className="mt-5" />
|
||||||
<div className="flex justify-between gap-20 lg:gap-50 mt-3 text-[#66585b] font-medium text-sm lg:text-base">
|
<div className="flex justify-between gap-20 lg:gap-50 mt-3 text-[#66585b] font-medium text-sm lg:text-base">
|
||||||
<span>قیمت</span>
|
<span>قیمت</span>
|
||||||
<span>118.000</span>
|
<span>118.000</span>
|
||||||
|
|
@ -405,7 +413,7 @@ const EditCafe = () => {
|
||||||
45 میلی لیتر، قهوه، 100% عربیکا، دم شده با دستگاه اسپرسو ساز،
|
45 میلی لیتر، قهوه، 100% عربیکا، دم شده با دستگاه اسپرسو ساز،
|
||||||
به همراه یک عدد آب معدنی مینی
|
به همراه یک عدد آب معدنی مینی
|
||||||
</span>
|
</span>
|
||||||
<hr className="mt-5 w-[90%] border-3 rounded-4xl text-[#e6e2de]"/>
|
<hr className="mt-5 w-[90%] border-3 rounded-4xl text-[#e6e2de]" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
@ -413,7 +421,7 @@ const EditCafe = () => {
|
||||||
<h1 className="text-[#402E32] font-bold text-sm lg:text-base lg:-mr-50">
|
<h1 className="text-[#402E32] font-bold text-sm lg:text-base lg:-mr-50">
|
||||||
کارامل ماکیاتو
|
کارامل ماکیاتو
|
||||||
</h1>
|
</h1>
|
||||||
<img src={Coffee1} alt="Logo" className="mt-5"/>
|
<img src={Coffee1} alt="Logo" className="mt-5" />
|
||||||
<div className="flex justify-between gap-20 lg:gap-50 mt-3 text-[#66585b] font-medium text-sm lg:text-base">
|
<div className="flex justify-between gap-20 lg:gap-50 mt-3 text-[#66585b] font-medium text-sm lg:text-base">
|
||||||
<span>قیمت</span>
|
<span>قیمت</span>
|
||||||
<span>149.000</span>
|
<span>149.000</span>
|
||||||
|
|
@ -422,7 +430,7 @@ const EditCafe = () => {
|
||||||
220 میلی لیتر، 2 شات اسپرسو 30% روبوستا، 70% عربیکا، یک لکه
|
220 میلی لیتر، 2 شات اسپرسو 30% روبوستا، 70% عربیکا، یک لکه
|
||||||
فوم شیر، سیروپ کارامل
|
فوم شیر، سیروپ کارامل
|
||||||
</span>
|
</span>
|
||||||
<hr className="mt-5 w-[90%] border-3 rounded-4xl text-[#e6e2de]"/>
|
<hr className="mt-5 w-[90%] border-3 rounded-4xl text-[#e6e2de]" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
@ -430,7 +438,7 @@ const EditCafe = () => {
|
||||||
<h1 className="text-[#402E32] font-bold text-sm lg:text-base lg:-mr-47">
|
<h1 className="text-[#402E32] font-bold text-sm lg:text-base lg:-mr-47">
|
||||||
اسپرسو آفوگاتو
|
اسپرسو آفوگاتو
|
||||||
</h1>
|
</h1>
|
||||||
<img src={Coffee2} alt="Logo" className="mt-5"/>
|
<img src={Coffee2} alt="Logo" className="mt-5" />
|
||||||
<div className="flex justify-between gap-20 lg:gap-50 mt-3 text-[#66585b] font-medium text-sm lg:text-base">
|
<div className="flex justify-between gap-20 lg:gap-50 mt-3 text-[#66585b] font-medium text-sm lg:text-base">
|
||||||
<span>قیمت</span>
|
<span>قیمت</span>
|
||||||
<span>118.000</span>
|
<span>118.000</span>
|
||||||
|
|
@ -439,7 +447,7 @@ const EditCafe = () => {
|
||||||
className="font-light text-[#66585b] mt-2 mb-10 break-words px-4 lg:-mr-23 text-xs lg:text-[14.90px]">
|
className="font-light text-[#66585b] mt-2 mb-10 break-words px-4 lg:-mr-23 text-xs lg:text-[14.90px]">
|
||||||
اسپرسو، یک اسکوپ بستنی وانیلی
|
اسپرسو، یک اسکوپ بستنی وانیلی
|
||||||
</span>
|
</span>
|
||||||
<hr className="w-[90%] border-3 rounded-4xl text-[#e6e2de]"/>
|
<hr className="w-[90%] border-3 rounded-4xl text-[#e6e2de]" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -448,6 +456,4 @@ const EditCafe = () => {
|
||||||
</section>
|
</section>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
export default EditCafe;
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,9 @@
|
||||||
|
import {useProfile} from "../../hooks/useProfile";
|
||||||
|
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
|
useProfile();
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
صفحه داشبورد
|
صفحه داشبورد
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState } from 'react';
|
||||||
import Loginpic from '../../assets/image/loginpic.jpg';
|
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 { FaRegUser } from "react-icons/fa6";
|
||||||
import axios from 'axios';
|
|
||||||
import LogoDM from "../../assets/icons/LogoDM.svg";
|
import LogoDM from "../../assets/icons/LogoDM.svg";
|
||||||
import { useNavigate, Navigate } from 'react-router-dom';
|
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 Login = () => {
|
||||||
const [userName, setUserName] = useState('');
|
const [userName, setUserName] = useState('');
|
||||||
|
|
@ -15,17 +16,10 @@ const Login = () => {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const navigate = useNavigate();
|
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();
|
e.preventDefault();
|
||||||
setError('');
|
setError('');
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
@ -35,50 +29,31 @@ const Login = () => {
|
||||||
password: password
|
password: password
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('Sending login request with:', loginData);
|
try {
|
||||||
|
const res = await authService.login(loginData);
|
||||||
axios.post('/api/admin/v1/login', loginData)
|
if (res.data.success && res.data.data && res.data.data.tokens.accessToken) {
|
||||||
.then((response) => {
|
dispatch(setToken(res.data.data.tokens.accessToken));
|
||||||
setLoading(false);
|
|
||||||
console.log('✅ Login successful!');
|
|
||||||
console.log('Response:', response.data);
|
|
||||||
|
|
||||||
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');
|
navigate('/dashboard');
|
||||||
} else {
|
dispatch(setProfile(res.data.data.admin));
|
||||||
console.error('❌ Invalid response format');
|
console.log('login:', res.data.data.admin);
|
||||||
setError('پاسخ سرور معتبر نیست');
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.catch((error) => {
|
console.log('Response:', res);
|
||||||
|
} catch (error) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
console.error('❌ Login error:', error);
|
console.error('❌ Login error:', error);
|
||||||
|
// if (error.response) {
|
||||||
if (error.response) {
|
// setError(error.response.data.message || error.response.data.en_message || 'نام کاربری یا رمز عبور اشتباه است');
|
||||||
console.error('Response status:', error.response.status);
|
// } else if (error.request) {
|
||||||
console.error('Response data:', error.response.data);
|
// setError('خطا در برقراری ارتباط با سرور');
|
||||||
setError(error.response.data.message || error.response.data.en_message || 'نام کاربری یا رمز عبور اشتباه است');
|
// } else {
|
||||||
} else if (error.request) {
|
// console.error('Error setting up request');
|
||||||
console.error('No response received from server');
|
// setError('خطای نامشخص رخ داده است');
|
||||||
setError('خطا در برقراری ارتباط با سرور');
|
// }
|
||||||
} else {
|
} finally {
|
||||||
console.error('Error setting up request');
|
setLoading(false);
|
||||||
setError('خطای نامشخص رخ داده است');
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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 <Navigate to="/login" replace />;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('ProtectedRoute - Rendering protected content');
|
|
||||||
return children;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Login;
|
export default Login;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -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 <Navigate to="/login" replace />;
|
||||||
|
}
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
@ -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 (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export const toastIgnore = [
|
||||||
|
// "/posts/like",
|
||||||
|
];
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import requests from "./api/base-api";
|
||||||
|
|
||||||
|
const dashboardService = {
|
||||||
|
getCafeList: () => requests.get("/cafe/v1/get-cafe-list"),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default dashboardService;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue