Initial commit - modstagram_panel
This commit is contained in:
4
.env
Normal file
4
.env
Normal file
@@ -0,0 +1,4 @@
|
||||
REACT_APP_API_URL=https://api.modstagram.com/api/v1
|
||||
REACT_APP_API_ORIGIN=https://api.modstagram.com
|
||||
REACT_APP_APP_URL=https://modstagram.com
|
||||
REACT_APP_PANEL_URL=https://panel.modstagram.com
|
||||
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
25
Dockerfile
Normal file
25
Dockerfile
Normal file
@@ -0,0 +1,25 @@
|
||||
FROM node:18-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN npm run build
|
||||
|
||||
FROM node:18-alpine AS runner
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/build ./build
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder /app/package*.json ./
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
|
||||
RUN npm install -g serve
|
||||
|
||||
EXPOSE 3001
|
||||
|
||||
CMD ["serve", "-s", "build", "-l", "3001"]
|
||||
70
README.md
Normal file
70
README.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Getting Started with Create React App
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
|
||||
|
||||
The page will reload when you make changes.\
|
||||
You may also see any lint errors in the console.
|
||||
|
||||
### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `npm run eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
|
||||
|
||||
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
|
||||
|
||||
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
|
||||
### Code Splitting
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
|
||||
|
||||
### Analyzing the Bundle Size
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
|
||||
|
||||
### Making a Progressive Web App
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
|
||||
|
||||
### Deployment
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
|
||||
|
||||
### `npm run build` fails to minify
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
||||
15
docker-compose.yml
Normal file
15
docker-compose.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
panel-app:
|
||||
build: .
|
||||
container_name: modstagram-panel
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3005:3001"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
volumes:
|
||||
- ./:/app
|
||||
- /app/node_modules
|
||||
- /app/build
|
||||
19096
package-lock.json
generated
Normal file
19096
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
54
package.json
Normal file
54
package.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "modstagram_panel",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"axios": "^1.6.8",
|
||||
"js-cookie": "^3.0.5",
|
||||
"moment-jalaali": "^0.10.4",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-modal": "^3.16.1",
|
||||
"react-multi-date-picker": "^4.5.2",
|
||||
"react-quill": "^2.0.0",
|
||||
"react-router-dom": "^6.23.1",
|
||||
"react-scripts": "5.0.1",
|
||||
"web-vitals": "^2.1.4",
|
||||
"yup": "^1.4.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "BROWSER=none react-scripts start",
|
||||
"build": "GENERATE_SOURCEMAP=false react-scripts build",
|
||||
"start:prod": "serve -s build -l 3001",
|
||||
"build:no-cache": "GENERATE_SOURCEMAP=false INLINE_RUNTIME_CHUNK=false react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"tailwindcss": "^3.4.3"
|
||||
}
|
||||
}
|
||||
54
package.jsy
Normal file
54
package.jsy
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "modstagram_panel",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"axios": "^1.6.8",
|
||||
"js-cookie": "^3.0.5",
|
||||
"moment-jalaali": "^0.10.4",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-modal": "^3.16.1",
|
||||
"react-multi-date-picker": "^4.5.2",
|
||||
"react-quill": "^2.0.0",
|
||||
"react-router-dom": "^6.23.1",
|
||||
"react-scripts": "5.0.1",
|
||||
"web-vitals": "^2.1.4",
|
||||
"yup": "^1.4.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "BROWSER=none react-scripts start",
|
||||
"build": "GENERATE_SOURCEMAP=false react-scripts build",
|
||||
"start:prod": "serve -s build -l 3001"
|
||||
"build:no-cache": "GENERATE_SOURCEMAP=false INLINE_RUNTIME_CHUNK=false react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"tailwindcss": "^3.4.3"
|
||||
}
|
||||
}
|
||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
public/fonts/KalamehWeb-Medium.woff
Normal file
BIN
public/fonts/KalamehWeb-Medium.woff
Normal file
Binary file not shown.
BIN
public/fonts/KalamehWeb-Regular.woff
Normal file
BIN
public/fonts/KalamehWeb-Regular.woff
Normal file
Binary file not shown.
BIN
public/fonts/KalamehWeb-SemiBold.woff
Normal file
BIN
public/fonts/KalamehWeb-SemiBold.woff
Normal file
Binary file not shown.
42
public/index.html
Normal file
42
public/index.html
Normal file
@@ -0,0 +1,42 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>مدستاگرام | پنل مدیریت</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
--></body>
|
||||
</html>
|
||||
BIN
public/logo192.png
Normal file
BIN
public/logo192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
BIN
public/logo512.png
Normal file
BIN
public/logo512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
25
public/manifest.json
Normal file
25
public/manifest.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
3
public/robots.txt
Normal file
3
public/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
17
src/App.jsx
Normal file
17
src/App.jsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { RouterProvider } from "react-router-dom";
|
||||
import AuthProvider from "./utils/AuthProvider";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import { router } from "./routers/appRoutes";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className=" lg:block">
|
||||
<AuthProvider isSignedIn={false}>
|
||||
<RouterProvider router={router} />
|
||||
<Toaster />
|
||||
</AuthProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
166
src/assets/css/index.css
Normal file
166
src/assets/css/index.css
Normal file
@@ -0,0 +1,166 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@font-face {
|
||||
font-family: Kalameh;
|
||||
src: url("/public/fonts/KalamehWeb-Regular.woff");
|
||||
}
|
||||
@font-face {
|
||||
font-family: KalamehMed;
|
||||
src: url("/public/fonts/KalamehWeb-Medium.woff");
|
||||
}
|
||||
@font-face {
|
||||
font-family: KalamehSemi;
|
||||
src: url("/public/fonts/KalamehWeb-SemiBold.woff");
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.hide-scrollbar {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.hide-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.datePickerStyle {
|
||||
width: 100%;
|
||||
background-color: black;
|
||||
}
|
||||
button,
|
||||
button:focus,
|
||||
textarea {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
button:focus,
|
||||
select:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Firefox */
|
||||
input[type="number"] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
* {
|
||||
font-family: "kalameh" !important;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
||||
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
direction: rtl;
|
||||
background-color: #252525;
|
||||
}
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
|
||||
monospace;
|
||||
}
|
||||
.dir-ltr {
|
||||
direction: ltr;
|
||||
}
|
||||
.big-circle {
|
||||
position: fixed;
|
||||
top: 60vh; /* 30vh پایینتر از پایین صفحه */
|
||||
left: 50%; /* در وسط صفحه افقی */
|
||||
transform: translateX(-50%); /* تنظیم موقعیت در وسط صفحه افقی */
|
||||
width: 170vw; /* عرض بیشتر از عرض صفحه */
|
||||
height: 100vw; /* ارتفاع برابر با عرض بیشتر از عرض صفحه */
|
||||
border-radius: 50%; /* تبدیل به دایره */
|
||||
background-color: #fff; /* رنگ پسزمینه */
|
||||
z-index: 15;
|
||||
}
|
||||
.bg-glass {
|
||||
box-shadow: 0px 4px 12px 0px #00000040;
|
||||
background-color: #ffffffa2;
|
||||
}
|
||||
.purple-line {
|
||||
width: 200px;
|
||||
height: 1px;
|
||||
background: linear-gradient(to right, #f9f2fb 0%, #482d4f 100%);
|
||||
}
|
||||
.bg-glass-absolute {
|
||||
width: 101%;
|
||||
height: 110%;
|
||||
box-shadow: 0px 4px 12px 0px #00000040;
|
||||
background-color: #e9e9e99c;
|
||||
position: absolute;
|
||||
bottom: -20px;
|
||||
border-radius: 20px;
|
||||
z-index: 99;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.ql-container,
|
||||
.ql-editor {
|
||||
min-height: 400px;
|
||||
color: #fff !important;
|
||||
text-align: right !important;
|
||||
}
|
||||
.ql-editor.ql-blank::before {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.rmdp-input {
|
||||
border-color: #dee2e6 !important;
|
||||
width: 130px !important;
|
||||
height: 48px !important;
|
||||
padding-right: 16px !important;
|
||||
color: #111;
|
||||
}
|
||||
@media (min-width: 640px) {
|
||||
.container {
|
||||
max-width: 640px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.container {
|
||||
max-width: 768px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.container {
|
||||
max-width: 1024px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.container {
|
||||
max-width: 1170px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1400px) {
|
||||
.container {
|
||||
max-width: 1300px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1500px) {
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1636px) {
|
||||
.container {
|
||||
max-width: 1650px;
|
||||
}
|
||||
}
|
||||
15
src/assets/icons/AdvertisingIcon.jsx
Normal file
15
src/assets/icons/AdvertisingIcon.jsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from "react";
|
||||
|
||||
function AdvertisingIcon(props) {
|
||||
return (
|
||||
<svg className={props.className}
|
||||
{...props} width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path opacity="0.34" d="M8.67957 12.7201H10.4196V16.7701C10.4196 17.3701 11.1596 17.6501 11.5596 17.2001L15.8196 12.3601C16.1896 11.9401 15.8896 11.2801 15.3296 11.2801H13.5896V7.23008C13.5896 6.63008 12.8496 6.35008 12.4496 6.80008L8.18957 11.6401C7.81957 12.0601 8.11957 12.7201 8.67957 12.7201Z" stroke="#fff" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M11.9697 22C17.4926 22 21.9697 17.5228 21.9697 12C21.9697 6.47715 17.4926 2 11.9697 2C6.44688 2 1.96973 6.47715 1.96973 12C1.96973 17.5228 6.44688 22 11.9697 22Z" stroke="#fff" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
export default AdvertisingIcon;
|
||||
18
src/assets/icons/CommentsIcon.jsx
Normal file
18
src/assets/icons/CommentsIcon.jsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
|
||||
function CommentsIcon(props) {
|
||||
return (
|
||||
<svg className={props.className}
|
||||
{...props} width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.98999 22.78C9.38999 22.78 8.81999 22.48 8.42999 21.95L7.23001 20.35C7.23001 20.36 7.18 20.33 7.16 20.33H6.79001C3.37001 20.33 1.25 19.4 1.25 14.79V10.79C1.25 6.58001 3.82001 5.48001 5.98001 5.29001C6.22001 5.26001 6.50001 5.25 6.79001 5.25H13.19C16.81 5.25 18.73 7.17001 18.73 10.79V14.79C18.73 15.08 18.72 15.36 18.68 15.63C18.5 17.76 17.4 20.33 13.19 20.33H12.79L11.55 21.95C11.16 22.48 10.59 22.78 9.98999 22.78ZM6.79001 6.75C6.56001 6.75 6.34 6.76 6.13 6.78C3.81 6.98 2.75 8.25001 2.75 10.79V14.79C2.75 18.22 3.81001 18.83 6.79001 18.83H7.19C7.64 18.83 8.14999 19.08 8.42999 19.44L9.63 21.05C9.85001 21.35 10.13 21.35 10.35 21.05L11.55 19.45C11.84 19.06 12.3 18.83 12.79 18.83H13.19C15.73 18.83 17 17.76 17.19 15.48C17.22 15.24 17.23 15.02 17.23 14.79V10.79C17.23 8.00001 15.98 6.75 13.19 6.75H6.79001Z" />
|
||||
<path d="M9.99023 14.19C9.43023 14.19 8.99023 13.74 8.99023 13.19C8.99023 12.64 9.44023 12.19 9.99023 12.19C10.5402 12.19 10.9902 12.64 10.9902 13.19C10.9902 13.74 10.5502 14.19 9.99023 14.19Z" />
|
||||
<path d="M13.1895 14.19C12.6295 14.19 12.1895 13.74 12.1895 13.19C12.1895 12.64 12.6395 12.19 13.1895 12.19C13.7395 12.19 14.1895 12.64 14.1895 13.19C14.1895 13.74 13.7395 14.19 13.1895 14.19Z" />
|
||||
<path d="M6.7998 14.19C6.2398 14.19 5.7998 13.74 5.7998 13.19C5.7998 12.64 6.2498 12.19 6.7998 12.19C7.3498 12.19 7.7998 12.64 7.7998 13.19C7.7998 13.74 7.3498 14.19 6.7998 14.19Z" />
|
||||
<path d="M17.9396 16.29C17.7396 16.29 17.5396 16.21 17.3996 16.06C17.2396 15.9 17.1697 15.67 17.1997 15.45C17.2297 15.24 17.2396 15.02 17.2396 14.79V10.79C17.2396 8.00001 15.9897 6.75 13.1997 6.75H6.79963C6.56963 6.75 6.34966 6.76 6.13966 6.78C5.91966 6.81 5.68964 6.72999 5.52964 6.57999C5.36964 6.41999 5.27963 6.20001 5.29963 5.98001C5.47963 3.82001 6.58963 1.25 10.7996 1.25H17.1997C20.8197 1.25 22.7396 3.17001 22.7396 6.79001V10.79C22.7396 15 20.1697 16.1 18.0097 16.29C17.9797 16.29 17.9596 16.29 17.9396 16.29ZM6.91966 5.25H13.1896C16.8096 5.25 18.7297 7.17001 18.7297 10.79V14.66C20.4297 14.24 21.2297 12.99 21.2297 10.79V6.79001C21.2297 4.00001 19.9796 2.75 17.1896 2.75H10.7897C8.58965 2.75 7.34966 3.55 6.91966 5.25Z" />
|
||||
</svg>
|
||||
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
export default CommentsIcon;
|
||||
18
src/assets/icons/FinancialsIcon.jsx
Normal file
18
src/assets/icons/FinancialsIcon.jsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
|
||||
function FinancialsIcon(props) {
|
||||
return (
|
||||
|
||||
<svg className={props.className}
|
||||
{...props} width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 13.36H2C1.59 13.36 1.25 13.02 1.25 12.61C1.25 12.2 1.59 11.86 2 11.86H19C19.41 11.86 19.75 12.2 19.75 12.61C19.75 13.02 19.41 13.36 19 13.36Z" />
|
||||
<path d="M15.22 21.75H5.78003C2.35003 21.75 1.25 20.66 1.25 17.27V10.28C1.25 7.58999 1.86002 5.99999 4.96002 5.81999C5.22002 5.80999 5.49003 5.79999 5.78003 5.79999H15.22C18.65 5.79999 19.75 6.88999 19.75 10.28V17.43C19.71 20.7 18.61 21.75 15.22 21.75ZM5.78003 7.29999C5.51003 7.29999 5.26003 7.30999 5.03003 7.31999C3.24003 7.42999 2.75 7.80999 2.75 10.28V17.27C2.75 19.83 3.17003 20.25 5.78003 20.25H15.22C17.8 20.25 18.22 19.85 18.25 17.42V10.28C18.25 7.71999 17.83 7.29999 15.22 7.29999H5.78003Z" />
|
||||
<path d="M19 18.18C18.81 18.18 18.62 18.11 18.49 17.98C18.34 17.84 18.25 17.64 18.25 17.43V10.28C18.25 7.72 17.83 7.3 15.22 7.3H5.78003C5.51003 7.3 5.26003 7.31 5.03003 7.32C4.83003 7.33 4.62998 7.25 4.47998 7.11C4.32998 6.97 4.25 6.77 4.25 6.56C4.29 3.3 5.39003 2.25 8.78003 2.25H18.22C21.65 2.25 22.75 3.34 22.75 6.73V13.72C22.75 16.41 22.14 18 19.04 18.18C19.03 18.18 19.01 18.18 19 18.18ZM5.78003 5.8H15.22C18.65 5.8 19.75 6.89 19.75 10.28V16.6C20.91 16.39 21.25 15.79 21.25 13.72V6.73C21.25 4.17 20.83 3.75 18.22 3.75H8.78003C6.50003 3.75 5.91003 4.06 5.78003 5.8Z" />
|
||||
<path d="M6.96027 18.56H5.24023C4.83023 18.56 4.49023 18.22 4.49023 17.81C4.49023 17.4 4.83023 17.06 5.24023 17.06H6.96027C7.37027 17.06 7.71027 17.4 7.71027 17.81C7.71027 18.22 7.38027 18.56 6.96027 18.56Z" />
|
||||
<path d="M12.5499 18.56H9.10986C8.69986 18.56 8.35986 18.22 8.35986 17.81C8.35986 17.4 8.69986 17.06 9.10986 17.06H12.5499C12.9599 17.06 13.2999 17.4 13.2999 17.81C13.2999 18.22 12.9699 18.56 12.5499 18.56Z" />
|
||||
</svg>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
export default FinancialsIcon;
|
||||
16
src/assets/icons/PostsIcon.jsx
Normal file
16
src/assets/icons/PostsIcon.jsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from "react";
|
||||
|
||||
function PostsIcon(props) {
|
||||
return (
|
||||
<svg className={props.className}
|
||||
{...props} width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15 22.75H9C3.57 22.75 1.25 20.43 1.25 15V9C1.25 3.57 3.57 1.25 9 1.25H15C20.43 1.25 22.75 3.57 22.75 9V15C22.75 20.43 20.43 22.75 15 22.75ZM9 2.75C4.39 2.75 2.75 4.39 2.75 9V15C2.75 19.61 4.39 21.25 9 21.25H15C19.61 21.25 21.25 19.61 21.25 15V9C21.25 4.39 19.61 2.75 15 2.75H9Z" />
|
||||
<path d="M9 10.75C7.48 10.75 6.25 9.52 6.25 8C6.25 6.48 7.48 5.25 9 5.25C10.52 5.25 11.75 6.48 11.75 8C11.75 9.52 10.52 10.75 9 10.75ZM9 6.75C8.31 6.75 7.75 7.31 7.75 8C7.75 8.69 8.31 9.25 9 9.25C9.69 9.25 10.25 8.69 10.25 8C10.25 7.31 9.69 6.75 9 6.75Z" />
|
||||
<path d="M2.66977 19.7C2.42977 19.7 2.18977 19.58 2.04977 19.37C1.81977 19.03 1.90977 18.56 2.25977 18.33L7.18977 15.02C8.26977 14.29 9.75977 14.38 10.7398 15.21L11.0698 15.5C11.5698 15.93 12.4198 15.93 12.9098 15.5L17.0698 11.93C18.1298 11.02 19.7998 11.02 20.8698 11.93L22.4998 13.33C22.8098 13.6 22.8498 14.07 22.5798 14.39C22.3098 14.7 21.8398 14.74 21.5198 14.47L19.8898 13.07C19.3898 12.64 18.5398 12.64 18.0398 13.07L13.8798 16.64C12.8198 17.55 11.1498 17.55 10.0798 16.64L9.74977 16.35C9.28977 15.96 8.52977 15.92 8.01977 16.27L3.08977 19.58C2.95977 19.66 2.80977 19.7 2.66977 19.7Z" />
|
||||
</svg>
|
||||
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
export default PostsIcon;
|
||||
17
src/assets/icons/ProjectsIcon.jsx
Normal file
17
src/assets/icons/ProjectsIcon.jsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from "react";
|
||||
|
||||
function ProjectsIcon(props) {
|
||||
return (
|
||||
<svg
|
||||
className={props.className}
|
||||
{...props} width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15 12.95H8C7.59 12.95 7.25 12.61 7.25 12.2C7.25 11.79 7.59 11.45 8 11.45H15C15.41 11.45 15.75 11.79 15.75 12.2C15.75 12.61 15.41 12.95 15 12.95Z" />
|
||||
<path d="M12.38 16.95H8C7.59 16.95 7.25 16.61 7.25 16.2C7.25 15.79 7.59 15.45 8 15.45H12.38C12.79 15.45 13.13 15.79 13.13 16.2C13.13 16.61 12.79 16.95 12.38 16.95Z" />
|
||||
<path d="M14 6.75H10C9.04 6.75 7.25 6.75 7.25 4C7.25 1.25 9.04 1.25 10 1.25H14C14.96 1.25 16.75 1.25 16.75 4C16.75 4.96 16.75 6.75 14 6.75ZM10 2.75C9.01 2.75 8.75 2.75 8.75 4C8.75 5.25 9.01 5.25 10 5.25H14C15.25 5.25 15.25 4.99 15.25 4C15.25 2.75 14.99 2.75 14 2.75H10Z" />
|
||||
<path d="M15 22.75H9C3.38 22.75 2.25 20.17 2.25 16V9.99999C2.25 5.43999 3.9 3.48999 7.96 3.27999C8.36 3.25999 8.73 3.56999 8.75 3.98999C8.77 4.40999 8.45 4.74999 8.04 4.76999C5.2 4.92999 3.75 5.77999 3.75 9.99999V16C3.75 19.7 4.48 21.25 9 21.25H15C19.52 21.25 20.25 19.7 20.25 16V9.99999C20.25 5.77999 18.8 4.92999 15.96 4.76999C15.55 4.74999 15.23 4.38999 15.25 3.97999C15.27 3.56999 15.63 3.24999 16.04 3.26999C20.1 3.48999 21.75 5.43999 21.75 9.98999V15.99C21.75 20.17 20.62 22.75 15 22.75Z" />
|
||||
</svg>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
export default ProjectsIcon;
|
||||
20
src/assets/icons/ShopIcon.jsx
Normal file
20
src/assets/icons/ShopIcon.jsx
Normal file
@@ -0,0 +1,20 @@
|
||||
|
||||
|
||||
|
||||
import React from "react";
|
||||
|
||||
function ShopIcon(props) {
|
||||
return (
|
||||
<svg className={props.className}
|
||||
{...props} width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.6998 22.75H9.29977C4.35977 22.75 2.25977 20.64 2.25977 15.71V11.22C2.25977 10.81 2.59977 10.47 3.00977 10.47C3.41977 10.47 3.75977 10.81 3.75977 11.22V15.71C3.75977 19.8 5.20977 21.25 9.29977 21.25H14.6898C18.7798 21.25 20.2298 19.8 20.2298 15.71V11.22C20.2298 10.81 20.5698 10.47 20.9798 10.47C21.3898 10.47 21.7298 10.81 21.7298 11.22V15.71C21.7398 20.64 19.6298 22.75 14.6998 22.75Z" />
|
||||
<path d="M12 12.75C10.9 12.75 9.9 12.32 9.19 11.53C8.48 10.74 8.15 9.71 8.26 8.61L8.93 1.93C8.97 1.55 9.29 1.25 9.68 1.25H14.35C14.74 1.25 15.06 1.54 15.1 1.93L15.77 8.61C15.88 9.71 15.55 10.74 14.84 11.53C14.1 12.32 13.1 12.75 12 12.75ZM10.35 2.75L9.75 8.76C9.68 9.43 9.88 10.06 10.3 10.52C11.15 11.46 12.85 11.46 13.7 10.52C14.12 10.05 14.32 9.42 14.25 8.76L13.65 2.75H10.35Z" />
|
||||
<path d="M18.31 12.75C16.28 12.75 14.47 11.11 14.26 9.09L13.56 2.08C13.54 1.87 13.61 1.66 13.75 1.5C13.89 1.34 14.09 1.25 14.31 1.25H17.36C20.3 1.25 21.67 2.48 22.08 5.5L22.36 8.28C22.48 9.46 22.12 10.58 21.35 11.43C20.58 12.28 19.5 12.75 18.31 12.75ZM15.14 2.75L15.76 8.94C15.89 10.19 17.05 11.25 18.31 11.25C19.07 11.25 19.75 10.96 20.24 10.43C20.72 9.9 20.94 9.19 20.87 8.43L20.59 5.68C20.28 3.42 19.55 2.75 17.36 2.75H15.14Z" />
|
||||
<path d="M5.64002 12.75C4.45002 12.75 3.37002 12.28 2.60002 11.43C1.83002 10.58 1.47002 9.46 1.59002 8.28L1.86002 5.53C2.28002 2.48 3.65002 1.25 6.59002 1.25H9.64002C9.85002 1.25 10.05 1.34 10.2 1.5C10.35 1.66 10.41 1.87 10.39 2.08L9.69002 9.09C9.48002 11.11 7.67002 12.75 5.64002 12.75ZM6.59002 2.75C4.40002 2.75 3.67002 3.41 3.35002 5.7L3.08002 8.43C3.00002 9.19 3.23002 9.9 3.71002 10.43C4.19002 10.96 4.87002 11.25 5.64002 11.25C6.90002 11.25 8.07002 10.19 8.19002 8.94L8.81002 2.75H6.59002Z" />
|
||||
<path d="M14.5 22.75H9.5C9.09 22.75 8.75 22.41 8.75 22V19.5C8.75 17.4 9.9 16.25 12 16.25C14.1 16.25 15.25 17.4 15.25 19.5V22C15.25 22.41 14.91 22.75 14.5 22.75ZM10.25 21.25H13.75V19.5C13.75 18.24 13.26 17.75 12 17.75C10.74 17.75 10.25 18.24 10.25 19.5V21.25Z" />
|
||||
</svg>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
export default ShopIcon;
|
||||
14
src/assets/icons/SupportIcon.jsx
Normal file
14
src/assets/icons/SupportIcon.jsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from "react";
|
||||
|
||||
function SupportIcon(props) {
|
||||
return (
|
||||
<svg
|
||||
className={props.className}
|
||||
{...props} width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.56971 22.7001C3.22971 22.7001 1.24971 20.72 1.24971 18.38V12.22C1.19971 9.30005 2.27971 6.55005 4.29971 4.49005C6.31971 2.44005 9.02971 1.30005 11.9497 1.30005C17.9097 1.30005 22.7497 6.15005 22.7497 12.1V18.26C22.7497 20.64 20.8097 22.58 18.4297 22.58C16.0897 22.58 14.1097 20.6 14.1097 18.26V15.45C14.1097 14 15.2497 12.86 16.6997 12.86C18.1497 12.86 19.2897 14 19.2897 15.45V18.48C19.2897 18.89 18.9497 19.23 18.5397 19.23C18.1297 19.23 17.7897 18.89 17.7897 18.48V15.45C17.7897 14.77 17.2397 14.36 16.6997 14.36C16.0197 14.36 15.6097 14.91 15.6097 15.45V18.26C15.6097 19.7901 16.8997 21.08 18.4297 21.08C19.9597 21.08 21.2497 19.7901 21.2497 18.26V12.1C21.2497 6.97005 17.0797 2.80005 11.9497 2.80005C9.43971 2.80005 7.10971 3.77005 5.36971 5.54005C3.62971 7.31005 2.69971 9.68005 2.74971 12.2V18.38C2.74971 19.91 4.03971 21.2001 5.56971 21.2001C7.09971 21.2001 8.38971 19.91 8.38971 18.38V15.57C8.38971 14.89 7.83971 14.48 7.29971 14.48C6.61971 14.48 6.20971 15.03 6.20971 15.57V18.49C6.20971 18.9 5.86971 19.24 5.45971 19.24C5.04971 19.24 4.70971 18.9 4.70971 18.49V15.57C4.70971 14.12 5.84971 12.98 7.29971 12.98C8.74971 12.98 9.88971 14.12 9.88971 15.57V18.38C9.88971 20.72 7.90971 22.7001 5.56971 22.7001Z" />
|
||||
</svg>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
export default SupportIcon;
|
||||
16
src/assets/icons/UsersIcon.jsx
Normal file
16
src/assets/icons/UsersIcon.jsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from "react";
|
||||
|
||||
function UsersIcon(props) {
|
||||
return (
|
||||
<svg
|
||||
className={props.className}
|
||||
{...props} width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.16006 11.62C9.13006 11.62 9.11006 11.62 9.08006 11.62C9.03006 11.61 8.96006 11.61 8.90006 11.62C6.00006 11.53 3.81006 9.25 3.81006 6.44C3.81006 3.58 6.14006 1.25 9.00006 1.25C11.8601 1.25 14.1901 3.58 14.1901 6.44C14.1801 9.25 11.9801 11.53 9.19006 11.62C9.18006 11.62 9.17006 11.62 9.16006 11.62ZM9.00006 2.75C6.97006 2.75 5.31006 4.41 5.31006 6.44C5.31006 8.44 6.87006 10.05 8.86006 10.12C8.92006 10.11 9.05006 10.11 9.18006 10.12C11.1401 10.03 12.6801 8.42 12.6901 6.44C12.6901 4.41 11.0301 2.75 9.00006 2.75Z" />
|
||||
<path d="M16.5399 11.75C16.5099 11.75 16.4799 11.75 16.4499 11.74C16.0399 11.78 15.6199 11.49 15.5799 11.08C15.5399 10.67 15.7899 10.3 16.1999 10.25C16.3199 10.24 16.4499 10.24 16.5599 10.24C18.0199 10.16 19.1599 8.96 19.1599 7.49C19.1599 5.97 17.9299 4.74 16.4099 4.74C15.9999 4.75 15.6599 4.41 15.6599 4C15.6599 3.59 15.9999 3.25 16.4099 3.25C18.7499 3.25 20.6599 5.16 20.6599 7.5C20.6599 9.8 18.8599 11.66 16.5699 11.75C16.5599 11.75 16.5499 11.75 16.5399 11.75Z" />
|
||||
<path d="M9.1701 22.55C7.2101 22.55 5.2401 22.05 3.7501 21.05C2.3601 20.13 1.6001 18.87 1.6001 17.5C1.6001 16.13 2.3601 14.86 3.7501 13.93C6.7501 11.94 11.6101 11.94 14.5901 13.93C15.9701 14.85 16.7401 16.11 16.7401 17.48C16.7401 18.85 15.9801 20.12 14.5901 21.05C13.0901 22.05 11.1301 22.55 9.1701 22.55ZM4.5801 15.19C3.6201 15.83 3.1001 16.65 3.1001 17.51C3.1001 18.36 3.6301 19.18 4.5801 19.81C7.0701 21.48 11.2701 21.48 13.7601 19.81C14.7201 19.17 15.2401 18.35 15.2401 17.49C15.2401 16.64 14.7101 15.82 13.7601 15.19C11.2701 13.53 7.0701 13.53 4.5801 15.19Z" />
|
||||
<path d="M18.3402 20.75C17.9902 20.75 17.6802 20.51 17.6102 20.15C17.5302 19.74 17.7902 19.35 18.1902 19.26C18.8202 19.13 19.4002 18.88 19.8502 18.53C20.4202 18.1 20.7302 17.56 20.7302 16.99C20.7302 16.42 20.4202 15.88 19.8602 15.46C19.4202 15.12 18.8702 14.88 18.2202 14.73C17.8202 14.64 17.5602 14.24 17.6502 13.83C17.7402 13.43 18.1402 13.17 18.5502 13.26C19.4102 13.45 20.1602 13.79 20.7702 14.26C21.7002 14.96 22.2302 15.95 22.2302 16.99C22.2302 18.03 21.6902 19.02 20.7602 19.73C20.1402 20.21 19.3602 20.56 18.5002 20.73C18.4402 20.75 18.3902 20.75 18.3402 20.75Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default UsersIcon;
|
||||
21
src/assets/icons/dashboardIcon.jsx
Normal file
21
src/assets/icons/dashboardIcon.jsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from "react";
|
||||
|
||||
function DashboardIcon(props) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
{...props}
|
||||
>
|
||||
<path d="M15 22.75H9c-5.43 0-7.75-2.32-7.75-7.75V9c0-5.43 2.32-7.75 7.75-7.75h6c5.43 0 7.75 2.32 7.75 7.75v6c0 5.43-2.32 7.75-7.75 7.75zm-6-20C4.39 2.75 2.75 4.39 2.75 9v6c0 4.61 1.64 6.25 6.25 6.25h6c4.61 0 6.25-1.64 6.25-6.25V9c0-4.61-1.64-6.25-6.25-6.25H9z"></path>
|
||||
<path d="M15.58 19.25c-.41 0-.75-.34-.75-.75v-3.9c0-.41.34-.75.75-.75s.75.34.75.75v3.9c0 .41-.34.75-.75.75zM15.58 8.2c-.41 0-.75-.34-.75-.75V5.5c0-.41.34-.75.75-.75s.75.34.75.75v1.95c0 .41-.34.75-.75.75z"></path>
|
||||
<path d="M15.58 13.4a3.35 3.35 0 113.35-3.35c0 1.85-1.51 3.35-3.35 3.35zm0-5.2c-1.02 0-1.85.83-1.85 1.85s.83 1.85 1.85 1.85 1.85-.83 1.85-1.85-.84-1.85-1.85-1.85zM8.42 19.25c-.41 0-.75-.34-.75-.75v-1.95c0-.41.34-.75.75-.75s.75.34.75.75v1.95c0 .41-.33.75-.75.75zM8.42 10.15c-.41 0-.75-.34-.75-.75V5.5c0-.41.34-.75.75-.75s.75.34.75.75v3.9c0 .41-.33.75-.75.75z"></path>
|
||||
<path d="M8.42 17.3a3.35 3.35 0 110-6.7 3.35 3.35 0 010 6.7zm0-5.2c-1.02 0-1.85.83-1.85 1.85s.83 1.85 1.85 1.85 1.85-.83 1.85-1.85-.82-1.85-1.85-1.85z"></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardIcon;
|
||||
24
src/components/BaseUrl.js
Normal file
24
src/components/BaseUrl.js
Normal file
@@ -0,0 +1,24 @@
|
||||
export const BaseUrl = "https://api.modstagram.com/api/v1/panel";
|
||||
export const AppBaseUrl = "https://api.modstagram.com/api/v1";
|
||||
export const ImageBaseUrl = "https://api.modstagram.com/storage";
|
||||
|
||||
export function buildStorageUrl(path) {
|
||||
if (!path) return "";
|
||||
if (path.startsWith("http://") || path.startsWith("https://")) return path;
|
||||
|
||||
let normalized = String(path).replace(/\\/g, "/");
|
||||
normalized = normalized.replace("/root/modstagram-back/storage", "");
|
||||
|
||||
if (normalized.startsWith("/storage/")) {
|
||||
normalized = normalized.slice("/storage".length);
|
||||
} else if (normalized.startsWith("storage/")) {
|
||||
normalized = `/${normalized.slice("storage/".length)}`;
|
||||
}
|
||||
|
||||
if (!normalized.startsWith("/")) normalized = `/${normalized}`;
|
||||
return `${ImageBaseUrl.replace(/\/$/, "")}${normalized}`;
|
||||
}
|
||||
|
||||
// export const BaseUrl = "http://localhost:8000/api/v1/panel";
|
||||
// export const AppBaseUrl = "http://localhost:8000/api/v1";
|
||||
// export const ImageBaseUrl = "http://localhost:8000/storage";
|
||||
126
src/components/Filters.jsx
Normal file
126
src/components/Filters.jsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import React from "react";
|
||||
|
||||
const Filters = ({
|
||||
statusOptions = [],
|
||||
provinceOptions = [],
|
||||
cityOptions = [],
|
||||
photographyModelOptions = [],
|
||||
blockStatusOptions = [],
|
||||
onFilterChange = () => {},
|
||||
}) => {
|
||||
const [filters, setFilters] = React.useState({
|
||||
startDate: "",
|
||||
endDate: "",
|
||||
status: "",
|
||||
province: "",
|
||||
city: "",
|
||||
photographyModel: "",
|
||||
blockStatus: "",
|
||||
});
|
||||
|
||||
const handleFilterChange = (name, value) => {
|
||||
const newFilters = { ...filters, [name]: value };
|
||||
if (name === "province") {
|
||||
newFilters.city = ""; // ریست شهر هنگام تغییر استان
|
||||
}
|
||||
setFilters(newFilters);
|
||||
onFilterChange(newFilters);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1 dark:text-gray-200">وضعیت مدارک</label>
|
||||
<select
|
||||
value={filters.status}
|
||||
onChange={(e) => handleFilterChange("status", e.target.value)}
|
||||
className="w-full p-3 rounded-lg border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-teal-500 dark:bg-gray-700 dark:text-white"
|
||||
>
|
||||
{statusOptions?.map((option) => (
|
||||
<option key={option.key} value={option.key}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1 dark:text-gray-200">استان</label>
|
||||
<select
|
||||
value={filters.province}
|
||||
onChange={(e) => handleFilterChange("province", e.target.value)}
|
||||
className="w-full p-3 rounded-lg border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-teal-500 dark:bg-gray-700 dark:text-white"
|
||||
>
|
||||
{provinceOptions?.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1 dark:text-gray-200">شهر</label>
|
||||
<select
|
||||
value={filters.city}
|
||||
onChange={(e) => handleFilterChange("city", e.target.value)}
|
||||
className="w-full p-3 rounded-lg border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-teal-500 dark:bg-gray-700 dark:text-white"
|
||||
>
|
||||
{cityOptions?.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1 dark:text-gray-200">مدل زیبایی عکاسی</label>
|
||||
<select
|
||||
value={filters.photographyModel}
|
||||
onChange={(e) => handleFilterChange("photographyModel", e.target.value)}
|
||||
className="w-full p-3 rounded-lg border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-teal-500 dark:bg-gray-700 dark:text-white"
|
||||
>
|
||||
{photographyModelOptions?.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1 dark:text-gray-200">وضعیت کاربر</label>
|
||||
<select
|
||||
value={filters.blockStatus}
|
||||
onChange={(e) => handleFilterChange("blockStatus", e.target.value)}
|
||||
className="w-full p-3 rounded-lg border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-teal-500 dark:bg-gray-700 dark:text-white"
|
||||
>
|
||||
{blockStatusOptions?.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1 dark:text-gray-200">از تاریخ</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="مثال: 1404-07-04"
|
||||
value={filters.startDate}
|
||||
onChange={(e) => handleFilterChange("startDate", e.target.value)}
|
||||
className="w-full p-3 rounded-lg border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-teal-500 dark:bg-gray-700 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1 dark:text-gray-200">تا تاریخ</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="مثال: 1404-07-04"
|
||||
value={filters.endDate}
|
||||
onChange={(e) => handleFilterChange("endDate", e.target.value)}
|
||||
className="w-full p-3 rounded-lg border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-teal-500 dark:bg-gray-700 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Filters;
|
||||
68
src/components/Pagination.jsx
Normal file
68
src/components/Pagination.jsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import React from "react";
|
||||
|
||||
const Pagination = ({ totalPages, currentPage, onPageChange }) => {
|
||||
if (!totalPages || totalPages <= 1) return null;
|
||||
|
||||
const handlePageClick = (page) => {
|
||||
if (page >= 1 && page <= totalPages) onPageChange(page);
|
||||
};
|
||||
|
||||
const getPageNumbers = () => {
|
||||
const pages = [];
|
||||
if (totalPages <= 7) {
|
||||
for (let i = 1; i <= totalPages; i++) pages.push(i);
|
||||
} else {
|
||||
pages.push(1);
|
||||
let start = Math.max(currentPage - 2, 2);
|
||||
let end = Math.min(currentPage + 2, totalPages - 1);
|
||||
|
||||
if (start > 2) pages.push("...");
|
||||
for (let i = start; i <= end; i++) pages.push(i);
|
||||
if (end < totalPages - 1) pages.push("...");
|
||||
pages.push(totalPages);
|
||||
}
|
||||
return pages;
|
||||
};
|
||||
|
||||
const pages = getPageNumbers();
|
||||
|
||||
return (
|
||||
<div className="flex flex-row-reverse justify-center items-center mt-5">
|
||||
<button
|
||||
onClick={() => handlePageClick(currentPage - 1)}
|
||||
className="px-4 py-2 bg-[#1f5f79] text-white rounded-l-md"
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
قبلی
|
||||
</button>
|
||||
|
||||
{pages.map((page, idx) =>
|
||||
page === "..." ? (
|
||||
<span key={idx} className="px-3 text-white">...</span>
|
||||
) : (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => handlePageClick(page)}
|
||||
className={`px-4 py-2 mx-1 rounded-md ${
|
||||
page === currentPage
|
||||
? "bg-[#247392] text-white"
|
||||
: "bg-[#252525] text-white hover:bg-[#678fa062]"
|
||||
}`}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => handlePageClick(currentPage + 1)}
|
||||
className="px-4 py-2 bg-[#1f5f79] text-white rounded-r-md"
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
بعدی
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Pagination;
|
||||
33
src/components/academy/Header.js
Normal file
33
src/components/academy/Header.js
Normal file
@@ -0,0 +1,33 @@
|
||||
// src/components/Layout/Header.js
|
||||
import React from "react";
|
||||
import { FaBell, FaUserCircle } from "react-icons/fa";
|
||||
|
||||
const Header = ({ toggleSidebar }) => {
|
||||
return (
|
||||
<header className="bg-white shadow-md px-6 py-4 flex justify-between items-center">
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={toggleSidebar}
|
||||
className="p-2 hover:bg-gray-100 rounded-lg transition md:hidden"
|
||||
>
|
||||
☰
|
||||
</button>
|
||||
<h2 className="text-xl font-bold text-gray-800">پنل مدیریت</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<button className="relative p-2 hover:bg-gray-100 rounded-lg transition">
|
||||
<FaBell className="text-gray-600" />
|
||||
<span className="absolute top-0 right-0 w-2 h-2 bg-red-500 rounded-full"></span>
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<FaUserCircle className="text-gray-600 text-2xl" />
|
||||
<span className="text-gray-700">مدیر سیستم</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
27
src/components/academy/Layout.js
Normal file
27
src/components/academy/Layout.js
Normal file
@@ -0,0 +1,27 @@
|
||||
// src/components/Layout/Layout.js
|
||||
import React, { useState } from "react";
|
||||
import Sidebar from "./Sidebar";
|
||||
import Header from "./Header";
|
||||
|
||||
const Layout = ({ children }) => {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(window.innerWidth > 768);
|
||||
|
||||
const toggleSidebar = () => {
|
||||
setSidebarOpen(!sidebarOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100">
|
||||
<Sidebar isOpen={sidebarOpen} toggleSidebar={toggleSidebar} />
|
||||
|
||||
<div className={`transition-all duration-300 ${sidebarOpen ? "md:mr-64" : "md:mr-20"}`}>
|
||||
{/* <Header toggleSidebar={toggleSidebar} /> */}
|
||||
<main className="p-6">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
50
src/components/academy/Sidebar.js
Normal file
50
src/components/academy/Sidebar.js
Normal file
@@ -0,0 +1,50 @@
|
||||
// src/components/Layout/Sidebar.js
|
||||
import React from "react";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import {
|
||||
FaTachometerAlt,
|
||||
FaWallet,
|
||||
FaBook,
|
||||
FaShoppingCart,
|
||||
FaUniversity,
|
||||
FaCog,
|
||||
FaBackspace
|
||||
} from "react-icons/fa";
|
||||
|
||||
const Sidebar = () => {
|
||||
const menuItems = [
|
||||
{ path: "/academy", label: "داشبورد", icon: <FaTachometerAlt /> },
|
||||
{ path: "/payments", label: "مدیریت پرداختها", icon: <FaWallet /> },
|
||||
{ path: "/courses", label: "مدیریت دورهها", icon: <FaBook /> },
|
||||
{ path: "/purchased", label: "دورههای خریداری شده", icon: <FaShoppingCart /> },
|
||||
{ path: "/academies", label: "مدیریت آکادمیها", icon: <FaUniversity /> },
|
||||
{ path: "/settings", label: "تنظیمات", icon: <FaCog /> },
|
||||
{ path: "/", label: "بازگشت", icon: <FaBackspace /> }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="w-64 bg-gray-900 text-white h-screen fixed right-0">
|
||||
<div className="p-4 text-center border-b border-gray-700">
|
||||
<h1 className="text-xl font-bold"> پنل مدیریت دورهها</h1>
|
||||
</div>
|
||||
<nav className="mt-4">
|
||||
{menuItems.map((item) => (
|
||||
<NavLink
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
className={({ isActive }) =>
|
||||
`flex items-center gap-3 px-4 py-3 hover:bg-gray-800 transition ${
|
||||
isActive ? "bg-gray-800 border-r-4 border-pink-500" : ""
|
||||
}`
|
||||
}
|
||||
>
|
||||
{item.icon}
|
||||
<span>{item.label}</span>
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
87
src/components/admin-header.jsx
Normal file
87
src/components/admin-header.jsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Loading from "./elements/loading";
|
||||
import { Link } from "react-router-dom/dist";
|
||||
|
||||
function AdminHeader() {
|
||||
const navigate = useNavigate();
|
||||
const handleLogOut = () => {
|
||||
localStorage.clear();
|
||||
document.getElementById("to_login").click();
|
||||
};
|
||||
function refreshPage() {
|
||||
setTimeout(() => {
|
||||
window.location.reload(false);
|
||||
}, 1000);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Link
|
||||
to={{ pathname: "/login" }}
|
||||
id="to_login"
|
||||
onClick={refreshPage}
|
||||
></Link>
|
||||
<div className="z-[52] sticky mx-auto top-0 w-full h-[65px] bg-[#1f5f79] py-2 px-5">
|
||||
<div className=" mx-auto flex justify-between items-center">
|
||||
<div className="flex flex-row items-center gap-[50px]">
|
||||
<div className="flex cursor-pointer bg-primary group transition-colors duration-500 hover:bg-cyann items-center h-[51px] rounded-[4px] font-Kalameh text-lg min-w-[250px]">
|
||||
<div className="w-[35px] h-[35px] border-[2px] border-[#1f5f79] rounded-full ml-3 flex items-center justify-center overflow-hidden ">
|
||||
<img
|
||||
// src={avatar}
|
||||
className="w-full h-full object-cover"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<h4 className="group-hover:text-white transition-colors duration-500 text-white">
|
||||
{/* {full_name} */}
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center relative">
|
||||
<div
|
||||
// onClick={() => logOutMutation.mutate()}
|
||||
className="flex bg-white cursor-pointer bg-primary hover:bg-[#cbade4] px-5 items-center h-9 group transition-colors duration-500 hover:bg-cyann rounded-[4px] font-KalamehMed font-medium"
|
||||
>
|
||||
<>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8.8999 7.55999C9.2099 3.95999 11.0599 2.48999 15.1099 2.48999H15.2399C19.7099 2.48999 21.4999 4.27999 21.4999 8.74999V15.27C21.4999 19.74 19.7099 21.53 15.2399 21.53H15.1099C11.0899 21.53 9.2399 20.08 8.9099 16.54"
|
||||
stroke="#240B39"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M15.0001 12H3.62012"
|
||||
stroke="#240B39"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M5.85 8.65002L2.5 12L5.85 15.35"
|
||||
stroke="#240B39"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<button className="mr-3 group-hover: transition-colors duration-500 text-[#240B39">
|
||||
خروج
|
||||
</button>
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default AdminHeader;
|
||||
42
src/components/adminApi.js
Normal file
42
src/components/adminApi.js
Normal file
@@ -0,0 +1,42 @@
|
||||
// src/services/adminApi.js
|
||||
import api from "./api";
|
||||
|
||||
// ==================== پرداختها ====================
|
||||
export const getPayments = (params) => api.get("/academy/payments", { params });
|
||||
export const updatePaymentStatus = (data) => api.put("/academy/payments/status", data);
|
||||
export const getPaymentStats = () => api.get("/academy/payments/stats");
|
||||
|
||||
// ==================== دورهها ====================
|
||||
export const getCourses = (params) => api.get("/academy/courses", { params });
|
||||
export const getCourseById = (id) => api.get(`/academy/courses/${id}`);
|
||||
export const createCourse = (data) => api.post("/academy/courses", data);
|
||||
export const updateCourse = (id, data) => api.put(`/academy/courses/${id}`, data);
|
||||
export const updateCourseStatus = (data) => api.put("/academy/academy/courses/status", data);
|
||||
export const deleteCourse = (courseId) => api.delete(`/academy/courses/${courseId}`);
|
||||
export const getCourseStats = () => api.get("/academy/courses/stats");
|
||||
|
||||
// ==================== دورههای خریداری شده ====================
|
||||
export const getPurchasedCourses = (params) => api.get("/academy/purchased", { params });
|
||||
export const getPurchasedStats = () => api.get("/academy/purchased/stats");
|
||||
|
||||
// ==================== آکادمیها ====================
|
||||
export const getAcademies = (params) => api.get("/academy/academies", { params });
|
||||
export const getAcademyById = (id) => api.get(`/academy/academies/${id}`);
|
||||
export const createAcademy = (data) => api.post("/academy/academies", data);
|
||||
export const updateAcademy = (id, data) => api.put(`/academy/academies/${id}`, data);
|
||||
export const updateAcademyStatus = (data) => api.put("/academy/academies/status", data);
|
||||
export const deleteAcademy = (id) => api.delete(`/academy/academies/${id}`);
|
||||
export const getAcademyStats = () => api.get("/academy/academies/stats");
|
||||
|
||||
// ==================== تنظیمات ====================
|
||||
// مسیرهای قدیمی (برای سازگاری)
|
||||
export const getSettings = () => api.get("/academy/settings/all");
|
||||
export const updateSettings = (data) => api.put("/academy/settings/all", data);
|
||||
|
||||
// مسیرهای جدید (جداگانه)
|
||||
export const getCoursePrices = () => api.get("/academy/settings/course-prices");
|
||||
export const updateCoursePrice = (data) => api.put("/academy/settings/course-price", data);
|
||||
export const getTax = () => api.get("/academy/settings/tax");
|
||||
export const updateTax = (data) => api.put("/academy/settings/tax", data);
|
||||
export const getAllSettings = () => api.get("/academy/settings/all");
|
||||
export const updateAllSettings = (data) => api.put("/academy/settings/all", data);
|
||||
45
src/components/api.js
Normal file
45
src/components/api.js
Normal file
@@ -0,0 +1,45 @@
|
||||
// src/services/api.js - نسخه ساده و مطمئن
|
||||
import axios from "axios";
|
||||
import Cookies from "js-cookie";
|
||||
import { AppBaseUrl } from "./BaseUrl";
|
||||
|
||||
const API_BASE_URL = AppBaseUrl || "http://localhost:5000/api";
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
});
|
||||
|
||||
api.interceptors.request.use(
|
||||
(config) => {
|
||||
// توکن
|
||||
const token = Cookies.get("token");
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
// فقط یک تغییر ساده: اضافه کردن timestamp به URL
|
||||
if (config.method === 'get') {
|
||||
const separator = config.url.includes('?') ? '&' : '?';
|
||||
config.url = `${config.url}${separator}_t=${Date.now()}`;
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
);
|
||||
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
Cookies.remove("token");
|
||||
window.location.href = "/login";
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default api;
|
||||
57
src/components/elements/loading.jsx
Normal file
57
src/components/elements/loading.jsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import React from "react";
|
||||
|
||||
function Loading(props) {
|
||||
return (
|
||||
<>
|
||||
<svg
|
||||
width="91px"
|
||||
height="91px"
|
||||
viewBox="0 0 100 100"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
{...props}
|
||||
>
|
||||
<circle
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="37"
|
||||
stroke-width="5"
|
||||
stroke="#4FB3BF80"
|
||||
stroke-dasharray="58.119464091411174 58.119464091411174"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
>
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
type="rotate"
|
||||
dur="1.408450704225352s"
|
||||
repeatCount="indefinite"
|
||||
keyTimes="0;1"
|
||||
values="0 50 50;360 50 50"
|
||||
/>
|
||||
</circle>
|
||||
<circle
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="31"
|
||||
stroke-width="5"
|
||||
stroke="#4FB3BF"
|
||||
stroke-dasharray="48.69468613064179 48.69468613064179"
|
||||
stroke-dashoffset="48.69468613064179"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
>
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
type="rotate"
|
||||
dur="1.408450704225352s"
|
||||
repeatCount="indefinite"
|
||||
keyTimes="0;1"
|
||||
values="0 50 50;-360 50 50"
|
||||
/>
|
||||
</circle>
|
||||
</svg>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Loading;
|
||||
436
src/components/shops/ShopDet.jsx
Normal file
436
src/components/shops/ShopDet.jsx
Normal file
@@ -0,0 +1,436 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { AppBaseUrl, BaseUrl, ImageBaseUrl } from "../BaseUrl";
|
||||
import axios from "axios";
|
||||
import toast from "react-hot-toast";
|
||||
import Modal from "react-modal";
|
||||
const customStyles = {
|
||||
content: {
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
position: "fixed",
|
||||
},
|
||||
};
|
||||
const ShopDet = ({ shopData, id, getUserDetails }) => {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [editedData, setEditedData] = useState({});
|
||||
const [categories, setCategories] = useState([]);
|
||||
const [modalIsOpen, setModalIsOpen] = useState(false);
|
||||
const [modalImage, setModalImage] = useState("");
|
||||
|
||||
const openModal = (image) => {
|
||||
setModalImage(image);
|
||||
setModalIsOpen(true);
|
||||
};
|
||||
const closeModal = () => {
|
||||
setModalIsOpen(false);
|
||||
};
|
||||
|
||||
const getCategories = (page = 1) => {
|
||||
const config = {
|
||||
url: `${AppBaseUrl}/advertising/categories`,
|
||||
method: "GET",
|
||||
};
|
||||
axios(config)
|
||||
.then((res) => {
|
||||
setCategories(res?.data?.categories);
|
||||
})
|
||||
.catch((err) => { });
|
||||
};
|
||||
useEffect(() => {
|
||||
getCategories();
|
||||
}, []);
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setEditedData((prevData) => ({
|
||||
...prevData,
|
||||
[name]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
console.log(editedData);
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
const response = await axios.put(
|
||||
`${BaseUrl}/shops/update?shopId=${shopData._id}`,
|
||||
editedData,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem(
|
||||
"token"
|
||||
)}`, // اگر نیاز به توکن احراز هویت دارید
|
||||
},
|
||||
}
|
||||
);
|
||||
getUserDetails();
|
||||
setIsEditing(false);
|
||||
} catch (error) {
|
||||
if (
|
||||
error.response &&
|
||||
error.response.data &&
|
||||
error.response.data.errors
|
||||
) {
|
||||
error.response.data.errors.forEach((err) => {
|
||||
if (typeof err.message === "string") {
|
||||
// نمایش پیام خطا در صورتیکه به صورت string باشد
|
||||
toast.error(err.message);
|
||||
} else {
|
||||
// نمایش پیام خطا برای موارد دیگر
|
||||
toast.error("خطای ناشناخته");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// نمایش پیام خطا برای سایر موارد
|
||||
toast.error("خطای ناشناخته");
|
||||
}
|
||||
}
|
||||
};
|
||||
const contactInfoArray = shopData?.contactInfo
|
||||
? Object.keys(shopData.contactInfo).map((key) => {
|
||||
let title;
|
||||
switch (key) {
|
||||
case "phone":
|
||||
title = "شماره ثابت";
|
||||
break;
|
||||
case "mobile":
|
||||
title = "شماره همراه";
|
||||
break;
|
||||
case "telegramLink":
|
||||
title = "لینک تلگرام";
|
||||
break;
|
||||
case "whatsappNumber":
|
||||
title = "شماره واتساپ";
|
||||
break;
|
||||
case "instagramLink":
|
||||
title = "لینک اینستاگرام";
|
||||
break;
|
||||
case "saveInfoForNextAds":
|
||||
title = "ذخیره اطلاعات برای آگهیهای بعدی";
|
||||
break;
|
||||
default:
|
||||
title = key;
|
||||
}
|
||||
return { title, value: shopData.contactInfo[key] };
|
||||
})
|
||||
: [];
|
||||
return (
|
||||
<div className="flex gap-3 ">
|
||||
<div className="flex flex-col gap-5 pr-5 mt-5">
|
||||
<Modal
|
||||
isOpen={modalIsOpen}
|
||||
onRequestClose={closeModal}
|
||||
className="post-image-modal"
|
||||
style={customStyles}
|
||||
>
|
||||
{/* استفاده از ImageMagnify */}
|
||||
<img
|
||||
src={modalImage}
|
||||
alt="Post"
|
||||
className="post-modal-image"
|
||||
/>
|
||||
</Modal>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{/* <svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="#00838F"
|
||||
d="M12.12 13.53h-.17c-2.27-.07-3.97-1.84-3.97-4.02 0-2.22 1.81-4.03 4.03-4.03s4.03 1.81 4.03 4.03a4.014 4.014 0 01-3.89 4.02h-.03zM12 6.97c-1.4 0-2.53 1.14-2.53 2.53a2.52 2.52 0 002.43 2.53.84.84 0 01.23 0 2.535 2.535 0 002.4-2.53c0-1.39-1.13-2.53-2.53-2.53zM12 22.75c-2.69 0-5.26-1-7.25-2.82a.755.755 0 01-.24-.63c.13-1.19.87-2.3 2.1-3.12 2.98-1.98 7.81-1.98 10.78 0 1.23.83 1.97 1.93 2.1 3.12.03.24-.06.47-.24.63A10.71 10.71 0 0112 22.75zM6.08 19.1A9.208 9.208 0 0012 21.25c2.17 0 4.26-.76 5.92-2.15-.18-.61-.66-1.2-1.37-1.68-2.46-1.64-6.63-1.64-9.11 0-.71.48-1.18 1.07-1.36 1.68z"
|
||||
></path>
|
||||
<path
|
||||
fill="#00838F"
|
||||
d="M12 22.75C6.07 22.75 1.25 17.93 1.25 12S6.07 1.25 12 1.25 22.75 6.07 22.75 12 17.93 22.75 12 22.75zm0-20C6.9 2.75 2.75 6.9 2.75 12S6.9 21.25 12 21.25s9.25-4.15 9.25-9.25S17.1 2.75 12 2.75z"
|
||||
></path>
|
||||
</svg> */}
|
||||
تصویر پروفایل:
|
||||
</div>
|
||||
<div className="flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 p-4">
|
||||
<img
|
||||
src={ImageBaseUrl + shopData?.profile_image}
|
||||
className="w-[200px] h-[200px] rounded-md"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* User Name */}
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
نام کاربری:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{shopData?.user_name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* First Name */}
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
نام:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{shopData?.first_name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Last Name */}
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
نام خانوادگی:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{shopData?.last_name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile */}
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
دسته بندی:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{isEditing ? (
|
||||
<select
|
||||
defaultValue={shopData.category}
|
||||
className="bg-[#222] text-primary px-2 py-1 rounded-md w-full min-h-9 text-white"
|
||||
onChange={handleChange}
|
||||
name="category"
|
||||
id="category"
|
||||
>
|
||||
{categories &&
|
||||
categories.map((item) => {
|
||||
return (
|
||||
<option value={item?.title}>
|
||||
{item?.title}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
) : (
|
||||
shopData?.category
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* National Code */}
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
عنوان فروشگاه:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="text"
|
||||
name="vitrine_name"
|
||||
defaultValue={shopData.vitrine_name}
|
||||
onChange={handleChange}
|
||||
className="bg-[#222] text-primary px-2 py-1 rounded-md w-full min-h-9"
|
||||
/>
|
||||
) : (
|
||||
shopData?.vitrine_name
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
استان:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{shopData?.province?.name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
شهر:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{shopData?.city?.name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
محله:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{shopData?.neighbourhood}
|
||||
</div>
|
||||
</div>
|
||||
{/* Address */}
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
آدرس:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="text"
|
||||
name="address"
|
||||
defaultValue={shopData.address}
|
||||
onChange={handleChange}
|
||||
className="bg-[#222] text-primary px-2 py-1 rounded-md w-full min-h-9"
|
||||
/>
|
||||
) : (
|
||||
shopData?.address
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
لیست خدمات و کالا
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4 py-4">
|
||||
<div>
|
||||
{shopData?.allServices &&
|
||||
shopData?.allServices?.map((item) => {
|
||||
return (
|
||||
<div className="mt-5">
|
||||
<img
|
||||
onClick={() => {
|
||||
openModal(
|
||||
ImageBaseUrl +
|
||||
item?.image
|
||||
);
|
||||
}}
|
||||
src={ImageBaseUrl + item?.image}
|
||||
className="w-[100px] h-[100px] rounded-sm"
|
||||
alt=""
|
||||
/>
|
||||
<p className="mt-2">
|
||||
نام : {item?.title}
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
درصد تخفیف :{" "}
|
||||
{item?.discountPercentage}
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
قیمت اصلی :{" "}
|
||||
{item?.originalPrice}
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
قیمت تخفیف خورده :{" "}
|
||||
{item?.discountPrice}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
لیست امکانات
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4 py-4">
|
||||
<div>
|
||||
{shopData?.allFeatures &&
|
||||
shopData?.allFeatures?.map((item) => {
|
||||
return (
|
||||
<div className="mt-5 flex">
|
||||
<p className="mt-2">
|
||||
{" "}
|
||||
{item?.title} :{" "}
|
||||
</p>
|
||||
<p className="mt-2 mr-5">
|
||||
{item?.value == true
|
||||
? "دارد"
|
||||
: "ندارد"}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
لیست تماس
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4 py-4">
|
||||
<div>
|
||||
{contactInfoArray.map((item, index) => (
|
||||
<div className="mt-5 flex" key={index}>
|
||||
<p className="mt-2"> {item.title} : </p>
|
||||
<p className="mt-2 mr-5">{item.value}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
تصاویر
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4 py-4 flex-wrap">
|
||||
{shopData?.allImages &&
|
||||
shopData?.allImages?.map((item) => {
|
||||
return (
|
||||
<div>
|
||||
<img
|
||||
onClick={() => {
|
||||
openModal(ImageBaseUrl + item);
|
||||
}}
|
||||
src={ImageBaseUrl + item}
|
||||
className="w-[100px] h-[100px] rounded-sm m-4"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
درباره ما:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="text"
|
||||
name="about_us"
|
||||
defaultValue={shopData.about_us}
|
||||
onChange={handleChange}
|
||||
className="bg-[#222] text-primary px-2 py-1 rounded-md w-full min-h-9"
|
||||
/>
|
||||
) : (
|
||||
shopData?.about_us
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
تاریخ ثبت فروشگاه:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{shopData?.createdAt}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Save Button */}
|
||||
<div className="flex items-center w-full">
|
||||
{isEditing ? (
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="bg-[#146820] hover:bg-[#247392] px-8 h-[35px] rounded-md duration-300"
|
||||
>
|
||||
ذخیره
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setIsEditing(!isEditing)}
|
||||
className="bg-[#146820] hover:bg-[#247392] px-8 h-[35px] rounded-md duration-300 mr-3"
|
||||
>
|
||||
ویرایش
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShopDet;
|
||||
618
src/components/sidebar.jsx
Normal file
618
src/components/sidebar.jsx
Normal file
@@ -0,0 +1,618 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Link, useLocation, useNavigate, useParams } from "react-router-dom";
|
||||
import DashboardIcon from "../assets/icons/dashboardIcon";
|
||||
import UsersIcon from "../assets/icons/UsersIcon";
|
||||
import PostsIcon from "../assets/icons/PostsIcon";
|
||||
import ProjectsIcon from "../assets/icons/ProjectsIcon";
|
||||
import SupportIcon from "../assets/icons/SupportIcon";
|
||||
import FinancialsIcon from "../assets/icons/FinancialsIcon";
|
||||
import CommentsIcon from "../assets/icons/CommentsIcon";
|
||||
import AdvertisingIcon from "../assets/icons/AdvertisingIcon";
|
||||
import ShopIcon from "../assets/icons/ShopIcon";
|
||||
import { GrLicense } from "react-icons/gr";
|
||||
|
||||
function Tooltip({ svgIcon, title }) {
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="relative flex items-center justify-center">
|
||||
<span
|
||||
className="z-50 pl-2 cursor-pointer"
|
||||
onMouseEnter={() => setShowTooltip(true)}
|
||||
onMouseLeave={() => setShowTooltip(false)}
|
||||
>
|
||||
{svgIcon}
|
||||
</span>
|
||||
<div
|
||||
className={`absolute left-12 top-1/2 transform -translate-y-1/2 transition-all duration-300 ease-in-out ${
|
||||
showTooltip ? "opacity-100 translate-x-2" : "opacity-0 translate-x-0"
|
||||
}`}
|
||||
>
|
||||
<div className="relative">
|
||||
<div className="font-sans text-xs font-medium text-white bg-gray-900/80 backdrop-blur-md rounded-md px-3 py-1.5 shadow-xl">
|
||||
{title}
|
||||
</div>
|
||||
<svg
|
||||
className="absolute z-10 w-3 h-3 text-gray-900/80 transform -translate-x-2 translate-y-1 fill-current"
|
||||
width="8"
|
||||
height="8"
|
||||
>
|
||||
<rect x="0" y="2" width="8" height="8" transform="rotate(45)" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Sidebar = () => {
|
||||
const { pathname } = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const param = useParams();
|
||||
const [items, setItems] = useState([]);
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setItems([
|
||||
{
|
||||
menu: "داشبورد",
|
||||
subshow: false,
|
||||
link: "/",
|
||||
submenu: [],
|
||||
icon(pathnamin) {
|
||||
return (
|
||||
<Tooltip
|
||||
svgIcon={
|
||||
<DashboardIcon
|
||||
className={`w-6 h-6 transition-colors duration-300 ${
|
||||
this.subshow || pathnamin === this.link
|
||||
? "fill-neutral-100"
|
||||
: "fill-gray-300 group-hover:fill-neutral-100"
|
||||
}`}
|
||||
/>
|
||||
}
|
||||
title="داشبورد"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
menu: "کاربران",
|
||||
link: "/users",
|
||||
subshow: false,
|
||||
submenu: [],
|
||||
icon(pathnamin) {
|
||||
return (
|
||||
<Tooltip
|
||||
svgIcon={
|
||||
<UsersIcon
|
||||
className={`w-6 h-6 transition-colors duration-300 ${
|
||||
this.subshow || pathnamin === this.link
|
||||
? "fill-neutral-100"
|
||||
: "fill-gray-300 group-hover:fill-neutral-100"
|
||||
}`}
|
||||
/>
|
||||
}
|
||||
title="کاربران"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
menu: "پست ها",
|
||||
link: "/posts",
|
||||
subshow: false,
|
||||
submenu: [],
|
||||
icon(pathnamin) {
|
||||
return (
|
||||
<Tooltip
|
||||
svgIcon={
|
||||
<PostsIcon
|
||||
className={`w-6 h-6 transition-colors duration-300 ${
|
||||
this.subshow || pathnamin === this.link
|
||||
? "fill-neutral-100"
|
||||
: "fill-gray-300 group-hover:fill-neutral-100"
|
||||
}`}
|
||||
/>
|
||||
}
|
||||
title="پست ها"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
menu: "پروژه ها",
|
||||
link: "/projects",
|
||||
subshow: false,
|
||||
submenu: [],
|
||||
icon(pathnamin) {
|
||||
return (
|
||||
<Tooltip
|
||||
svgIcon={
|
||||
<ProjectsIcon
|
||||
className={`w-6 h-6 transition-colors duration-300 ${
|
||||
this.subshow || pathnamin === this.link
|
||||
? "fill-neutral-100"
|
||||
: "fill-gray-300 group-hover:fill-neutral-100"
|
||||
}`}
|
||||
/>
|
||||
}
|
||||
title="پروژه ها"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
menu: "تیکت ها",
|
||||
link: "/tickets",
|
||||
subshow: false,
|
||||
submenu: [],
|
||||
icon(pathnamin) {
|
||||
return (
|
||||
<Tooltip
|
||||
svgIcon={
|
||||
<SupportIcon
|
||||
className={`w-6 h-6 transition-colors duration-300 ${
|
||||
this.subshow || pathnamin === this.link
|
||||
? "fill-neutral-100"
|
||||
: "fill-gray-300 group-hover:fill-neutral-100"
|
||||
}`}
|
||||
/>
|
||||
}
|
||||
title="تیکت ها"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
menu: "مالی",
|
||||
link: "/financials",
|
||||
subshow: false,
|
||||
submenu: [],
|
||||
icon(pathnamin) {
|
||||
return (
|
||||
<Tooltip
|
||||
svgIcon={
|
||||
<FinancialsIcon
|
||||
className={`w-6 h-6 transition-colors duration-300 ${
|
||||
this.subshow || pathnamin === this.link
|
||||
? "fill-neutral-100"
|
||||
: "fill-gray-300 group-hover:fill-neutral-100"
|
||||
}`}
|
||||
/>
|
||||
}
|
||||
title="مالی"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
menu: "نظرات",
|
||||
link: "/comments",
|
||||
subshow: false,
|
||||
submenu: [],
|
||||
icon(pathnamin) {
|
||||
return (
|
||||
<Tooltip
|
||||
svgIcon={
|
||||
<CommentsIcon
|
||||
className={`w-6 h-6 transition-colors duration-300 ${
|
||||
this.subshow || pathnamin === this.link
|
||||
? "fill-neutral-100"
|
||||
: "fill-gray-300 group-hover:fill-neutral-100"
|
||||
}`}
|
||||
/>
|
||||
}
|
||||
title="نظرات"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
menu: "ویترین",
|
||||
link: "/vitrine",
|
||||
subshow: false,
|
||||
submenu: [],
|
||||
icon(pathnamin) {
|
||||
return (
|
||||
<Tooltip
|
||||
svgIcon={
|
||||
<AdvertisingIcon
|
||||
className={`w-6 h-6 transition-colors duration-300 ${
|
||||
this.subshow || pathnamin === this.link
|
||||
? "fill-neutral-100"
|
||||
: "fill-gray-300 group-hover:neutral-100"
|
||||
}`}
|
||||
/>
|
||||
}
|
||||
title="ویترین"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
menu: "نظرات ویترین",
|
||||
link: "/vitrine-comments",
|
||||
subshow: false,
|
||||
submenu: [],
|
||||
icon(pathnamin) {
|
||||
return (
|
||||
<Tooltip
|
||||
svgIcon={
|
||||
<ShopIcon
|
||||
className={`w-6 h-6 transition-colors duration-300 ${
|
||||
this.subshow || pathnamin === this.link
|
||||
? "fill-gray-500"
|
||||
: "fill-gray-300 group-hover:fill-neutral-100"
|
||||
}`}
|
||||
/>
|
||||
}
|
||||
title="نظرات ویترین"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
menu: "فروشگاه",
|
||||
link: "/shops",
|
||||
subshow: false,
|
||||
submenu: [],
|
||||
icon(pathnamin) {
|
||||
return (
|
||||
<Tooltip
|
||||
svgIcon={
|
||||
<ShopIcon
|
||||
className={`w-6 h-6 transition-colors duration-300 ${
|
||||
this.subshow || pathnamin === this.link
|
||||
? "fill-neutral-100"
|
||||
: "fill-gray-300 group-hover:fill-neutral-100"
|
||||
}`}
|
||||
/>
|
||||
}
|
||||
title="فروشگاه"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
menu: "اموزشگاه",
|
||||
link: "/academy",
|
||||
subshow: false,
|
||||
submenu: [],
|
||||
icon(pathnamin) {
|
||||
return (
|
||||
<Tooltip
|
||||
svgIcon={
|
||||
<ShopIcon
|
||||
className={`w-6 h-6 transition-colors duration-300 ${
|
||||
this.subshow || pathnamin === this.link
|
||||
? "fill-neutral-100"
|
||||
: "fill-gray-300 group-hover:fill-neutral-100"
|
||||
}`}
|
||||
/>
|
||||
}
|
||||
title="اموزشگاه"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
menu: "مجوز",
|
||||
link: "/license",
|
||||
subshow: false,
|
||||
submenu: [],
|
||||
icon(pathnamin) {
|
||||
return (
|
||||
<Tooltip
|
||||
svgIcon={
|
||||
<GrLicense
|
||||
className={`w-6 h-6 transition-colors duration-300 ${
|
||||
this.subshow || pathnamin === this.link
|
||||
? "fill-neutral-100"
|
||||
: "fill-gray-300 group-hover:fill-neutral-100"
|
||||
}`}
|
||||
/>
|
||||
}
|
||||
title="مجوز"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
menu: "ورژن برنامه",
|
||||
link: "/version",
|
||||
subshow: false,
|
||||
submenu: [],
|
||||
icon(pathnamin) {
|
||||
return (
|
||||
<Tooltip
|
||||
svgIcon={
|
||||
<svg
|
||||
className={`w-6 h-6 transition-colors duration-300 ${
|
||||
this.subshow || pathnamin === this.link
|
||||
? "fill-neutral-100"
|
||||
: "fill-gray-300 group-hover:fill-neutral-100"
|
||||
}`}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M12 2a10 10 0 0 0-10 10c0 5.52 4.48 10 10 10s10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-14a6 6 0 0 0-6 6c0 3.31 2.69 6 6 6s6-2.69 6-6-2.69-6-6-6zm0 10c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4z" />
|
||||
</svg>
|
||||
}
|
||||
title="ورژن برنامه"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
menu: "تخصص",
|
||||
link: "/expertise",
|
||||
subshow: false,
|
||||
submenu: [],
|
||||
icon(pathnamin) {
|
||||
return (
|
||||
<Tooltip
|
||||
svgIcon={
|
||||
<svg
|
||||
className={`w-6 h-6 transition-colors duration-300 ${
|
||||
this.subshow || pathnamin === this.link
|
||||
? "fill-neutral-100"
|
||||
: "fill-gray-300 group-hover:fill-neutral-100"
|
||||
}`}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M12 2a10 10 0 0 0-10 10c0 5.52 4.48 10 10 10s10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1h-6v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z" />
|
||||
</svg>
|
||||
}
|
||||
title="تخصص"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
menu: "قوانین مقررات",
|
||||
link: "/terms",
|
||||
subshow: false,
|
||||
submenu: [],
|
||||
icon(pathnamin) {
|
||||
return (
|
||||
<Tooltip
|
||||
svgIcon={
|
||||
<ShopIcon
|
||||
className={`w-6 h-6 transition-colors duration-300 ${
|
||||
this.subshow || pathnamin === this.link
|
||||
? "fill-neutral-100"
|
||||
: "fill-gray-300 group-hover:neutral-100"
|
||||
}`}
|
||||
/>
|
||||
}
|
||||
title="قوانین مقررات"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
menu: "تنظیمات",
|
||||
link: "/system-settings",
|
||||
subshow: false,
|
||||
submenu: [],
|
||||
icon(pathnamin) {
|
||||
return (
|
||||
<Tooltip
|
||||
svgIcon={
|
||||
<svg
|
||||
className={`w-6 h-6 transition-colors duration-300 ${
|
||||
this.subshow || pathnamin === this.link
|
||||
? "fill-neutral-100"
|
||||
: "fill-gray-300 group-hover:fill-neutral-100"
|
||||
}`}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M19.14 12.94c.04-.31.06-.63.06-.94 0-.31-.02-.63-.06-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z" />
|
||||
</svg>
|
||||
}
|
||||
title="تنظیمات"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
menu: "خروج",
|
||||
subshow: false,
|
||||
link: "#",
|
||||
submenu: [],
|
||||
icon(pathnamin) {
|
||||
return (
|
||||
<Tooltip
|
||||
svgIcon={
|
||||
<button className="flex items-center group outline-none">
|
||||
<svg
|
||||
className={`w-6 h-6 transition-colors duration-300 ${
|
||||
this.subshow || pathnamin.includes(this.link)
|
||||
? "fill-neutral-100"
|
||||
: "fill-gray-300 group-hover:fill-neutral-100"
|
||||
}`}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M17.44 15.87c-.19 0-.38-.07-.53-.22a.754.754 0 010-1.06l2.03-2.03-2.03-2.03a.754.754 0 010-1.06c.29-.29.77-.29 1.06 0l2.56 2.56c.29.29.29.77 0 1.06l-2.56 2.56c-.15.15-.34.22-.53.22z" />
|
||||
<path d="M19.93 13.31H9.76c-.41 0-.75-.34-.75-.75s.34-.75.75-.75h10.17c.41 0 .75.34.75.75s-.34.75-.75.75z" />
|
||||
<path d="M11.76 21.25c-5.15 0-8.75-3.6-8.75-8.75s3.6-8.75 8.75-8.75c.41 0 .75.34.75.75s-.34.75-.75.75c-4.27 0-7.25 2.98-7.25 7.25s2.98 7.25 7.25 7.25c.41 0 .75.34.75.75s-.34.75-.75.75z" />
|
||||
</svg>
|
||||
</button>
|
||||
}
|
||||
title="خروج"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let indexOfItem = 0;
|
||||
for (let item of items) {
|
||||
indexOfItem++;
|
||||
for (let submenu of item.submenu) {
|
||||
if (pathname.replace("/", "").includes(submenu?.link.replace("/", ""))) {
|
||||
setItems((prev) =>
|
||||
prev.map((sidebarItem, i) =>
|
||||
i === indexOfItem - 1
|
||||
? { ...sidebarItem, subshow: true }
|
||||
: sidebarItem
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [pathname, items]);
|
||||
|
||||
const navigateToMenu = (item) => {
|
||||
navigate(item?.link);
|
||||
setItems((prev) =>
|
||||
prev.map((sidebarItem) => ({ ...sidebarItem, subshow: false }))
|
||||
);
|
||||
};
|
||||
|
||||
const showSubMenu = (index) => {
|
||||
setItems((prev) =>
|
||||
prev.map((sidebarItem, i) =>
|
||||
i === index
|
||||
? { ...sidebarItem, subshow: !sidebarItem.subshow }
|
||||
: { ...sidebarItem, subshow: false }
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const handleLogOut = () => {
|
||||
localStorage.clear();
|
||||
document.getElementById("to_login").click();
|
||||
};
|
||||
|
||||
function refreshPage() {
|
||||
setTimeout(() => {
|
||||
window.location.reload(false);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
const toggleSidebar = () => {
|
||||
setIsCollapsed(!isCollapsed);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`fixed top-0 z-[9999] h-screen bg-gradient-to-b from-gray-800 to-gray-900 text-white font-sans overflow-y-auto scrollbar-hidden flex flex-col p-4 pt-12 shadow-2xl transition-all duration-300 ${
|
||||
isCollapsed ? "w-16" : "w-64"
|
||||
}`}
|
||||
>
|
||||
|
||||
<Link to="/login" id="to_login" onClick={refreshPage} className="hidden" />
|
||||
<div className="flex-1">
|
||||
{items.map((item, index) => (
|
||||
<div key={index} className=" mt-4">
|
||||
{item.submenu.length === 0 ? (
|
||||
<button
|
||||
onClick={() => {
|
||||
if (item.menu === "خروج") {
|
||||
handleLogOut();
|
||||
} else {
|
||||
navigateToMenu(item);
|
||||
}
|
||||
}}
|
||||
className={`flex items-center w-full p-2 rounded-xl transition-all duration-300 group relative ${
|
||||
pathname.replace("/", "").includes(item.link.replace("/", ""))
|
||||
? "bg-gray-500/80 text-white backdrop-blur-sm shadow-md"
|
||||
: "hover:bg-gray-700/80 hover:backdrop-blur-sm hover:shadow-md"
|
||||
}`}
|
||||
>
|
||||
<div className={`flex items-center ${isCollapsed ? "justify-center" : "space-x-3"}`}>
|
||||
{item.icon(pathname)}
|
||||
{!isCollapsed && (
|
||||
<span
|
||||
className={`text-sm font-medium transition-colors duration-300 ${
|
||||
pathname.replace("/", "").includes(item.link.replace("/", ""))
|
||||
? "text-white"
|
||||
: "text-gray-200 group-hover:text-white"
|
||||
}`}
|
||||
>
|
||||
{item.menu}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => showSubMenu(index)}
|
||||
className={`flex items-center w-full p-3 rounded-xl transition-all duration-300 group relative ${
|
||||
item.subshow
|
||||
? "bg-teal-600/80 text-white backdrop-blur-sm shadow-md"
|
||||
: "hover:bg-teal-700/80 hover:backdrop-blur-sm hover:shadow-md"
|
||||
}`}
|
||||
>
|
||||
<div className={`flex items-center ${isCollapsed ? "justify-center" : "space-x-3"}`}>
|
||||
{item.icon(pathname)}
|
||||
{!isCollapsed && (
|
||||
<span
|
||||
className={`text-sm font-medium transition-colors duration-300 ${
|
||||
item.subshow ? "text-white" : "text-gray-200 group-hover:text-white"
|
||||
}`}
|
||||
>
|
||||
{item.menu}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{!isCollapsed && item.submenu.length > 0 && (
|
||||
<svg
|
||||
className={`w-4 h-4 mr-auto transition-transform duration-300 ${
|
||||
item.subshow ? "rotate-180" : ""
|
||||
}`}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M7 10l5 5 5-5H7z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{!isCollapsed && (
|
||||
<div
|
||||
className={`ml-4 transition-all duration-300 ease-in-out ${
|
||||
item.subshow ? "max-h-[800px] opacity-100" : "max-h-0 opacity-0 overflow-hidden"
|
||||
}`}
|
||||
>
|
||||
{item.submenu.map((submenuItem, subIndex) => {
|
||||
const isActive = pathname
|
||||
.replace("/", "")
|
||||
.includes(submenuItem.link.replace("/", ""));
|
||||
return (
|
||||
<Link
|
||||
key={subIndex}
|
||||
to={submenuItem.link}
|
||||
className={`flex items-center p-2 rounded-lg transition-all duration-200 text-sm ${
|
||||
isActive
|
||||
? "bg-neutral-100/80 text-white backdrop-blur-sm"
|
||||
: "text-gray-300 hover:bg-neutral-100/80 hover:text-white hover:backdrop-blur-sm"
|
||||
}`}
|
||||
>
|
||||
{submenuItem.menu}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
158
src/components/system-settings/CdnSettingsTab.jsx
Normal file
158
src/components/system-settings/CdnSettingsTab.jsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import axios from "axios";
|
||||
import { BaseUrl } from "../BaseUrl";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
const inputClass =
|
||||
"w-full p-3 rounded bg-gray-900 text-white focus:ring-2 focus:ring-[#00cc99] focus:outline-none text-sm";
|
||||
|
||||
function CdnSettingsTab() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [form, setForm] = useState({
|
||||
cdn_enabled: false,
|
||||
cdn_domain: "cdn.modstagram.com",
|
||||
cdn_origin_url: "https://api.modstagram.com",
|
||||
cdn_ns1: "ns1.modstagram.com",
|
||||
cdn_ns2: "ns2.modstagram.com",
|
||||
cdn_cache_ttl: "3600",
|
||||
});
|
||||
|
||||
const fetchSettings = () => {
|
||||
setLoading(true);
|
||||
axios
|
||||
.get(`${BaseUrl}/settings/cdn`)
|
||||
.then((res) => {
|
||||
const s = res.data?.settings || {};
|
||||
setForm({
|
||||
cdn_enabled: s.cdn_enabled === "true",
|
||||
cdn_domain: s.cdn_domain || "cdn.modstagram.com",
|
||||
cdn_origin_url: s.cdn_origin_url || "https://api.modstagram.com",
|
||||
cdn_ns1: s.cdn_ns1 || "ns1.modstagram.com",
|
||||
cdn_ns2: s.cdn_ns2 || "ns2.modstagram.com",
|
||||
cdn_cache_ttl: s.cdn_cache_ttl || "3600",
|
||||
});
|
||||
})
|
||||
.catch(() => toast.error("خطا در دریافت تنظیمات CDN"))
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
}, []);
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value, type, checked } = e.target;
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
[name]: type === "checkbox" ? checked : value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
setSaving(true);
|
||||
axios
|
||||
.put(`${BaseUrl}/settings/cdn`, {
|
||||
settings: {
|
||||
cdn_enabled: String(form.cdn_enabled),
|
||||
cdn_domain: form.cdn_domain,
|
||||
cdn_origin_url: form.cdn_origin_url,
|
||||
cdn_ns1: form.cdn_ns1,
|
||||
cdn_ns2: form.cdn_ns2,
|
||||
cdn_cache_ttl: form.cdn_cache_ttl,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("تنظیمات CDN ذخیره شد");
|
||||
fetchSettings();
|
||||
})
|
||||
.catch(() => toast.error("خطا در ذخیره تنظیمات"))
|
||||
.finally(() => setSaving(false));
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <p className="text-center text-gray-400 mt-10">در حال بارگذاری...</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="p-4 border border-gray-700 rounded-lg bg-gray-800 text-sm leading-7">
|
||||
<h3 className="text-[#00cc99] font-bold mb-2">راهنمای DNS</h3>
|
||||
<p>۱. در پنل رجیسترار دامنه، Nameserverها را به این مقادیر تغییر دهید:</p>
|
||||
<p className="font-mono text-yellow-300 mt-1">{form.cdn_ns1}</p>
|
||||
<p className="font-mono text-yellow-300">{form.cdn_ns2}</p>
|
||||
<p className="mt-2">۲. رکورد A دامنه CDN ({form.cdn_domain}) را به IP سرور CDN اشاره دهید.</p>
|
||||
<p>۳. Origin سرور: {form.cdn_origin_url}</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-5 bg-gray-800 p-6 rounded-lg">
|
||||
<label className="inline-flex items-center text-gray-300">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="cdn_enabled"
|
||||
checked={form.cdn_enabled}
|
||||
onChange={handleChange}
|
||||
className="ml-2 h-4 w-4"
|
||||
/>
|
||||
فعالسازی CDN
|
||||
</label>
|
||||
|
||||
<div>
|
||||
<label className="block mb-1 text-gray-300">دامنه CDN</label>
|
||||
<input
|
||||
className={inputClass}
|
||||
name="cdn_domain"
|
||||
value={form.cdn_domain}
|
||||
onChange={handleChange}
|
||||
placeholder="cdn.modstagram.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block mb-1 text-gray-300">آدرس Origin (سرور اصلی)</label>
|
||||
<input
|
||||
className={inputClass}
|
||||
name="cdn_origin_url"
|
||||
value={form.cdn_origin_url}
|
||||
onChange={handleChange}
|
||||
placeholder="https://api.modstagram.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block mb-1 text-gray-300">NS1</label>
|
||||
<input className={inputClass} name="cdn_ns1" value={form.cdn_ns1} onChange={handleChange} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-1 text-gray-300">NS2</label>
|
||||
<input className={inputClass} name="cdn_ns2" value={form.cdn_ns2} onChange={handleChange} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block mb-1 text-gray-300">مدت کش (ثانیه)</label>
|
||||
<input
|
||||
className={inputClass}
|
||||
name="cdn_cache_ttl"
|
||||
value={form.cdn_cache_ttl}
|
||||
onChange={handleChange}
|
||||
type="number"
|
||||
min="60"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={saving}
|
||||
className="bg-[#00cc99] text-black py-3 px-8 rounded-lg font-bold hover:bg-[#00b388] disabled:opacity-60"
|
||||
>
|
||||
{saving ? "در حال ذخیره..." : "ذخیره تنظیمات CDN"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CdnSettingsTab;
|
||||
161
src/components/system-settings/SslSettingsTab.jsx
Normal file
161
src/components/system-settings/SslSettingsTab.jsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import axios from "axios";
|
||||
import { BaseUrl } from "../BaseUrl";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
const statusLabels = {
|
||||
active: "فعال",
|
||||
pending: "در حال صدور",
|
||||
expired: "منقضی شده",
|
||||
failed: "خطا",
|
||||
none: "غیرفعال",
|
||||
};
|
||||
|
||||
const statusColors = {
|
||||
active: "text-green-400",
|
||||
pending: "text-yellow-400",
|
||||
expired: "text-red-400",
|
||||
failed: "text-red-400",
|
||||
none: "text-gray-400",
|
||||
};
|
||||
|
||||
function SslSettingsTab() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activating, setActivating] = useState(false);
|
||||
const [sslInfo, setSslInfo] = useState(null);
|
||||
const [autoRenew, setAutoRenew] = useState(true);
|
||||
const [email, setEmail] = useState("admin@modstagram.com");
|
||||
|
||||
const fetchSslStatus = () => {
|
||||
setLoading(true);
|
||||
axios
|
||||
.get(`${BaseUrl}/settings/cdn/ssl/status`)
|
||||
.then((res) => {
|
||||
setSslInfo(res.data);
|
||||
})
|
||||
.catch(() => toast.error("خطا در دریافت وضعیت SSL"))
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
const fetchAutoRenew = () => {
|
||||
axios.get(`${BaseUrl}/settings/cdn`).then((res) => {
|
||||
setAutoRenew(res.data?.settings?.ssl_auto_renew === "true");
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchSslStatus();
|
||||
fetchAutoRenew();
|
||||
}, []);
|
||||
|
||||
const handleActivateSsl = () => {
|
||||
setActivating(true);
|
||||
axios
|
||||
.post(`${BaseUrl}/settings/cdn/ssl/activate`, { email })
|
||||
.then((res) => {
|
||||
toast.success(res.data?.message || "SSL فعال شد");
|
||||
fetchSslStatus();
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.response?.data?.message || "خطا در فعالسازی SSL");
|
||||
})
|
||||
.finally(() => setActivating(false));
|
||||
};
|
||||
|
||||
const handleSaveAutoRenew = () => {
|
||||
axios
|
||||
.put(`${BaseUrl}/settings/cdn`, {
|
||||
settings: { ssl_auto_renew: String(autoRenew) },
|
||||
})
|
||||
.then(() => toast.success("تمدید خودکار SSL ذخیره شد"))
|
||||
.catch(() => toast.error("خطا در ذخیره"));
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <p className="text-center text-gray-400 mt-10">در حال بارگذاری...</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="p-4 border border-gray-700 rounded-lg bg-gray-800">
|
||||
<h3 className="text-[#00cc99] font-bold mb-3">وضعیت SSL رایگان (Let's Encrypt)</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
|
||||
<p>
|
||||
<span className="text-gray-400">دامنه: </span>
|
||||
{sslInfo?.domain || "-"}
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-gray-400">وضعیت: </span>
|
||||
<span className={statusColors[sslInfo?.status] || "text-gray-300"}>
|
||||
{statusLabels[sslInfo?.status] || sslInfo?.status}
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-gray-400">تاریخ انقضا: </span>
|
||||
{sslInfo?.expires_at
|
||||
? new Date(sslInfo.expires_at).toLocaleDateString("fa-IR")
|
||||
: "نامشخص"}
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-gray-400">گواهی روی سرور: </span>
|
||||
{sslInfo?.cert_found ? "یافت شد" : "یافت نشد"}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={fetchSslStatus}
|
||||
className="mt-4 text-sm text-[#00cc99] hover:underline"
|
||||
type="button"
|
||||
>
|
||||
بروزرسانی وضعیت
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-6 border border-gray-700 rounded-lg bg-gray-800 space-y-4">
|
||||
<p className="text-sm text-gray-300 leading-7">
|
||||
SSL رایگان از طریق Let's Encrypt صادر میشود. قبل از فعالسازی، مطمئن شوید DNS دامنه CDN
|
||||
به IP سرور اشاره کرده و Nginx نصب است.
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<label className="block mb-1 text-gray-300 text-sm">ایمیل ادمین (برای Let's Encrypt)</label>
|
||||
<input
|
||||
className="w-full p-3 rounded bg-gray-900 text-white focus:ring-2 focus:ring-[#00cc99] focus:outline-none text-sm"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="admin@modstagram.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="inline-flex items-center text-gray-300 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={autoRenew}
|
||||
onChange={(e) => setAutoRenew(e.target.checked)}
|
||||
className="ml-2 h-4 w-4"
|
||||
/>
|
||||
تمدید خودکار SSL (هر روز ساعت ۳ صبح)
|
||||
</label>
|
||||
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleActivateSsl}
|
||||
disabled={activating}
|
||||
className="bg-[#00cc99] text-black py-3 px-6 rounded-lg font-bold hover:bg-[#00b388] disabled:opacity-60"
|
||||
>
|
||||
{activating ? "در حال فعالسازی..." : "فعالسازی SSL رایگان"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSaveAutoRenew}
|
||||
className="bg-[#1f5f79] text-white py-3 px-6 rounded-lg font-bold hover:bg-[#478F95]"
|
||||
>
|
||||
ذخیره تمدید خودکار
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SslSettingsTab;
|
||||
94
src/components/system-settings/TrafficSettingsTab.jsx
Normal file
94
src/components/system-settings/TrafficSettingsTab.jsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import axios from "axios";
|
||||
import { BaseUrl } from "../BaseUrl";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
const inputClass =
|
||||
"w-full p-3 rounded bg-gray-900 text-white focus:ring-2 focus:ring-[#00cc99] focus:outline-none text-sm";
|
||||
|
||||
function TrafficSettingsTab() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [form, setForm] = useState({
|
||||
rate_limit_requests: "3000",
|
||||
rate_limit_window_minutes: "3",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
axios
|
||||
.get(`${BaseUrl}/settings/cdn`)
|
||||
.then((res) => {
|
||||
const s = res.data?.settings || {};
|
||||
setForm({
|
||||
rate_limit_requests: s.rate_limit_requests || "3000",
|
||||
rate_limit_window_minutes: s.rate_limit_window_minutes || "3",
|
||||
});
|
||||
})
|
||||
.catch(() => toast.error("خطا در دریافت تنظیمات ترافیک"))
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setForm((prev) => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
setSaving(true);
|
||||
axios
|
||||
.put(`${BaseUrl}/settings/cdn`, { settings: form })
|
||||
.then(() => toast.success("تنظیمات ترافیک ذخیره شد"))
|
||||
.catch(() => toast.error("خطا در ذخیره"))
|
||||
.finally(() => setSaving(false));
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <p className="text-center text-gray-400 mt-10">در حال بارگذاری...</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="p-4 border border-gray-700 rounded-lg bg-gray-800 text-sm text-gray-300 leading-7">
|
||||
<p>
|
||||
با این تنظیمات میتوانید سقف درخواستهای API را کنترل کنید. برای اعمال روی سرور،
|
||||
بعد از ذخیره یکبار <code className="text-yellow-300">pm2 restart all</code> بزنید.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-5 bg-gray-800 p-6 rounded-lg max-w-lg">
|
||||
<div>
|
||||
<label className="block mb-1 text-gray-300">حداکثر درخواست در بازه</label>
|
||||
<input
|
||||
className={inputClass}
|
||||
name="rate_limit_requests"
|
||||
value={form.rate_limit_requests}
|
||||
onChange={handleChange}
|
||||
type="number"
|
||||
min="100"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-1 text-gray-300">بازه زمانی (دقیقه)</label>
|
||||
<input
|
||||
className={inputClass}
|
||||
name="rate_limit_window_minutes"
|
||||
value={form.rate_limit_window_minutes}
|
||||
onChange={handleChange}
|
||||
type="number"
|
||||
min="1"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={saving}
|
||||
className="bg-[#00cc99] text-black py-3 px-8 rounded-lg font-bold hover:bg-[#00b388] disabled:opacity-60"
|
||||
>
|
||||
{saving ? "در حال ذخیره..." : "ذخیره تنظیمات ترافیک"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TrafficSettingsTab;
|
||||
208
src/components/users/UserChangePassword.jsx
Normal file
208
src/components/users/UserChangePassword.jsx
Normal file
@@ -0,0 +1,208 @@
|
||||
import React, { useState } from 'react';
|
||||
import axios from 'axios';
|
||||
import { BaseUrl } from '../BaseUrl';
|
||||
import toast, { Toaster } from 'react-hot-toast';
|
||||
import { FiEye, FiEyeOff, FiLock, FiCheckCircle } from 'react-icons/fi';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
const UserChangePassword = () => {
|
||||
const { id } = useParams(); // آیدی کاربر
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
});
|
||||
const [showPassword, setShowPassword] = useState({
|
||||
new: false,
|
||||
confirm: false,
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||
if (errors[name]) setErrors((prev) => ({ ...prev, [name]: '' }));
|
||||
};
|
||||
|
||||
const togglePasswordVisibility = (field) => {
|
||||
setShowPassword((prev) => ({ ...prev, [field]: !prev[field] }));
|
||||
};
|
||||
|
||||
const validatePassword = (password) => {
|
||||
const errors = [];
|
||||
if (password.length < 8) errors.push('حداقل 8 کاراکتر');
|
||||
if (!/[A-Z]/.test(password)) errors.push('حداقل یک حرف بزرگ');
|
||||
if (!/[a-z]/.test(password)) errors.push('حداقل یک حرف کوچک');
|
||||
if (!/[0-9]/.test(password)) errors.push('حداقل یک عدد');
|
||||
return errors;
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
const newErrors = {};
|
||||
|
||||
// اعتبارسنجی پسورد جدید
|
||||
const passwordErrors = validatePassword(formData.newPassword);
|
||||
if (passwordErrors.length > 0) {
|
||||
newErrors.newPassword = passwordErrors.join('، ');
|
||||
}
|
||||
|
||||
// تطابق پسوردها
|
||||
if (formData.newPassword !== formData.confirmPassword) {
|
||||
newErrors.confirmPassword = 'پسوردها مطابقت ندارند';
|
||||
}
|
||||
|
||||
if (Object.keys(newErrors).length > 0) {
|
||||
setErrors(newErrors);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
await axios.post(
|
||||
`${BaseUrl}/users/change-password`,
|
||||
{
|
||||
userId: id,
|
||||
password: formData.newPassword, // فقط پسورد جدید
|
||||
}
|
||||
|
||||
);
|
||||
|
||||
// toast.success('رمز عبور کاربر با موفقیت تغییر کرد', {
|
||||
// icon: <FiCheckCircle className="text-green-500" />,
|
||||
// style: { background: '#10b981', color: 'white' },
|
||||
// });
|
||||
|
||||
// ریست فرم
|
||||
setFormData({ newPassword: '', confirmPassword: '' });
|
||||
} catch (err) {
|
||||
const message = err.response?.data?.message || 'خطا در تغییر رمز عبور';
|
||||
toast.error(message, { style: { background: '#ef4444', color: 'white' } });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<div className="min-h-screen bg-gradient-to-br flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-md">
|
||||
<div className="bg-slate-800/50 backdrop-blur-xl rounded-2xl shadow-2xl border border-slate-700 p-8">
|
||||
<div className="text-center mb-8">
|
||||
<div className="mx-auto w-16 h-16 bg-gradient-to-r from-cyan-500 to-purple-600 rounded-full flex items-center justify-center mb-4">
|
||||
<FiLock className="text-white text-2xl" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-white">تغییر رمز عبور کاربر</h1>
|
||||
<p className="text-slate-400 mt-2">
|
||||
آیدی کاربر: <span className="text-cyan-400 font-mono">{id}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
|
||||
{/* پسورد جدید */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-300 mb-2">
|
||||
رمز عبور جدید
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showPassword.new ? 'text' : 'password'}
|
||||
name="newPassword"
|
||||
value={formData.newPassword}
|
||||
onChange={handleChange}
|
||||
className={`w-full px-4 py-3 bg-slate-700/50 border ${
|
||||
errors.newPassword ? 'border-red-500' : 'border-slate-600'
|
||||
} rounded-xl text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-cyan-500 transition-all`}
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => togglePasswordVisibility('new')}
|
||||
className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-white"
|
||||
>
|
||||
{showPassword.new ? <FiEyeOff /> : <FiEye />}
|
||||
</button>
|
||||
</div>
|
||||
{errors.newPassword && (
|
||||
<p className="mt-1 text-xs text-red-400">{errors.newPassword}</p>
|
||||
)}
|
||||
<div className="mt-2 space-y-1 text-xs text-slate-400">
|
||||
<p className={formData.newPassword.length >= 8 ? 'text-green-400' : ''}>
|
||||
حداقل 8 کاراکتر
|
||||
</p>
|
||||
<p className={/[A-Z]/.test(formData.newPassword) ? 'text-green-400' : ''}>
|
||||
یک حرف بزرگ
|
||||
</p>
|
||||
<p className={/[a-z]/.test(formData.newPassword) ? 'text-green-400' : ''}>
|
||||
یک حرف کوچک
|
||||
</p>
|
||||
<p className={/[0-9]/.test(formData.newPassword) ? 'text-green-400' : ''}>
|
||||
یک عدد
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* تأیید پسورد */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-300 mb-2">
|
||||
تکرار رمز عبور جدید
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showPassword.confirm ? 'text' : 'password'}
|
||||
name="confirmPassword"
|
||||
value={formData.confirmPassword}
|
||||
onChange={handleChange}
|
||||
className={`w-full px-4 py-3 bg-slate-700/50 border ${
|
||||
errors.confirmPassword ? 'border-red-500' : 'border-slate-600'
|
||||
} rounded-xl text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-cyan-500 transition-all`}
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => togglePasswordVisibility('confirm')}
|
||||
className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-white"
|
||||
>
|
||||
{showPassword.confirm ? <FiEyeOff /> : <FiEye />}
|
||||
</button>
|
||||
</div>
|
||||
{errors.confirmPassword && (
|
||||
<p className="mt-1 text-xs text-red-400">{errors.confirmPassword}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* دکمه ارسال */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className={`w-full py-3 px-4 rounded-xl font-medium text-white transition-all duration-300 flex items-center justify-center gap-2 ${
|
||||
loading
|
||||
? 'bg-slate-600 cursor-not-allowed'
|
||||
: 'bg-gradient-to-r from-cyan-500 to-purple-600 hover:from-cyan-600 hover:to-purple-700 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5'
|
||||
}`}
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
|
||||
در حال تغییر...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FiLock />
|
||||
تغییر رمز عبور کاربر
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserChangePassword;
|
||||
234
src/components/users/UserComments.jsx
Normal file
234
src/components/users/UserComments.jsx
Normal file
@@ -0,0 +1,234 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import AdminHeader from "../../components/admin-header";
|
||||
import Sidebar from "../../components/sidebar";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { BaseUrl } from "../../components/BaseUrl";
|
||||
import axios from "axios";
|
||||
import Loading from "../../components/elements/loading";
|
||||
import Pagination from "../../components/Pagination";
|
||||
import Filters from "../Filters";
|
||||
|
||||
const Comments = ({}) => {
|
||||
// دریافت id از props
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [allComments, setAllComments] = useState([]); // تمام کامنتها
|
||||
const [commentList, setCommentList] = useState([]); // کامنتهای فیلتر شده
|
||||
const [totalPages, setTotalPages] = useState(0);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [filters, setFilters] = useState({
|
||||
startDate: "",
|
||||
endDate: "",
|
||||
status: "",
|
||||
});
|
||||
|
||||
|
||||
const token = document.cookie
|
||||
.split("; ")
|
||||
.find((row) => row.startsWith("token="))
|
||||
?.split("=")[1];
|
||||
|
||||
const param = useParams();
|
||||
const id = param?.id;
|
||||
|
||||
const getCommentList = (page = 1) => {
|
||||
setLoading(true);
|
||||
|
||||
const { startDate, endDate, status } = filters;
|
||||
const config = {
|
||||
url: `${BaseUrl}/comments?limit=20&page=${page}${
|
||||
startDate ? "&startDate=" + startDate : ""
|
||||
}${endDate ? "&endDate=" + endDate : ""}&status=${status}`,
|
||||
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`, // اضافه کردن توکن به هدر
|
||||
},
|
||||
};
|
||||
axios(config)
|
||||
.then((res) => {
|
||||
// setAllComments(res?.data?.comments);
|
||||
setTotalPages(res?.data?.totalPages);
|
||||
|
||||
// بررسی اینکه کامنتها وجود دارند
|
||||
const comments = res?.data?.comments || [];
|
||||
|
||||
console.log(comments);
|
||||
|
||||
// فیلتر فقط زمانی انجام میشه که creator وجود داشته باشه
|
||||
const userComments = comments.filter(
|
||||
(c) => c?.creator?._id === id
|
||||
);
|
||||
|
||||
setCommentList(userComments);
|
||||
setLoading(false);
|
||||
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
getCommentList(currentPage);
|
||||
}, [currentPage, filters]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="min-h-screen mx-auto ">
|
||||
<div className="flex h-full w-full">
|
||||
<div className="w-full h-full p-14 mb-11 mx-auto text-white">
|
||||
{/* Filters */}
|
||||
<Filters
|
||||
statusOptions={[
|
||||
{ key: "", label: "همه" },
|
||||
{ key: "pending", label: "در انتظار تایید" },
|
||||
{ key: "accepted", label: "تایید شده" },
|
||||
{ key: "rejected", label: "رد شده" },
|
||||
]}
|
||||
provinceOptions={[{ value: "", label: "همه استانها" }]}
|
||||
cityOptions={[{ value: "", label: "همه شهرها" }]}
|
||||
photographyModelOptions={[{ value: "", label: "همه مدلها" }]}
|
||||
blockStatusOptions={[{ value: "", label: "همه کاربران" }]}
|
||||
onFilterChange={setFilters}
|
||||
/>
|
||||
|
||||
<div className="w-full mt-4 flex items-center border-b ">
|
||||
<div className="flex-1 w-full flex items-center h-11 px-5">
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
عنوان پروژه
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
نام کاربری سازنده
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
نام و نام خانوادگی سازنده
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
نام کاربری یوزر
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
نام و نام خانوادگی یوزر
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
تاریخ ثبت کامنت
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[12%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">وضعیت</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[11%]">
|
||||
<p className="font-KalamehMed text-sm font-medium"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{loading ? (
|
||||
<div className="w-full flex items-center justify-center mt-5">
|
||||
<Loading className="w-14 h-14 text-blacklead animate-pulse" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-y-scroll hide-scrollbar w-full">
|
||||
{Array.isArray(commentList) && commentList.length > 0 ? (
|
||||
commentList.map((comment, index) => {
|
||||
return (
|
||||
<div
|
||||
key={comment?._id || index}
|
||||
className={`flex-1 w-full flex items-center h-[90px] px-5 py-8 rounded-lg ${
|
||||
index % 2 === 0 ? "bg-[#c0e2f04b]" : "bg-[#678fa062]"
|
||||
}`}
|
||||
>
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium mr-2">
|
||||
{comment?.project || "-"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium mr-2">
|
||||
{comment?.creator?.user_name || "-"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium mr-2">
|
||||
{comment?.creator?.first_name || ""}{" "}
|
||||
{comment?.creator?.last_name || ""}
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium mr-2">
|
||||
{comment?.user?.user_name || "-"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium mr-2">
|
||||
{comment?.user?.first_name || ""}{" "}
|
||||
{comment?.user?.last_name || ""}
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium mr-2">
|
||||
{comment?.createdAt || "-"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[12%]">
|
||||
<div className="flex items-center">
|
||||
<p
|
||||
className={`font-KalamehMed text-sm font-medium ${
|
||||
comment?.status === "pending"
|
||||
? "text-yellow-400"
|
||||
: comment?.status === "accepted"
|
||||
? "text-green-400"
|
||||
: "text-red-400"
|
||||
}`}
|
||||
>
|
||||
{comment?.status === "pending"
|
||||
? "در انتظار تایید"
|
||||
: comment?.status === "accepted"
|
||||
? "تایید شده"
|
||||
: comment?.status === "rejected"
|
||||
? "رد شده"
|
||||
: "-"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-1 w-[11%]">
|
||||
<button
|
||||
onClick={() => navigate(`${comment?._id}`)}
|
||||
className="bg-[#1f5f79] hover:bg-[#247392] px-8 h-[35px] rounded-md duration-300 mr-5"
|
||||
>
|
||||
جزئیات
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<p className="text-center text-gray-400 mt-6">
|
||||
هیچ کامنتی یافت نشد.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Pagination
|
||||
totalItems={commentList.length}
|
||||
totalPages={totalPages}
|
||||
currentPage={currentPage}
|
||||
onPageChange={(page) => setCurrentPage(page)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Comments;
|
||||
607
src/components/users/UserDet.jsx
Normal file
607
src/components/users/UserDet.jsx
Normal file
@@ -0,0 +1,607 @@
|
||||
import React, { useState } from "react";
|
||||
import { BaseUrl, ImageBaseUrl } from "../BaseUrl";
|
||||
import axios from "axios";
|
||||
import Modal from "react-modal";
|
||||
import toast from "react-hot-toast";
|
||||
import Sidebar from "../sidebar";
|
||||
|
||||
const customStyles = {
|
||||
content: {
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
position: "fixed",
|
||||
},
|
||||
};
|
||||
|
||||
const UserDet = ({ userData, id, getUserDet }) => {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [editedData, setEditedData] = useState({});
|
||||
const [modalIsOpen, setModalIsOpen] = useState(false);
|
||||
const [modalImage, setModalImage] = useState("");
|
||||
|
||||
const openModal = (image) => {
|
||||
setModalImage(image);
|
||||
setModalIsOpen(true);
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setModalIsOpen(false);
|
||||
};
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setEditedData((prevData) => ({
|
||||
...prevData,
|
||||
[name]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
const response = await axios.put(
|
||||
`${BaseUrl}/users/update?userId=${userData._id}`,
|
||||
editedData,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
setIsEditing(false);
|
||||
toast.success("اطلاعات با موفقیت ذخیره شد");
|
||||
getUserDet();
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data && error.response.data.errors) {
|
||||
error.response.data.errors.forEach((err) => {
|
||||
toast.error(
|
||||
typeof err.message === "string" ? err.message : "خطای ناشناخته"
|
||||
);
|
||||
});
|
||||
} else {
|
||||
toast.error("خطای ناشناخته");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const acceptVerify = () => {
|
||||
axios
|
||||
.post(`${BaseUrl}/users/verify?userId=${id}`, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(async (response) => {
|
||||
toast.success(response?.data?.message);
|
||||
getUserDet();
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(error?.response?.data?.message || "خطای ناشناخته");
|
||||
});
|
||||
};
|
||||
|
||||
const rejectVerify = () => {
|
||||
axios
|
||||
.post(`${BaseUrl}/users/reject?userId=${id}`, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(async (response) => {
|
||||
toast.success(response?.data?.message);
|
||||
getUserDet();
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(error?.response?.data?.message || "خطای ناشناخته");
|
||||
});
|
||||
};
|
||||
|
||||
const acceptService = async (serviceId) => {
|
||||
try {
|
||||
// بهروزرسانی آرایه services با تغییر status سرویس موردنظر به "true"
|
||||
const updatedServices = userData.services.map((service) =>
|
||||
service._id === serviceId ? { ...service, status: "true" } : service
|
||||
);
|
||||
|
||||
const response = await axios.put(
|
||||
`${BaseUrl}/users/update?userId=${userData._id}`,
|
||||
{ services: updatedServices },
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
toast.success(response?.data?.message || "سرویس با موفقیت تأیید شد");
|
||||
getUserDet();
|
||||
} catch (error) {
|
||||
toast.error(error?.response?.data?.message || "خطای ناشناخته");
|
||||
}
|
||||
};
|
||||
|
||||
const rejectService = async (serviceId) => {
|
||||
// فقط پیام رد نمایش داده میشود و status تغییر نمیکند
|
||||
toast.success("سرویس رد شد");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className=" gap-3 ">
|
||||
<div className="flex flex-col justify-center items-center gap-5 mt-5">
|
||||
<Modal
|
||||
isOpen={modalIsOpen}
|
||||
onRequestClose={closeModal}
|
||||
className="post-image-modal "
|
||||
style={customStyles}
|
||||
>
|
||||
<img src={modalImage} alt="Post" className="post-modal-image" />
|
||||
</Modal>
|
||||
|
||||
{/* Profile Image */}
|
||||
<div className="flex items-center w-full ">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
تصویر پروفایل:
|
||||
</div>
|
||||
<div className="flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 p-4">
|
||||
<img
|
||||
onClick={() => openModal(ImageBaseUrl + userData?.profile_image)}
|
||||
src={ImageBaseUrl + userData?.profile_image}
|
||||
className="w-[200px] h-[200px] rounded-md"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* User Name */}
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
نام کاربری:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="text"
|
||||
name="user_name"
|
||||
defaultValue={userData.user_name}
|
||||
onChange={handleChange}
|
||||
className="bg-[#222] text-primary px-2 py-1 rounded-md w-full min-h-9"
|
||||
/>
|
||||
) : (
|
||||
userData?.user_name
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* First Name */}
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
نام:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="text"
|
||||
name="first_name"
|
||||
defaultValue={userData.first_name}
|
||||
onChange={handleChange}
|
||||
className="bg-[#222] text-primary px-2 py-1 rounded-md w-full min-h-9"
|
||||
/>
|
||||
) : (
|
||||
userData?.first_name
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Last Name */}
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
نام خانوادگی:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="text"
|
||||
name="last_name"
|
||||
defaultValue={userData.last_name}
|
||||
onChange={handleChange}
|
||||
className="bg-[#222] text-primary px-2 py-1 rounded-md w-full min-h-9"
|
||||
/>
|
||||
) : (
|
||||
userData?.last_name
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile */}
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
موبایل:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="text"
|
||||
name="mobile"
|
||||
defaultValue={userData.mobile}
|
||||
onChange={handleChange}
|
||||
className="bg-[#222] text-primary px-2 py-1 rounded-md w-full min-h-9"
|
||||
/>
|
||||
) : (
|
||||
userData?.mobile
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* National Code */}
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
کد ملی:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="text"
|
||||
name="national_code"
|
||||
defaultValue={userData.national_code}
|
||||
onChange={handleChange}
|
||||
className="bg-[#222] text-primary px-2 py-1 rounded-md w-full min-h-9"
|
||||
/>
|
||||
) : (
|
||||
userData?.national_code
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Province */}
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
استان:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{userData?.province?.name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* City */}
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
شهر:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{userData?.city?.name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Address */}
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
آدرس:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="text"
|
||||
name="address"
|
||||
defaultValue={userData.address}
|
||||
onChange={handleChange}
|
||||
className="bg-[#222] text-primary px-2 py-1 rounded-md w-full min-h-9"
|
||||
/>
|
||||
) : (
|
||||
userData?.address
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Height */}
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
قد:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="text"
|
||||
name="height"
|
||||
defaultValue={userData.height}
|
||||
onChange={handleChange}
|
||||
className="bg-[#222] text-primary px-2 py-1 rounded-md w-full min-h-9"
|
||||
/>
|
||||
) : (
|
||||
userData?.height
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Weight */}
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
وزن:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="text"
|
||||
name="weight"
|
||||
defaultValue={userData.weight}
|
||||
onChange={handleChange}
|
||||
className="bg-[#222] text-primary px-2 py-1 rounded-md w-full min-h-9"
|
||||
/>
|
||||
) : (
|
||||
userData?.weight
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Size */}
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
سایز:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="text"
|
||||
name="size"
|
||||
defaultValue={userData.size}
|
||||
onChange={handleChange}
|
||||
className="bg-[#222] text-primary px-2 py-1 rounded-md w-full min-h-9"
|
||||
/>
|
||||
) : (
|
||||
userData?.size
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bio */}
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
بیو:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="text"
|
||||
name="bio"
|
||||
defaultValue={userData.bio}
|
||||
onChange={handleChange}
|
||||
className="bg-[#222] text-primary px-2 py-1 rounded-md w-full min-h-9"
|
||||
/>
|
||||
) : (
|
||||
userData?.bio
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Birthday */}
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
تاریخ تولد:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{userData?.birthday}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Cooperation Abroad */}
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
خارج از محدوده سکونت به من پیشنهاد بده:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{userData?.cooperation_abroad == true ? "بله" : "خیر"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Conversation Projects */}
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
پروژه با روابط عمومی بالا:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{userData?.conversation_projects == true ? "بله" : "خیر"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expertise */}
|
||||
<div className="flex items-start w-full gap-3">
|
||||
{/* عنوان */}
|
||||
<div className="w-[180px] flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
تخصص:
|
||||
</div>
|
||||
|
||||
{/* تخصص + زیرتخصصها */}
|
||||
<div className="flex flex-wrap gap-1 font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4 py-2 flex-1">
|
||||
<span>{userData?.expertise} :</span>
|
||||
|
||||
{userData?.sub_expertise?.map((item, index) => (
|
||||
<span key={index}>
|
||||
{item}
|
||||
{index !== userData?.sub_expertise?.length - 1 ? " _ " : ""}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Shaba */}
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
شماره شبا:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="text"
|
||||
name="shaba"
|
||||
defaultValue={userData.shaba}
|
||||
onChange={handleChange}
|
||||
className="bg-[#222] text-primary px-2 py-1 rounded-md w-full min-h-9"
|
||||
/>
|
||||
) : (
|
||||
userData?.shaba
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* National Card Image */}
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
تصویر کارت ملی:
|
||||
</div>
|
||||
<div className="flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 p-4">
|
||||
<img
|
||||
onClick={() =>
|
||||
openModal(ImageBaseUrl + userData?.national_card_image)
|
||||
}
|
||||
src={ImageBaseUrl + userData?.national_card_image}
|
||||
className="w-[200px] h-[200px] rounded-md"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Last Online */}
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
آخرین زمان ورود:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{userData?.last_online}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Created At */}
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
تاریخ ثبت نام:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{userData?.createdAt}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Verification Status */}
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
وضعیت مدارک:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
{userData?.is_verified === "verified"
|
||||
? "تایید شده"
|
||||
: userData?.is_verified === "pending"
|
||||
? "در انتظار تایید"
|
||||
: ""}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => acceptVerify()}
|
||||
className="bg-[#146820] hover:bg-[#247392] px-8 h-[35px] rounded-md duration-300 mr-3"
|
||||
>
|
||||
تایید مدارک
|
||||
</button>
|
||||
<button
|
||||
onClick={() => rejectVerify()}
|
||||
className="bg-[#a02121d3] hover:bg-[#247392] px-8 h-[35px] rounded-md duration-300 mr-3"
|
||||
>
|
||||
رد مدارک
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Services Section */}
|
||||
<div className="flex items-start w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
خدمات:
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4 py-4">
|
||||
{userData?.services?.length > 0 ? (
|
||||
userData.services.map((service) => (
|
||||
<div
|
||||
key={service._id}
|
||||
className="flex items-center gap-4 p-4 bg-[#222] rounded-lg"
|
||||
>
|
||||
<img
|
||||
src={ImageBaseUrl + service.image}
|
||||
alt={service.title}
|
||||
className="w-[100px] h-[100px] rounded-md cursor-pointer"
|
||||
onClick={() => openModal(ImageBaseUrl + service.image)}
|
||||
/>
|
||||
<div className="flex flex-col gap-2">
|
||||
<p>عنوان: {service.title}</p>
|
||||
<p>قیمت اصلی: {service.originalPrice} تومان</p>
|
||||
<p>قیمت با تخفیف: {service.discountPrice} تومان</p>
|
||||
<p>درصد تخفیف: {service.discountPercentage}%</p>
|
||||
<p>
|
||||
وضعیت:{" "}
|
||||
{service.status === "true"
|
||||
? "تأیید شده"
|
||||
: service.status === "UnderReview"
|
||||
? "در انتظار بررسی"
|
||||
: "رد شده"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => acceptService(service._id)}
|
||||
className={`bg-[#146820] hover:bg-[#247392] px-4 h-[35px] rounded-md duration-300 ${
|
||||
service.status === "true" ? "hidden" : ""
|
||||
}`}
|
||||
disabled={service.status === "true"}
|
||||
>
|
||||
تأیید
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p>هیچ سرویسی یافت نشد</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* User Rate */}
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
امتیاز کاربر:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{userData?.rate}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* User Level */}
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
سطح کاربر:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{userData?.user_level}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Save Button */}
|
||||
<div className="flex items-center w-full">
|
||||
{isEditing ? (
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="bg-[#146820] hover:bg-[#247392] px-8 h-[35px] rounded-md duration-300"
|
||||
>
|
||||
ذخیره
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setIsEditing(!isEditing)}
|
||||
className="bg-[#146820] hover:bg-[#247392] px-8 h-[35px] rounded-md duration-300 mr-3"
|
||||
>
|
||||
ویرایش
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserDet;
|
||||
9
src/components/users/UserFinancial.jsx
Normal file
9
src/components/users/UserFinancial.jsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react'
|
||||
|
||||
const UserFinancial = () => {
|
||||
return (
|
||||
<div>UserFinancial</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserFinancial
|
||||
9
src/components/users/UserMessages.jsx
Normal file
9
src/components/users/UserMessages.jsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react'
|
||||
|
||||
const UserMessages = () => {
|
||||
return (
|
||||
<div>UserMessages</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserMessages
|
||||
211
src/components/users/UserPosts.jsx
Normal file
211
src/components/users/UserPosts.jsx
Normal file
@@ -0,0 +1,211 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { BaseUrl, buildStorageUrl } from "../../components/BaseUrl";
|
||||
import axios from "axios";
|
||||
import Loading from "../../components/elements/loading";
|
||||
import Pagination from "../../components/Pagination";
|
||||
|
||||
const UserPosts = ({ id }) => {
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [allPosts, setAllPosts] = useState([]); // تمام پستها
|
||||
const [postsList, setPostsList] = useState([]); // پستهای صفحه جاری
|
||||
const [totalPages, setTotalPages] = useState(0);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [filters, setFilters] = useState({
|
||||
startDate: "",
|
||||
endDate: "",
|
||||
status: "",
|
||||
});
|
||||
|
||||
const itemsPerPage = 10; // تعداد آیتم در هر صفحه
|
||||
|
||||
// دریافت همه پستها از API (بدون user_id)
|
||||
const fetchAllPosts = () => {
|
||||
setLoading(true);
|
||||
const { startDate, endDate, status } = filters;
|
||||
|
||||
let url = `${BaseUrl}/posts?limit=1000`; // گرفتن تعداد زیاد برای فیلتر فرانت
|
||||
if (startDate) url += `&startDate=${startDate}`;
|
||||
if (endDate) url += `&endDate=${endDate}`;
|
||||
if (status) url += `&status=${status}`;
|
||||
|
||||
axios
|
||||
.get(url)
|
||||
.then((res) => {
|
||||
const posts = res?.data?.posts || [];
|
||||
setAllPosts(posts);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(() => setLoading(false));
|
||||
};
|
||||
|
||||
// فیلتر و صفحهبندی روی فرانت
|
||||
const paginatePosts = () => {
|
||||
const filteredPosts = allPosts.filter((post) => post.user_id === id);
|
||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||
const paginatedPosts = filteredPosts.slice(
|
||||
startIndex,
|
||||
startIndex + itemsPerPage
|
||||
);
|
||||
setPostsList(paginatedPosts);
|
||||
setTotalPages(Math.ceil(filteredPosts.length / itemsPerPage));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchAllPosts();
|
||||
}, [filters]);
|
||||
|
||||
useEffect(() => {
|
||||
paginatePosts();
|
||||
}, [allPosts, currentPage, id]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="min-h-screen mx-auto">
|
||||
<div className="flex h-full w-full">
|
||||
<div className="w-full h-full p-4 mb-11 mx-auto bg-[#252525] text-white">
|
||||
{/* Table Header */}
|
||||
<div className="w-full mt-4 flex items-center bg-[#252525] border-b">
|
||||
<div className="flex-1 w-full flex items-center h-11 px-5">
|
||||
<div className="w-[10%]">
|
||||
<p>تصویر</p>
|
||||
</div>
|
||||
<div className="w-[10%]">
|
||||
<p>نام کاربری</p>
|
||||
</div>
|
||||
<div className="w-[10%]">
|
||||
<p>نوع کاربری</p>
|
||||
</div>
|
||||
<div className="w-[12%]">
|
||||
<p>نام و نام خانوادگی</p>
|
||||
</div>
|
||||
<div className="w-[10%]">
|
||||
<p>موبایل</p>
|
||||
</div>
|
||||
<div className="w-[10%]">
|
||||
<p>کد ملی</p>
|
||||
</div>
|
||||
<div className="w-[12%]">
|
||||
<p>تاریخ ثبت پست</p>
|
||||
</div>
|
||||
<div className="w-[16%]">
|
||||
<p>وضعیت</p>
|
||||
</div>
|
||||
<div className="w-[10%]">
|
||||
<p>جزئیات</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="w-full flex items-center justify-center mt-5">
|
||||
<Loading className="w-14 h-14 text-blacklead animate-pulse" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-y-scroll hide-scrollbar w-full">
|
||||
{postsList?.map((post, index) => (
|
||||
<div
|
||||
key={post._id}
|
||||
className={`w-full mt-4 flex items-center`}
|
||||
>
|
||||
<div
|
||||
className={`flex-1 w-full flex items-center h-[90px] px-5 rounded-lg py-8 ${
|
||||
index % 2 === 0 ? "bg-[#c0e2f04b]" : "bg-[#678fa062]"
|
||||
}`}
|
||||
>
|
||||
<div className="w-[10%]">
|
||||
{post?.files?.length > 0 ? (
|
||||
post.files[0].type === "image" ? (
|
||||
<img
|
||||
className="w-[80px] h-[80px] rounded-md ml-2 object-cover"
|
||||
src={buildStorageUrl(post.files[0].path)}
|
||||
alt="Post"
|
||||
/>
|
||||
) : (
|
||||
<video
|
||||
className="w-[80px] h-[80px] rounded-md ml-2 object-cover"
|
||||
src={buildStorageUrl(post.files[0].path)}
|
||||
controls
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<img
|
||||
className="w-[80px] h-[80px] rounded-md ml-2 object-cover"
|
||||
src="/images/placeholder.jpg"
|
||||
alt="No file"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-[10%]">
|
||||
<p>{post?.user_name}</p>
|
||||
</div>
|
||||
<div className="w-[10%]">
|
||||
{" "}
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
{post?.user_type === "employer"
|
||||
? "کارفرما"
|
||||
: post?.user_type === "user"
|
||||
? post?.expertise || "-"
|
||||
: "-"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-[12%]">
|
||||
<p>
|
||||
{post?.first_name} {post?.last_name}
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-[10%]">
|
||||
<p>{post?.mobile}</p>
|
||||
</div>
|
||||
<div className="w-[10%]">
|
||||
<p>{post?.national_code}</p>
|
||||
</div>
|
||||
<div className="w-[12%]">
|
||||
<p>{post?.createdAt}</p>
|
||||
</div>
|
||||
<div className="w-[16%]">
|
||||
<p
|
||||
className={
|
||||
post?.status === "accept"
|
||||
? "text-green-300"
|
||||
: post?.status === "pending"
|
||||
? "text-yellow-300"
|
||||
: "text-red-300"
|
||||
}
|
||||
>
|
||||
{post?.status === "accept"
|
||||
? "منتشر شده"
|
||||
: post?.status === "pending"
|
||||
? "در انتظار تایید"
|
||||
: "رد شده"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-[10%]">
|
||||
<button
|
||||
onClick={() => navigate(`/posts/${post?._id}`)}
|
||||
className="bg-[#1f5f79] hover:bg-[#247392] px-8 h-[35px] rounded-md duration-300 mr-5"
|
||||
>
|
||||
جزئیات
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Pagination
|
||||
totalItems={allPosts.filter((post) => post.user_id === id).length}
|
||||
totalPages={totalPages}
|
||||
currentPage={currentPage}
|
||||
onPageChange={(page) => setCurrentPage(page)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserPosts;
|
||||
200
src/components/users/UserWorkroom.jsx
Normal file
200
src/components/users/UserWorkroom.jsx
Normal file
@@ -0,0 +1,200 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
import { AppBaseUrl } from '../BaseUrl';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import moment from 'moment-jalaali';
|
||||
import { FiUser, FiCalendar, FiClock, FiCheckCircle, FiXCircle, FiAlertCircle } from 'react-icons/fi';
|
||||
|
||||
const UserWorkroom = () => {
|
||||
const [projects, setProjects] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const param = useParams();
|
||||
const id = param?.id;
|
||||
|
||||
useEffect(() => {
|
||||
const fetchProjects = async () => {
|
||||
try {
|
||||
const response = await axios.get(`${AppBaseUrl}/offers`, {
|
||||
params: { status_filter: "دریافت", id },
|
||||
});
|
||||
|
||||
const offersFromSender = response.data.offerbyidsender?.docs || [];
|
||||
const offersFromReceiver = response.data.offerbyid?.docs || [];
|
||||
|
||||
const mergedOffers = [...offersFromSender, ...offersFromReceiver];
|
||||
const sortedOffers = mergedOffers.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
||||
|
||||
setProjects(sortedOffers);
|
||||
} catch (err) {
|
||||
setError('خطا در بارگذاری درخواستها');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchProjects();
|
||||
}, [id]);
|
||||
|
||||
const formatJalaliDate = (date) => {
|
||||
if (!date) return 'نامشخص';
|
||||
return moment(date).format('jYYYY/jMM/jDD - HH:mm');
|
||||
};
|
||||
|
||||
const StatusTag = ({ status }) => {
|
||||
const config = {
|
||||
accepted: { text: 'تایید شده', color: 'bg-green-900/30 text-green-300 border border-green-800', icon: <FiCheckCircle /> },
|
||||
rejected: { text: 'رد شده', color: 'bg-red-900/30 text-red-300 border border-red-800', icon: <FiXCircle /> },
|
||||
pending: { text: 'در انتظار', color: 'bg-yellow-900/30 text-yellow-300 border border-yellow-800', icon: <FiAlertCircle /> },
|
||||
};
|
||||
|
||||
const { text, color, icon } = config[status] || { text: status, color: 'bg-gray-800 text-gray-400 border border-gray-700', icon: null };
|
||||
|
||||
return (
|
||||
<span className={`inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs font-medium ${color}`}>
|
||||
{icon}
|
||||
{text}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-20 bg-slate-950">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-4 border-cyan-500 border-t-transparent"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="bg-red-900/20 border border-red-800 text-red-300 px-6 py-4 rounded-xl text-center max-w-md mx-auto mt-10">
|
||||
<p className="font-medium">{error}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (projects.length === 0) {
|
||||
return (
|
||||
<div className="bg-slate-900 border border-slate-800 text-slate-400 px-6 py-12 rounded-xl text-center max-w-md mx-auto mt-10">
|
||||
<FiAlertCircle className="mx-auto text-5xl mb-3 text-slate-600" />
|
||||
<p className="text-lg font-medium">هیچ درخواستی یافت نشد.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen text-slate-100 p-4 sm:p-6 lg:p-8">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-slate-50 flex items-center gap-2">
|
||||
<FiUser className="text-cyan-400" />
|
||||
لیست درخواستها
|
||||
</h2>
|
||||
<p className="text-slate-400 mt-1">مجموع: <span className="text-cyan-400">{projects.length}</span> درخواست</p>
|
||||
</div>
|
||||
|
||||
{/* موبایل: کارتها */}
|
||||
<div className="block md:hidden space-y-4">
|
||||
{projects.map((project) => (
|
||||
<div
|
||||
key={project._id}
|
||||
className="bg-slate-800/50 backdrop-blur-sm rounded-xl border border-slate-700 p-5 hover:border-cyan-700/50 transition-all duration-300 hover:shadow-xl hover:shadow-cyan-900/20"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<p className="text-xs font-medium text-slate-400">درخواستکننده</p>
|
||||
<p className="text-lg font-bold text-cyan-400">
|
||||
{project.sender
|
||||
? `${project.sender.first_name} ${project.sender.last_name}`
|
||||
: 'نامشخص'}
|
||||
</p>
|
||||
<p className="text-sm text-slate-500">@{project.sender?.user_name}</p>
|
||||
</div>
|
||||
<StatusTag status={project.status} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-xs font-medium text-slate-400">درخواستشونده</p>
|
||||
<p className="text-lg font-bold text-purple-400">
|
||||
{project.receiver
|
||||
? `${project.receiver.first_name} ${project.receiver.last_name}`
|
||||
: 'نامشخص'}
|
||||
</p>
|
||||
<p className="text-sm text-slate-500">@{project.receiver?.user_name}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 text-xs text-slate-400">
|
||||
<FiCalendar className="text-cyan-500" />
|
||||
<span>{formatJalaliDate(project.createdAt)}</span>
|
||||
</div>
|
||||
{project.updatedAt && (
|
||||
<div className="flex items-center gap-2 text-xs text-slate-500">
|
||||
<FiClock className="text-purple-500" />
|
||||
<span>آخرین تغییر: {formatJalaliDate(project.updatedAt)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* دسکتاپ: جدول */}
|
||||
<div className="hidden md:block overflow-hidden rounded-xl border border-slate-700 bg-slate-900/50 backdrop-blur-sm shadow-2xl">
|
||||
<table className="w-full">
|
||||
<thead className="bg-gradient-to-r from-cyan-900/20 to-purple-900/20 border-b border-slate-700">
|
||||
<tr>
|
||||
<th className="px-6 py-4 text-right text-xs font-semibold text-slate-300">درخواستکننده</th>
|
||||
<th className="px-6 py-4 text-right text-xs font-semibold text-slate-300">درخواستشونده</th>
|
||||
<th className="px-6 py-4 text-center text-xs font-semibold text-slate-300">وضعیت</th>
|
||||
<th className="px-6 py-4 text-right text-xs font-semibold text-slate-300">تاریخ ثبت</th>
|
||||
<th className="px-6 py-4 text-right text-xs font-semibold text-slate-300">آخرین تغییر</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-700">
|
||||
{projects.map((project) => (
|
||||
<tr
|
||||
key={project._id}
|
||||
className="hover:bg-slate-800/50 transition-all duration-200"
|
||||
>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div>
|
||||
<p className="font-bold text-slate-100">
|
||||
{project.sender
|
||||
? `${project.sender.first_name} ${project.sender.last_name}`
|
||||
: 'نامشخص'}
|
||||
</p>
|
||||
<p className="text-sm text-cyan-400">@{project.sender?.user_name}</p>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div>
|
||||
<p className="font-bold text-slate-100">
|
||||
{project.receiver
|
||||
? `${project.receiver.first_name} ${project.receiver.last_name}`
|
||||
: 'نامشخص'}
|
||||
</p>
|
||||
<p className="text-sm text-purple-400">@{project.receiver?.user_name}</p>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-center">
|
||||
<StatusTag status={project.status} />
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-slate-300">
|
||||
{formatJalaliDate(project.createdAt)}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-slate-400">
|
||||
{project.updatedAt ? formatJalaliDate(project.updatedAt) : '—'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserWorkroom;
|
||||
73
src/index.js
Normal file
73
src/index.js
Normal file
@@ -0,0 +1,73 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './assets/css/index.css';
|
||||
import App from './App';
|
||||
import toast from 'react-hot-toast';
|
||||
import axios from 'axios';
|
||||
import { BaseUrl } from './components/BaseUrl';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
|
||||
const handleUnauthorized = () => {
|
||||
localStorage.removeItem("userData");
|
||||
localStorage.removeItem("token");
|
||||
localStorage.removeItem("login");
|
||||
window.location = "/login";
|
||||
};
|
||||
|
||||
const getToken = () => {
|
||||
const token = localStorage.getItem("token");
|
||||
return token == null ? "" : token;
|
||||
};
|
||||
axios.interceptors.request.use(
|
||||
(config) => {
|
||||
if (!config.url.endsWith('/panel/login')) {
|
||||
config.headers["Authorization"] = `Bearer ${getToken()}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
axios.interceptors.response.use(
|
||||
(response) => {
|
||||
if (response.data.message && response.data.message != "")
|
||||
toast.success(response.data.message);
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
if (error.response.status == 422) {
|
||||
if (error.response.data.errors) {
|
||||
Object.keys(error.response.data.errors).map((err) =>
|
||||
error.response.data.errors[err].map((item) => toast.error(item))
|
||||
);
|
||||
}
|
||||
|
||||
} else if (error.response.status == 401) {
|
||||
handleUnauthorized();
|
||||
} else if (
|
||||
error.response.status == 429 ||
|
||||
error.response.status == 500 ||
|
||||
error.response.status == 503
|
||||
) {
|
||||
toast.error("خطایی رخ داده");
|
||||
} else if (error.response.status == 403) {
|
||||
// window.location.reload();
|
||||
toast.error(
|
||||
"متاسفانه دسترسی شما به بخش هایی از این صفحه محدود شده است"
|
||||
);
|
||||
} else {
|
||||
toast.error(error.response.data.message);
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
50
src/pages/Dashboard.jsx
Normal file
50
src/pages/Dashboard.jsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import React, { useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import AdminHeader from "../components/admin-header";
|
||||
import Sidebar from "../components/sidebar";
|
||||
|
||||
const items = [
|
||||
{ id: 1, label: "کاربران جدید", href: "/users?status=pending" },
|
||||
{ id: 2, label: "پستهای جدید", href: "/posts?status=pending" },
|
||||
{ id: 3, label: "بیلبورد جدید", href: "/vitrine?status=pending" },
|
||||
{ id: 4, label: "نظرات جدید", href: "/comments?status=pending" },
|
||||
{ id: 5, label: "تیکت جدید", href: "/tickets?status=pending" },
|
||||
{ id: 6, label: "نظرات بیلبورد جدید", href: "/vitrine-comments?status=pending" },
|
||||
];
|
||||
|
||||
const Dashboard = () => {
|
||||
const [active, setActive] = useState(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<div className="min-h-screen mx-auto bg-gray-900">
|
||||
<div className="flex h-full w-full">
|
||||
<div className=" w-64"><Sidebar /></div>
|
||||
|
||||
<div className="w-full h-full p-12 mb-11 mx-auto text-white">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{items.map((item) => (
|
||||
<Link
|
||||
key={item.id}
|
||||
to={item.href}
|
||||
onClick={() => setActive(item.id)}
|
||||
className={`cursor-pointer rounded-xl border p-6 shadow-md transition block
|
||||
${
|
||||
active === item.id
|
||||
? "bg-cyan-600 text-white font-bold shadow-lg scale-105"
|
||||
: "bg-[#232a31] hover:bg-[#2a3036] text-gray-200"
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
116
src/pages/Login.jsx
Normal file
116
src/pages/Login.jsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import * as Yup from "yup";
|
||||
import Loading from "../components/elements/loading";
|
||||
import { useAuth } from "../utils/AuthProvider";
|
||||
import axios from "axios";
|
||||
import { BaseUrl } from "../components/BaseUrl";
|
||||
import toast, { useToaster } from "react-hot-toast";
|
||||
|
||||
const Login = () => {
|
||||
const navigate = useNavigate();
|
||||
const [loading, setloading] = useState(false);
|
||||
const token = useAuth();
|
||||
const [userName, setUserName] = useState("")
|
||||
const [password, setPassword] = useState("")
|
||||
useEffect(() => {
|
||||
if (token !== null) {
|
||||
navigate("/", { replace: true });
|
||||
}
|
||||
}, []);
|
||||
const loginHandler = () => {
|
||||
const data = {
|
||||
user_name: userName,
|
||||
password,
|
||||
};
|
||||
axios
|
||||
.post(`${BaseUrl}/login`, data, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(async (response) => {
|
||||
try {
|
||||
localStorage.setItem("token", response?.data?.token);
|
||||
document.getElementById("to_dashboard").click();
|
||||
} catch (error) {
|
||||
toast.error("اطلاعات وارد شده صحیح نمیباشد");
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
toast.error(error?.response?.data?.message);
|
||||
});
|
||||
}
|
||||
function refreshPage() {
|
||||
setTimeout(() => {
|
||||
window.location.reload(false);
|
||||
}, 1000);
|
||||
}
|
||||
return (
|
||||
<div className="bg-[#259b9b] relative min-h-screen">
|
||||
<Link
|
||||
to={{ pathname: "/" }}
|
||||
id="to_dashboard"
|
||||
onClick={refreshPage}
|
||||
></Link>
|
||||
<div className="mx-auto w-full max-w-[500px] min-h-screen flex justify-center relative z-20 items-center font-medium font-KalamehMed">
|
||||
<div className=" p-12 rounded-xl w-full">
|
||||
<div className="text-black py-12 rounded-xl flex bg-white flex-col justify-center items-center text[##030303] px-10">
|
||||
<h5 className="text-black text-2xl font-bold">
|
||||
ورود
|
||||
</h5>
|
||||
<div
|
||||
className="w-full pt-5"
|
||||
>
|
||||
<div className="w-full py-3 flex flex-col">
|
||||
<label
|
||||
htmlFor="user_name"
|
||||
className="text-right"
|
||||
>
|
||||
نام کاربری:
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
x-model="input3"
|
||||
className="appearance-none dir-ltr text-left px-3 bg-blue-lightt text-black rounded-xl mt-1 w-full h-11 border-solid border-[1px] border-[#CBCBCB] focus:outline-none font-KalamehMed placeholder:text-[#C4C7C7]"
|
||||
value={userName}
|
||||
onChange={e => setUserName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full flex flex-col">
|
||||
<label
|
||||
htmlFor="password"
|
||||
className=" text-right"
|
||||
>
|
||||
کلمه عبور:
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
x-model="input3"
|
||||
className="appearance-none dir-ltr px-3 bg-blue-lightt text-black rounded-xl mt-1 w-full h-11 text-left border-solid border-[1px] border-[#CBCBCB] focus:outline-none font-KalamehMed placeholder:text-[#C4C7C7]"
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full py-3 pt-10 flex justify-center">
|
||||
<button
|
||||
onClick={() => loginHandler()}
|
||||
className="text-white bg-[#259b9b] flex justify-center items-center rounded-xl w-full h-11"
|
||||
>
|
||||
{loading ? (
|
||||
<Loading className="w-10 h-10 text-blacklead animate-pulse" />
|
||||
) : (
|
||||
"ورود"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
412
src/pages/academies/academies.jsx
Normal file
412
src/pages/academies/academies.jsx
Normal file
@@ -0,0 +1,412 @@
|
||||
// src/pages/Academies.js
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import {
|
||||
getAcademies,
|
||||
updateAcademyStatus,
|
||||
deleteAcademy,
|
||||
getAcademyStats
|
||||
} from "../../components/adminApi"; // اصلاح مسیر
|
||||
import Layout from "../../components/academy/Layout";
|
||||
import { FaSearch, FaEdit, FaTrash, FaEye, FaStar, FaUniversity, FaTimes } from "react-icons/fa";
|
||||
import { ImageBaseUrl } from "../../components/BaseUrl";
|
||||
|
||||
// در صورت نیاز به BASE_URL
|
||||
const IMAGE_BASE_URL = ImageBaseUrl|| "";
|
||||
|
||||
const Academies = () => {
|
||||
const [academies, setAcademies] = useState([]);
|
||||
const [stats, setStats] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [page, setPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [search, setSearch] = useState("");
|
||||
const [searchInput, setSearchInput] = useState("");
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [selectedAcademy, setSelectedAcademy] = useState(null);
|
||||
|
||||
const fetchAcademies = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await getAcademies({ page, limit: 12, search });
|
||||
console.log("Academies response:", response.data);
|
||||
|
||||
if (response.data.success) {
|
||||
const responseData = response.data.data;
|
||||
setAcademies(responseData?.academies || []);
|
||||
setStats(responseData?.stats || null);
|
||||
setTotalPages(responseData?.pagination?.totalPages || 1);
|
||||
} else {
|
||||
toast.error(response.data.message || "خطا در دریافت آکادمیها");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching academies:", error);
|
||||
toast.error("خطا در دریافت آکادمیها");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [page, search]);
|
||||
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
const response = await getAcademyStats();
|
||||
console.log("Stats response:", response.data);
|
||||
|
||||
if (response.data.success) {
|
||||
setStats(response.data.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching stats:", error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchAcademies();
|
||||
fetchStats();
|
||||
}, [fetchAcademies]);
|
||||
|
||||
const handleStatusChange = async (academyId, status) => {
|
||||
try {
|
||||
const response = await updateAcademyStatus({ academyId, status });
|
||||
if (response.data.success) {
|
||||
toast.success("وضعیت آکادمی تغییر کرد");
|
||||
fetchAcademies();
|
||||
fetchStats();
|
||||
} else {
|
||||
toast.error(response.data.message || "خطا در تغییر وضعیت");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating status:", error);
|
||||
toast.error("خطا در تغییر وضعیت");
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (academyId) => {
|
||||
if (window.confirm("آیا از حذف این آکادمی مطمئن هستید؟\nتوجه: با حذف آکادمی، تمام دورههای آن نیز حذف خواهند شد.")) {
|
||||
try {
|
||||
const response = await deleteAcademy(academyId);
|
||||
if (response.data.success) {
|
||||
toast.success("آکادمی حذف شد");
|
||||
fetchAcademies();
|
||||
fetchStats();
|
||||
} else {
|
||||
toast.error(response.data.message || "خطا در حذف آکادمی");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error deleting academy:", error);
|
||||
toast.error("خطا در حذف آکادمی");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = (e) => {
|
||||
e.preventDefault();
|
||||
setSearch(searchInput);
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const handleClearSearch = () => {
|
||||
setSearchInput("");
|
||||
setSearch("");
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const getImageUrl = (imagePath) => {
|
||||
if (!imagePath) return null;
|
||||
if (imagePath.startsWith("http")) return imagePath;
|
||||
return `${IMAGE_BASE_URL}${imagePath}`;
|
||||
};
|
||||
|
||||
const getStatusText = (status) => {
|
||||
const statusMap = {
|
||||
active: { text: "فعال", className: "bg-green-100 text-green-700" },
|
||||
inactive: { text: "غیرفعال", className: "bg-gray-100 text-gray-700" },
|
||||
blocked: { text: "مسدود", className: "bg-red-100 text-red-700" }
|
||||
};
|
||||
return statusMap[status] || { text: status || "فعال", className: "bg-gray-100 text-gray-700" };
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
if (loading && page === 1) {
|
||||
return (
|
||||
<Layout>
|
||||
<div className="flex justify-center items-center h-96">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-pink-500"></div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold">مدیریت آکادمیها</h1>
|
||||
<p className="text-gray-500 mt-1">لیست تمام آکادمیهای ثبت شده در سامانه</p>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
{stats && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
<div className="bg-gradient-to-r from-purple-500 to-purple-600 rounded-lg p-4 text-white">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<p className="text-sm opacity-90">تعداد آکادمیها</p>
|
||||
<p className="text-3xl font-bold">{stats.totalAcademies?.toLocaleString() || 0}</p>
|
||||
</div>
|
||||
<FaUniversity className="text-4xl opacity-50" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg p-4 shadow-sm">
|
||||
<p className="text-gray-500 text-sm">میانگین امتیاز</p>
|
||||
<p className="text-2xl font-bold text-yellow-500">{stats.avgRate?.toFixed(1) || 0}</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg p-4 shadow-sm">
|
||||
<p className="text-gray-500 text-sm">مجموع امتیاز</p>
|
||||
<p className="text-2xl font-bold">{stats.totalRate?.toLocaleString() || 0}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Search */}
|
||||
<form onSubmit={handleSearch} className="mb-6">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="جستجوی آکادمی..."
|
||||
value={searchInput}
|
||||
onChange={(e) => setSearchInput(e.target.value)}
|
||||
className="w-full px-4 py-3 pl-12 pr-4 border rounded-xl focus:outline-none focus:ring-2 focus:ring-pink-500"
|
||||
/>
|
||||
<FaSearch className="absolute left-4 top-3.5 text-gray-400" />
|
||||
{searchInput && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClearSearch}
|
||||
className="absolute left-12 top-3.5 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<FaTimes />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Search result info */}
|
||||
{search && (
|
||||
<div className="mb-4 text-sm text-gray-500">
|
||||
نتایج جستجو برای: <span className="font-semibold">{search}</span>
|
||||
<button
|
||||
onClick={handleClearSearch}
|
||||
className="mr-2 text-pink-500 hover:text-pink-600"
|
||||
>
|
||||
(پاک کردن)
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Academies Grid */}
|
||||
{academies.length === 0 && !loading ? (
|
||||
<div className="text-center py-12 bg-gray-50 rounded-xl">
|
||||
<FaUniversity className="text-6xl text-gray-300 mx-auto mb-4" />
|
||||
<p className="text-gray-500">آکادمیای یافت نشد</p>
|
||||
{search && (
|
||||
<button
|
||||
onClick={handleClearSearch}
|
||||
className="mt-4 text-pink-500 hover:text-pink-600"
|
||||
>
|
||||
پاک کردن جستجو
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{academies.map((academy) => {
|
||||
const statusInfo = getStatusText(academy.status);
|
||||
return (
|
||||
<div
|
||||
key={academy._id}
|
||||
className="bg-white rounded-xl shadow-md overflow-hidden hover:shadow-lg transition-all hover:-translate-y-1"
|
||||
>
|
||||
<div className="relative h-32 bg-gradient-to-r from-pink-500 to-purple-500">
|
||||
<div className="absolute top-2 left-2">
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${statusInfo.className}`}>
|
||||
{statusInfo.text}
|
||||
</span>
|
||||
</div>
|
||||
<div className="absolute -bottom-8 right-4">
|
||||
<div className="w-16 h-16 rounded-full bg-white p-1 shadow-lg">
|
||||
<div className="w-full h-full rounded-full overflow-hidden bg-gray-200">
|
||||
{academy.academy_image ? (
|
||||
<img
|
||||
src={getImageUrl(academy.academy_image)}
|
||||
alt={academy.academy_name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center text-gray-400">
|
||||
<FaUniversity />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-10 p-4">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 className="font-bold text-lg line-clamp-1">{academy.academy_name}</h3>
|
||||
<p className="text-sm text-gray-500 mt-1">مدیر: {academy.userId?.user_name || "نامشخص"}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 bg-yellow-100 px-2 py-1 rounded-lg">
|
||||
<FaStar className="text-yellow-500 text-sm" />
|
||||
<span className="text-sm font-bold">{academy.rate || 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-gray-600 mt-2 line-clamp-2">
|
||||
{academy.bio || "توضیحاتی ثبت نشده"}
|
||||
</p>
|
||||
|
||||
<div className="flex justify-between items-center mt-4 pt-3 border-t">
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedAcademy(academy);
|
||||
setShowModal(true);
|
||||
}}
|
||||
className="p-2 text-blue-500 hover:bg-blue-50 rounded-lg transition"
|
||||
title="جزئیات"
|
||||
>
|
||||
<FaEye />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(academy._id)}
|
||||
className="p-2 text-red-500 hover:bg-red-50 rounded-lg transition"
|
||||
title="حذف آکادمی"
|
||||
>
|
||||
<FaTrash />
|
||||
</button>
|
||||
</div>
|
||||
{/* <select
|
||||
value={academy.status || "active"}
|
||||
onChange={(e) => handleStatusChange(academy._id, e.target.value)}
|
||||
className="text-xs px-2 py-1 border rounded-lg focus:outline-none focus:ring-1 focus:ring-pink-500"
|
||||
>
|
||||
<option value="active">فعال</option>
|
||||
<option value="inactive">غیرفعال</option>
|
||||
<option value="blocked">مسدود</option>
|
||||
</select> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex justify-center gap-2 mt-6">
|
||||
<button
|
||||
onClick={() => setPage(p => Math.max(1, p - 1))}
|
||||
disabled={page === 1}
|
||||
className="px-4 py-2 border rounded-lg disabled:opacity-50 hover:bg-gray-50 transition"
|
||||
>
|
||||
قبلی
|
||||
</button>
|
||||
<span className="px-4 py-2">
|
||||
صفحه {page} از {totalPages}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
|
||||
disabled={page === totalPages}
|
||||
className="px-4 py-2 border rounded-lg disabled:opacity-50 hover:bg-gray-50 transition"
|
||||
>
|
||||
بعدی
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Modal */}
|
||||
{showModal && selectedAcademy && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-xl max-w-md w-full mx-4 max-h-[85vh] overflow-y-auto">
|
||||
<div className="flex justify-between items-center p-4 border-b sticky top-0 bg-white">
|
||||
<h3 className="text-lg font-bold">جزئیات آکادمی</h3>
|
||||
<button
|
||||
onClick={() => setShowModal(false)}
|
||||
className="text-gray-500 hover:text-gray-700 transition"
|
||||
>
|
||||
<FaTimes />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-4 space-y-3">
|
||||
<div className="flex justify-center">
|
||||
<div className="w-24 h-24 rounded-full overflow-hidden bg-gray-200 shadow-md">
|
||||
{selectedAcademy.academy_image ? (
|
||||
<img
|
||||
src={getImageUrl(selectedAcademy.academy_image)}
|
||||
alt={selectedAcademy.academy_name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center text-gray-400 text-3xl">
|
||||
<FaUniversity />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-500">نام آکادمی:</span>
|
||||
<span className="font-medium">{selectedAcademy.academy_name}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-500">مدیر:</span>
|
||||
<span>{selectedAcademy.userId?.user_name || "-"}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-500">ایمیل:</span>
|
||||
<span className="text-sm">{selectedAcademy.userId?.email || "-"}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-500">امتیاز:</span>
|
||||
<span className="text-yellow-500 font-bold">{selectedAcademy.rate || 0}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-500">تعداد نظرات:</span>
|
||||
<span>{selectedAcademy.number_of_rate || 0}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-500">شماره شبا:</span>
|
||||
<span className="font-mono text-sm">{selectedAcademy.sheba || "ثبت نشده"}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-500">تاریخ ثبت:</span>
|
||||
<span>{new Date(selectedAcademy.createdAt).toLocaleDateString("fa-IR")}</span>
|
||||
</div>
|
||||
<div className="border-t pt-3">
|
||||
<p className="text-gray-500 mb-1">درباره:</p>
|
||||
<p className="text-sm leading-relaxed">{selectedAcademy.bio || "توضیحاتی ثبت نشده"}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 border-t flex justify-end">
|
||||
<button
|
||||
onClick={() => setShowModal(false)}
|
||||
className="px-4 py-2 bg-gray-200 rounded-lg hover:bg-gray-300 transition"
|
||||
>
|
||||
بستن
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Academies;
|
||||
293
src/pages/academy/academy.jsx
Normal file
293
src/pages/academy/academy.jsx
Normal file
@@ -0,0 +1,293 @@
|
||||
// src/pages/Dashboard.js
|
||||
import React, { useState, useEffect } from "react";
|
||||
import Layout from "../../components/academy/Layout";
|
||||
import {
|
||||
getPaymentStats,
|
||||
getCourseStats,
|
||||
getPurchasedStats,
|
||||
getAcademyStats,
|
||||
getPayments,
|
||||
getCourses
|
||||
} from "../../components/adminApi";
|
||||
import { Link } from "react-router-dom";
|
||||
import { FaWallet, FaBook, FaShoppingCart, FaUniversity, FaStar } from "react-icons/fa";
|
||||
|
||||
const Dashboard = () => {
|
||||
const [stats, setStats] = useState({
|
||||
payments: { totalAmount: 0, totalCount: 0, pendingCount: 0, successCount: 0, settledCount: 0 },
|
||||
courses: { totalCourses: 0, acceptCount: 0, pendingCount: 0, normalCount: 0, proCount: 0, legendCount: 0 },
|
||||
purchased: { totalPurchases: 0, uniqueUsers: 0 },
|
||||
academies: { totalAcademies: 0, avgRate: 0 }
|
||||
});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [recentPayments, setRecentPayments] = useState([]);
|
||||
const [recentCourses, setRecentCourses] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchStats = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [paymentsRes, coursesRes, purchasedRes, academiesRes] = await Promise.all([
|
||||
getPaymentStats(),
|
||||
getCourseStats(),
|
||||
getPurchasedStats(),
|
||||
getAcademyStats()
|
||||
]);
|
||||
|
||||
console.log("Payment Stats:", paymentsRes.data);
|
||||
console.log("Course Stats:", coursesRes.data);
|
||||
console.log("Purchased Stats:", purchasedRes.data);
|
||||
console.log("Academy Stats:", academiesRes.data);
|
||||
|
||||
setStats({
|
||||
payments: paymentsRes.data?.data || {},
|
||||
courses: coursesRes.data?.data || {},
|
||||
purchased: purchasedRes.data?.data || {},
|
||||
academies: academiesRes.data?.data || {}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching stats:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchRecentData = async () => {
|
||||
try {
|
||||
// دریافت 5 پرداخت آخر
|
||||
const paymentsRes = await getPayments({ page: 1, limit: 5 });
|
||||
if (paymentsRes.data?.success) {
|
||||
setRecentPayments(paymentsRes.data.data?.payments || []);
|
||||
}
|
||||
|
||||
// دریافت 5 دوره آخر
|
||||
const coursesRes = await getCourses({ page: 1, limit: 5 });
|
||||
if (coursesRes.data?.success) {
|
||||
setRecentCourses(coursesRes.data.data?.courses || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching recent data:", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchStats();
|
||||
fetchRecentData();
|
||||
}, []);
|
||||
|
||||
const statCards = [
|
||||
// {
|
||||
// title: "مجموع فروش",
|
||||
// value: stats.payments.totalAmount?.toLocaleString() || 0,
|
||||
// unit: "تومان",
|
||||
// icon: <FaWallet className="text-2xl" />,
|
||||
// bgColor: "bg-blue-100",
|
||||
// textColor: "text-blue-600",
|
||||
// iconBg: "bg-blue-500"
|
||||
// },
|
||||
{
|
||||
title: "تعداد پرداختها",
|
||||
value: stats.payments.totalCount || 0,
|
||||
unit: "",
|
||||
icon: <FaWallet className="text-2xl" />,
|
||||
bgColor: "bg-green-100",
|
||||
textColor: "text-green-600",
|
||||
iconBg: "bg-green-500"
|
||||
},
|
||||
{
|
||||
title: "دورههای تایید شده",
|
||||
value: stats.courses.acceptCount || 0,
|
||||
unit: "",
|
||||
subValue: `در انتظار: ${stats.courses.pendingCount || 0}`,
|
||||
icon: <FaBook className="text-2xl" />,
|
||||
bgColor: "bg-purple-100",
|
||||
textColor: "text-purple-600",
|
||||
iconBg: "bg-purple-500"
|
||||
},
|
||||
{
|
||||
title: "کل دورهها",
|
||||
value: stats.courses.totalCourses || 0,
|
||||
unit: "",
|
||||
icon: <FaBook className="text-2xl" />,
|
||||
bgColor: "bg-indigo-100",
|
||||
textColor: "text-indigo-600",
|
||||
iconBg: "bg-indigo-500"
|
||||
},
|
||||
{
|
||||
title: "خریدهای انجام شده",
|
||||
value: stats.purchased.totalPurchases || 0,
|
||||
unit: "",
|
||||
icon: <FaShoppingCart className="text-2xl" />,
|
||||
bgColor: "bg-orange-100",
|
||||
textColor: "text-orange-600",
|
||||
iconBg: "bg-orange-500"
|
||||
},
|
||||
{
|
||||
title: "کاربران خریدار",
|
||||
value: stats.purchased.uniqueUsers || 0,
|
||||
unit: "نفر",
|
||||
icon: <FaShoppingCart className="text-2xl" />,
|
||||
bgColor: "bg-rose-100",
|
||||
textColor: "text-rose-600",
|
||||
iconBg: "bg-rose-500"
|
||||
},
|
||||
{
|
||||
title: "آکادمیها",
|
||||
value: stats.academies.totalAcademies || 0,
|
||||
unit: "",
|
||||
icon: <FaUniversity className="text-2xl" />,
|
||||
bgColor: "bg-teal-100",
|
||||
textColor: "text-teal-600",
|
||||
iconBg: "bg-teal-500"
|
||||
},
|
||||
// {
|
||||
// title: "میانگین امتیاز",
|
||||
// value: stats.academies.avgRate?.toFixed(1) || 0,
|
||||
// unit: "از 5",
|
||||
// icon: <FaStar className="text-2xl" />,
|
||||
// bgColor: "bg-yellow-100",
|
||||
// textColor: "text-yellow-600",
|
||||
// iconBg: "bg-yellow-500"
|
||||
// }
|
||||
];
|
||||
|
||||
const getStatusBadge = (status) => {
|
||||
const badges = {
|
||||
pending: "bg-yellow-100 text-yellow-700",
|
||||
accept: "bg-green-100 text-green-700",
|
||||
success: "bg-blue-100 text-blue-700",
|
||||
settled: "bg-purple-100 text-purple-700",
|
||||
};
|
||||
const texts = {
|
||||
pending: "در انتظار",
|
||||
accept: "تایید شده",
|
||||
success: "موفق",
|
||||
settled: "تسویه شده",
|
||||
};
|
||||
return (
|
||||
<span className={`px-2 py-1 rounded text-xs ${badges[status] || "bg-gray-100 text-gray-700"}`}>
|
||||
{texts[status] || status}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
console.log(stats);
|
||||
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Layout>
|
||||
<div className="flex justify-center items-center h-96">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-pink-500"></div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-gray-800">داشبورد مدیریتی</h1>
|
||||
<p className="text-gray-500 mt-1">خلاصه وضعیت سیستم</p>
|
||||
</div>
|
||||
|
||||
{/* Stat Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
{statCards.map((card, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-white rounded-xl shadow-md p-6 hover:shadow-lg transition-all hover:-translate-y-1"
|
||||
>
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<p className="text-gray-500 text-sm">{card.title}</p>
|
||||
<p className="text-2xl font-bold text-gray-800 mt-2">
|
||||
{card.value}
|
||||
{card.unit && <span className="text-sm mr-1 text-gray-500">{card.unit}</span>}
|
||||
</p>
|
||||
{card.subValue && (
|
||||
<p className="text-xs text-gray-400 mt-1">{card.subValue}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className={`${card.bgColor} p-3 rounded-lg`}>
|
||||
<div className={card.textColor}>{card.icon}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Recent Payments & Courses */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* آخرین پرداختها */}
|
||||
<div className="bg-white rounded-xl shadow-md overflow-hidden">
|
||||
<div className="px-6 py-4 border-b flex justify-between items-center">
|
||||
<h3 className="text-lg font-semibold">آخرین پرداختها</h3>
|
||||
<Link to="/payments" className="text-sm text-pink-500 hover:text-pink-600">
|
||||
مشاهده همه
|
||||
</Link>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
{recentPayments.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<FaWallet className="text-4xl mx-auto mb-2 text-gray-300" />
|
||||
<span>هیچ پرداختی یافت نشد</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{recentPayments.map((payment) => (
|
||||
<div key={payment._id} className="flex justify-between items-center p-3 bg-gray-50 rounded-lg">
|
||||
<div>
|
||||
<p className="font-medium">{payment.course_id?.cuorse_name || "-"}</p>
|
||||
<p className="text-xs text-gray-500">{payment.user_id?.user_name}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="font-bold text-pink-600">{payment.price?.toLocaleString()} تومان</p>
|
||||
{getStatusBadge(payment.status)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* آخرین دورهها */}
|
||||
<div className="bg-white rounded-xl shadow-md overflow-hidden">
|
||||
<div className="px-6 py-4 border-b flex justify-between items-center">
|
||||
<h3 className="text-lg font-semibold">آخرین دورهها</h3>
|
||||
<Link to="/courses" className="text-sm text-pink-500 hover:text-pink-600">
|
||||
مشاهده همه
|
||||
</Link>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
{recentCourses.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<FaBook className="text-4xl mx-auto mb-2 text-gray-300" />
|
||||
<span>هیچ دورهای یافت نشد</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{recentCourses.map((course) => (
|
||||
<div key={course._id} className="flex justify-between items-center p-3 bg-gray-50 rounded-lg">
|
||||
<div>
|
||||
<p className="font-medium">{course.cuorse_name}</p>
|
||||
<p className="text-xs text-gray-500">{course.teacher_name}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="font-bold text-pink-600">{parseInt(course.price).toLocaleString()} تومان</p>
|
||||
{getStatusBadge(course.status)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
286
src/pages/comments/CommentDetail.jsx
Normal file
286
src/pages/comments/CommentDetail.jsx
Normal file
@@ -0,0 +1,286 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { BaseUrl } from '../../components/BaseUrl';
|
||||
import axios from 'axios';
|
||||
import AdminHeader from '../../components/admin-header';
|
||||
import Sidebar from '../../components/sidebar';
|
||||
import Loading from '../../components/elements/loading';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const CommentDetail = () => {
|
||||
const navigate = useNavigate();
|
||||
const param = useParams();
|
||||
const id = param?.id;
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [commentDetails, setCommentDetails] = useState([]);
|
||||
const [commentStatus, setCommentStatus] = useState('');
|
||||
const [editingComment, setEditingComment] = useState(false);
|
||||
const [newComment, setNewComment] = useState('');
|
||||
const getCommentDetails = (page = 1) => {
|
||||
setLoading(true);
|
||||
const config = {
|
||||
url: `${BaseUrl}/comments/detail?comment_id=${id}`,
|
||||
method: "GET",
|
||||
};
|
||||
axios(config)
|
||||
.then((res) => {
|
||||
setCommentDetails(res?.data?.comment);
|
||||
setLoading(false);
|
||||
setNewComment(res?.data?.comment?.comment)
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
getCommentDetails();
|
||||
}, [id]);
|
||||
|
||||
const acceptComment = () => {
|
||||
const data = {
|
||||
id: commentDetails?._id,
|
||||
status: "accepted",
|
||||
text: newComment
|
||||
};
|
||||
axios
|
||||
.post(`${BaseUrl}/comments/accept`, data, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(async (response) => {
|
||||
try {
|
||||
toast.success(response?.data?.message)
|
||||
getCommentDetails()
|
||||
} catch (error) {
|
||||
if (
|
||||
error?.response?.data?.error
|
||||
) {
|
||||
toast.error(error.response.data.error);
|
||||
} else {
|
||||
// نمایش پیام خطا برای سایر موارد
|
||||
toast.error("خطای ناشناخته");
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
toast.show(error?.response?.data?.message, {
|
||||
type: "danger",
|
||||
placement: "top",
|
||||
duration: 4000,
|
||||
offset: 30,
|
||||
animationType: "slide-in",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const rejectComment = () => {
|
||||
const data = {
|
||||
id: commentDetails?._id,
|
||||
status: "rejected"
|
||||
};
|
||||
axios
|
||||
.post(`${BaseUrl}/comments/accept`, data, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(async (response) => {
|
||||
try {
|
||||
toast.success(response?.data?.message)
|
||||
getCommentDetails()
|
||||
} catch (error) {
|
||||
if (
|
||||
error?.response?.data?.error
|
||||
) {
|
||||
toast.error(error.response.data.error);
|
||||
} else {
|
||||
// نمایش پیام خطا برای سایر موارد
|
||||
toast.error("خطای ناشناخته");
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
toast.show(error?.response?.data?.message, {
|
||||
type: "danger",
|
||||
placement: "top",
|
||||
duration: 4000,
|
||||
offset: 30,
|
||||
animationType: "slide-in",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleEditComment = () => {
|
||||
// ارسال درخواست برای ویرایش کامنت به سرور
|
||||
// سپس بروزرسانی متغیر مربوط به متن کامنت
|
||||
setEditingComment(true);
|
||||
};
|
||||
return (<>
|
||||
{loading && <Loading />}
|
||||
{!loading && commentDetails && (
|
||||
<>
|
||||
|
||||
<div className="min-h-screen mx-auto">
|
||||
<div className="flex h-full w-full">
|
||||
<div className=" w-64"><Sidebar /></div>
|
||||
<div className="w-full h-full p-14 mb-11 mx-auto bg-[#252525] text-white ">
|
||||
<div className="w-full flex items-center justify-between">
|
||||
<h4 className="font-medium font-KalamehMed text-lg">
|
||||
اطلاعات کامنت:
|
||||
</h4>
|
||||
<button
|
||||
onClick={() => navigate(-1)}
|
||||
className="flex gap-1 bg-[#1f5f79] font-semibold font-KalamehSemi rounded-lg hover:bg-[#E0E3E3] transition-colors duration-500 p-[10px]"
|
||||
>
|
||||
بازگشت
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-5 pr-5 mt-5">
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
عنوان پروژه:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{commentDetails?.project}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
نام کاربری سازنده:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{commentDetails?.creator?.user_name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
نام و نام خانوادگی سازنده:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{commentDetails?.creator?.first_name} {commentDetails?.creator?.last_name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
نام کاربری کاربر:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{commentDetails?.user?.user_name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
نام و نام خانوادگی کاربر:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{commentDetails?.user?.first_name} {commentDetails?.user?.last_name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
تاریخ ثبت کامنت:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{commentDetails?.createdAt}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
تعداد ستاره:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{commentDetails?.rating}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
متن کامنت:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{commentDetails?.comment}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
وضعیت:
|
||||
</div>
|
||||
<div className={`flex items-center font-medium min-w-[500px] font-KalamehMed text-sm rounded-lg bg-[#111] min-h-14 px-4 ${commentDetails?.status
|
||||
=== "pending" ?
|
||||
"text-yellow-400" :
|
||||
commentDetails?.status == "accepted" ?
|
||||
"text-green-400" :
|
||||
"text-red-400"
|
||||
}`}>
|
||||
{commentDetails?.status
|
||||
=== "pending" ?
|
||||
"در انتظار تایید" :
|
||||
commentDetails?.status == "accepted" ?
|
||||
"تایید شده" :
|
||||
"رد شده"
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 mt-5">
|
||||
{!editingComment ? (
|
||||
<>
|
||||
<button
|
||||
onClick={handleEditComment}
|
||||
className="bg-green-500 text-white px-3 py-1 rounded-lg"
|
||||
>
|
||||
ویرایش نظر
|
||||
</button>
|
||||
<button
|
||||
onClick={acceptComment}
|
||||
className="bg-blue-500 text-white px-3 py-1 rounded-lg"
|
||||
>
|
||||
قبول کامنت
|
||||
</button>
|
||||
<button
|
||||
onClick={rejectComment}
|
||||
className="bg-red-500 text-white px-3 py-1 rounded-lg"
|
||||
>
|
||||
رد کامنت
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className='flex flex-col'>
|
||||
<textarea
|
||||
value={newComment}
|
||||
onChange={(e) => setNewComment(e.target.value)}
|
||||
className="bg-gray-100 text-black rounded-lg p-2 w-[300px] h-[200px] mb-3"
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditingComment(false)
|
||||
acceptComment()
|
||||
}} className="bg-green-500 text-white px-3 py-1 rounded-lg"
|
||||
>
|
||||
ذخیره تغییرات
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setEditingComment(false)}
|
||||
className="bg-red-500 text-white px-3 py-1 rounded-lg"
|
||||
>
|
||||
انصراف
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default CommentDetail
|
||||
225
src/pages/comments/Comments.jsx
Normal file
225
src/pages/comments/Comments.jsx
Normal file
@@ -0,0 +1,225 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import AdminHeader from "../../components/admin-header";
|
||||
import Sidebar from "../../components/sidebar";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { BaseUrl } from "../../components/BaseUrl";
|
||||
import axios from "axios";
|
||||
import Loading from "../../components/elements/loading";
|
||||
import Pagination from "../../components/Pagination";
|
||||
import Filters from "../../components/Filters";
|
||||
|
||||
const Comments = () => {
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [commentList, setCommentList] = useState([]);
|
||||
const [totalPages, setTotalPages] = useState(0);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
//
|
||||
const [filters, setFilters] = useState({ startDate: "", endDate: "", status: "" });
|
||||
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
const statusParam = searchParams.get("status"); // مثلا "pending"
|
||||
const token = document.cookie
|
||||
.split('; ')
|
||||
.find(row => row.startsWith('token='))
|
||||
?.split('=')[1];
|
||||
|
||||
|
||||
const getCommentList = (page = 1) => {
|
||||
setLoading(true);
|
||||
//
|
||||
|
||||
const { startDate, endDate , status} = filters;
|
||||
const config = {
|
||||
url: `${BaseUrl}/comments?limit=20&page=${page}${startDate ? "&startDate=" + startDate : ""}${endDate ? "&endDate=" + endDate : ""}&status=${statusParam? "pending" : status}`,
|
||||
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}` // اضافه کردن توکن به هدر
|
||||
}
|
||||
};
|
||||
axios(config)
|
||||
.then((res) => {
|
||||
setCommentList(res?.data?.comments);
|
||||
setTotalPages(res?.data?.totalPages); // دریافت تعداد صفحات از API
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
// console.log("Error in getCommentList:", err.response?.status, err.response?.data || err.message);
|
||||
console.log("Full error object:", JSON.stringify(err, null, 2));
|
||||
console.log("Message:", err.message);
|
||||
console.log("Response:", err.response);
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
};
|
||||
useEffect(() => {
|
||||
getCommentList(currentPage);
|
||||
}, [currentPage, filters]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="min-h-screen mx-auto bg-gray-900">
|
||||
<div className="flex h-full w-full">
|
||||
<div className="bg-[#1f5f79] w-64">
|
||||
<Sidebar />
|
||||
</div>
|
||||
<div className="w-full h-full p-14 mb-11 mx-auto text-white">
|
||||
{/* Filters */}
|
||||
<Filters
|
||||
statusOptions={[
|
||||
{ key: "", label: "همه" },
|
||||
{ key: "pending", label: "در انتظار تایید" },
|
||||
{ key: "accepted", label: "تایید شده" },
|
||||
{ key: "rejected", label: "رد شده" },
|
||||
]}
|
||||
provinceOptions={[{ value: "", label: "همه استانها" }]}
|
||||
cityOptions={[{ value: "", label: "همه شهرها" }]}
|
||||
photographyModelOptions={[{ value: "", label: "همه مدلها" }]}
|
||||
blockStatusOptions={[{ value: "", label: "همه کاربران" }]}
|
||||
onFilterChange={setFilters}
|
||||
/>
|
||||
|
||||
<div className="w-full mt-4 flex items-center border-b ">
|
||||
<div className="flex-1 w-full flex items-center h-11 px-5">
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
عنوان پروژه
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
نام کاربری سازنده
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
نام و نام خانوادگی سازنده
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
نام کاربری یوزر
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
نام و نام خانوادگی یوزر
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
تاریخ ثبت کامنت
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[12%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">وضعیت</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[11%]">
|
||||
<p className="font-KalamehMed text-sm font-medium"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{loading ? (
|
||||
<div className="w-full flex items-center justify-center mt-5">
|
||||
<Loading className="w-14 h-14 text-blacklead animate-pulse" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-y-scroll hide-scrollbar w-full">
|
||||
{Array.isArray(commentList) && commentList.length > 0 ? (
|
||||
commentList.map((comment, index) => {
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
key={comment?._id || index}
|
||||
className={`flex-1 w-full flex items-center h-[90px] px-5 py-8 rounded-lg ${
|
||||
index % 2 === 0 ? "bg-[#c0e2f04b]" : "bg-[#678fa062]"
|
||||
}`}
|
||||
>
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium mr-2">
|
||||
{comment?.project || "-"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium mr-2">
|
||||
{comment?.creator?.user_name || "-"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium mr-2">
|
||||
{comment?.creator?.first_name || ""}{" "}
|
||||
{comment?.creator?.last_name || ""}
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium mr-2">
|
||||
{comment?.user?.user_name || "-"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium mr-2">
|
||||
{comment?.user?.first_name || ""}{" "}
|
||||
{comment?.user?.last_name || ""}
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium mr-2">
|
||||
{comment?.createdAt || "-"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[12%]">
|
||||
<div className="flex items-center">
|
||||
<p
|
||||
className={`font-KalamehMed text-sm font-medium ${
|
||||
comment?.status === "pending"
|
||||
? "text-yellow-400"
|
||||
: comment?.status === "accepted"
|
||||
? "text-green-400"
|
||||
: "text-red-400"
|
||||
}`}
|
||||
>
|
||||
{comment?.status === "pending"
|
||||
? "در انتظار تایید"
|
||||
: comment?.status === "accepted"
|
||||
? "تایید شده"
|
||||
: comment?.status === "rejected"
|
||||
? "رد شده"
|
||||
: "-"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-1 w-[11%]">
|
||||
<button
|
||||
onClick={() => navigate(`${comment?._id}`)}
|
||||
className="bg-[#1f5f79] hover:bg-[#247392] px-8 h-[35px] rounded-md duration-300 mr-5"
|
||||
>
|
||||
جزئیات
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<p className="text-center text-gray-400 mt-6">
|
||||
هیچ کامنتی یافت نشد.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Pagination
|
||||
totalItems={commentList.length}
|
||||
totalPages={totalPages}
|
||||
currentPage={currentPage}
|
||||
onPageChange={(page) => setCurrentPage(page)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Comments;
|
||||
447
src/pages/courses/courses.jsx
Normal file
447
src/pages/courses/courses.jsx
Normal file
@@ -0,0 +1,447 @@
|
||||
// src/pages/Courses.js
|
||||
import React, { useState, useEffect } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import {
|
||||
getCourses,
|
||||
updateCourseStatus,
|
||||
deleteCourse,
|
||||
getCourseStats,
|
||||
} from "../../components/adminApi";
|
||||
import Layout from "../../components/academy/Layout";
|
||||
import {
|
||||
FaTrash,
|
||||
FaFilter,
|
||||
} from "react-icons/fa";
|
||||
|
||||
const Courses = () => {
|
||||
const [courses, setCourses] = useState([]);
|
||||
const [stats, setStats] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [page, setPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [filters, setFilters] = useState({
|
||||
status: "",
|
||||
type: "",
|
||||
category: "",
|
||||
search: "",
|
||||
});
|
||||
const [showFilters, setShowFilters] = useState(false);
|
||||
|
||||
const fetchCourses = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await getCourses({ page, limit: 10, ...filters });
|
||||
console.log("Courses response:", response.data);
|
||||
|
||||
if (response.data.success) {
|
||||
const responseData = response.data.data;
|
||||
setCourses(responseData?.courses || []);
|
||||
setStats(responseData?.stats || null);
|
||||
setTotalPages(responseData?.pagination?.totalPages || 1);
|
||||
} else {
|
||||
toast.error(response.data.message || "خطا در دریافت دورهها");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching courses:", error);
|
||||
toast.error("خطا در دریافت دورهها");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
const response = await getCourseStats();
|
||||
console.log("Stats response:", response.data);
|
||||
|
||||
if (response.data.success) {
|
||||
setStats(response.data.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching stats:", error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchCourses();
|
||||
fetchStats();
|
||||
}, [page, filters.status, filters.type, filters.category, filters.search]);
|
||||
|
||||
// تغییر وضعیت (status)
|
||||
const handleStatusChange = async (courseId, newStatus) => {
|
||||
console.log("Changing status:", { courseId, newStatus });
|
||||
try {
|
||||
const response = await updateCourseStatus({ courseId, status: newStatus });
|
||||
console.log("Response:", response.data);
|
||||
|
||||
if (response.data.success) {
|
||||
toast.success("وضعیت دوره تغییر کرد");
|
||||
|
||||
// ✅ آپدیت مستقیم state بدون نیاز به fetch جدید
|
||||
setCourses(prevCourses =>
|
||||
prevCourses.map(course =>
|
||||
course._id === courseId
|
||||
? { ...course, status: newStatus }
|
||||
: course
|
||||
)
|
||||
);
|
||||
|
||||
// آپدیت stats
|
||||
if (stats) {
|
||||
// کاهش/افزایش آمار
|
||||
const oldStatus = courses.find(c => c._id === courseId)?.status;
|
||||
if (oldStatus === "pending" && newStatus === "accept") {
|
||||
setStats(prev => ({
|
||||
...prev,
|
||||
pendingCount: (prev.pendingCount || 0) - 1,
|
||||
acceptCount: (prev.acceptCount || 0) + 1
|
||||
}));
|
||||
} else if (oldStatus === "pending" && newStatus === "reject") {
|
||||
setStats(prev => ({
|
||||
...prev,
|
||||
pendingCount: (prev.pendingCount || 0) - 1,
|
||||
rejectCount: (prev.rejectCount || 0) + 1
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// فقط stats رو رفرش کن
|
||||
fetchStats();
|
||||
} else {
|
||||
toast.error(response.data.message || "خطا در تغییر وضعیت");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating status:", error);
|
||||
toast.error("خطا در تغییر وضعیت");
|
||||
}
|
||||
};
|
||||
|
||||
// تغییر نوع (type)
|
||||
const handleTypeChange = async (courseId, newType) => {
|
||||
console.log("Changing type:", { courseId, newType });
|
||||
try {
|
||||
const response = await updateCourseStatus({ courseId, type: newType });
|
||||
console.log("Response:", response.data);
|
||||
|
||||
if (response.data.success) {
|
||||
toast.success("نوع دوره تغییر کرد");
|
||||
|
||||
// ✅ آپدیت مستقیم state
|
||||
setCourses(prevCourses =>
|
||||
prevCourses.map(course =>
|
||||
course._id === courseId
|
||||
? { ...course, type: newType }
|
||||
: course
|
||||
)
|
||||
);
|
||||
} else {
|
||||
toast.error(response.data.message || "خطا در تغییر نوع");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating type:", error);
|
||||
toast.error("خطا در تغییر نوع");
|
||||
}
|
||||
};
|
||||
const handleDelete = async (courseId) => {
|
||||
if (window.confirm("آیا از حذف این دوره مطمئن هستید؟")) {
|
||||
try {
|
||||
const response = await deleteCourse(courseId);
|
||||
if (response.data.success) {
|
||||
toast.success("دوره حذف شد");
|
||||
fetchCourses();
|
||||
fetchStats();
|
||||
} else {
|
||||
toast.error(response.data.message || "خطا در حذف دوره");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error deleting course:", error);
|
||||
toast.error("خطا در حذف دوره");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleFilterChange = (e) => {
|
||||
setFilters({
|
||||
...filters,
|
||||
[e.target.name]: e.target.value,
|
||||
});
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const clearFilters = () => {
|
||||
setFilters({
|
||||
status: "",
|
||||
type: "",
|
||||
category: "",
|
||||
search: "",
|
||||
});
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const getStatusBadge = (status) => {
|
||||
const badges = {
|
||||
pending: "bg-yellow-500",
|
||||
accept: "bg-green-500",
|
||||
reject: "bg-red-500",
|
||||
};
|
||||
const texts = {
|
||||
pending: "در انتظار",
|
||||
accept: "تایید شده",
|
||||
reject: "رد شده",
|
||||
};
|
||||
return (
|
||||
<span
|
||||
className={`px-2 py-1 rounded text-xs text-white ${
|
||||
badges[status] || "bg-gray-500"
|
||||
}`}
|
||||
>
|
||||
{texts[status] || status}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const getTypeBadge = (type) => {
|
||||
const badges = {
|
||||
normal: "bg-blue-500",
|
||||
pro: "bg-purple-500",
|
||||
legend: "bg-pink-500",
|
||||
};
|
||||
const texts = {
|
||||
normal: "معمولی",
|
||||
pro: "پرو",
|
||||
legend: "لجند",
|
||||
};
|
||||
return (
|
||||
<span
|
||||
className={`px-2 py-1 rounded text-xs text-white ${
|
||||
badges[type] || "bg-gray-500"
|
||||
}`}
|
||||
>
|
||||
{texts[type] || type}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
if (loading && page === 1) {
|
||||
return (
|
||||
<Layout>
|
||||
<div className="flex justify-center items-center h-96">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-pink-500"></div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h1 className="text-2xl font-bold">مدیریت دورهها</h1>
|
||||
<button
|
||||
onClick={() => setShowFilters(!showFilters)}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-gray-100 rounded-lg hover:bg-gray-200 transition"
|
||||
>
|
||||
<FaFilter />
|
||||
فیلترها
|
||||
{Object.values(filters).some((v) => v) && (
|
||||
<span className="w-2 h-2 bg-pink-500 rounded-full"></span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
{stats && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-6">
|
||||
<div className="bg-white rounded-lg p-4 shadow-sm">
|
||||
<p className="text-gray-500 text-sm">کل دورهها</p>
|
||||
<p className="text-2xl font-bold">{stats.totalCourses || 0}</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg p-4 shadow-sm">
|
||||
<p className="text-gray-500 text-sm">تایید شده</p>
|
||||
<p className="text-2xl font-bold text-green-600">
|
||||
{stats.acceptCount || 0}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg p-4 shadow-sm">
|
||||
<p className="text-gray-500 text-sm">در انتظار</p>
|
||||
<p className="text-2xl font-bold text-yellow-600">
|
||||
{stats.pendingCount || 0}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg p-4 shadow-sm">
|
||||
<p className="text-gray-500 text-sm">پرو</p>
|
||||
<p className="text-2xl font-bold text-purple-600">
|
||||
{stats.proCount || 0}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Filters Panel */}
|
||||
{showFilters && (
|
||||
<div className="bg-white rounded-lg p-4 shadow-sm mb-6 border">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="font-semibold">فیلترها</h3>
|
||||
<button
|
||||
onClick={clearFilters}
|
||||
className="text-sm text-pink-500 hover:text-pink-600"
|
||||
>
|
||||
پاک کردن همه
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<input
|
||||
type="text"
|
||||
name="search"
|
||||
placeholder="جستجو..."
|
||||
value={filters.search}
|
||||
onChange={handleFilterChange}
|
||||
className="px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-pink-500"
|
||||
/>
|
||||
<select
|
||||
name="status"
|
||||
value={filters.status}
|
||||
onChange={handleFilterChange}
|
||||
className="px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-pink-500"
|
||||
>
|
||||
<option value="">همه وضعیتها</option>
|
||||
<option value="pending">در انتظار</option>
|
||||
<option value="accept">تایید شده</option>
|
||||
<option value="reject">رد شده</option>
|
||||
</select>
|
||||
<select
|
||||
name="type"
|
||||
value={filters.type}
|
||||
onChange={handleFilterChange}
|
||||
className="px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-pink-500"
|
||||
>
|
||||
<option value="">همه نوعها</option>
|
||||
<option value="normal">معمولی</option>
|
||||
<option value="pro">پرو</option>
|
||||
<option value="legend">لجند</option>
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
name="category"
|
||||
placeholder="دسته بندی"
|
||||
value={filters.category}
|
||||
onChange={handleFilterChange}
|
||||
className="px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-pink-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Courses Table */}
|
||||
<div className="bg-white rounded-lg shadow overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full min-w-[900px]">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-right">نام دوره</th>
|
||||
<th className="px-4 py-3 text-right">مدرس</th>
|
||||
<th className="px-4 py-3 text-right">دسته بندی</th>
|
||||
<th className="px-4 py-3 text-right">قیمت</th>
|
||||
<th className="px-4 py-3 text-right">تخفیف</th>
|
||||
<th className="px-4 py-3 text-right">نوع</th>
|
||||
<th className="px-4 py-3 text-right">وضعیت</th>
|
||||
<th className="px-4 py-3 text-right">عملیات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{courses.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={8} className="text-center py-8 text-gray-500">
|
||||
هیچ دورهای یافت نشد
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
courses.map((course) => (
|
||||
<tr key={course._id} className="border-t hover:bg-gray-50">
|
||||
<td className="px-4 py-3">
|
||||
<div>
|
||||
<p className="font-medium">{course.cuorse_name}</p>
|
||||
<p className="text-xs text-gray-500 line-clamp-1">
|
||||
{course.caption?.substring(0, 50)}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3">{course.teacher_name}</td>
|
||||
<td className="px-4 py-3">{course.category}</td>
|
||||
<td className="px-4 py-3">
|
||||
{parseInt(course.price).toLocaleString()} تومان
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{course.offer ? `${course.offer}%` : "-"}
|
||||
</td>
|
||||
<td className="px-4 py-3">{getTypeBadge(course.type)}</td>
|
||||
<td className="px-4 py-3">
|
||||
{getStatusBadge(course.status)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<select
|
||||
onChange={(e) => handleStatusChange(course._id, e.target.value)}
|
||||
value={course.status}
|
||||
className="text-xs px-2 py-1 border rounded focus:outline-none focus:ring-1 focus:ring-pink-500"
|
||||
>
|
||||
<option value="pending">در انتظار</option>
|
||||
<option value="accept">تایید</option>
|
||||
<option value="reject">رد</option>
|
||||
</select>
|
||||
|
||||
<select
|
||||
onChange={(e) => handleTypeChange(course._id, e.target.value)}
|
||||
value={course.type}
|
||||
className="text-xs px-2 py-1 border rounded focus:outline-none focus:ring-1 focus:ring-pink-500"
|
||||
>
|
||||
<option value="normal">معمولی</option>
|
||||
<option value="pro">پرو</option>
|
||||
<option value="legend">لجند</option>
|
||||
</select>
|
||||
|
||||
<button
|
||||
onClick={() => handleDelete(course._id)}
|
||||
className="p-1 text-red-500 hover:text-red-700 transition"
|
||||
title="حذف دوره"
|
||||
>
|
||||
<FaTrash />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex justify-center gap-2 mt-6">
|
||||
<button
|
||||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||
disabled={page === 1}
|
||||
className="px-4 py-2 border rounded-lg disabled:opacity-50 hover:bg-gray-50 transition"
|
||||
>
|
||||
قبلی
|
||||
</button>
|
||||
<span className="px-4 py-2">
|
||||
صفحه {page} از {totalPages}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
|
||||
disabled={page === totalPages}
|
||||
className="px-4 py-2 border rounded-lg disabled:opacity-50 hover:bg-gray-50 transition"
|
||||
>
|
||||
بعدی
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Courses;
|
||||
587
src/pages/expertise/Expertise.jsx
Normal file
587
src/pages/expertise/Expertise.jsx
Normal file
@@ -0,0 +1,587 @@
|
||||
// داخل Expertise.jsx
|
||||
import React, { useEffect, useState } from "react";
|
||||
import axios from "axios";
|
||||
import { AppBaseUrl, BaseUrl } from "../../components/BaseUrl";
|
||||
import Sidebar from "../../components/sidebar";
|
||||
|
||||
function Expertise() {
|
||||
const [expertises, setExpertises] = useState([]);
|
||||
const [categories, setCategories] = useState([]); // دستهبندی تبلیغات
|
||||
const [academyCategories, setAcademyCategories] = useState([]); // دستهبندی آکادمی
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [formData, setFormData] = useState({
|
||||
expertise: "",
|
||||
subExpertise: "",
|
||||
categoryTitle: "",
|
||||
academyCategoryTitle: "",
|
||||
academyCategoryDescription: "",
|
||||
academyCategoryIcon: "",
|
||||
academyCategoryColor: "#3B82F6",
|
||||
academyCategoryParent: "",
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// گرفتن تخصصها
|
||||
// ==========================================
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
axios
|
||||
.get(`${BaseUrl}/expertise`)
|
||||
.then((res) => {
|
||||
setExpertises(res.data.expertises);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
setError("خطا در دریافت اطلاعات تخصصها");
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
// ==========================================
|
||||
// گرفتن دستهبندی تبلیغات
|
||||
// ==========================================
|
||||
useEffect(() => {
|
||||
axios
|
||||
.get(`${BaseUrl}/advertising/advertising-categories`)
|
||||
.then((res) => {
|
||||
setCategories(res.data?.docs || []);
|
||||
console.log(res.data.docs);
|
||||
})
|
||||
.catch(() => setError("خطا در دریافت دستهبندی بیلبورد"));
|
||||
}, []);
|
||||
|
||||
// ==========================================
|
||||
// گرفتن دستهبندی آکادمی
|
||||
// ==========================================
|
||||
useEffect(() => {
|
||||
fetchAcademyCategories();
|
||||
}, []);
|
||||
|
||||
const fetchAcademyCategories = async () => {
|
||||
try {
|
||||
const response = await axios.get(`${AppBaseUrl}/academy/categories`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||
},
|
||||
});
|
||||
setAcademyCategories(response.data.data.docs || response.data.data || []);
|
||||
} catch (err) {
|
||||
console.error("خطا در دریافت دستهبندی آکادمی:", err);
|
||||
setError("خطا در دریافت دستهبندی آکادمی");
|
||||
}
|
||||
};
|
||||
|
||||
// ==========================================
|
||||
// تغییر اینپوتها
|
||||
// ==========================================
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData({
|
||||
...formData,
|
||||
[name]: value,
|
||||
});
|
||||
};
|
||||
|
||||
// ==========================================
|
||||
// افزودن تخصص
|
||||
// ==========================================
|
||||
const handleSubmitExpertise = (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
axios
|
||||
.post(`${BaseUrl}/expertise/create`, { expertise: formData.expertise })
|
||||
.then(() => {
|
||||
setExpertises([
|
||||
...expertises,
|
||||
{ expertise: formData.expertise, sub_expertise: [] },
|
||||
]);
|
||||
setFormData({ ...formData, expertise: "" });
|
||||
alert("تخصص با موفقیت اضافه شد");
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
setError("خطا در ذخیره تخصص");
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
// ==========================================
|
||||
// افزودن زیر تخصص
|
||||
// ==========================================
|
||||
const handleSubmitSubExpertise = (e, expertiseId) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
axios
|
||||
.post(`${BaseUrl}/expertise/create/sub?id=${expertiseId}`, {
|
||||
name: formData.subExpertise,
|
||||
})
|
||||
.then(() => {
|
||||
const updated = expertises.map((exp) =>
|
||||
exp._id === expertiseId
|
||||
? {
|
||||
...exp,
|
||||
sub_expertise: [
|
||||
...exp.sub_expertise,
|
||||
{ name: formData.subExpertise },
|
||||
],
|
||||
}
|
||||
: exp
|
||||
);
|
||||
setExpertises(updated);
|
||||
setFormData({ ...formData, subExpertise: "" });
|
||||
alert("زیرتخصص با موفقیت اضافه شد");
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
setError("خطا در ذخیره زیرتخصص");
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
// ==========================================
|
||||
// افزودن دستهبندی تبلیغات
|
||||
// ==========================================
|
||||
const handleSubmitCategory = (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
axios
|
||||
.post(`${BaseUrl}/advertising/advertising-categories`, {
|
||||
title: formData.categoryTitle,
|
||||
})
|
||||
.then((res) => {
|
||||
setCategories([...categories, res.data.data]);
|
||||
setFormData({ ...formData, categoryTitle: "" });
|
||||
alert("دستهبندی با موفقیت اضافه شد");
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
setError("خطا در ذخیره دستهبندی");
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
// ==========================================
|
||||
// حذف دستهبندی تبلیغات
|
||||
// ==========================================
|
||||
const handleDeleteCategory = (id) => {
|
||||
if (!window.confirm("آیا مطمئن هستید که میخواهید حذف کنید؟")) return;
|
||||
|
||||
axios
|
||||
.delete(`${BaseUrl}/advertising/advertising-categories/${id}`)
|
||||
.then(() => {
|
||||
setCategories(categories.filter((cat) => cat._id !== id));
|
||||
alert("دستهبندی حذف شد");
|
||||
})
|
||||
.catch(() => setError("خطا در حذف دستهبندی"));
|
||||
};
|
||||
|
||||
// ==========================================
|
||||
// افزودن دستهبندی آکادمی
|
||||
// ==========================================
|
||||
const handleSubmitAcademyCategory = async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const token = localStorage.getItem("token");
|
||||
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${AppBaseUrl}/academy/categories`,
|
||||
{
|
||||
title: formData.academyCategoryTitle,
|
||||
description: formData.academyCategoryDescription,
|
||||
icon: formData.academyCategoryIcon,
|
||||
color: formData.academyCategoryColor,
|
||||
parent: formData.academyCategoryParent || null,
|
||||
order: academyCategories.length,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (response.data.success) {
|
||||
await fetchAcademyCategories(); // رفرش لیست
|
||||
setFormData({
|
||||
...formData,
|
||||
academyCategoryTitle: "",
|
||||
academyCategoryDescription: "",
|
||||
academyCategoryIcon: "",
|
||||
academyCategoryColor: "#3B82F6",
|
||||
academyCategoryParent: "",
|
||||
});
|
||||
alert("دستهبندی آکادمی با موفقیت اضافه شد");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("خطا در افزودن دستهبندی آکادمی:", err);
|
||||
setError(err.response?.data?.message || "خطا در ذخیره دستهبندی آکادمی");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// ==========================================
|
||||
// حذف دستهبندی آکادمی (سافت دیلیت)
|
||||
// ==========================================
|
||||
const handleDeleteAcademyCategory = async (id) => {
|
||||
if (!window.confirm("آیا مطمئن هستید که میخواهید این دستهبندی را حذف کنید؟")) return;
|
||||
|
||||
const token = localStorage.getItem("token");
|
||||
|
||||
try {
|
||||
await axios.delete(`${AppBaseUrl}/academy/categories/${id}`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
await fetchAcademyCategories(); // رفرش لیست
|
||||
alert("دستهبندی آکادمی با موفقیت حذف شد");
|
||||
} catch (err) {
|
||||
console.error("خطا در حذف دستهبندی آکادمی:", err);
|
||||
setError(err.response?.data?.message || "خطا در حذف دستهبندی آکادمی");
|
||||
}
|
||||
};
|
||||
|
||||
// ==========================================
|
||||
// تغییر وضعیت دستهبندی آکادمی (فعال/غیرفعال)
|
||||
// ==========================================
|
||||
const handleToggleAcademyCategoryStatus = async (id, currentStatus) => {
|
||||
const newStatus = currentStatus === "active" ? "inactive" : "active";
|
||||
const token = localStorage.getItem("token");
|
||||
|
||||
try {
|
||||
await axios.patch(
|
||||
`${AppBaseUrl}/academy/categories/${id}/status`,
|
||||
{ status: newStatus },
|
||||
{ headers: { Authorization: `Bearer ${token}` } }
|
||||
);
|
||||
await fetchAcademyCategories(); // رفرش لیست
|
||||
alert(`وضعیت با موفقیت به ${newStatus === "active" ? "فعال" : "غیرفعال"} تغییر کرد`);
|
||||
} catch (err) {
|
||||
console.error("خطا در تغییر وضعیت:", err);
|
||||
setError("خطا در تغییر وضعیت دستهبندی");
|
||||
}
|
||||
};
|
||||
|
||||
// ==========================================
|
||||
// ویرایش دستهبندی آکادمی
|
||||
// ==========================================
|
||||
const handleEditAcademyCategory = async (id, newTitle) => {
|
||||
const newName = prompt("عنوان جدید را وارد کنید:", newTitle);
|
||||
if (!newName || newName === newTitle) return;
|
||||
|
||||
const token = localStorage.getItem("token");
|
||||
|
||||
try {
|
||||
await axios.put(
|
||||
`${AppBaseUrl}/academy/categories/${id}`,
|
||||
{ title: newName },
|
||||
{ headers: { Authorization: `Bearer ${token}` } }
|
||||
);
|
||||
await fetchAcademyCategories(); // رفرش لیست
|
||||
alert("دستهبندی با موفقیت ویرایش شد");
|
||||
} catch (err) {
|
||||
console.error("خطا در ویرایش:", err);
|
||||
setError("خطا در ویرایش دستهبندی");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen mx-auto flex flex-col bg-gray-900">
|
||||
<div className="flex flex-1 h-full w-full">
|
||||
<div className="w-64">
|
||||
<Sidebar />
|
||||
</div>
|
||||
<div className="w-full h-full p-12 mb-11 mx-auto text-white relative rounded-xl shadow-lg overflow-y-auto">
|
||||
<h1 className="text-2xl font-bold mb-6">
|
||||
مدیریت تخصصها و دستهبندیها
|
||||
</h1>
|
||||
|
||||
{/* خطا */}
|
||||
{error && <p className="text-red-500 mb-4">{error}</p>}
|
||||
|
||||
{/* ========================================== */}
|
||||
{/* بخش مدیریت تخصصها */}
|
||||
{/* ========================================== */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold mb-4">مدیریت تخصصها</h2>
|
||||
<form
|
||||
onSubmit={handleSubmitExpertise}
|
||||
className="mb-4 flex items-center space-x-4 rtl:space-x-reverse"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
name="expertise"
|
||||
value={formData.expertise}
|
||||
onChange={handleChange}
|
||||
placeholder="نام تخصص"
|
||||
required
|
||||
className="p-2 w-1/3 bg-gray-800 text-white border border-gray-600 rounded-md"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="p-2 bg-green-600 text-white rounded-md"
|
||||
>
|
||||
{loading ? "در حال بارگذاری..." : "افزودن تخصص"}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{expertises.map((exp) => (
|
||||
<div
|
||||
key={exp._id}
|
||||
className="mb-6 p-4 bg-gray-800 rounded-md shadow-md"
|
||||
>
|
||||
<h3 className="text-xl font-semibold">{exp.expertise}</h3>
|
||||
<form
|
||||
onSubmit={(e) => handleSubmitSubExpertise(e, exp._id)}
|
||||
className="mt-4 flex items-center space-x-4 rtl:space-x-reverse"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
name="subExpertise"
|
||||
value={formData.subExpertise}
|
||||
onChange={handleChange}
|
||||
placeholder="نام زیرتخصص"
|
||||
required
|
||||
className="p-2 w-1/3 bg-gray-800 text-white border border-gray-600 rounded-md"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="p-2 bg-blue-600 text-white rounded-md"
|
||||
>
|
||||
{loading ? "..." : "افزودن زیرتخصص"}
|
||||
</button>
|
||||
</form>
|
||||
{exp.sub_expertise?.length > 0 && (
|
||||
<ul className="mt-4 list-disc pr-5">
|
||||
{exp.sub_expertise.map((sub, i) => (
|
||||
<li key={i} className="text-sm text-gray-300">
|
||||
{sub.name}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
|
||||
{/* ========================================== */}
|
||||
{/* بخش مدیریت دستهبندی تبلیغات (بیلبورد) */}
|
||||
{/* ========================================== */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold mb-4">
|
||||
مدیریت دستهبندی بیلبورد
|
||||
</h2>
|
||||
<form
|
||||
onSubmit={handleSubmitCategory}
|
||||
className="mb-4 flex items-center space-x-4 rtl:space-x-reverse"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
name="categoryTitle"
|
||||
value={formData.categoryTitle}
|
||||
onChange={handleChange}
|
||||
placeholder="عنوان دستهبندی بیلبورد"
|
||||
required
|
||||
className="p-2 w-1/3 bg-gray-800 text-white border border-gray-600 rounded-md"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="p-2 bg-green-600 text-white rounded-md"
|
||||
>
|
||||
{loading ? "..." : "افزودن دستهبندی"}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{Array.isArray(categories) && categories.length > 0 ? (
|
||||
categories.map((cat) => (
|
||||
<div
|
||||
key={cat._id}
|
||||
className="flex items-center justify-between p-3 bg-gray-800 rounded-md mb-2"
|
||||
>
|
||||
<span>{cat.title}</span>
|
||||
<button
|
||||
onClick={() => handleDeleteCategory(cat._id)}
|
||||
className="p-1 bg-red-600 text-white rounded-md"
|
||||
>
|
||||
حذف
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="text-gray-400">هیچ دستهبندیای وجود ندارد</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* ========================================== */}
|
||||
{/* بخش مدیریت دستهبندی آکادمی (جدید) */}
|
||||
{/* ========================================== */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold mb-4 text-purple-400">
|
||||
🏷️ مدیریت دستهبندی آکادمی
|
||||
</h2>
|
||||
|
||||
{/* فرم افزودن دستهبندی آکادمی */}
|
||||
<form
|
||||
onSubmit={handleSubmitAcademyCategory}
|
||||
className="mb-6 p-4 bg-gray-800 rounded-lg"
|
||||
>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm mb-1">عنوان دستهبندی *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="academyCategoryTitle"
|
||||
value={formData.academyCategoryTitle}
|
||||
onChange={handleChange}
|
||||
placeholder="مثال: برنامهنویسی، طراحی، بازار..."
|
||||
required
|
||||
className="p-2 w-full bg-gray-700 text-white border border-gray-600 rounded-md"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm mb-1">توضیحات (اختیاری)</label>
|
||||
<input
|
||||
type="text"
|
||||
name="academyCategoryDescription"
|
||||
value={formData.academyCategoryDescription}
|
||||
onChange={handleChange}
|
||||
placeholder="توضیحات کوتاه"
|
||||
className="p-2 w-full bg-gray-700 text-white border border-gray-600 rounded-md"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm mb-1">آیکون (اختیاری)</label>
|
||||
<input
|
||||
type="text"
|
||||
name="academyCategoryIcon"
|
||||
value={formData.academyCategoryIcon}
|
||||
onChange={handleChange}
|
||||
placeholder="مثال: 🎓, 💻, 🎨"
|
||||
className="p-2 w-full bg-gray-700 text-white border border-gray-600 rounded-md"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm mb-1">رنگ (اختیاری)</label>
|
||||
<input
|
||||
type="color"
|
||||
name="academyCategoryColor"
|
||||
value={formData.academyCategoryColor}
|
||||
onChange={handleChange}
|
||||
className="p-1 w-full h-10 bg-gray-700 border border-gray-600 rounded-md"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm mb-1">دستهبندی والد (اختیاری)</label>
|
||||
<select
|
||||
name="academyCategoryParent"
|
||||
value={formData.academyCategoryParent}
|
||||
onChange={handleChange}
|
||||
className="p-2 w-full bg-gray-700 text-white border border-gray-600 rounded-md"
|
||||
>
|
||||
<option value="">بدون والد (دسته اصلی)</option>
|
||||
{academyCategories
|
||||
.filter((cat) => cat.status !== "deleted")
|
||||
.map((cat) => (
|
||||
<option key={cat._id} value={cat._id}>
|
||||
{cat.icon} {cat.title}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="mt-4 p-2 bg-purple-600 text-white rounded-md w-full md:w-auto"
|
||||
>
|
||||
{loading ? "در حال افزودن..." : "➕ افزودن دستهبندی آکادمی"}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{/* لیست دستهبندیهای آکادمی */}
|
||||
<h3 className="text-lg font-semibold mb-3">لیست دستهبندیها</h3>
|
||||
|
||||
{academyCategories.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{academyCategories.map((cat) => (
|
||||
<div
|
||||
key={cat._id}
|
||||
className={`flex items-center justify-between p-3 rounded-md ${
|
||||
cat.status === "inactive"
|
||||
? "bg-gray-800 opacity-50"
|
||||
: "bg-gray-800"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-2xl">{cat.icon || "🏷️"}</span>
|
||||
<span style={{ color: cat.color }} className="font-medium">
|
||||
{cat.title}
|
||||
</span>
|
||||
{cat.parent && (
|
||||
<span className="text-xs text-gray-400">
|
||||
← والد: {cat.parent.title}
|
||||
</span>
|
||||
)}
|
||||
{cat.course_count > 0 && (
|
||||
<span className="text-xs bg-blue-600 px-2 py-0.5 rounded-full">
|
||||
{cat.course_count} دوره
|
||||
</span>
|
||||
)}
|
||||
{cat.status === "inactive" && (
|
||||
<span className="text-xs bg-red-600 px-2 py-0.5 rounded-full">
|
||||
غیرفعال
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleEditAcademyCategory(cat._id, cat.title)}
|
||||
className="p-1 bg-yellow-600 text-white rounded-md text-sm px-3"
|
||||
>
|
||||
✏️ ویرایش
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleToggleAcademyCategoryStatus(cat._id, cat.status)}
|
||||
className={`p-1 rounded-md text-sm px-3 ${
|
||||
cat.status === "active"
|
||||
? "bg-orange-600"
|
||||
: "bg-green-600"
|
||||
} text-white`}
|
||||
>
|
||||
{cat.status === "active" ? "🔒 غیرفعال" : "🔓 فعال"}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteAcademyCategory(cat._id)}
|
||||
className="p-1 bg-red-600 text-white rounded-md text-sm px-3"
|
||||
>
|
||||
🗑️ حذف
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-gray-400">هیچ دستهبندی آکادمی وجود ندارد</p>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Expertise;
|
||||
226
src/pages/financials/FinancialDetail.jsx
Normal file
226
src/pages/financials/FinancialDetail.jsx
Normal file
@@ -0,0 +1,226 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { BaseUrl } from '../../components/BaseUrl';
|
||||
import axios from 'axios';
|
||||
import AdminHeader from '../../components/admin-header';
|
||||
import Sidebar from '../../components/sidebar';
|
||||
import Loading from '../../components/elements/loading';
|
||||
|
||||
const FinancialDetail = () => {
|
||||
const navigate = useNavigate();
|
||||
const param = useParams();
|
||||
const id = param?.id;
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [financialDetails, setFinancialDetails] = useState([]);
|
||||
|
||||
const getFinancialDetails = (page = 1) => {
|
||||
setLoading(true);
|
||||
const config = {
|
||||
url: `${BaseUrl}/financial/detail?financial_id=${id}`,
|
||||
method: "GET",
|
||||
};
|
||||
axios(config)
|
||||
.then((res) => {
|
||||
setFinancialDetails(res?.data?.financial);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
getFinancialDetails();
|
||||
}, [id]);
|
||||
return (
|
||||
<>
|
||||
|
||||
<div className="min-h-screen mx-auto">
|
||||
<div className="flex h-full w-full">
|
||||
<div className=" w-64"><Sidebar /></div>
|
||||
<div className="w-full h-full p-14 mb-11 mx-auto bg-[#252525] text-white ">
|
||||
<div className="w-full flex items-center justify-between">
|
||||
<h4 className="font-medium font-KalamehMed text-lg">
|
||||
اطلاعات تراکنش:
|
||||
</h4>
|
||||
<button
|
||||
onClick={() => navigate(-1)}
|
||||
className="flex gap-1 bg-[#1f5f79] font-semibold font-KalamehSemi rounded-lg hover:bg-[#E0E3E3] transition-colors duration-500 p-[10px] "
|
||||
>
|
||||
بازگشت
|
||||
<svg
|
||||
width="24"
|
||||
height="25"
|
||||
viewBox="0 0 24 25"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M9.56994 19.3191C9.37994 19.3191 9.18994 19.2491 9.03994 19.0991L2.96994 13.0291C2.67994 12.7391 2.67994 12.2591 2.96994 11.9691L9.03994 5.89914C9.32994 5.60914 9.80994 5.60914 10.0999 5.89914C10.3899 6.18914 10.3899 6.66914 10.0999 6.95914L4.55994 12.4991L10.0999 18.0391C10.3899 18.3291 10.3899 18.8091 10.0999 19.0991C9.95994 19.2491 9.75994 19.3191 9.56994 19.3191Z"
|
||||
fill="#fff"
|
||||
/>
|
||||
<path
|
||||
d="M20.4999 13.25H3.66992C3.25992 13.25 2.91992 12.91 2.91992 12.5C2.91992 12.09 3.25992 11.75 3.66992 11.75H20.4999C20.9099 11.75 21.2499 12.09 21.2499 12.5C21.2499 12.91 20.9099 13.25 20.4999 13.25Z"
|
||||
fill="#fff"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{loading ? (
|
||||
<div className="w-full flex items-center justify-center mt-5">
|
||||
<Loading className="w-14 h-14 text-blacklead animate-pulse" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex gap-3 ">
|
||||
<div className="flex flex-col gap-5 pr-5 mt-5">
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
عنوان پروژه:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{financialDetails?.project_id?.title}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
نام کاربری پرداخت کننده:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{financialDetails?.user_id?.user_name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
نام و نام خانوادگی پرداخت کننده:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{financialDetails?.user_id?.first_name}
|
||||
{" "}
|
||||
{financialDetails?.user_id?.last_name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
مبلغ پرداخت شده:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{Number(financialDetails?.amount).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
شبا کارفرما:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
ننوشتم که اشتباه به این نفرستیم
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
نوبت پرداخت:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{financialDetails?.type === "create" ?
|
||||
financialDetails?.project_id?.project_type == "normal" ?
|
||||
"ایجاد پروژه _ معمولی" : financialDetails?.project_id?.project_type == "force" ?
|
||||
"ایجاد پروژه _ فوری" : "ایجاد پروژه _ هایلایت"
|
||||
:
|
||||
financialDetails?.type === "installment" ?
|
||||
financialDetails?.installment_step == "1" ?
|
||||
"پرداخت بخش اول" :
|
||||
financialDetails?.installment_step == "2" ?
|
||||
"پرداخت بخش دوم" :
|
||||
financialDetails?.installment_step == "3" ?
|
||||
"پرداخت بخش سوم" :
|
||||
financialDetails?.installment_step == "all" ?
|
||||
"پرداخت یکجای بخش 2 و 3" : ""
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
نام کاربری کاربر دریافت کننده:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{financialDetails?.project_id?.selected_user?.user_name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
نام و نام خانوادگی کاربر دریافت کننده:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{financialDetails?.project_id?.selected_user?.first_name}
|
||||
{" "}
|
||||
{financialDetails?.project_id?.selected_user?.last_name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
شبا کاربر:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{financialDetails?.project_id?.selected_user?.shaba}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
مبلغ قابل پرداخت برای کاربر:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{Number(financialDetails?.amount - financialDetails?.amount * 0.15)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
وضعیت پرداختی:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{financialDetails?.type === "create" ?
|
||||
financialDetails?.project_id?.project_type == "normal" ?
|
||||
"ایجاد پروژه _ معمولی" : financialDetails?.project_id?.project_type == "force" ?
|
||||
"ایجاد پروژه _ فوری" : "ایجاد پروژه _ هایلایت"
|
||||
:
|
||||
financialDetails?.type === "installment" ?
|
||||
financialDetails?.installment_step == "1" ?
|
||||
"پرداخت بخش اول" :
|
||||
financialDetails?.installment_step == "2" ?
|
||||
"پرداخت بخش دوم" :
|
||||
financialDetails?.installment_step == "3" ?
|
||||
"پرداخت بخش سوم" :
|
||||
financialDetails?.installment_step == "all" ?
|
||||
"پرداخت یکجای بخش 2 و 3" : ""
|
||||
: ""
|
||||
}
|
||||
<p className={`font-KalamehMed text-sm font-medium mr-4 ${financialDetails?.status === "successful" ?
|
||||
"text-green-400" :
|
||||
"text-red-400"}`}>
|
||||
{financialDetails?.status === "successful" ?
|
||||
"موفق" :
|
||||
"ناموفق"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
تاریخ پرداخت:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
<div className="flex items-center">
|
||||
{
|
||||
financialDetails?.createdAt
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default FinancialDetail
|
||||
222
src/pages/financials/Financials.jsx
Normal file
222
src/pages/financials/Financials.jsx
Normal file
@@ -0,0 +1,222 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import AdminHeader from "../../components/admin-header";
|
||||
import Sidebar from "../../components/sidebar";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { BaseUrl } from "../../components/BaseUrl";
|
||||
import axios from "axios";
|
||||
import Loading from "../../components/elements/loading";
|
||||
import Pagination from "../../components/Pagination";
|
||||
import Filters from "../../components/Filters";
|
||||
|
||||
const Financials = () => {
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [financialList, setFinancialList] = useState([]);
|
||||
const [totalPages, setTotalPages] = useState(0);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
||||
//
|
||||
const [filters, setFilters] = useState({
|
||||
startDate: "",
|
||||
endDate: "",
|
||||
status: "",
|
||||
});
|
||||
|
||||
const getFinancialList = (page = 1) => {
|
||||
setLoading(true);
|
||||
//
|
||||
const { startDate, endDate, status } = filters;
|
||||
const config = {
|
||||
url: `${BaseUrl}/financial?limit=20&page=${page}${
|
||||
startDate ? "&startDate=" + startDate : ""
|
||||
}${endDate ? "&endDate=" + endDate : ""}&status=${status}`,
|
||||
method: "GET",
|
||||
};
|
||||
axios(config)
|
||||
.then((res) => {
|
||||
setFinancialList(res?.data?.financials);
|
||||
setTotalPages(res?.data?.totalPages); // دریافت تعداد صفحات از API
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
getFinancialList(currentPage);
|
||||
}, [currentPage, filters]);
|
||||
return (
|
||||
<>
|
||||
<div className="min-h-screen mx-auto bg-gray-900">
|
||||
<div className="flex h-full w-full">
|
||||
<div className=" w-64">
|
||||
<Sidebar />
|
||||
</div>
|
||||
<div className="w-full h-full p-14 mb-11 mx-auto text-white ">
|
||||
<Filters
|
||||
statusOptions={[
|
||||
{ key: "successful", label: "موفق" },
|
||||
{ key: "failed", label: "ناموفق" },
|
||||
]}
|
||||
provinceOptions={[{ value: "", label: "همه استانها" }]}
|
||||
cityOptions={[{ value: "", label: "همه شهرها" }]}
|
||||
photographyModelOptions={[{ value: "", label: "همه مدلها" }]}
|
||||
blockStatusOptions={[{ value: "", label: "همه کاربران" }]}
|
||||
onFilterChange={setFilters}
|
||||
/>
|
||||
|
||||
<div className="w-full mt-4 flex items-center border-b ">
|
||||
<div className="flex-1 w-full flex items-center h-11 px-5">
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
نام کاربری پرداخت کننده
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
نام و نام خانوادگی
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
عنوان پروژه
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
مبلغ پرداخت شده
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
نوبت پرداخت
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[12%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
وضعیت پرداختی
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[12%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
تاریخ پرداخت
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[11%]">
|
||||
<p className="font-KalamehMed text-sm font-medium"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{loading ? (
|
||||
<div className="w-full flex items-center justify-center mt-5">
|
||||
<Loading className="w-14 h-14 text-blacklead animate-pulse" />
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="overflow-y-scroll hide-scrollbar w-full"
|
||||
// style={{ maxHeight: maxHeightTable }}
|
||||
>
|
||||
{financialList?.map((financial, index) => (
|
||||
<div className="w-full mt-4 flex items-center bg-[#252525]">
|
||||
<div
|
||||
className={`flex-1 w-full flex items-center h-[90px] px-5 rounded-lg py-8 ${
|
||||
index % 2 == 0 ? "bg-[#c0e2f04b]" : "bg-[#678fa062]"
|
||||
}`}
|
||||
>
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium mr-2">
|
||||
{financial?.user_id?.user_name}
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium mr-2">
|
||||
{financial?.user_id?.first_name}{" "}
|
||||
{financial?.user_id?.last_name}
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium mr-2">
|
||||
{financial?.project_id?.title}
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium mr-2">
|
||||
{Number(financial?.amount).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<div className="flex items-center">
|
||||
<p className="font-KalamehMed text-sm font-medium mr-2">
|
||||
{financial?.type === "create"
|
||||
? financial?.project_id?.project_type == "normal"
|
||||
? "ایجاد پروژه _ معمولی"
|
||||
: financial?.project_id?.project_type == "force"
|
||||
? "ایجاد پروژه _ فوری"
|
||||
: "ایجاد پروژه _ هایلایت"
|
||||
: financial?.type === "installment"
|
||||
? financial?.installment_step == "1"
|
||||
? "پرداخت بخش اول"
|
||||
: financial?.installment_step == "2"
|
||||
? "پرداخت بخش دوم"
|
||||
: financial?.installment_step == "3"
|
||||
? "پرداخت بخش سوم"
|
||||
: financial?.installment_step == "all"
|
||||
? "پرداخت یکجای بخش 2 و 3"
|
||||
: ""
|
||||
: ""}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-1 w-[12%]">
|
||||
<div className="flex items-center">
|
||||
<p
|
||||
className={`font-KalamehMed text-sm font-medium ${
|
||||
financial?.status === "successful"
|
||||
? "text-green-400"
|
||||
: "text-red-400"
|
||||
}`}
|
||||
>
|
||||
{financial?.status === "successful"
|
||||
? "موفق"
|
||||
: "ناموفق"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-1 w-[12%]">
|
||||
<div className="flex items-center">
|
||||
<p className={`font-KalamehMed text-sm font-medium`}>
|
||||
{financial?.createdAt}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-1 w-[11%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate(`${financial?._id}`);
|
||||
}}
|
||||
className="bg-[#1f5f79] hover:bg-[#247392] px-8 h-[35px] rounded-md duration-300 mr-5"
|
||||
>
|
||||
جزئیات
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<Pagination
|
||||
totalItems={financialList.length}
|
||||
totalPages={totalPages}
|
||||
currentPage={currentPage}
|
||||
onPageChange={(page) => setCurrentPage(page)} // تغییر صفحه
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Financials;
|
||||
141
src/pages/license/License.jsx
Normal file
141
src/pages/license/License.jsx
Normal file
@@ -0,0 +1,141 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import axios from "axios";
|
||||
import Cookies from "js-cookie";
|
||||
import Sidebar from "../../components/sidebar";
|
||||
|
||||
import { AppBaseUrl } from "../../components/BaseUrl";
|
||||
|
||||
const LicensesPage = () => {
|
||||
const [licenses, setLicenses] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// گرفتن لایسنسها
|
||||
const fetchLicenses = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const token = Cookies.get("token");
|
||||
if (!token) {
|
||||
}
|
||||
|
||||
const response = await axios.get(`${AppBaseUrl}/users/License`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
setLicenses(response.data.licenses || []);
|
||||
} catch (error) {
|
||||
console.error("Error fetching licenses:", error);
|
||||
// toast.error("خطا در دریافت لایسنسها");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// تغییر وضعیت تایید
|
||||
const toggleConfirmation = async (userId, currentStatus) => {
|
||||
try {
|
||||
const token = Cookies.get("token");
|
||||
const newStatus = !currentStatus;
|
||||
|
||||
const response = await axios.put(
|
||||
`${AppBaseUrl}/users/License/${userId}`,
|
||||
{ Confirmation: newStatus },
|
||||
{
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
}
|
||||
);
|
||||
|
||||
if (response.status === 200) {
|
||||
// toast.success(`وضعیت به ${newStatus ? "تأیید" : "عدم تأیید"} تغییر یافت`);
|
||||
fetchLicenses();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating confirmation:", error);
|
||||
// toast.error("خطا در تغییر وضعیت تایید");
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchLicenses();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen w-full bg-gray-900 flex text-gray-100 ">
|
||||
<div className=" w-64">
|
||||
<Sidebar />
|
||||
</div>
|
||||
<div className="w-full h-full p-4 mb-11 mx-auto text-white">
|
||||
<h1 className="text-3xl font-bold mb-8 text-center pt-6">
|
||||
📜 لیست لایسنس کاربران
|
||||
</h1>
|
||||
|
||||
{loading ? (
|
||||
<div className="text-center text-gray-400">در حال بارگذاری...</div>
|
||||
) : licenses.length === 0 ? (
|
||||
<div className="text-center text-gray-400">هیچ لایسنسی یافت نشد</div>
|
||||
) : (
|
||||
<div className="flex justify-center flex-row flex-wrap px-24 items-center gap-6 ">
|
||||
{licenses
|
||||
.map((item) => (
|
||||
<div
|
||||
key={item._id}
|
||||
className="bg-gray-800 max-w-96 min-w-96 rounded-2xl shadow-lg p-4 flex flex-col items-center transition-all hover:shadow-xl"
|
||||
>
|
||||
{item.licenseImg ? (
|
||||
<img
|
||||
src={item.licenseImg}
|
||||
alt="license"
|
||||
className="w-40 h-40 object-cover rounded-xl border border-gray-700 mb-4"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-40 h-40 flex items-center justify-center bg-gray-700 rounded-xl mb-4 text-sm text-gray-400">
|
||||
بدون تصویر
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="w-full text-center space-y-1">
|
||||
<p className="text-lg font-semibold">
|
||||
{item.user?.first_name || ""} {item.user?.last_name || ""}
|
||||
</p>
|
||||
<p className="text-sm text-gray-400">
|
||||
@{item.user?.user_name}
|
||||
</p>
|
||||
<p
|
||||
className={`text-sm font-medium ${
|
||||
item.user?.is_verified === "true"
|
||||
? "text-green-400"
|
||||
: "text-red-400"
|
||||
}`}
|
||||
>
|
||||
{item.user?.is_verified === "true"
|
||||
? "تأیید شده"
|
||||
: "تأیید نشده"}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
{item.createdAt} | {item.updatedAt}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() =>
|
||||
toggleConfirmation(item.userId?._id, item.Confirmation)
|
||||
}
|
||||
className={`mt-4 px-4 py-2 rounded-lg font-medium ${
|
||||
item.Confirmation
|
||||
? "bg-red-600 hover:bg-red-700"
|
||||
: "bg-green-600 hover:bg-green-700"
|
||||
}`}
|
||||
>
|
||||
{item.Confirmation ? "لغو تأیید" : "تأیید کاربر"}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LicensesPage;
|
||||
440
src/pages/payments/payments.jsx
Normal file
440
src/pages/payments/payments.jsx
Normal file
@@ -0,0 +1,440 @@
|
||||
// src/pages/Payments.js
|
||||
import React, { useState, useEffect } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import {
|
||||
getPayments,
|
||||
updatePaymentStatus,
|
||||
getPaymentStats
|
||||
} from "../../components/adminApi"; // اصلاح مسیر
|
||||
import Layout from "../../components/academy/Layout";
|
||||
import { FaFilter, FaTimes, FaFileExport, FaEye } from "react-icons/fa";
|
||||
import { ImageBaseUrl } from "../../components/BaseUrl";
|
||||
|
||||
const Payments = () => {
|
||||
const [payments, setPayments] = useState([]);
|
||||
const [stats, setStats] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [page, setPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [selectedPayment, setSelectedPayment] = useState(null);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [filters, setFilters] = useState({
|
||||
status: "",
|
||||
startDate: "",
|
||||
endDate: "",
|
||||
search: ""
|
||||
});
|
||||
const [showFilters, setShowFilters] = useState(false);
|
||||
|
||||
const fetchPayments = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await getPayments({ page, limit: 15, ...filters });
|
||||
console.log("Payments response:", response.data);
|
||||
|
||||
if (response.data.success) {
|
||||
// بررسی ساختار داده - طبق بکاند، data داخل response.data.data هست
|
||||
const responseData = response.data.data;
|
||||
setPayments(responseData?.payments || []);
|
||||
setStats(responseData?.stats || null);
|
||||
setTotalPages(responseData?.pagination?.totalPages || 1);
|
||||
} else {
|
||||
toast.error(response.data.message || "خطا در دریافت پرداختها");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching payments:", error);
|
||||
toast.error("خطا در دریافت پرداختها");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
const response = await getPaymentStats();
|
||||
console.log("Stats response:", response.data);
|
||||
|
||||
if (response.data.success) {
|
||||
setStats(response.data.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching stats:", error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchPayments();
|
||||
fetchStats();
|
||||
}, [page, filters.status, filters.startDate, filters.endDate, filters.search]);
|
||||
|
||||
const handleStatusChange = async (paymentId, newStatus) => {
|
||||
try {
|
||||
const response = await updatePaymentStatus({ paymentId, status: newStatus });
|
||||
if (response.data.success) {
|
||||
toast.success("وضعیت پرداخت تغییر کرد");
|
||||
fetchPayments();
|
||||
fetchStats();
|
||||
} else {
|
||||
toast.error(response.data.message || "خطا در تغییر وضعیت");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating status:", error);
|
||||
toast.error("خطا در تغییر وضعیت");
|
||||
}
|
||||
};
|
||||
|
||||
const handleFilterChange = (e) => {
|
||||
setFilters({
|
||||
...filters,
|
||||
[e.target.name]: e.target.value
|
||||
});
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const clearFilters = () => {
|
||||
setFilters({
|
||||
status: "",
|
||||
startDate: "",
|
||||
endDate: "",
|
||||
search: ""
|
||||
});
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const exportToCSV = () => {
|
||||
const headers = ["کاربر", "دوره", "آکادمی", "مبلغ", "تاریخ", "وضعیت"];
|
||||
const rows = payments.map(p => [
|
||||
p.user_id?.user_name || "-",
|
||||
p.course_id?.cuorse_name || "-",
|
||||
p.academy_id?.academy_name || "-",
|
||||
p.price?.toLocaleString(),
|
||||
new Date(p.createdAt).toLocaleDateString("fa-IR"),
|
||||
p.status === "pending" ? "در انتظار" : p.status === "success" ? "موفق" : "تسویه شده"
|
||||
]);
|
||||
|
||||
const csvContent = [headers, ...rows].map(row => row.join(",")).join("\n");
|
||||
const blob = new Blob(["\uFEFF" + csvContent], { type: "text/csv;charset=utf-8;" }); // اضافه کردن BOM برای پشتیبانی از فارسی
|
||||
const link = document.createElement("a");
|
||||
const url = URL.createObjectURL(blob);
|
||||
link.href = url;
|
||||
link.setAttribute("download", `payments_${new Date().toISOString().slice(0, 19)}.csv`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
toast.success("خروجی با موفقیت گرفته شد");
|
||||
};
|
||||
|
||||
const getStatusBadge = (status) => {
|
||||
const badges = {
|
||||
pending: "bg-yellow-500",
|
||||
success: "bg-blue-500",
|
||||
settled: "bg-green-500"
|
||||
};
|
||||
const texts = {
|
||||
pending: "در انتظار",
|
||||
success: "موفق",
|
||||
settled: "تسویه شده"
|
||||
};
|
||||
return (
|
||||
<span className={`px-2 py-1 rounded text-xs text-white ${badges[status] || "bg-gray-500"}`}>
|
||||
{texts[status] || status}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
if (loading && page === 1) {
|
||||
return (
|
||||
<Layout>
|
||||
<div className="flex justify-center items-center h-96">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-pink-500"></div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
console.log(payments);
|
||||
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-center mb-6 flex-wrap gap-4">
|
||||
<h1 className="text-2xl font-bold">مدیریت پرداختها</h1>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={exportToCSV}
|
||||
disabled={payments.length === 0}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<FaFileExport />
|
||||
خروجی Excel
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowFilters(!showFilters)}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-gray-100 rounded-lg hover:bg-gray-200 transition"
|
||||
>
|
||||
<FaFilter />
|
||||
فیلترها
|
||||
{Object.values(filters).some(v => v) && (
|
||||
<span className="w-2 h-2 bg-pink-500 rounded-full"></span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
{stats && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-6">
|
||||
{/* <div className="bg-gradient-to-r from-blue-500 to-blue-600 rounded-lg p-4 text-white">
|
||||
<p className="text-sm opacity-90">کل مبلغ</p>
|
||||
<p className="text-2xl font-bold">{stats.totalAmount?.toLocaleString() || 0} تومان</p>
|
||||
</div> */}
|
||||
<div className="bg-white rounded-lg p-4 shadow-sm">
|
||||
<p className="text-gray-500 text-sm">تعداد کل</p>
|
||||
<p className="text-2xl font-bold">{stats.totalCount || 0}</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg p-4 shadow-sm">
|
||||
<p className="text-gray-500 text-sm">در انتظار</p>
|
||||
<p className="text-2xl font-bold text-yellow-600">{stats.pendingCount || 0}</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg p-4 shadow-sm">
|
||||
<p className="text-gray-500 text-sm">موفق</p>
|
||||
<p className="text-2xl font-bold text-blue-600">{stats.successCount || 0}</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg p-4 shadow-sm">
|
||||
<p className="text-gray-500 text-sm">تسویه شده</p>
|
||||
<p className="text-2xl font-bold text-green-600">{stats.settledCount || 0}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Filters Panel */}
|
||||
{showFilters && (
|
||||
<div className="bg-white rounded-lg p-4 shadow-sm mb-6 border">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="font-semibold">فیلترهای پیشرفته</h3>
|
||||
<button onClick={clearFilters} className="text-sm text-pink-500 hover:text-pink-600">
|
||||
پاک کردن همه
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<input
|
||||
type="text"
|
||||
name="search"
|
||||
placeholder="جستجوی کاربر، دوره، آکادمی..."
|
||||
value={filters.search}
|
||||
onChange={handleFilterChange}
|
||||
className="px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-pink-500"
|
||||
/>
|
||||
<select
|
||||
name="status"
|
||||
value={filters.status}
|
||||
onChange={handleFilterChange}
|
||||
className="px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-pink-500"
|
||||
>
|
||||
<option value="">همه وضعیتها</option>
|
||||
<option value="pending">در انتظار</option>
|
||||
<option value="success">موفق</option>
|
||||
<option value="settled">تسویه شده</option>
|
||||
</select>
|
||||
<input
|
||||
type="date"
|
||||
name="startDate"
|
||||
placeholder="از تاریخ"
|
||||
value={filters.startDate}
|
||||
onChange={handleFilterChange}
|
||||
className="px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-pink-500"
|
||||
/>
|
||||
<input
|
||||
type="date"
|
||||
name="endDate"
|
||||
placeholder="تا تاریخ"
|
||||
value={filters.endDate}
|
||||
onChange={handleFilterChange}
|
||||
className="px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-pink-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Payments Table */}
|
||||
<div className="bg-white rounded-lg shadow overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full min-w-[1000px]">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-right">کاربر</th>
|
||||
<th className="px-4 py-3 text-right">دوره</th>
|
||||
<th className="px-4 py-3 text-right">آکادمی</th>
|
||||
<th className="px-4 py-3 text-right">مبلغ</th>
|
||||
<th className="px-4 py-3 text-right">کد پیگیری</th>
|
||||
<th className="px-4 py-3 text-right">تاریخ</th>
|
||||
<th className="px-4 py-3 text-right">وضعیت</th>
|
||||
<th className="px-4 py-3 text-right">عملیات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{payments.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={8} className="text-center py-8 text-gray-500">
|
||||
هیچ پرداختی یافت نشد
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
payments.map((payment) => (
|
||||
<tr key={payment._id} className="border-t hover:bg-gray-50">
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-full bg-gray-200 flex items-center justify-center overflow-hidden">
|
||||
{payment.user_id?.profile_image ? (
|
||||
<img src={ImageBaseUrl + payment.user_id.profile_image} alt="" className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<span className="text-xs">👤</span>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">{payment.user_id?.user_name || "-"}</p>
|
||||
<p className="text-xs text-gray-500">{payment.user_id?.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<p className="font-medium">{payment.course_id?.cuorse_name || "-"}</p>
|
||||
<p className="text-xs text-gray-500">مدرس: {payment.course_id?.teacher_name || "-"}</p>
|
||||
</td>
|
||||
<td className="px-4 py-3">{payment.academy_id?.academy_name || "-"}</td>
|
||||
<td className="px-4 py-3 font-bold">{payment.taxAmount?.toLocaleString()} تومان</td>
|
||||
<td className="px-4 py-3 text-xs font-mono">{payment.payment_ref_id?.slice(-8) || "-"}</td>
|
||||
<td className="px-4 py-3">{new Date(payment.createdAt).toLocaleDateString("fa-IR")}</td>
|
||||
<td className="px-4 py-3">{getStatusBadge(payment.status)}</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedPayment(payment);
|
||||
setShowModal(true);
|
||||
}}
|
||||
className="p-1 text-blue-500 hover:text-blue-700"
|
||||
title="جزئیات"
|
||||
>
|
||||
<FaEye />
|
||||
</button>
|
||||
{payment.status === "success" && (
|
||||
<button
|
||||
onClick={() => handleStatusChange(payment._id, "settled")}
|
||||
className="px-2 py-1 bg-green-500 text-white rounded text-xs hover:bg-green-600"
|
||||
>
|
||||
تسویه
|
||||
</button>
|
||||
)}
|
||||
{payment.status === "pending" && (
|
||||
<button
|
||||
onClick={() => handleStatusChange(payment._id, "success")}
|
||||
className="px-2 py-1 bg-blue-500 text-white rounded text-xs hover:bg-blue-600"
|
||||
>
|
||||
تأیید
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex justify-center gap-2 mt-6">
|
||||
<button
|
||||
onClick={() => setPage(p => Math.max(1, p - 1))}
|
||||
disabled={page === 1}
|
||||
className="px-4 py-2 border rounded-lg disabled:opacity-50 hover:bg-gray-50"
|
||||
>
|
||||
قبلی
|
||||
</button>
|
||||
<span className="px-4 py-2">
|
||||
صفحه {page} از {totalPages}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
|
||||
disabled={page === totalPages}
|
||||
className="px-4 py-2 border rounded-lg disabled:opacity-50 hover:bg-gray-50"
|
||||
>
|
||||
بعدی
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Modal for Payment Details */}
|
||||
{showModal && selectedPayment && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-xl max-w-md w-full mx-4 max-h-[90vh] overflow-y-auto">
|
||||
<div className="flex justify-between items-center p-4 border-b sticky top-0 bg-white">
|
||||
<h3 className="text-lg font-bold">جزئیات پرداخت</h3>
|
||||
<button onClick={() => setShowModal(false)} className="text-gray-500 hover:text-gray-700">
|
||||
<FaTimes />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-4 space-y-3">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">کاربر:</span>
|
||||
<span className="font-medium">{selectedPayment.user_id?.user_name}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">دوره:</span>
|
||||
<span>{selectedPayment.course_id?.cuorse_name}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">آکادمی:</span>
|
||||
<span>{selectedPayment.academy_id?.academy_name}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">شبا اکادمی:</span>
|
||||
<span>{selectedPayment.academy_id?.sheba}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">مبلغ:</span>
|
||||
<span className="font-bold text-pink-500">{selectedPayment.price?.toLocaleString()} تومان</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">تخفیف:</span>
|
||||
<span>{selectedPayment.discountAmount || 0} تومان</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">مبلغ با مالیات:</span>
|
||||
<span>{selectedPayment.taxAmount || 0} تومان</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">کد پیگیری:</span>
|
||||
<span className="font-mono text-sm">{selectedPayment.payment_ref_id}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">تاریخ:</span>
|
||||
<span>{new Date(selectedPayment.createdAt).toLocaleDateString("fa-IR")}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">وضعیت:</span>
|
||||
<span>{getStatusBadge(selectedPayment.status)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 border-t flex justify-end">
|
||||
<button
|
||||
onClick={() => setShowModal(false)}
|
||||
className="px-4 py-2 bg-gray-200 rounded-lg hover:bg-gray-300"
|
||||
>
|
||||
بستن
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Payments;
|
||||
198
src/pages/posts/PostDetail.jsx
Normal file
198
src/pages/posts/PostDetail.jsx
Normal file
@@ -0,0 +1,198 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Sidebar from "../../components/sidebar";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { BaseUrl, buildStorageUrl } from "../../components/BaseUrl";
|
||||
import axios from "axios";
|
||||
import Loading from "../../components/elements/loading";
|
||||
import toast from "react-hot-toast";
|
||||
import Modal from "react-modal";
|
||||
|
||||
const customStyles = {
|
||||
content: {
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
position: "fixed",
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
},
|
||||
};
|
||||
|
||||
const PostDetail = () => {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [postDetails, setPostDetails] = useState({});
|
||||
const [modalIsOpen, setModalIsOpen] = useState(false);
|
||||
const [modalMedia, setModalMedia] = useState(null);
|
||||
|
||||
const openModal = (media) => {
|
||||
setModalMedia(media);
|
||||
setModalIsOpen(true);
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setModalIsOpen(false);
|
||||
setModalMedia(null);
|
||||
};
|
||||
|
||||
const getPostDetails = () => {
|
||||
setLoading(true);
|
||||
axios
|
||||
.get(`${BaseUrl}/posts/detail?post_id=${id}`)
|
||||
.then((res) => {
|
||||
setPostDetails(res?.data?.post || {});
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error fetching post details:", err);
|
||||
toast.error(
|
||||
"خطا در دریافت جزئیات پست: " +
|
||||
(err.response?.data?.message || err.message)
|
||||
);
|
||||
setPostDetails({});
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (id) getPostDetails();
|
||||
}, [id]);
|
||||
|
||||
const togglePostAccept = (_id, user_id, status) => {
|
||||
const data = {
|
||||
id: _id,
|
||||
user_id,
|
||||
status: status === "accept" ? "reject" : "accept",
|
||||
};
|
||||
axios
|
||||
.post(`${BaseUrl}/posts/accept`, data, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
toast.success(response?.data?.message);
|
||||
getPostDetails();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error toggling post status:", error);
|
||||
toast.error(error?.response?.data?.message || "خطای ناشناخته");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen mx-auto">
|
||||
<div className="flex h-full w-full">
|
||||
<div className="w-64">
|
||||
<Sidebar />
|
||||
</div>
|
||||
<div className="w-full h-full p-14 mb-11 mx-auto bg-[#252525] text-white">
|
||||
<div className="w-full flex items-center justify-between">
|
||||
<h4 className="font-medium font-KalamehMed text-lg">
|
||||
اطلاعات پست:
|
||||
</h4>
|
||||
<button
|
||||
onClick={() => navigate(-1)}
|
||||
className="flex gap-1 bg-[#1f5f79] font-semibold font-KalamehMed rounded-lg hover:bg-[#E0E3E3] transition-colors duration-500 p-[10px]"
|
||||
>
|
||||
بازگشت
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="w-full flex items-center justify-center mt-5">
|
||||
<Loading className="w-14 h-14 text-blacklead animate-pulse" />
|
||||
</div>
|
||||
) : !postDetails || Object.keys(postDetails).length === 0 ? (
|
||||
<div className="w-full flex items-center justify-center mt-5">
|
||||
<p className="font-KalamehMed text-sm font-medium text-gray-400">
|
||||
پستی یافت نشد
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-5 pr-5 mt-5">
|
||||
{/* نمایش فایلهای پست */}
|
||||
<div className="flex items-start w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
فایلهای پست:
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-3 bg-[#111] p-4 rounded-lg">
|
||||
{postDetails?.files?.length > 0 ? (
|
||||
postDetails.files.map((file, i) =>
|
||||
file.type === "image" ? (
|
||||
<img
|
||||
key={i}
|
||||
src={buildStorageUrl(file.path)}
|
||||
alt={`Post file ${i}`}
|
||||
className="w-[200px] h-[200px] object-cover rounded-md cursor-pointer"
|
||||
onClick={() =>
|
||||
openModal({
|
||||
type: "image",
|
||||
src: buildStorageUrl(file.path),
|
||||
})
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<video
|
||||
key={i}
|
||||
src={buildStorageUrl(file.path)}
|
||||
className="w-[200px] h-[200px] rounded-md cursor-pointer"
|
||||
controls
|
||||
onClick={() =>
|
||||
openModal({
|
||||
type: "video",
|
||||
src: buildStorageUrl(file.path),
|
||||
})
|
||||
}
|
||||
/>
|
||||
)
|
||||
)
|
||||
) : (
|
||||
<p className="text-gray-400">هیچ فایلی موجود نیست</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* کپشن */}
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
کپشن:
|
||||
</div>
|
||||
<div className="flex items-center font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4 py-4">
|
||||
{postDetails?.caption || "-"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modal برای نمایش بزرگتر عکس/ویدیو */}
|
||||
<Modal
|
||||
isOpen={modalIsOpen}
|
||||
onRequestClose={closeModal}
|
||||
style={customStyles}
|
||||
>
|
||||
{modalMedia ? (
|
||||
modalMedia.type === "image" ? (
|
||||
<img
|
||||
src={modalMedia.src}
|
||||
alt="Post"
|
||||
className="max-w-[90vw] max-h-[90vh] rounded-lg"
|
||||
/>
|
||||
) : (
|
||||
<video
|
||||
src={modalMedia.src}
|
||||
controls
|
||||
className="max-w-[90vw] max-h-[90vh] rounded-lg"
|
||||
/>
|
||||
)
|
||||
) : null}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PostDetail;
|
||||
301
src/pages/posts/Posts.jsx
Normal file
301
src/pages/posts/Posts.jsx
Normal file
@@ -0,0 +1,301 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import AdminHeader from "../../components/admin-header";
|
||||
import Sidebar from "../../components/sidebar";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { BaseUrl, buildStorageUrl } from "../../components/BaseUrl";
|
||||
import axios from "axios";
|
||||
import Loading from "../../components/elements/loading";
|
||||
import toast from "react-hot-toast";
|
||||
import Pagination from "../../components/Pagination";
|
||||
import Filters from "../../components/Filters";
|
||||
|
||||
const Posts = () => {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const statusParam = searchParams.get("status"); // مقدار پارامتر status
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [postsList, setPostsList] = useState([]); // آرایه خالی به عنوان مقدار اولیه
|
||||
const [totalPages, setTotalPages] = useState(0);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [filters, setFilters] = useState({
|
||||
startDate: "",
|
||||
endDate: "",
|
||||
status: "",
|
||||
});
|
||||
|
||||
const getPostList = (page = 1) => {
|
||||
setLoading(true);
|
||||
|
||||
const { startDate, endDate, status } = filters;
|
||||
const finalStatus = statusParam || status;
|
||||
|
||||
const config = {
|
||||
url: `${BaseUrl}/posts?limit=10&page=${page}${
|
||||
startDate ? "&startDate=" + startDate : ""
|
||||
}${endDate ? "&endDate=" + endDate : ""}&status=${finalStatus}`,
|
||||
method: "GET",
|
||||
};
|
||||
axios(config)
|
||||
.then((res) => {
|
||||
setPostsList(res?.data?.posts || []);
|
||||
setTotalPages(res?.data?.totalPages || 0);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error fetching posts:", err);
|
||||
toast.error(
|
||||
"خطا در دریافت پستها: " +
|
||||
(err.response?.data?.message || err.message)
|
||||
);
|
||||
setPostsList([]);
|
||||
setTotalPages(0);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getPostList(currentPage);
|
||||
}, [currentPage, filters, statusParam]);
|
||||
|
||||
const togglePostAccept = (_id, user_id, status) => {
|
||||
const data = {
|
||||
id: _id,
|
||||
user_id,
|
||||
status: status === "accept" ? "reject" : "accept",
|
||||
};
|
||||
axios
|
||||
.post(`${BaseUrl}/posts/accept`, data, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
toast.success(response?.data?.message);
|
||||
getPostList(currentPage); // بارگذاری مجدد لیست پستها
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error toggling post status:", error);
|
||||
toast.error(error?.response?.data?.message || "خطای ناشناخته");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="min-h-screen mx-auto bg-gray-900">
|
||||
<div className="flex h-full w-full">
|
||||
<div className="w-64">
|
||||
<Sidebar />
|
||||
</div>
|
||||
<div className="w-full h-full p-14 mb-11 mx-auto text-white">
|
||||
<Filters
|
||||
statusOptions={[
|
||||
{ key: "", label: "همه وضعیتها" },
|
||||
{ key: "pending", label: "درانتظار تایید" },
|
||||
{ key: "accept", label: "منتشر شده" },
|
||||
{ key: "reject", label: "رد شده" },
|
||||
]}
|
||||
onFilterChange={setFilters}
|
||||
/>
|
||||
<div className="w-full mt-4 flex items-center border-b">
|
||||
<div className="flex-1 w-full flex items-center h-11 px-5">
|
||||
<div className="col-span-1 w-[10%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">تصویر</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[10%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
نام کاربری
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[10%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
نوع کاربری
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[12%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
نام و نام خانوادگی
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[10%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">موبایل</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[10%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">کد ملی</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[12%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
تاریخ ثبت پست
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[16%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">وضعیت</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[10%]">
|
||||
<p className="font-KalamehMed text-sm font-medium"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{loading ? (
|
||||
<div className="w-full flex items-center justify-center mt-5">
|
||||
<Loading className="w-14 h-14 text-blacklead animate-pulse" />
|
||||
</div>
|
||||
) : postsList.length === 0 ? (
|
||||
<div className="w-full flex items-center justify-center mt-5">
|
||||
<p className="font-KalamehMed text-sm font-medium text-gray-400">
|
||||
هیچ پستی یافت نشد
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full">
|
||||
{postsList.map((post, index) => (
|
||||
<div
|
||||
key={post._id}
|
||||
className="w-full mt-4 flex items-center bg-[#252525]"
|
||||
>
|
||||
<div
|
||||
className={`flex-1 w-full flex items-center h-[90px] px-5 rounded-lg py-8 ${
|
||||
index % 2 === 0 ? "bg-[#c0e2f04b]" : "bg-[#678fa062]"
|
||||
}`}
|
||||
>
|
||||
{/* تصویر / ویدیو */}
|
||||
<div className="col-span-1 w-[10%]">
|
||||
<div className="flex items-center">
|
||||
{post?.files?.length > 0 ? (
|
||||
post.files[0].type === "image" ? (
|
||||
<img
|
||||
className="w-[80px] h-[80px] rounded-md ml-2 object-cover"
|
||||
src={buildStorageUrl(post.files[0].path)}
|
||||
alt="Post"
|
||||
/>
|
||||
) : (
|
||||
<video
|
||||
className="w-[80px] h-[80px] rounded-md ml-2 object-cover"
|
||||
src={buildStorageUrl(post.files[0].path)}
|
||||
controls
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<img
|
||||
className="w-[80px] h-[80px] rounded-md ml-2 object-cover"
|
||||
src="/images/placeholder.jpg"
|
||||
alt="No file"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* نام کاربری */}
|
||||
<div className="col-span-1 w-[10%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
{post?.user_name || "-"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* نوع کاربری */}
|
||||
<div className="col-span-1 w-[10%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
{post?.user_type === "employer"
|
||||
? "کارفرما"
|
||||
: post?.user_type === "user"
|
||||
? post?.expertise || "-"
|
||||
: "-"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* نام و نام خانوادگی */}
|
||||
<div className="col-span-1 w-[12%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
{post?.first_name && post?.last_name
|
||||
? `${post.first_name} ${post.last_name}`
|
||||
: "-"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* موبایل */}
|
||||
<div className="col-span-1 w-[10%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
{post?.mobile || "-"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* کد ملی */}
|
||||
<div className="col-span-1 w-[10%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
{post?.national_code || "-"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* تاریخ */}
|
||||
<div className="col-span-1 w-[12%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
{post?.createdAt || "-"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* وضعیت */}
|
||||
<div className="col-span-1 w-[16%]">
|
||||
<div className="flex items-center">
|
||||
<p
|
||||
className={`font-KalamehMed text-sm font-medium ${
|
||||
post?.status === "accept"
|
||||
? "text-green-300"
|
||||
: post?.status === "pending"
|
||||
? "text-yellow-300"
|
||||
: "text-red-300"
|
||||
}`}
|
||||
>
|
||||
{post?.status === "accept"
|
||||
? "منتشر شده"
|
||||
: post?.status === "pending"
|
||||
? "در انتظار تایید"
|
||||
: "رد شده"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* دکمهها */}
|
||||
<div className="col-span-1 w-[10%]">
|
||||
<button
|
||||
onClick={() => navigate(`${post?._id}`)}
|
||||
className="bg-[#1f5f79] hover:bg-[#247392] px-8 h-[35px] rounded-md duration-300 mr-5"
|
||||
>
|
||||
جزئیات
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
togglePostAccept(
|
||||
post._id,
|
||||
post.user_id,
|
||||
post.status
|
||||
)
|
||||
}
|
||||
className={`ml-2 px-4 h-[35px] rounded-md duration-300 ${
|
||||
post.status === "accept"
|
||||
? "bg-red-500 hover:bg-red-600"
|
||||
: "bg-green-500 hover:bg-green-600"
|
||||
}`}
|
||||
>
|
||||
{post.status === "accept" ? "رد" : "تایید"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Pagination
|
||||
totalItems={postsList.length}
|
||||
totalPages={totalPages}
|
||||
currentPage={currentPage}
|
||||
onPageChange={(page) => setCurrentPage(page)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Posts;
|
||||
562
src/pages/projects/ProjectDetail.jsx
Normal file
562
src/pages/projects/ProjectDetail.jsx
Normal file
@@ -0,0 +1,562 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import AdminHeader from "../../components/admin-header";
|
||||
import Sidebar from "../../components/sidebar";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { BaseUrl, ImageBaseUrl } from "../../components/BaseUrl";
|
||||
import axios from "axios";
|
||||
import Loading from "../../components/elements/loading";
|
||||
import toast from "react-hot-toast";
|
||||
import Modal from "react-modal";
|
||||
|
||||
const customStyles = {
|
||||
content: {
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
position: "fixed",
|
||||
},
|
||||
};
|
||||
const ProjectDetail = () => {
|
||||
const navigate = useNavigate();
|
||||
const param = useParams();
|
||||
const id = param?.id;
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [projectDetails, setProjectDetails] = useState([]);
|
||||
const [modalIsOpen, setModalIsOpen] = useState(false);
|
||||
const [modalImage, setModalImage] = useState("");
|
||||
const [rejectReason, setRejectReason] = useState("");
|
||||
const openModal = (image) => {
|
||||
setModalImage(image);
|
||||
setModalIsOpen(true);
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setModalIsOpen(false);
|
||||
};
|
||||
|
||||
const getProjectDetails = (page = 1) => {
|
||||
setLoading(true);
|
||||
const config = {
|
||||
url: `${BaseUrl}/projects/detail?project_id=${id}`,
|
||||
method: "GET",
|
||||
};
|
||||
axios(config)
|
||||
.then((res) => {
|
||||
setProjectDetails(res?.data?.project);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
getProjectDetails();
|
||||
}, [id]);
|
||||
|
||||
const acceptProjectAccept = (_id, user_name) => {
|
||||
const data = {
|
||||
id: _id,
|
||||
user_name,
|
||||
};
|
||||
axios
|
||||
.post(`${BaseUrl}/projects/accept`, data, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(async (response) => {
|
||||
try {
|
||||
toast.success(response?.data?.message);
|
||||
getProjectDetails();
|
||||
} catch (error) {
|
||||
if (error?.response?.data?.error) {
|
||||
toast.error(error.response.data.error);
|
||||
} else {
|
||||
// نمایش پیام خطا برای سایر موارد
|
||||
toast.error("خطای ناشناخته");
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
toast.show(error?.response?.data?.message, {
|
||||
type: "danger",
|
||||
placement: "top",
|
||||
duration: 4000,
|
||||
offset: 30,
|
||||
animationType: "slide-in",
|
||||
});
|
||||
});
|
||||
};
|
||||
const rejectProjectAccept = (_id, user_id) => {
|
||||
const data = {
|
||||
id: _id,
|
||||
user_id,
|
||||
rejectReason,
|
||||
};
|
||||
axios
|
||||
.post(`${BaseUrl}/projects/reject`, data, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(async (response) => {
|
||||
try {
|
||||
toast.success(response?.data?.message);
|
||||
getProjectDetails();
|
||||
} catch (error) {
|
||||
if (error?.response?.data?.error) {
|
||||
toast.error(error.response.data.error);
|
||||
} else {
|
||||
// نمایش پیام خطا برای سایر موارد
|
||||
toast.error("خطای ناشناخته");
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
toast.show(error?.response?.data?.message, {
|
||||
type: "danger",
|
||||
placement: "top",
|
||||
duration: 4000,
|
||||
offset: 30,
|
||||
animationType: "slide-in",
|
||||
});
|
||||
});
|
||||
};
|
||||
return (
|
||||
<>
|
||||
|
||||
<div className="min-h-screen mx-auto">
|
||||
<div className="flex h-full w-full">
|
||||
<div className=" w-64"><Sidebar /></div>
|
||||
<div className="w-full h-full p-14 mb-11 mx-auto bg-[#252525] text-white ">
|
||||
<div className="w-full flex items-center justify-between">
|
||||
<h4 className="font-medium font-KalamehMed text-lg">
|
||||
اطلاعات پروژه:
|
||||
</h4>
|
||||
<button
|
||||
onClick={() => navigate(-1)}
|
||||
className="flex gap-1 bg-[#1f5f79] font-semibold font-KalamehSemi rounded-lg hover:bg-[#E0E3E3] transition-colors duration-500 p-[10px] "
|
||||
>
|
||||
بازگشت
|
||||
<svg
|
||||
width="24"
|
||||
height="25"
|
||||
viewBox="0 0 24 25"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M9.56994 19.3191C9.37994 19.3191 9.18994 19.2491 9.03994 19.0991L2.96994 13.0291C2.67994 12.7391 2.67994 12.2591 2.96994 11.9691L9.03994 5.89914C9.32994 5.60914 9.80994 5.60914 10.0999 5.89914C10.3899 6.18914 10.3899 6.66914 10.0999 6.95914L4.55994 12.4991L10.0999 18.0391C10.3899 18.3291 10.3899 18.8091 10.0999 19.0991C9.95994 19.2491 9.75994 19.3191 9.56994 19.3191Z"
|
||||
fill="#fff"
|
||||
/>
|
||||
<path
|
||||
d="M20.4999 13.25H3.66992C3.25992 13.25 2.91992 12.91 2.91992 12.5C2.91992 12.09 3.25992 11.75 3.66992 11.75H20.4999C20.9099 11.75 21.2499 12.09 21.2499 12.5C21.2499 12.91 20.9099 13.25 20.4999 13.25Z"
|
||||
fill="#fff"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{loading ? (
|
||||
<div className="w-full flex items-center justify-center mt-5">
|
||||
<Loading className="w-14 h-14 text-blacklead animate-pulse" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex gap-3 ">
|
||||
<div className="flex flex-col gap-5 pr-5 mt-5">
|
||||
<Modal
|
||||
isOpen={modalIsOpen}
|
||||
onRequestClose={closeModal}
|
||||
className="post-image-modal"
|
||||
style={customStyles}
|
||||
>
|
||||
{/* استفاده از ImageMagnify */}
|
||||
<img
|
||||
src={modalImage}
|
||||
alt="Project"
|
||||
className="post-modal-image"
|
||||
/>
|
||||
</Modal>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
عنوان پروژه:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{projectDetails?.title}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
توضیحات پروژه:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{projectDetails?.description}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
نام کارفرما:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{
|
||||
projectDetails?.creator
|
||||
?.first_name
|
||||
}{" "}
|
||||
{projectDetails?.creator?.last_name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
نام کاربری کارفرما:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{projectDetails?.creator?.user_name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
موبایل کارفرما:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{projectDetails?.creator?.mobile}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
نام کاربر:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{
|
||||
projectDetails?.selected_user
|
||||
?.first_name
|
||||
}{" "}
|
||||
{
|
||||
projectDetails?.selected_user
|
||||
?.last_name
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
نام کاربری کاربر:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{
|
||||
projectDetails?.selected_user
|
||||
?.user_name
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
موبایل کاربر:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{
|
||||
projectDetails?.selected_user
|
||||
?.mobile
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
استان محل اجرای پروژه:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{projectDetails?.province?.name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
شهر محل اجرای پروژه:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{projectDetails?.city?.name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
دسته بندی اصلی:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{projectDetails?.expertise}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
نوع تخصصها:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{projectDetails?.sub_expertise?.map(
|
||||
(item, index) => {
|
||||
return (
|
||||
item +
|
||||
(index !==
|
||||
projectDetails
|
||||
?.sub_expertise
|
||||
?.length -
|
||||
1
|
||||
? " _ "
|
||||
: "")
|
||||
);
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
زمان پیشنهادی:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{projectDetails?.offer_time}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
مبلغ پیشنهادی:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{Number(
|
||||
projectDetails?.offer_price
|
||||
).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
زمان نهایی:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{projectDetails?.final_time}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
مبلغ نهایی:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{Number(
|
||||
projectDetails?.final_price
|
||||
).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
تاریخ ثبت درخواست:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{projectDetails?.createdAt}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
وضعیت:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
<div className="flex items-center">
|
||||
<p
|
||||
className={`font-KalamehMed text-sm font-medium ${projectDetails?.status ===
|
||||
"accept"
|
||||
? "text-green-300"
|
||||
: projectDetails?.status ===
|
||||
"pre_payment" ||
|
||||
projectDetails?.status ===
|
||||
"paid"
|
||||
? "text-yellow-300"
|
||||
: projectDetails?.status ===
|
||||
"accepted" ||
|
||||
projectDetails?.status ===
|
||||
"done" ||
|
||||
projectDetails?.status ===
|
||||
"ongoing"
|
||||
? "text-green-300"
|
||||
: "text-red-300"
|
||||
}`}
|
||||
>
|
||||
{projectDetails?.status ===
|
||||
"pre_payment"
|
||||
? "پرداخت نشده"
|
||||
: projectDetails?.status ===
|
||||
"paid"
|
||||
? "در دست بررسی"
|
||||
: projectDetails?.status ===
|
||||
"accepted"
|
||||
? "منتشر شده"
|
||||
: projectDetails?.status ===
|
||||
"rejected"
|
||||
? "رد شده"
|
||||
: projectDetails?.status ===
|
||||
"ongoing"
|
||||
? "در دست اقدام"
|
||||
: projectDetails?.status ===
|
||||
"done"
|
||||
? "انجام شده"
|
||||
: "کنسل شده"}
|
||||
</p>
|
||||
<>
|
||||
<div className="flex flex-col mr-[100px] items-center py-4">
|
||||
<textarea
|
||||
className="bg-gray-100 rounded-lg p-2 w-[500px] text-black min-h-[100px]"
|
||||
placeholder="دلیل رد پروژه"
|
||||
name=""
|
||||
id=""
|
||||
onChange={(e) =>
|
||||
setRejectReason(
|
||||
e.target
|
||||
.value
|
||||
)
|
||||
}
|
||||
value={rejectReason}
|
||||
></textarea>
|
||||
<button
|
||||
onClick={() =>
|
||||
rejectProjectAccept(
|
||||
projectDetails?._id,
|
||||
projectDetails?.user_id
|
||||
)
|
||||
}
|
||||
className="bg-red-500 mt-5 w-[120px] text-white px-3 py-1 rounded-lg"
|
||||
>
|
||||
رد کردن
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={() =>
|
||||
acceptProjectAccept(
|
||||
projectDetails?._id,
|
||||
projectDetails
|
||||
?.creator
|
||||
?.user_name
|
||||
)
|
||||
}
|
||||
className="bg-[#146820] hover:bg-[#247392] px-8 h-[35px] rounded-md duration-300 mr-3"
|
||||
>
|
||||
انتشار
|
||||
</button>
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
دلیل رد پروژه:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{projectDetails?.reject_reason}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
کاربران پیشنهاد دهنده :
|
||||
</div>
|
||||
<div className="flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 p-4">
|
||||
<div>
|
||||
<div className="flex-1 w-full flex items-center h-11 px-5">
|
||||
<div className="col-span-1 w-[200px]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
نام و نام خانوادگی
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[150px]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
نام کاربری
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[150px]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
قیمت پینهادی
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[100px]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
زمان پیشنهادی
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[100px]"></div>
|
||||
</div>
|
||||
{projectDetails?.requested_users?.map(
|
||||
(user, index) => (
|
||||
<div className="w-full mt-4 flex items-center bg-[#252525]">
|
||||
<div
|
||||
className={`flex-1 w-full flex items-center h-11 px-5 rounded-lg py-8 ${index % 2 ==
|
||||
0
|
||||
? "bg-[#c0e2f04b]"
|
||||
: "bg-[#678fa062]"
|
||||
}`}
|
||||
>
|
||||
<div className="col-span-1 w-[200px]">
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
className="w-[50px] h-[50px] rounded-md ml-2"
|
||||
src={
|
||||
ImageBaseUrl +
|
||||
user?.profile_image
|
||||
}
|
||||
alt=""
|
||||
/>
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
{
|
||||
user?.first_name
|
||||
}{" "}
|
||||
{
|
||||
user?.last_name
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-1 w-[150px]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
{
|
||||
user?.user_name
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[150px]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
{Number(
|
||||
user?.price
|
||||
).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[150px]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
{
|
||||
user?.time
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="col-span-1 w-[150px] ">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate(
|
||||
`/users/${user?._id}`
|
||||
);
|
||||
}}
|
||||
className="bg-[#1f5f79] hover:bg-[#247392] px-8 h-[35px] rounded-md duration-300 mr-5"
|
||||
>
|
||||
جزئیات
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectDetail;
|
||||
161
src/pages/projects/Projects.jsx
Normal file
161
src/pages/projects/Projects.jsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import React, { useEffect, useState, useMemo } from "react";
|
||||
import AdminHeader from "../../components/admin-header";
|
||||
import Sidebar from "../../components/sidebar";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { BaseUrl } from "../../components/BaseUrl";
|
||||
import axios from "axios";
|
||||
import Loading from "../../components/elements/loading";
|
||||
import toast from "react-hot-toast";
|
||||
import Pagination from "../../components/Pagination";
|
||||
import Filters from "../../components/Filters";
|
||||
|
||||
const statusMap = {
|
||||
all: { label: "همه", class: "text-yellow-300" },
|
||||
pre_payment: { label: "پرداخت نشده", class: "text-yellow-300" },
|
||||
paid: { label: "در دست بررسی", class: "text-yellow-300" },
|
||||
accepted: { label: "منتشر شده", class: "text-green-300" },
|
||||
ongoing: { label: "در دست اقدام", class: "text-green-300" },
|
||||
done: { label: "انجام شده", class: "text-green-300" },
|
||||
rejected: { label: "رد شده", class: "text-red-300" },
|
||||
cancled: { label: "کنسل شده", class: "text-red-300" },
|
||||
};
|
||||
|
||||
const Projects = () => {
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [projectsList, setProjectsList] = useState([]);
|
||||
const [totalPages, setTotalPages] = useState(0);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [filters, setFilters] = useState({ startDate: "", endDate: "", status: "" });
|
||||
|
||||
const getProjectList = async (page = 1) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { startDate, endDate, status } = filters;
|
||||
const res = await axios.get(
|
||||
`${BaseUrl}/projects?limit=20&page=${page}${startDate ? "&startDate=" + startDate : ""
|
||||
}${endDate ? "&endDate=" + endDate : ""}${status ? "&status=" + status : ""}`
|
||||
);
|
||||
setProjectsList(res?.data?.projects || []);
|
||||
setTotalPages(res?.data?.totalPages || 0);
|
||||
} catch (err) {
|
||||
toast.error("خطا در دریافت لیست پروژهها");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getProjectList(currentPage);
|
||||
}, [currentPage, filters]);
|
||||
|
||||
const toggleProjectStatus = async (project) => {
|
||||
try {
|
||||
const newStatus = project.status === "accepted" ? "rejected" : "accepted";
|
||||
await axios.post(`${BaseUrl}/projects/accept`, {
|
||||
id: project._id,
|
||||
user_id: project?.selected_user?._id,
|
||||
status: newStatus,
|
||||
});
|
||||
toast.success(`پروژه ${newStatus === "accepted" ? "تأیید" : "رد"} شد`);
|
||||
getProjectList(currentPage);
|
||||
} catch (error) {
|
||||
toast.error(error?.response?.data?.message || "خطا در تغییر وضعیت");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<div className="min-h-screen mx-auto flex bg-gray-900">
|
||||
<div className=" w-64"><Sidebar /></div>
|
||||
<div className="w-full h-full p-14 mb-11 mx-auto text-white">
|
||||
{/* فیلترها */}
|
||||
<Filters
|
||||
statusOptions={Object.entries(statusMap).map(([key, val]) => ({
|
||||
key,
|
||||
label: val.label,
|
||||
}))}
|
||||
provinceOptions={[{ value: "", label: "همه استانها" }]}
|
||||
cityOptions={[{ value: "", label: "همه شهرها" }]}
|
||||
photographyModelOptions={[{ value: "", label: "همه مدلها" }]}
|
||||
blockStatusOptions={[{ value: "", label: "همه کاربران" }]}
|
||||
onFilterChange={setFilters}
|
||||
/>
|
||||
|
||||
{/* هدر جدول */}
|
||||
<div className="w-full mt-4 flex items-center border-b pb-2 text-sm font-KalamehMed">
|
||||
{[
|
||||
"عنوان پروژه",
|
||||
"کارفرما",
|
||||
"کاربر",
|
||||
"تخصص",
|
||||
"زمان پیشنهادی",
|
||||
"زمان نهایی",
|
||||
"قیمت پیشنهادی",
|
||||
"قیمت نهایی",
|
||||
"تاریخ ثبت",
|
||||
"وضعیت",
|
||||
"عملیات",
|
||||
].map((title, idx) => (
|
||||
<div key={idx} className="w-[10%]">{title}</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* لیست پروژهها */}
|
||||
{loading ? (
|
||||
<div className="flex justify-center mt-5">
|
||||
<Loading className="w-14 h-14 text-blacklead animate-pulse" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-y-scroll hide-scrollbar w-full max-h-[70vh] mt-2">
|
||||
{projectsList.map((project, index) => {
|
||||
const statusInfo = statusMap[project?.status] || { label: "-", class: "text-gray-300" };
|
||||
return (
|
||||
<div
|
||||
key={project._id}
|
||||
className={`flex items-center px-4 py-3 rounded-md mb-2 ${index % 2 === 0 ? "bg-[#2e2e2e]" : "bg-[#3a3a3a]"}`}
|
||||
>
|
||||
<div className="w-[10%]">{project?.title}</div>
|
||||
<div className="w-[10%]">{project?.creator?.first_name} {project?.creator?.last_name}</div>
|
||||
<div className="w-[10%]">{project?.selected_user?.first_name} {project?.selected_user?.last_name}</div>
|
||||
<div className="w-[10%]">{project?.expertise}</div>
|
||||
<div className="w-[10%]">{project?.offer_time}</div>
|
||||
<div className="w-[10%]">{project?.final_time}</div>
|
||||
<div className="w-[10%]">{Number(project?.offer_price).toLocaleString()}</div>
|
||||
<div className="w-[10%]">{Number(project?.final_price).toLocaleString()}</div>
|
||||
<div className="w-[10%]">{new Date(project?.createdAt).toLocaleDateString("fa-IR")}</div>
|
||||
<div className={`w-[10%] ${statusInfo.class}`}>{statusInfo.label}</div>
|
||||
<div className="w-[10%] flex gap-2">
|
||||
<button
|
||||
onClick={() => navigate(`${project?._id}`)}
|
||||
className="bg-blue-600 hover:bg-blue-700 px-3 py-1 rounded"
|
||||
>
|
||||
جزئیات
|
||||
</button>
|
||||
<button
|
||||
onClick={() => toggleProjectStatus(project)}
|
||||
className="bg-green-600 hover:bg-green-700 px-3 py-1 rounded"
|
||||
>
|
||||
{project.status === "accepted" ? "رد" : "تأیید"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* صفحهبندی */}
|
||||
<Pagination
|
||||
totalPages={totalPages}
|
||||
currentPage={currentPage}
|
||||
onPageChange={(page) => setCurrentPage(page)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Projects;
|
||||
307
src/pages/purchased/purchased.jsx
Normal file
307
src/pages/purchased/purchased.jsx
Normal file
@@ -0,0 +1,307 @@
|
||||
// src/pages/Purchased.js
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { getPurchasedCourses, getPurchasedStats } from "../../components/adminApi"; // اصلاح مسیر
|
||||
import Layout from "../../components/academy/Layout";
|
||||
import { FaSearch, FaUser, FaBook, FaCalendar, FaEye, FaTimes } from "react-icons/fa";
|
||||
|
||||
const Purchased = () => {
|
||||
const [purchases, setPurchases] = useState([]);
|
||||
const [stats, setStats] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [page, setPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [search, setSearch] = useState("");
|
||||
const [searchInput, setSearchInput] = useState("");
|
||||
const [selectedPurchase, setSelectedPurchase] = useState(null);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
|
||||
const fetchPurchases = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await getPurchasedCourses({ page, limit: 15, search });
|
||||
console.log("Purchased response:", response.data);
|
||||
|
||||
if (response.data.success) {
|
||||
const responseData = response.data.data;
|
||||
setPurchases(responseData?.purchases || []);
|
||||
setStats(responseData?.stats || null);
|
||||
setTotalPages(responseData?.pagination?.totalPages || 1);
|
||||
} else {
|
||||
toast.error(response.data.message || "خطا در دریافت اطلاعات");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching purchases:", error);
|
||||
toast.error("خطا در دریافت اطلاعات");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [page, search]);
|
||||
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
const response = await getPurchasedStats();
|
||||
console.log("Stats response:", response.data);
|
||||
|
||||
if (response.data.success) {
|
||||
setStats(response.data.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching stats:", error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchPurchases();
|
||||
fetchStats();
|
||||
}, [fetchPurchases]);
|
||||
|
||||
const handleSearch = (e) => {
|
||||
e.preventDefault();
|
||||
setSearch(searchInput);
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const handleClearSearch = () => {
|
||||
setSearchInput("");
|
||||
setSearch("");
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const getImageUrl = (imagePath) => {
|
||||
if (!imagePath) return null;
|
||||
if (imagePath.startsWith("http")) return imagePath;
|
||||
// اگر نیاز به BASE_URL داری، اینجا اضافه کن
|
||||
return imagePath;
|
||||
};
|
||||
|
||||
if (loading && page === 1) {
|
||||
return (
|
||||
<Layout>
|
||||
<div className="flex justify-center items-center h-96">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-pink-500"></div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold">دورههای خریداری شده</h1>
|
||||
<p className="text-gray-500 mt-1">لیست تمام دورههایی که کاربران خریداری کردهاند</p>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
{stats && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
<div className="bg-gradient-to-r from-green-500 to-green-600 rounded-lg p-4 text-white">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<p className="text-sm opacity-90">تعداد کل خریدها</p>
|
||||
<p className="text-3xl font-bold">{stats.totalPurchases?.toLocaleString() || 0}</p>
|
||||
</div>
|
||||
<FaBook className="text-4xl opacity-50" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gradient-to-r from-blue-500 to-blue-600 rounded-lg p-4 text-white">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<p className="text-sm opacity-90">کاربران خریدار</p>
|
||||
<p className="text-3xl font-bold">{stats.uniqueUsers?.toLocaleString() || 0}</p>
|
||||
</div>
|
||||
<FaUser className="text-4xl opacity-50" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Search */}
|
||||
<form onSubmit={handleSearch} className="mb-6">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="جستجوی کاربر، دوره یا آکادمی..."
|
||||
value={searchInput}
|
||||
onChange={(e) => setSearchInput(e.target.value)}
|
||||
className="w-full px-4 py-3 pl-12 pr-4 border rounded-xl focus:outline-none focus:ring-2 focus:ring-pink-500"
|
||||
/>
|
||||
<FaSearch className="absolute left-4 top-3.5 text-gray-400" />
|
||||
{searchInput && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClearSearch}
|
||||
className="absolute left-12 top-3.5 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<FaTimes />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Search result info */}
|
||||
{search && (
|
||||
<div className="mb-4 text-sm text-gray-500">
|
||||
نتایج جستجو برای: <span className="font-semibold">{search}</span>
|
||||
<button
|
||||
onClick={handleClearSearch}
|
||||
className="mr-2 text-pink-500 hover:text-pink-600"
|
||||
>
|
||||
(پاک کردن)
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Purchases List */}
|
||||
<div className="space-y-4">
|
||||
{purchases.length === 0 && !loading ? (
|
||||
<div className="text-center py-12 bg-gray-50 rounded-xl">
|
||||
<FaBook className="text-6xl text-gray-300 mx-auto mb-4" />
|
||||
<p className="text-gray-500">هیچ خریدی یافت نشد</p>
|
||||
{search && (
|
||||
<button
|
||||
onClick={handleClearSearch}
|
||||
className="mt-4 text-pink-500 hover:text-pink-600"
|
||||
>
|
||||
پاک کردن جستجو
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
purchases.map((purchase) => (
|
||||
<div
|
||||
key={purchase._id}
|
||||
className="bg-white rounded-lg shadow-md p-4 hover:shadow-lg transition-all hover:-translate-y-1"
|
||||
>
|
||||
<div className="flex flex-col md:flex-row justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-14 h-14 rounded-lg bg-gray-100 flex items-center justify-center overflow-hidden flex-shrink-0">
|
||||
{purchase.course_id?.course_image ? (
|
||||
<img
|
||||
src={getImageUrl(purchase.course_id.course_image)}
|
||||
alt={purchase.course_id?.cuorse_name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<FaBook className="text-gray-400 text-2xl" />
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-bold text-lg">{purchase.course_id?.cuorse_name || "نامشخص"}</h3>
|
||||
<p className="text-sm text-gray-500">مدرس: {purchase.course_id?.teacher_name || "نامشخص"}</p>
|
||||
<div className="flex flex-wrap gap-3 mt-2 text-xs text-gray-500">
|
||||
<span className="flex items-center gap-1">
|
||||
<FaUser className="text-pink-500" />
|
||||
{purchase.user_id?.user_name || "کاربر ناشناس"}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<FaCalendar className="text-pink-500" />
|
||||
{new Date(purchase.createdAt).toLocaleDateString("fa-IR")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
<div className="text-lg font-bold text-pink-500">
|
||||
{parseInt(purchase.course_id?.price || 0).toLocaleString()} تومان
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedPurchase(purchase);
|
||||
setShowModal(true);
|
||||
}}
|
||||
className="flex items-center gap-1 text-sm text-blue-500 hover:text-blue-600 transition"
|
||||
>
|
||||
<FaEye /> جزئیات بیشتر
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex justify-center gap-2 mt-6">
|
||||
<button
|
||||
onClick={() => setPage(p => Math.max(1, p - 1))}
|
||||
disabled={page === 1}
|
||||
className="px-4 py-2 border rounded-lg disabled:opacity-50 hover:bg-gray-50 transition"
|
||||
>
|
||||
قبلی
|
||||
</button>
|
||||
<span className="px-4 py-2">
|
||||
صفحه {page} از {totalPages}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
|
||||
disabled={page === totalPages}
|
||||
className="px-4 py-2 border rounded-lg disabled:opacity-50 hover:bg-gray-50 transition"
|
||||
>
|
||||
بعدی
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Modal */}
|
||||
{showModal && selectedPurchase && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-xl max-w-md w-full mx-4 max-h-[90vh] overflow-y-auto">
|
||||
<div className="flex justify-between items-center p-4 border-b sticky top-0 bg-white">
|
||||
<h3 className="text-lg font-bold">جزئیات خرید</h3>
|
||||
<button
|
||||
onClick={() => setShowModal(false)}
|
||||
className="text-gray-500 hover:text-gray-700 transition"
|
||||
>
|
||||
<FaTimes />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-4 space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-500">کاربر:</span>
|
||||
<span className="font-medium">{selectedPurchase.user_id?.user_name || "-"}</span>
|
||||
</div>
|
||||
{/* <div className="flex justify-between items-center">
|
||||
<span className="text-gray-500">ایمیل:</span>
|
||||
<span className="text-sm">{selectedPurchase.user_id?.email || "-"}</span>
|
||||
</div> */}
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-500">دوره:</span>
|
||||
<span>{selectedPurchase.course_id?.cuorse_name || "-"}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-500">مدرس:</span>
|
||||
<span>{selectedPurchase.course_id?.teacher_name || "-"}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-500">تاریخ خرید:</span>
|
||||
<span>{new Date(selectedPurchase.createdAt).toLocaleDateString("fa-IR")}</span>
|
||||
</div>
|
||||
{selectedPurchase.payment_ref_id && (
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-500">کد پیگیری:</span>
|
||||
<span className="font-mono text-sm">{selectedPurchase.payment_ref_id}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="p-4 border-t flex justify-end">
|
||||
<button
|
||||
onClick={() => setShowModal(false)}
|
||||
className="px-4 py-2 bg-gray-200 rounded-lg hover:bg-gray-300 transition"
|
||||
>
|
||||
بستن
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Purchased;
|
||||
281
src/pages/settings/settings.jsx
Normal file
281
src/pages/settings/settings.jsx
Normal file
@@ -0,0 +1,281 @@
|
||||
// src/pages/Settings.js
|
||||
import React, { useState, useEffect } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { getAllSettings, updateAllSettings } from "../../components/adminApi";
|
||||
import Layout from "../../components/academy/Layout";
|
||||
import { FaSave, FaPercent, FaGem, FaSpinner, FaExclamationTriangle } from "react-icons/fa";
|
||||
|
||||
const Settings = () => {
|
||||
const [settings, setSettings] = useState({
|
||||
tax: 8,
|
||||
proPrice: "60000",
|
||||
legendPrice: "199000",
|
||||
});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
const fetchSettings = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await getAllSettings();
|
||||
console.log("Settings response:", response.data);
|
||||
|
||||
if (response.data.success) {
|
||||
setSettings({
|
||||
tax: response.data.data.tax || 8,
|
||||
proPrice: response.data.data.prices?.pro || "60000",
|
||||
legendPrice: response.data.data.prices?.legend || "199000",
|
||||
});
|
||||
} else {
|
||||
toast.error(response.data.message || "خطا در دریافت تنظیمات");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching settings:", error);
|
||||
toast.error("خطا در دریافت تنظیمات");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
}, []);
|
||||
|
||||
const validateSettings = () => {
|
||||
const newErrors = {};
|
||||
|
||||
// اعتبارسنجی مالیات
|
||||
const taxNum = parseFloat(settings.tax);
|
||||
if (isNaN(taxNum)) {
|
||||
newErrors.tax = "مقدار مالیات باید عدد باشد";
|
||||
} else if (taxNum < 0) {
|
||||
newErrors.tax = "مالیات نمیتواند منفی باشد";
|
||||
} else if (taxNum > 100) {
|
||||
newErrors.tax = "مالیات نمیتواند بیشتر از 100 درصد باشد";
|
||||
}
|
||||
|
||||
// اعتبارسنجی قیمت پرو
|
||||
const proPriceNum = parseInt(settings.proPrice);
|
||||
if (isNaN(proPriceNum)) {
|
||||
newErrors.proPrice = "قیمت پرو باید عدد باشد";
|
||||
} else if (proPriceNum < 0) {
|
||||
newErrors.proPrice = "قیمت نمیتواند منفی باشد";
|
||||
}
|
||||
|
||||
// اعتبارسنجی قیمت لجند
|
||||
const legendPriceNum = parseInt(settings.legendPrice);
|
||||
if (isNaN(legendPriceNum)) {
|
||||
newErrors.legendPrice = "قیمت لجند باید عدد باشد";
|
||||
} else if (legendPriceNum < 0) {
|
||||
newErrors.legendPrice = "قیمت نمیتواند منفی باشد";
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setSettings({
|
||||
...settings,
|
||||
[name]: value,
|
||||
});
|
||||
// پاک کردن خطای مربوط به این فیلد
|
||||
if (errors[name]) {
|
||||
setErrors({ ...errors, [name]: "" });
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// اعتبارسنجی
|
||||
if (!validateSettings()) {
|
||||
toast.error("لطفاً مقادیر را به درستی وارد کنید");
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
const response = await updateAllSettings({
|
||||
tax: parseFloat(settings.tax),
|
||||
proPrice: parseInt(settings.proPrice) || 0,
|
||||
legendPrice: parseInt(settings.legendPrice) || 0,
|
||||
});
|
||||
|
||||
console.log("Save response:", response.data);
|
||||
|
||||
if (response.data.success) {
|
||||
toast.success("تنظیمات با موفقیت ذخیره شد");
|
||||
fetchSettings(); // رفرش کردن مقادیر از سرور
|
||||
} else {
|
||||
toast.error(response.data.message || "خطا در ذخیره تنظیمات");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error saving settings:", error);
|
||||
toast.error(error.response?.data?.message || "خطا در ذخیره تنظیمات");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Layout>
|
||||
<div className="flex justify-center items-center h-96">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-pink-500"></div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold">تنظیمات سیستم</h1>
|
||||
<p className="text-gray-500 mt-1">مدیریت مالیات و قیمت دورههای ویژه</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="max-w-2xl space-y-6">
|
||||
{/* درصد مالیات */}
|
||||
<div className="bg-white rounded-xl shadow-md p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-10 h-10 bg-orange-100 rounded-lg flex items-center justify-center">
|
||||
<FaPercent className="text-orange-500" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold">درصد مالیات (تکس)</h2>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 flex-wrap">
|
||||
<div className="flex-1 max-w-[200px]">
|
||||
<input
|
||||
type="number"
|
||||
name="tax"
|
||||
value={settings.tax}
|
||||
onChange={handleChange}
|
||||
className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-pink-500 ${
|
||||
errors.tax ? "border-red-500" : "border-gray-300"
|
||||
}`}
|
||||
min="0"
|
||||
max="100"
|
||||
step="0.5"
|
||||
/>
|
||||
{errors.tax && (
|
||||
<p className="text-red-500 text-xs mt-1 flex items-center gap-1">
|
||||
<FaExclamationTriangle className="text-xs" />
|
||||
{errors.tax}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<span>درصد</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mt-2">
|
||||
این درصد به مبلغ هر فروش اضافه خواهد شد (محدوده: 0 تا 100)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* قیمت دورههای ویژه */}
|
||||
<div className="bg-white rounded-xl shadow-md p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center">
|
||||
<FaGem className="text-purple-500" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold">قیمت دورههای ویژه</h2>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-4 flex-wrap">
|
||||
<span className="w-32">دوره پرو:</span>
|
||||
<div className="flex-1 max-w-[200px]">
|
||||
<input
|
||||
type="number"
|
||||
name="proPrice"
|
||||
value={settings.proPrice}
|
||||
onChange={handleChange}
|
||||
className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-pink-500 ${
|
||||
errors.proPrice ? "border-red-500" : "border-gray-300"
|
||||
}`}
|
||||
min="0"
|
||||
/>
|
||||
{errors.proPrice && (
|
||||
<p className="text-red-500 text-xs mt-1 flex items-center gap-1">
|
||||
<FaExclamationTriangle className="text-xs" />
|
||||
{errors.proPrice}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<span>تومان</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 flex-wrap">
|
||||
<span className="w-32">دوره لجند:</span>
|
||||
<div className="flex-1 max-w-[200px]">
|
||||
<input
|
||||
type="number"
|
||||
name="legendPrice"
|
||||
value={settings.legendPrice}
|
||||
onChange={handleChange}
|
||||
className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-pink-500 ${
|
||||
errors.legendPrice ? "border-red-500" : "border-gray-300"
|
||||
}`}
|
||||
min="0"
|
||||
/>
|
||||
{errors.legendPrice && (
|
||||
<p className="text-red-500 text-xs mt-1 flex items-center gap-1">
|
||||
<FaExclamationTriangle className="text-xs" />
|
||||
{errors.legendPrice}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<span>تومان</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mt-2">
|
||||
قیمت دوره معمولی از قیمت تعیین شده توسط آکادمی محاسبه میشود
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* دکمه ذخیره */}
|
||||
<div className="flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={fetchSettings}
|
||||
disabled={saving}
|
||||
className="flex items-center gap-2 px-6 py-3 bg-gray-200 text-gray-700 rounded-xl hover:bg-gray-300 transition disabled:opacity-50"
|
||||
>
|
||||
بازیابی
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={saving}
|
||||
className="flex items-center gap-2 px-6 py-3 bg-pink-500 text-white rounded-xl hover:bg-pink-600 transition disabled:opacity-50"
|
||||
>
|
||||
{saving ? (
|
||||
<>
|
||||
<FaSpinner className="animate-spin" />
|
||||
در حال ذخیره...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FaSave />
|
||||
ذخیره تنظیمات
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* توضیحات پایین صفحه */}
|
||||
<div className="mt-8 p-4 bg-gray-50 rounded-lg text-sm text-gray-500">
|
||||
<p className="font-semibold mb-2">ℹ️ نکات مهم:</p>
|
||||
<ul className="space-y-1 list-disc list-inside">
|
||||
<li>تغییر مالیات بر روی تمام فروشهای جدید تأثیر میگذارد</li>
|
||||
<li>قیمت دورههای پرو و لجند برای ساخت دورههای جدید اعمال میشود</li>
|
||||
<li>دورههای موجود با قیمت قبلی خود باقی میمانند</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
106
src/pages/shops/ShopDetail.jsx
Normal file
106
src/pages/shops/ShopDetail.jsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import AdminHeader from "../../components/admin-header";
|
||||
import Sidebar from "../../components/sidebar";
|
||||
import Loading from "../../components/elements/loading";
|
||||
import { BaseUrl, ImageBaseUrl } from "../../components/BaseUrl";
|
||||
import axios from "axios";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import ShopDet from "../../components/shops/ShopDet";
|
||||
|
||||
const ShopDetail = () => {
|
||||
const navigate = useNavigate();
|
||||
const param = useParams();
|
||||
const id = param?.id;
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [section, setSection] = useState(0);
|
||||
const sectionTitles = [
|
||||
"جزئیات",
|
||||
// "تبلیغ ها",
|
||||
];
|
||||
const [shopDetails, setShopDetails] = useState([]);
|
||||
const getUserDetails = (page = 1) => {
|
||||
setLoading(true);
|
||||
const config = {
|
||||
url: `${BaseUrl}/shops/detail?profile_id=${id}`,
|
||||
method: "GET",
|
||||
};
|
||||
axios(config)
|
||||
.then((res) => {
|
||||
setShopDetails(res?.data?.shop);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
getUserDetails();
|
||||
}, [id]);
|
||||
const sectionItems = [
|
||||
<ShopDet getUserDetails={getUserDetails} shopData={shopDetails} id={id} />,
|
||||
|
||||
];
|
||||
return (
|
||||
<>
|
||||
|
||||
<div className="min-h-screen mx-auto">
|
||||
<div className="flex h-full w-full">
|
||||
<div className=" w-64"><Sidebar /></div>
|
||||
<div className="w-full h-full p-14 mb-11 mx-auto bg-[#252525] text-white ">
|
||||
<div className="w-full flex items-center justify-between">
|
||||
<h4 className="font-medium font-KalamehMed text-lg">
|
||||
اطلاعات کاربر:
|
||||
</h4>
|
||||
<button
|
||||
onClick={() => navigate(-1)}
|
||||
className="flex gap-1 bg-[#1f5f79] font-semibold font-KalamehSemi rounded-lg hover:bg-[#E0E3E3] transition-colors duration-500 p-[10px] "
|
||||
>
|
||||
بازگشت
|
||||
<svg
|
||||
width="24"
|
||||
height="25"
|
||||
viewBox="0 0 24 25"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M9.56994 19.3191C9.37994 19.3191 9.18994 19.2491 9.03994 19.0991L2.96994 13.0291C2.67994 12.7391 2.67994 12.2591 2.96994 11.9691L9.03994 5.89914C9.32994 5.60914 9.80994 5.60914 10.0999 5.89914C10.3899 6.18914 10.3899 6.66914 10.0999 6.95914L4.55994 12.4991L10.0999 18.0391C10.3899 18.3291 10.3899 18.8091 10.0999 19.0991C9.95994 19.2491 9.75994 19.3191 9.56994 19.3191Z"
|
||||
fill="#fff"
|
||||
/>
|
||||
<path
|
||||
d="M20.4999 13.25H3.66992C3.25992 13.25 2.91992 12.91 2.91992 12.5C2.91992 12.09 3.25992 11.75 3.66992 11.75H20.4999C20.9099 11.75 21.2499 12.09 21.2499 12.5C21.2499 12.91 20.9099 13.25 20.4999 13.25Z"
|
||||
fill="#fff"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="w-full flex items-center gap-3 rounded-lg bg-blue-lightt px-3 py-4">
|
||||
{sectionTitles?.map((title, index) => (
|
||||
<button
|
||||
onClick={() => setSection(index)}
|
||||
className={`w-[10%] rounded h-11 flex items-center justify-center text-sm transition-all duration-500 ${index == section
|
||||
? "shadow-[inset_-1px_-1px_4px_#5BB6BD,inset_1px_1px_2px_#2D4F52] bg-[#478F95] text-white"
|
||||
: "bg-[#1f5f79] hover:bg-[#478F95] hover:text-white hover:shadow-[inset_-1px_-1px_4px_#5BB6BD,inset_1px_1px_2px_#2D4F52]"
|
||||
}`}
|
||||
>
|
||||
{title}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{loading ? (
|
||||
<div className="w-full flex items-center justify-center mt-5">
|
||||
<Loading className="w-14 h-14 text-blacklead animate-pulse" />
|
||||
</div>
|
||||
) : (
|
||||
sectionItems?.filter(
|
||||
(item, index) => index == section
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShopDetail;
|
||||
203
src/pages/shops/Shops.jsx
Normal file
203
src/pages/shops/Shops.jsx
Normal file
@@ -0,0 +1,203 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import AdminHeader from "../../components/admin-header";
|
||||
import Sidebar from "../../components/sidebar";
|
||||
import Loading from "../../components/elements/loading";
|
||||
import { BaseUrl, ImageBaseUrl } from "../../components/BaseUrl";
|
||||
import axios from "axios";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Pagination from "../../components/Pagination";
|
||||
|
||||
const Shops = () => {
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [shopsList, setShopList] = useState([]);
|
||||
const [totalPages, setTotalPages] = useState(0);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const getUserList = (page = 1) => {
|
||||
setLoading(true);
|
||||
const config = {
|
||||
url: `${BaseUrl}/shops?limit=20&page=${page}`,
|
||||
method: "GET",
|
||||
};
|
||||
axios(config)
|
||||
.then((res) => {
|
||||
setShopList(res?.data?.shops);
|
||||
setTotalPages(res?.data?.totalPages); // دریافت تعداد صفحات از API
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
getUserList(currentPage);
|
||||
}, [currentPage]);
|
||||
return (
|
||||
<>
|
||||
|
||||
<div className="min-h-screen mx-auto bg-gray-900">
|
||||
<div className="flex h-full w-full">
|
||||
<div className=" w-64"><Sidebar /></div>
|
||||
<div className="w-full h-full p-14 mb-11 mx-auto text-white ">
|
||||
<div className="w-full mt-4 flex items-center border-b ">
|
||||
<div className="flex-1 w-full flex items-center h-11 px-5">
|
||||
<div className="col-span-1 w-[16%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
نام و نام خانوادگی کاربر
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[15%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
عنوان فروشگاه
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[15%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
دسته بندی
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[10%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
استان
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[10%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
شهر
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[10%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
محله
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[15%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
تاریخ ایجاد فروشگاه
|
||||
</p>
|
||||
</div>
|
||||
{/* <div className="col-span-1 w-[14%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
وضعیت
|
||||
</p>
|
||||
</div> */}
|
||||
<div className="col-span-1 w-[10%]">
|
||||
<p className="font-KalamehMed text-sm font-medium"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{loading ? (
|
||||
<div className="w-full flex items-center justify-center mt-5">
|
||||
<Loading className="w-14 h-14 text-blacklead animate-pulse" />
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="overflow-y-scroll hide-scrollbar w-full"
|
||||
// style={{ maxHeight: maxHeightTable }}
|
||||
>
|
||||
{shopsList?.map((shop, index) => (
|
||||
<div className="w-full mt-4 flex items-center bg-[#252525]">
|
||||
<div
|
||||
className={`flex-1 w-full flex items-center h-11 px-5 rounded-lg py-8 ${index % 2 == 0
|
||||
? "bg-[#c0e2f04b]"
|
||||
: "bg-[#678fa062]"
|
||||
}`}
|
||||
>
|
||||
<div className="col-span-1 w-[16%]">
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
className="w-[50px] h-[50px] rounded-md ml-2"
|
||||
src={
|
||||
ImageBaseUrl +
|
||||
shop?.profile_image
|
||||
}
|
||||
alt=""
|
||||
/>
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
{shop?.first_name}{" "}
|
||||
{shop?.last_name}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-1 w-[15%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
{shop?.vitrine_name}
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[15%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
{shop?.category}
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[10%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
{shop?.province?.name}
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[10%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
{shop?.city?.name}
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[10%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
{shop?.neighbourhood}
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[15%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
{shop?.createdAt}
|
||||
</p>
|
||||
</div>
|
||||
{/* <div className="col-span-1 w-[14%]">
|
||||
<div className="flex items-center">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
{shop?.blocked
|
||||
? "غیر فعال"
|
||||
: "فعال"}
|
||||
</p>
|
||||
{shop?.blocked ? (
|
||||
<button className="bg-[#146820] hover:bg-[#247392] px-8 h-[35px] rounded-md duration-300 mr-3">
|
||||
تایید
|
||||
</button>
|
||||
) : (
|
||||
<button className="bg-[#a02121d3] hover:bg-[#247392] px-8 h-[35px] rounded-md duration-300 mr-3">
|
||||
غیر فعال
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div> */}
|
||||
<div className="col-span-1 w-[10%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate(
|
||||
`${shop?._id}`
|
||||
);
|
||||
}}
|
||||
className="bg-[#1f5f79] hover:bg-[#247392] px-8 h-[35px] rounded-md duration-300 mr-5"
|
||||
>
|
||||
جزئیات
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Pagination
|
||||
totalItems={shopsList.length}
|
||||
totalPages={totalPages}
|
||||
currentPage={currentPage}
|
||||
onPageChange={(page) => setCurrentPage(page)} // تغییر صفحه
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Shops;
|
||||
51
src/pages/system-settings/SystemSettings.jsx
Normal file
51
src/pages/system-settings/SystemSettings.jsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import React, { useState } from "react";
|
||||
import Sidebar from "../../components/sidebar";
|
||||
import CdnSettingsTab from "../../components/system-settings/CdnSettingsTab";
|
||||
import SslSettingsTab from "../../components/system-settings/SslSettingsTab";
|
||||
import TrafficSettingsTab from "../../components/system-settings/TrafficSettingsTab";
|
||||
|
||||
const sectionTitles = ["CDN", "SSL", "ترافیک"];
|
||||
|
||||
function SystemSettings() {
|
||||
const [section, setSection] = useState(0);
|
||||
|
||||
const sectionItems = [
|
||||
<CdnSettingsTab key="cdn" />,
|
||||
<SslSettingsTab key="ssl" />,
|
||||
<TrafficSettingsTab key="traffic" />,
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen mx-auto">
|
||||
<div className="flex h-full w-full">
|
||||
<div className="w-64">
|
||||
<Sidebar />
|
||||
</div>
|
||||
<div className="w-full h-full p-14 mb-11 mx-auto bg-[#252525] text-white">
|
||||
<h4 className="font-medium font-KalamehMed text-lg mb-6">تنظیمات سیستم</h4>
|
||||
|
||||
<div className="w-full flex items-center gap-3 rounded-lg bg-blue-lightt px-3 py-4 flex-wrap">
|
||||
{sectionTitles.map((title, index) => (
|
||||
<button
|
||||
key={title}
|
||||
onClick={() => setSection(index)}
|
||||
type="button"
|
||||
className={`min-w-[120px] rounded h-11 flex items-center justify-center text-sm transition-all duration-500 ${
|
||||
index === section
|
||||
? "shadow-[inset_-1px_-1px_4px_#5BB6BD,inset_1px_1px_2px_#2D4F52] bg-[#478F95] text-white"
|
||||
: "bg-[#1f5f79] hover:bg-[#478F95] hover:text-white hover:shadow-[inset_-1px_-1px_4px_#5BB6BD,inset_1px_1px_2px_#2D4F52]"
|
||||
}`}
|
||||
>
|
||||
{title}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-6">{sectionItems[section]}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SystemSettings;
|
||||
107
src/pages/terms/TermsAndConditionsEditor.js
Normal file
107
src/pages/terms/TermsAndConditionsEditor.js
Normal file
@@ -0,0 +1,107 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import ReactQuill from "react-quill";
|
||||
import "react-quill/dist/quill.snow.css";
|
||||
import AdminHeader from "../../components/admin-header";
|
||||
import Sidebar from "../../components/sidebar";
|
||||
import axios from "axios";
|
||||
import { BaseUrl } from "../../components/BaseUrl";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
const TermsAndConditionsEditor = () => {
|
||||
const [editorContent, setEditorContent] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
// دریافت قوانین و مقررات موجود از سرور در بارگذاری اولیه
|
||||
axios
|
||||
.get(`${BaseUrl}/settings/terms-and-conditions`)
|
||||
.then((response) => {
|
||||
setEditorContent(response.data.value);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error fetching terms and conditions:", error);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleSave = () => {
|
||||
axios
|
||||
.put(`${BaseUrl}/settings/terms-and-conditions`, { value: editorContent }, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
toast.success(response?.data?.message)
|
||||
// نمایش پیغام موفقیت (در صورت نیاز از toast یا هر کتابخانه مشابه دیگر استفاده کنید)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error updating terms and conditions:", error);
|
||||
// نمایش پیغام خطا (در صورت نیاز از toast یا هر کتابخانه مشابه دیگر استفاده کنید)
|
||||
});
|
||||
};
|
||||
return (
|
||||
<>
|
||||
|
||||
<div className="min-h-screen mx-auto flex flex-col bg-gray-900">
|
||||
<div className="flex flex-1 h-full w-full">
|
||||
<div className=" w-64"><Sidebar /></div>
|
||||
<div className="w-full h-full p-14 mb-11 mx-auto text-white relative">
|
||||
<h1>ویرایشگر قوانین و مقررات</h1>
|
||||
<ReactQuill
|
||||
value={editorContent}
|
||||
onChange={setEditorContent}
|
||||
modules={TermsAndConditionsEditor.modules}
|
||||
formats={TermsAndConditionsEditor.formats}
|
||||
placeholder="متن خود را اینجا وارد کنید..."
|
||||
className="min-h-[500px] mt-5"
|
||||
/>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="w-[200px] bg-green-700 py-2 rounded-lg"
|
||||
>
|
||||
ذخیره
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
TermsAndConditionsEditor.modules = {
|
||||
toolbar: [
|
||||
[
|
||||
{ header: "1" },
|
||||
{ header: "2" },
|
||||
{ header: "3" },
|
||||
{ header: "4" },
|
||||
{ header: "5" },
|
||||
{ header: "6" },
|
||||
{ font: [] },
|
||||
],
|
||||
[{ list: "ordered" }, { list: "bullet" }],
|
||||
["bold", "italic", "underline", "strike", "blockquote"],
|
||||
[{ align: [] }],
|
||||
[{ color: [] }, { background: [] }],
|
||||
["link"],
|
||||
["clean"],
|
||||
],
|
||||
};
|
||||
|
||||
TermsAndConditionsEditor.formats = [
|
||||
"header",
|
||||
"font",
|
||||
"list",
|
||||
"bullet",
|
||||
"bold",
|
||||
"italic",
|
||||
"underline",
|
||||
"strike",
|
||||
"blockquote",
|
||||
"align",
|
||||
"color",
|
||||
"background",
|
||||
"link",
|
||||
];
|
||||
|
||||
export default TermsAndConditionsEditor;
|
||||
176
src/pages/tickets/TicketDetail.jsx
Normal file
176
src/pages/tickets/TicketDetail.jsx
Normal file
@@ -0,0 +1,176 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import AdminHeader from "../../components/admin-header";
|
||||
import Sidebar from "../../components/sidebar";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { BaseUrl, ImageBaseUrl } from "../../components/BaseUrl";
|
||||
import axios from "axios";
|
||||
import Loading from "../../components/elements/loading";
|
||||
import Modal from 'react-modal';
|
||||
const customStyles = {
|
||||
content: {
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
position: "fixed",
|
||||
},
|
||||
};
|
||||
const TicketDetail = () => {
|
||||
const param = useParams();
|
||||
console.log(param);
|
||||
const ticketId = param?.id;
|
||||
const status = param?.status
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [messageList, setMessageList] = useState([]);
|
||||
const [newMessage, setNewMessage] = useState("");
|
||||
const [modalIsOpen, setModalIsOpen] = useState(false);
|
||||
const [modalImage, setModalImage] = useState('');
|
||||
|
||||
const openModal = (image) => {
|
||||
setModalImage(image);
|
||||
setModalIsOpen(true);
|
||||
}
|
||||
|
||||
const closeModal = () => {
|
||||
setModalIsOpen(false);
|
||||
}
|
||||
const getMessageList = (page = 1) => {
|
||||
setLoading(true);
|
||||
const config = {
|
||||
url: `${BaseUrl}/tickets/messages?ticketId=${ticketId}&page=${page}&limit=20`,
|
||||
method: "GET",
|
||||
};
|
||||
axios(config)
|
||||
.then((res) => {
|
||||
setMessageList(res?.data?.messages);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
getMessageList();
|
||||
}, []);
|
||||
const handleSendMessage = () => {
|
||||
if (!newMessage.trim()) return;
|
||||
|
||||
const config = {
|
||||
url: `${BaseUrl}/tickets/message`,
|
||||
method: "POST",
|
||||
data: {
|
||||
ticketId,
|
||||
text: newMessage,
|
||||
},
|
||||
};
|
||||
axios(config)
|
||||
.then((res) => {
|
||||
setNewMessage("");
|
||||
getMessageList();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
};
|
||||
|
||||
const handleCloseTicket = () => {
|
||||
const config = {
|
||||
url: `${BaseUrl}/tickets/close`,
|
||||
method: "POST",
|
||||
data: { ticketId },
|
||||
};
|
||||
axios(config)
|
||||
.then((res) => {
|
||||
navigate(-1);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<div className="min-h-screen mx-auto flex flex-col">
|
||||
<Modal
|
||||
isOpen={modalIsOpen}
|
||||
onRequestClose={closeModal}
|
||||
className="post-image-modal"
|
||||
style={customStyles}
|
||||
>
|
||||
<div className="bg-white p-10 rounded-xl">
|
||||
<h2>آیا مطمئنید میخواهید تیکت را ببندید ؟</h2>
|
||||
<div className="flex mt-10">
|
||||
<button className="w-[120px] h-[37px] rounded-lg bg-red-500 ml-3">خیر</button>
|
||||
<button className="w-[120px] h-[37px] rounded-lg bg-green-500" onClick={handleCloseTicket}>بله</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<div className="flex flex-1 h-full w-full">
|
||||
|
||||
<div className="w-full h-full p-4 mb-11 mx-auto bg-[#252525] text-white relative">
|
||||
{loading ? (
|
||||
<Loading />
|
||||
) : (
|
||||
<div className="space-y-4 overflow-y-auto pb-[200px]">
|
||||
{messageList.map((message) => (
|
||||
<div
|
||||
key={message._id}
|
||||
className={`p-4 rounded-lg max-w-md ${message.senderType === "Admin" ? "bg-blue-500 text-white self-start" : "bg-gray-700 text-white self-end"
|
||||
}`}
|
||||
>
|
||||
<div className="font-bold">
|
||||
{message.senderType === "Admin" ? "Admin" : `${message.senderId?.first_name} ${message.senderId?.last_name}`}
|
||||
</div>
|
||||
<div className="text-sm">{message.createdAt}</div>
|
||||
<div className="mt-2">{message.text}</div>
|
||||
{message.file && (
|
||||
<img src={ImageBaseUrl + message.file} alt="" className="max-w-[400px]" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="fixed bottom-0 left-0 right-0 p-4 bg-[#1f1f1f] flex items-center flex-col">
|
||||
<span className="mb-3 text-red-500 text-xl">
|
||||
{
|
||||
status && status == "Closed" && "تیکت بسته شده است"
|
||||
}
|
||||
</span>
|
||||
{
|
||||
status !== "Closed" &&
|
||||
<>
|
||||
|
||||
<textarea
|
||||
className="flex-grow p-2 bg-gray-800 text-white min-h-[80px] rounded-xl w-full"
|
||||
rows="1"
|
||||
value={newMessage}
|
||||
onChange={(e) => setNewMessage(e.target.value)}
|
||||
placeholder="بنویسید ..."
|
||||
></textarea>
|
||||
<div className="flex">
|
||||
<button
|
||||
className=" p-2 bg-blue-500 text-white mt-4 min-w-[200px] rounded-xl"
|
||||
onClick={handleSendMessage}
|
||||
>
|
||||
ارسال
|
||||
</button>
|
||||
<button
|
||||
className=" p-2 bg-red-500 text-white mt-4 min-w-[200px] rounded-xl mr-3"
|
||||
onClick={openModal}
|
||||
>
|
||||
بستن تیکت
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TicketDetail;
|
||||
208
src/pages/tickets/Tickets.jsx
Normal file
208
src/pages/tickets/Tickets.jsx
Normal file
@@ -0,0 +1,208 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import AdminHeader from "../../components/admin-header";
|
||||
import Sidebar from "../../components/sidebar";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { BaseUrl } from "../../components/BaseUrl";
|
||||
import axios from "axios";
|
||||
import Loading from "../../components/elements/loading";
|
||||
import Pagination from "../../components/Pagination";
|
||||
import Filters from "../../components/Filters";
|
||||
|
||||
const Tickets = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation(); // اضافه شد
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [ticketList, setTicketList] = useState([]);
|
||||
const [totalPages, setTotalPages] = useState(0);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
||||
const [filters, setFilters] = useState({ startDate: "", endDate: "", status: "" });
|
||||
|
||||
const getTicketList = useCallback((page = 1) => {
|
||||
setLoading(true);
|
||||
|
||||
// بررسی پارامتر URL
|
||||
const urlParams = new URLSearchParams(location.search);
|
||||
let statusParam = filters.status;
|
||||
|
||||
if (urlParams.get("status") === "pending") {
|
||||
statusParam = "Customer Response"; // فقط تیکتهای در انتظار پاسخ
|
||||
}
|
||||
|
||||
const { startDate, endDate } = filters;
|
||||
|
||||
const config = {
|
||||
url: `${BaseUrl}/tickets?limit=20&page=${page}${
|
||||
startDate ? "&startDate=" + startDate : ""
|
||||
}${endDate ? "&endDate=" + endDate : ""}${statusParam ? "&status=" + statusParam : ""}`,
|
||||
method: "GET",
|
||||
};
|
||||
|
||||
axios(config)
|
||||
.then((res) => {
|
||||
setTicketList(res?.data?.tickets || []);
|
||||
setTotalPages(res?.data?.totalPages || 0);
|
||||
})
|
||||
.catch(() => {
|
||||
setTicketList([]);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}, [filters, location.search]);
|
||||
|
||||
useEffect(() => {
|
||||
getTicketList(currentPage);
|
||||
}, [currentPage, filters, getTicketList]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<div className="min-h-screen mx-auto bg-gray-900">
|
||||
<div className="flex h-full w-full">
|
||||
<div className=" w-64"><Sidebar /></div>
|
||||
<div className="w-full h-full p-14 mb-11 mx-auto text-white ">
|
||||
<Filters
|
||||
statusOptions={[
|
||||
{ key: "", label: "همه" },
|
||||
{ key: "Pending", label: "در انتظار پاسخ" },
|
||||
{ key: "Answered", label: "پاسخ داده شده" },
|
||||
{ key: "Customer Response", label: "پاسخ مشتری" },
|
||||
{ key: "Closed", label: "بسته شده" },
|
||||
]}
|
||||
provinceOptions={[{ value: "", label: "همه استانها" }]}
|
||||
cityOptions={[{ value: "", label: "همه شهرها" }]}
|
||||
photographyModelOptions={[{ value: "", label: "همه مدلها" }]}
|
||||
blockStatusOptions={[{ value: "", label: "همه کاربران" }]}
|
||||
onFilterChange={setFilters}
|
||||
/>
|
||||
<div className="w-full mt-4 flex items-center border-b ">
|
||||
<div className="flex-1 w-full flex items-center h-11 px-5">
|
||||
<div className="col-span-1 w-[16%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
نام کاربری
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[16%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
نوع کاربری
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[16%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
نام و نام خانوادگی
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[14%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
موبایل
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[14%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
تاریخ ثبت تیکت
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[12%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
وضعیت
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[12%]">
|
||||
<p className="font-KalamehMed text-sm font-medium"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{loading ? (
|
||||
<div className="w-full flex items-center justify-center mt-5">
|
||||
<Loading className="w-14 h-14 text-blacklead animate-pulse" />
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="overflow-y-scroll hide-scrollbar w-full"
|
||||
// style={{ maxHeight: maxHeightTable }}
|
||||
>
|
||||
{ticketList?.map((ticket, index) => (
|
||||
<div className="w-full mt-4 flex items-center bg-[#252525]">
|
||||
<div
|
||||
className={`flex-1 w-full flex items-center h-[90px] px-5 rounded-lg py-8 ${index % 2 == 0
|
||||
? "bg-[#c0e2f04b]"
|
||||
: "bg-[#678fa062]"
|
||||
}`}
|
||||
>
|
||||
<div className="col-span-1 w-[16%]">
|
||||
<p className="font-KalamehMed text-sm font-medium mr-2">
|
||||
{ticket?.user?.user_name}
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[16%]">
|
||||
<p className="font-KalamehMed text-sm font-medium mr-2">
|
||||
{ticket?.user?.expertise}
|
||||
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[16%]">
|
||||
<p className="font-KalamehMed text-sm font-medium mr-2">
|
||||
{ticket?.user?.first_name}{" "}{ticket?.user?.last_name}
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[14%]">
|
||||
<p className="font-KalamehMed text-sm font-medium mr-2">
|
||||
{ticket?.user?.mobile}
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[14%]">
|
||||
<div className="flex items-center">
|
||||
<p className="font-KalamehMed text-sm font-medium mr-2">
|
||||
{ticket?.createdAt}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-1 w-[12%]">
|
||||
<div className="flex items-center">
|
||||
<p className={`font-KalamehMed text-sm font-medium ${ticket?.status === "Pending" ?
|
||||
"text-yellow-300" :
|
||||
ticket?.status === "Answered" ?
|
||||
"text-green-300" :
|
||||
ticket?.status === "Customer Response" ?
|
||||
"text-yellow-300" : "text-red-300"}`}>
|
||||
{ticket?.status === "Pending" ?
|
||||
"در انتظار پاسخ" :
|
||||
ticket?.status === "Answered" ?
|
||||
"پاسخ داده شده" :
|
||||
ticket?.status === "Customer Response" ?
|
||||
"پاسخ مشتری" : "بسته شده"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-1 w-[12%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate(
|
||||
`${ticket?._id}/${ticket?.status}`,
|
||||
);
|
||||
}}
|
||||
className="bg-[#1f5f79] hover:bg-[#247392] px-8 h-[35px] rounded-md duration-300 mr-5"
|
||||
>
|
||||
جزئیات
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<Pagination
|
||||
totalItems={ticketList.length}
|
||||
totalPages={totalPages}
|
||||
currentPage={currentPage}
|
||||
onPageChange={(page) => setCurrentPage(page)} // تغییر صفحه
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div >
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tickets;
|
||||
122
src/pages/users/UserDetail.jsx
Normal file
122
src/pages/users/UserDetail.jsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import AdminHeader from "../../components/admin-header";
|
||||
import Sidebar from "../../components/sidebar";
|
||||
import Loading from "../../components/elements/loading";
|
||||
import { BaseUrl, ImageBaseUrl } from "../../components/BaseUrl";
|
||||
import axios from "axios";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import UserPosts from "../../components/users/UserPosts";
|
||||
import UserDet from "../../components/users/UserDet";
|
||||
import UserWorkroom from "../../components/users/UserWorkroom";
|
||||
import UserComments from "../../components/users/UserComments";
|
||||
import UserMessages from "../../components/users/UserMessages";
|
||||
import UserFinancial from "../../components/users/UserFinancial";
|
||||
import UserChangePassword from "../../components/users/UserChangePassword";
|
||||
|
||||
const UserDetail = () => {
|
||||
const navigate = useNavigate();
|
||||
const param = useParams();
|
||||
const id = param?.id;
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [section, setSection] = useState(0);
|
||||
const sectionTitles = [
|
||||
"جزئیات",
|
||||
"پستها",
|
||||
"درخواستها",
|
||||
"نظرات",
|
||||
"پیامها",
|
||||
"مالی",
|
||||
"تغییر رمز عبور",
|
||||
];
|
||||
const [userDetails, setUsersDetails] = useState([]);
|
||||
const getUserDetails = (page = 1) => {
|
||||
setLoading(true);
|
||||
const config = {
|
||||
url: `${BaseUrl}/users/detail?user_id=${id}`,
|
||||
method: "GET",
|
||||
};
|
||||
axios(config)
|
||||
.then((res) => {
|
||||
setUsersDetails(res?.data?.user);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
getUserDetails();
|
||||
}, [id]);
|
||||
const sectionItems = [
|
||||
<UserDet getUserDet={getUserDetails} userData={userDetails} id={id} />,
|
||||
<UserPosts id={id} />,
|
||||
<UserWorkroom id={id} />,
|
||||
<UserComments id={id} />,
|
||||
<UserMessages id={id} />,
|
||||
<UserFinancial id={id} />,
|
||||
<UserChangePassword id={id} />,
|
||||
];
|
||||
return (
|
||||
<>
|
||||
|
||||
<div className="min-h-screen mx-auto">
|
||||
<div className="flex h-full w-full">
|
||||
<div className=" w-64"><Sidebar /></div>
|
||||
<div className="w-full h-full p-14 mb-11 mx-auto bg-[#252525] text-white ">
|
||||
<div className="w-full flex items-center justify-between">
|
||||
<h4 className="font-medium font-KalamehMed text-lg">
|
||||
اطلاعات کاربر:
|
||||
</h4>
|
||||
<button
|
||||
onClick={() => navigate(-1)}
|
||||
className="flex gap-1 bg-[#1f5f79] font-semibold font-KalamehSemi rounded-lg hover:bg-[#E0E3E3] transition-colors duration-500 p-[10px] "
|
||||
>
|
||||
بازگشت
|
||||
<svg
|
||||
width="24"
|
||||
height="25"
|
||||
viewBox="0 0 24 25"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M9.56994 19.3191C9.37994 19.3191 9.18994 19.2491 9.03994 19.0991L2.96994 13.0291C2.67994 12.7391 2.67994 12.2591 2.96994 11.9691L9.03994 5.89914C9.32994 5.60914 9.80994 5.60914 10.0999 5.89914C10.3899 6.18914 10.3899 6.66914 10.0999 6.95914L4.55994 12.4991L10.0999 18.0391C10.3899 18.3291 10.3899 18.8091 10.0999 19.0991C9.95994 19.2491 9.75994 19.3191 9.56994 19.3191Z"
|
||||
fill="#fff"
|
||||
/>
|
||||
<path
|
||||
d="M20.4999 13.25H3.66992C3.25992 13.25 2.91992 12.91 2.91992 12.5C2.91992 12.09 3.25992 11.75 3.66992 11.75H20.4999C20.9099 11.75 21.2499 12.09 21.2499 12.5C21.2499 12.91 20.9099 13.25 20.4999 13.25Z"
|
||||
fill="#fff"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="w-full flex items-center gap-3 rounded-lg bg-blue-lightt px-3 py-4">
|
||||
{sectionTitles?.map((title, index) => (
|
||||
<button
|
||||
onClick={() => setSection(index)}
|
||||
className={`w-[10%] rounded h-11 flex items-center justify-center text-sm transition-all duration-500 ${index == section
|
||||
? "shadow-[inset_-1px_-1px_4px_#5BB6BD,inset_1px_1px_2px_#2D4F52] bg-[#478F95] text-white"
|
||||
: "bg-[#1f5f79] hover:bg-[#478F95] hover:text-white hover:shadow-[inset_-1px_-1px_4px_#5BB6BD,inset_1px_1px_2px_#2D4F52]"
|
||||
}`}
|
||||
>
|
||||
{title}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{loading ? (
|
||||
<div className="w-full flex items-center justify-center mt-5">
|
||||
<Loading className="w-14 h-14 text-blacklead animate-pulse" />
|
||||
</div>
|
||||
) : (
|
||||
sectionItems?.filter(
|
||||
(item, index) => index == section
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserDetail;
|
||||
438
src/pages/users/Users.jsx
Normal file
438
src/pages/users/Users.jsx
Normal file
@@ -0,0 +1,438 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Sidebar from "../../components/sidebar";
|
||||
import Loading from "../../components/elements/loading";
|
||||
import { BaseUrl, ImageBaseUrl } from "../../components/BaseUrl";
|
||||
import axios from "axios";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import Pagination from "../../components/Pagination";
|
||||
import Filters from "../../components/Filters";
|
||||
import { FaCheck, FaTimes, FaInfoCircle, FaMoon, FaSun } from "react-icons/fa";
|
||||
|
||||
// تابع کمکی برای تبدیل تاریخ شمسی به فرمت قابل مقایسه
|
||||
const parsePersianDate = (dateStr) => {
|
||||
if (!dateStr) return null;
|
||||
const [year, month, day] = dateStr.split(" ")[0].split("-").map(Number);
|
||||
// تبدیل تاریخ شمسی به میلادی برای مقایسه (تقریبی)
|
||||
const gregorianYear = year - 621; // تقریبی: 1404 شمسی ≈ 2025 میلادی
|
||||
return new Date(gregorianYear, month - 1, day);
|
||||
};
|
||||
|
||||
// تابع فرمت تاریخ شمسی برای نمایش
|
||||
const formatPersianDate = (dateStr) => {
|
||||
if (!dateStr) return "-";
|
||||
return dateStr.split(" ")[0]; // فقط تاریخ (مثل 1404-07-04)
|
||||
};
|
||||
|
||||
// کامپوننت اصلی صفحه کاربران
|
||||
const Users = () => {
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [allUsers, setAllUsers] = useState([]); // ذخیره همه کاربران
|
||||
const [filteredUsers, setFilteredUsers] = useState([]); // کاربران فیلترشده
|
||||
const [totalPages, setTotalPages] = useState(0);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [error, setError] = useState(""); // حالت خطا
|
||||
const [filters, setFilters] = useState({
|
||||
startDate: "",
|
||||
endDate: "",
|
||||
status: "",
|
||||
province: "",
|
||||
city: "",
|
||||
photographyModel: "",
|
||||
blockStatus: "",
|
||||
});
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [darkMode, setDarkMode] = useState(false);
|
||||
|
||||
// لیستهای دینامیک برای فیلترها
|
||||
const [provinces, setProvinces] = useState([{ value: "", label: "همه استانها" }]);
|
||||
const [cities, setCities] = useState({ "": [{ value: "", label: "همه شهرها" }] });
|
||||
const [photographyModels, setPhotographyModels] = useState([{ value: "", label: "همه مدلها" }]);
|
||||
|
||||
const itemsPerPage = 20;
|
||||
|
||||
const blockStatusOptions = [
|
||||
{ value: "", label: "همه وضعیتها" },
|
||||
{ value: "active", label: "فعال" },
|
||||
{ value: "inactive", label: "غیرفعال" },
|
||||
];
|
||||
|
||||
const statusOptions = [
|
||||
{ key: "", label: "همه وضعیتها" },
|
||||
{ key: "none", label: "بدون مدارک" },
|
||||
{ key: "pending", label: "در انتظار تایید" },
|
||||
{ key: "verified", label: "تایید شده" },
|
||||
{ key: "rejected", label: "رد شده" },
|
||||
];
|
||||
|
||||
// تابع استخراج لیستهای فیلتر از کاربران
|
||||
const extractFilterOptions = (users) => {
|
||||
// استخراج استانها
|
||||
const provinceSet = new Map();
|
||||
users.forEach((user) => {
|
||||
if (user.province?.id && user.province?.name && !provinceSet.has(String(user.province.id))) {
|
||||
provinceSet.set(String(user.province.id), user.province.name);
|
||||
}
|
||||
});
|
||||
const provinceList = [
|
||||
{ value: "", label: "همه استانها" },
|
||||
...Array.from(provinceSet, ([value, label]) => ({ value, label }))
|
||||
.sort((a, b) => a.label.localeCompare(b.label)), // مرتبسازی الفبایی
|
||||
];
|
||||
setProvinces(provinceList);
|
||||
console.log("استانهای استخراجشده:", provinceList);
|
||||
|
||||
// استخراج شهرها بر اساس استان
|
||||
const cityMap = {};
|
||||
users.forEach((user) => {
|
||||
const provId = String(user.province?.id || "");
|
||||
if (!cityMap[provId]) cityMap[provId] = new Map();
|
||||
if (user.city?.id && user.city?.name && !cityMap[provId].has(String(user.city.id))) {
|
||||
cityMap[provId].set(String(user.city.id), user.city.name);
|
||||
}
|
||||
});
|
||||
const cityObj = { "": [{ value: "", label: "همه شهرها" }] };
|
||||
Object.keys(cityMap).forEach((provId) => {
|
||||
cityObj[provId] = [
|
||||
{ value: "", label: "همه شهرها" },
|
||||
...Array.from(cityMap[provId], ([value, label]) => ({ value, label }))
|
||||
.sort((a, b) => a.label.localeCompare(b.label)), // مرتبسازی الفبایی
|
||||
];
|
||||
});
|
||||
setCities(cityObj);
|
||||
console.log("شهرهای استخراجشده:", cityObj);
|
||||
|
||||
// استخراج مدلهای عکاسی
|
||||
const modelSet = new Set();
|
||||
users.forEach((user) => {
|
||||
if (user.sub_expertise) {
|
||||
user.sub_expertise.forEach((model) => modelSet.add(model));
|
||||
}
|
||||
});
|
||||
const modelList = [
|
||||
{ value: "", label: "همه مدلها" },
|
||||
...Array.from(modelSet, (value) => ({ value, label: value }))
|
||||
.sort((a, b) => a.label.localeCompare(b.label)), // مرتبسازی الفبایی
|
||||
];
|
||||
setPhotographyModels(modelList);
|
||||
console.log("مدلهای عکاسی استخراجشده:", modelList);
|
||||
};
|
||||
|
||||
// تابع تغییر وضعیت کاربر (فعال/غیرفعال)
|
||||
const toggleUserStatus = async (userId, currentStatus) => {
|
||||
try {
|
||||
await axios.post(
|
||||
`${BaseUrl}/users/update-status`,
|
||||
{ userId, block_status: !currentStatus },
|
||||
{
|
||||
headers: { Authorization: `Bearer ${localStorage.getItem("token")}` },
|
||||
}
|
||||
);
|
||||
// بهروزرسانی حالت محلی
|
||||
setAllUsers((prev) =>
|
||||
prev.map((u) => (u._id === userId ? { ...u, block_status: !currentStatus } : u))
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("خطا در تغییر وضعیت کاربر:", err);
|
||||
setError("خطا در تغییر وضعیت کاربر. لطفاً دوباره تلاش کنید.");
|
||||
}
|
||||
};
|
||||
|
||||
// تابع تایید یا رد تصویر پروفایل
|
||||
const handleApproveProfileImage = async (userId, approve) => {
|
||||
try {
|
||||
await axios.put(
|
||||
`${BaseUrl}/users/update?userId=${userId}`,
|
||||
{
|
||||
approveProfileImage: approve,
|
||||
...(approve === false && { profile_image: "" }),
|
||||
},
|
||||
{
|
||||
headers: { Authorization: `Bearer ${localStorage.getItem("token")}` },
|
||||
}
|
||||
);
|
||||
// بهروزرسانی حالت محلی
|
||||
setAllUsers((prev) =>
|
||||
prev.map((u) => (u._id === userId ? { ...u, profile_image: approve ? u.profile_image : "" } : u))
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("خطا در تایید/رد تصویر پروفایل:", err);
|
||||
setError("خطا در تایید/رد تصویر پروفایل. لطفاً دوباره تلاش کنید.");
|
||||
}
|
||||
};
|
||||
|
||||
// تابع دریافت کاربران از API
|
||||
const fetchUsers = () => {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
const url = `${BaseUrl}/users?limit=1000`; // دریافت تعداد زیادی کاربر
|
||||
console.log("درخواست API:", url);
|
||||
|
||||
axios
|
||||
.get(url, {
|
||||
headers: { Authorization: `Bearer ${localStorage.getItem("token")}` },
|
||||
})
|
||||
.then((res) => {
|
||||
if (res?.data?.users) {
|
||||
console.log("دادههای کاربران:", res.data.users);
|
||||
setAllUsers(res.data.users);
|
||||
extractFilterOptions(res.data.users); // استخراج گزینههای فیلتر
|
||||
} else {
|
||||
setAllUsers([]);
|
||||
setError("دادهای از سرور دریافت نشد.");
|
||||
}
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("خطا در دریافت کاربران:", err);
|
||||
setError("خطا در دریافت لیست کاربران. لطفاً دوباره تلاش کنید.");
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
// تابع اعمال فیلترهای محلی
|
||||
const applyFilters = () => {
|
||||
let result = [...allUsers];
|
||||
|
||||
// فیلتر جستجو
|
||||
if (searchQuery) {
|
||||
const query = searchQuery.toLowerCase();
|
||||
result = result.filter(
|
||||
(user) =>
|
||||
user.first_name?.toLowerCase().includes(query) ||
|
||||
user.last_name?.toLowerCase().includes(query) ||
|
||||
user.user_name?.toLowerCase().includes(query) ||
|
||||
user.mobile?.toLowerCase().includes(query)
|
||||
);
|
||||
}
|
||||
|
||||
// فیلتر وضعیت مدارک
|
||||
if (filters.status) {
|
||||
result = result.filter((user) => user.is_verified === filters.status);
|
||||
}
|
||||
|
||||
|
||||
if (statusParam === "pending") {
|
||||
result = result.filter((user) => user.is_verified === "pending");
|
||||
}
|
||||
|
||||
// فیلتر استان
|
||||
if (filters.province) {
|
||||
result = result.filter((user) => String(user.province?.id) === filters.province);
|
||||
}
|
||||
if (filters.city) {
|
||||
result = result.filter((user) => String(user.city?.id) === filters.city);
|
||||
}
|
||||
if (filters.photographyModel) {
|
||||
result = result.filter((user) => user.sub_expertise?.includes(filters.photographyModel));
|
||||
}
|
||||
if (filters.blockStatus) {
|
||||
const isActive = filters.blockStatus === "active";
|
||||
result = result.filter((user) => user.block_status === !isActive);
|
||||
}
|
||||
|
||||
// فیلتر تاریخ
|
||||
if (filters.startDate) {
|
||||
const start = parsePersianDate(filters.startDate);
|
||||
result = result.filter((user) => {
|
||||
const userDate = parsePersianDate(user.createdAt);
|
||||
return userDate && start && userDate >= start;
|
||||
});
|
||||
}
|
||||
if (filters.endDate) {
|
||||
const end = parsePersianDate(filters.endDate);
|
||||
result = result.filter((user) => {
|
||||
const userDate = parsePersianDate(user.createdAt);
|
||||
return userDate && end && userDate <= end;
|
||||
});
|
||||
}
|
||||
|
||||
// لاگ برای دیباگ
|
||||
console.log("کاربران فیلترشده:", result);
|
||||
|
||||
// بهروزرسانی صفحهبندی و کاربران نمایشدادهشده
|
||||
setTotalPages(Math.ceil(result.length / itemsPerPage));
|
||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||
setFilteredUsers(result.slice(startIndex, startIndex + itemsPerPage));
|
||||
};
|
||||
|
||||
// بارگذاری کاربران هنگام بارگذاری صفحه
|
||||
useEffect(() => {
|
||||
fetchUsers();
|
||||
}, []);
|
||||
|
||||
// اعمال فیلترها هنگام تغییر
|
||||
useEffect(() => {
|
||||
applyFilters();
|
||||
}, [allUsers, filters, searchQuery, currentPage]);
|
||||
|
||||
// مدیریت تغییر صفحه
|
||||
const handlePageChange = (page) => setCurrentPage(page);
|
||||
|
||||
// تغییر حالت تاریک/روشن
|
||||
const toggleDarkMode = () => {
|
||||
setDarkMode(!darkMode);
|
||||
};
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
const statusParam = searchParams.get("status"); // مثلا "pending"
|
||||
|
||||
return (
|
||||
<div className={`${darkMode ? "dark" : ""}`}>
|
||||
<div className="flex min-h-screen bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100 transition-colors duration-300">
|
||||
{/* سایدبار پاسخگو */}
|
||||
<div className="w-64 transition-all duration-300">
|
||||
<Sidebar />
|
||||
</div>
|
||||
<div className="flex-1 p-6">
|
||||
|
||||
|
||||
{/* نمایش پیام خطا */}
|
||||
{error && (
|
||||
<div className="mb-4 p-4 bg-red-100 dark:bg-red-900/50 text-red-600 dark:text-red-300 rounded-lg">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* جستجو و فیلترها */}
|
||||
<div className="p-6 mb-6 bg-white/80 dark:bg-gray-800/80 backdrop-blur-md rounded-xl shadow-lg">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="جستجو بر اساس نام، نام خانوادگی، نام کاربری یا موبایل..."
|
||||
className="w-full p-3 mb-4 rounded-lg border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-teal-500 dark:bg-gray-700 dark:text-white transition-all duration-200"
|
||||
value={searchQuery}
|
||||
onChange={(e) => {
|
||||
setSearchQuery(e.target.value);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
/>
|
||||
<Filters
|
||||
statusOptions={statusOptions}
|
||||
provinceOptions={provinces}
|
||||
cityOptions={cities[filters.province || ""]}
|
||||
photographyModelOptions={photographyModels}
|
||||
blockStatusOptions={blockStatusOptions}
|
||||
onFilterChange={(newFilters) => {
|
||||
setFilters((prev) => ({
|
||||
...prev,
|
||||
...newFilters,
|
||||
city: newFilters.province !== prev.province ? "" : newFilters.city, // ریست شهر هنگام تغییر استان
|
||||
}));
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* جدول کاربران */}
|
||||
<div className="bg-white/80 dark:bg-gray-800/80 backdrop-blur-md rounded-xl shadow-lg overflow-hidden">
|
||||
<div className="hidden md:grid md:grid-cols-[20%_15%_15%_15%_15%_20%] items-center h-14 px-6 bg-gray-50/80 dark:bg-gray-700/80 border-b dark:border-gray-600 sticky top-0 z-10">
|
||||
<p className="font-semibold text-sm">نام و نام خانوادگی</p>
|
||||
<p className="font-semibold text-sm">نام کاربری</p>
|
||||
<p className="font-semibold text-sm">موبایل</p>
|
||||
<p className="font-semibold text-sm">تاریخ ثبت نام</p>
|
||||
<p className="font-semibold text-sm">وضعیت</p>
|
||||
<p className="font-semibold text-sm">عملیات</p>
|
||||
</div>
|
||||
|
||||
<div className="max-h-[calc(100vh-300px)] overflow-y-auto">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-32">
|
||||
<Loading className="w-12 h-12 text-teal-500 animate-spin" />
|
||||
</div>
|
||||
) : filteredUsers.length === 0 ? (
|
||||
<div className="flex items-center justify-center h-32 text-gray-500 dark:text-gray-400">
|
||||
هیچ کاربری یافت نشد
|
||||
</div>
|
||||
) : (
|
||||
filteredUsers.map((user, index) => (
|
||||
<div
|
||||
key={user._id}
|
||||
className={`p-4 md:grid md:grid-cols-[20%_15%_15%_15%_15%_20%] items-center hover:bg-gray-100/80 dark:hover:bg-gray-700/80 transition-all duration-200 ${
|
||||
index % 2 === 0
|
||||
? "bg-white/80 dark:bg-gray-800/80"
|
||||
: "bg-gray-50/80 dark:bg-gray-700/80"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<img
|
||||
className="w-12 h-12 rounded-full object-cover border-2 border-teal-500/20"
|
||||
src={
|
||||
user?.profile_image
|
||||
? ImageBaseUrl + user.profile_image
|
||||
: "/default-avatar.png"
|
||||
}
|
||||
alt={`${user.first_name} ${user.last_name}`}
|
||||
onError={(e) => (e.target.src = "/default-avatar.png")}
|
||||
/>
|
||||
<p className="md:block hidden">{user?.first_name} {user?.last_name}</p>
|
||||
<p className="md:hidden font-semibold">
|
||||
{user?.first_name} {user?.last_name}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm">{user?.user_name || "-"}</p>
|
||||
<p className="text-sm">{user?.mobile || "-"}</p>
|
||||
<p className="text-sm">{formatPersianDate(user?.createdAt)}</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={`px-2 py-1 rounded-full text-xs ${
|
||||
user.block_status
|
||||
? "bg-red-100 text-red-600 dark:bg-red-900/50 dark:text-red-300"
|
||||
: "bg-green-100 text-green-600 dark:bg-green-900/50 dark:text-green-300"
|
||||
}`}
|
||||
>
|
||||
{user?.block_status ? "غیرفعال" : "فعال"}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => toggleUserStatus(user._id, user.block_status)}
|
||||
className={`flex items-center gap-1 px-3 py-1 rounded-lg text-sm transition-all duration-200 ${
|
||||
user.block_status
|
||||
? "bg-red-500 hover:bg-red-600 text-white"
|
||||
: "bg-green-500 hover:bg-green-600 text-white"
|
||||
}`}
|
||||
aria-label={`تغییر وضعیت کاربر ${user.user_name}`}
|
||||
>
|
||||
{user.block_status ? <FaCheck /> : <FaTimes />}
|
||||
{user.block_status ? "فعال کردن" : "غیرفعال کردن"}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<button
|
||||
onClick={() => handleApproveProfileImage(user._id, true)}
|
||||
className="flex items-center gap-1 bg-green-500 hover:bg-green-600 text-white px-3 py-1 rounded-lg text-sm transition-all duration-200"
|
||||
aria-label={`تایید تصویر پروفایل ${user.user_name}`}
|
||||
>
|
||||
<FaCheck /> تایید
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleApproveProfileImage(user._id, false)}
|
||||
className="flex items-center gap-1 bg-red-500 hover:bg-red-600 text-white px-3 py-1 rounded-lg text-sm transition-all duration-200"
|
||||
aria-label={`رد تصویر پروفایل ${user.user_name}`}
|
||||
>
|
||||
<FaTimes /> رد
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigate(`${user._id}`)}
|
||||
className="flex items-center gap-1 bg-teal-500 hover:bg-teal-600 text-white px-3 py-1 rounded-lg text-sm transition-all duration-200"
|
||||
aria-label={`مشاهده جزئیات کاربر ${user.user_name}`}
|
||||
>
|
||||
<FaInfoCircle /> جزئیات
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Pagination
|
||||
totalPages={totalPages}
|
||||
currentPage={currentPage}
|
||||
onPageChange={handlePageChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Users;
|
||||
181
src/pages/version/Version.jsx
Normal file
181
src/pages/version/Version.jsx
Normal file
@@ -0,0 +1,181 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import AdminHeader from '../../components/admin-header';
|
||||
import Sidebar from '../../components/sidebar';
|
||||
import axios from 'axios';
|
||||
import { BaseUrl } from '../../components/BaseUrl';
|
||||
|
||||
function Version() {
|
||||
const [version, setVersion] = useState(null); // اطلاعات نسخه فعلی
|
||||
const [loading, setLoading] = useState(false); // حالت بارگذاری
|
||||
const [error, setError] = useState(null); // خطای احتمالی
|
||||
const [formData, setFormData] = useState({
|
||||
version: '',
|
||||
mandatory: false,
|
||||
releaseNotes: '',
|
||||
updateUrls: '',
|
||||
}); // اطلاعات فرم
|
||||
|
||||
// دریافت نسخه فعلی از سرور
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
axios
|
||||
.get(`${BaseUrl}/version`)
|
||||
.then((response) => {
|
||||
setVersion(response.data);
|
||||
setFormData({
|
||||
version: response.data.version || '',
|
||||
mandatory: response.data.mandatory || false,
|
||||
releaseNotes: response.data.releaseNotes || '',
|
||||
updateUrls: response.data.updateUrls ? response.data.updateUrls.join(', ') : '',
|
||||
});
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
setError('خطا در دریافت اطلاعات نسخه');
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
// مدیریت تغییرات فرم
|
||||
const handleChange = (e) => {
|
||||
const { name, value, type, checked } = e.target;
|
||||
setFormData({
|
||||
...formData,
|
||||
[name]: type === 'checkbox' ? checked : value,
|
||||
});
|
||||
};
|
||||
|
||||
// ارسال اطلاعات به سرور
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// تبدیل `updateUrls` به آرایه
|
||||
const formattedData = {
|
||||
...formData,
|
||||
updateUrls: formData.updateUrls.split(',').map((url) => url.trim()),
|
||||
};
|
||||
|
||||
axios
|
||||
.post(`${BaseUrl}/version`, formattedData)
|
||||
.then((response) => {
|
||||
setVersion(response.data.version);
|
||||
setLoading(false);
|
||||
alert('نسخه با موفقیت ذخیره شد.');
|
||||
})
|
||||
.catch((error) => {
|
||||
setError('خطا در ذخیره اطلاعات نسخه');
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<div className="min-h-screen mx-auto flex flex-col bg-gray-900">
|
||||
<div className="flex flex-1 h-full w-full">
|
||||
<div className=" w-64"><Sidebar /></div>
|
||||
<div className="w-full h-full p-14 mb-11 mx-auto text-white relative rounded-xl shadow-lg">
|
||||
<h1 className="text-3xl font-extrabold mb-6 text-center text-[#00cc99]">مدیریت ورژن برنامه</h1>
|
||||
|
||||
{/* نمایش خطا */}
|
||||
{error && <p className="text-red-500 mb-4 text-center">{error}</p>}
|
||||
|
||||
{/* حالت بارگذاری */}
|
||||
{loading ? (
|
||||
<div className="flex justify-center items-center h-48">
|
||||
<p className="text-xl text-gray-300">در حال بارگذاری...</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* نمایش اطلاعات نسخه */}
|
||||
{version && (
|
||||
<div className="mb-8 p-4 border border-gray-700 rounded-lg bg-gray-800">
|
||||
<h2 className="text-lg font-semibold text-[#00cc99] mb-2">نسخه فعلی:</h2>
|
||||
<p className="text-sm"><strong>ورژن:</strong> {version.version}</p>
|
||||
<p className="text-sm"><strong>اجباری:</strong> {version.mandatory ? 'بله' : 'خیر'}</p>
|
||||
<p className="text-sm"><strong>توضیحات:</strong> {version.releaseNotes}</p>
|
||||
<p className="text-sm"><strong>لینکها:</strong> {version.updateUrls.join(', ')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<hr className="border-gray-700 my-8" />
|
||||
|
||||
<h1 className="text-2xl font-bold mb-6 text-center text-[#00cc99]">فرم ایجاد/ویرایش نسخه</h1>
|
||||
<form onSubmit={handleSubmit} className="space-y-6 bg-gray-800 p-6 rounded-lg shadow-md">
|
||||
<div>
|
||||
<label htmlFor="version" className="block mb-1 text-gray-300">
|
||||
نسخه:
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="version"
|
||||
name="version"
|
||||
value={formData.version}
|
||||
onChange={handleChange}
|
||||
className="w-full p-3 rounded bg-gray-900 text-white focus:ring-2 focus:ring-[#00cc99] focus:outline-none"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="mandatory" className="inline-flex items-center text-gray-300">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="mandatory"
|
||||
name="mandatory"
|
||||
checked={formData.mandatory}
|
||||
onChange={handleChange}
|
||||
className="mr-2 h-4 w-4 text-[#00cc99] focus:ring-[#00cc99]"
|
||||
/>
|
||||
اجباری
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="releaseNotes" className="block mb-1 text-gray-300">
|
||||
توضیحات نسخه:
|
||||
</label>
|
||||
<textarea
|
||||
id="releaseNotes"
|
||||
name="releaseNotes"
|
||||
value={formData.releaseNotes}
|
||||
onChange={handleChange}
|
||||
className="w-full p-3 rounded bg-gray-900 text-white focus:ring-2 focus:ring-[#00cc99] focus:outline-none"
|
||||
rows="4"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="updateUrls" className="block mb-1 text-gray-300">
|
||||
لینکهای آپدیت (جدا شده با کاما):
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="updateUrls"
|
||||
name="updateUrls"
|
||||
value={formData.updateUrls}
|
||||
onChange={handleChange}
|
||||
className="w-full p-3 rounded bg-gray-900 text-white focus:ring-2 focus:ring-[#00cc99] focus:outline-none"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full bg-[#00cc99] text-black py-3 rounded-lg font-bold hover:bg-[#00b388] transition duration-200"
|
||||
>
|
||||
ذخیره
|
||||
</button>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Version;
|
||||
427
src/pages/vitrine/VitrineDetail.jsx
Normal file
427
src/pages/vitrine/VitrineDetail.jsx
Normal file
@@ -0,0 +1,427 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { BaseUrl, ImageBaseUrl } from '../../components/BaseUrl';
|
||||
import axios from 'axios';
|
||||
import AdminHeader from '../../components/admin-header';
|
||||
import Sidebar from '../../components/sidebar';
|
||||
import Loading from '../../components/elements/loading';
|
||||
import toast from 'react-hot-toast';
|
||||
import Modal from 'react-modal';
|
||||
const customStyles = {
|
||||
content: {
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
position: "fixed",
|
||||
},
|
||||
};
|
||||
const VitrineDetail = () => {
|
||||
const navigate = useNavigate();
|
||||
const param = useParams();
|
||||
const id = param?.id;
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [vitrineDetails, setVitrineDetails] = useState([]);
|
||||
const [editingVitrine, setEditingVitrine] = useState(false);
|
||||
const [newVitrine, setNewVitrine] = useState('');
|
||||
const [rejectReason, setRejectReason] = useState("")
|
||||
const [modalIsOpen, setModalIsOpen] = useState(false);
|
||||
const [modalImage, setModalImage] = useState('');
|
||||
|
||||
const openModal = (image) => {
|
||||
setModalImage(image);
|
||||
setModalIsOpen(true);
|
||||
}
|
||||
const closeModal = () => {
|
||||
setModalIsOpen(false);
|
||||
}
|
||||
const getVitrineDetails = (page = 1) => {
|
||||
setLoading(true);
|
||||
const config = {
|
||||
url: `${BaseUrl}/advertising/detail?advertisingId=${id}`,
|
||||
method: "GET",
|
||||
};
|
||||
axios(config)
|
||||
.then((res) => {
|
||||
setVitrineDetails(res?.data?.advertising);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
getVitrineDetails();
|
||||
}, [id]);
|
||||
|
||||
const acceptVitrine = () => {
|
||||
const data = {
|
||||
advertisingId: vitrineDetails?._id,
|
||||
};
|
||||
axios
|
||||
.post(`${BaseUrl}/advertising/accept`, data, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(async (response) => {
|
||||
try {
|
||||
toast.success(response?.data?.message)
|
||||
getVitrineDetails()
|
||||
} catch (error) {
|
||||
if (
|
||||
error?.response?.data?.error
|
||||
) {
|
||||
toast.error(error.response.data.error);
|
||||
} else {
|
||||
// نمایش پیام خطا برای سایر موارد
|
||||
toast.error("خطای ناشناخته");
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
toast.show(error?.response?.data?.message, {
|
||||
type: "danger",
|
||||
placement: "top",
|
||||
duration: 4000,
|
||||
offset: 30,
|
||||
animationType: "slide-in",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const rejectVitrine = () => {
|
||||
const data = {
|
||||
advertisingId: vitrineDetails?._id,
|
||||
rejectReason: rejectReason,
|
||||
};
|
||||
axios
|
||||
.post(`${BaseUrl}/advertising/reject`, data, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(async (response) => {
|
||||
try {
|
||||
toast.success(response?.data?.message)
|
||||
getVitrineDetails()
|
||||
} catch (error) {
|
||||
if (
|
||||
error?.response?.data?.error
|
||||
) {
|
||||
toast.error(error.response.data.error);
|
||||
} else {
|
||||
// نمایش پیام خطا برای سایر موارد
|
||||
toast.error("خطای ناشناخته");
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
toast.show(error?.response?.data?.message, {
|
||||
type: "danger",
|
||||
placement: "top",
|
||||
duration: 4000,
|
||||
offset: 30,
|
||||
animationType: "slide-in",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleEditVitrine = () => {
|
||||
// ارسال درخواست برای ویرایش ویترین به سرور
|
||||
// سپس بروزرسانی متغیر مربوط به متن ویترین
|
||||
setEditingVitrine(true);
|
||||
};
|
||||
|
||||
|
||||
|
||||
const contactInfoArray = vitrineDetails?.contactInfo ? Object.keys(vitrineDetails.contactInfo).map(key => {
|
||||
let title;
|
||||
switch (key) {
|
||||
case "phone":
|
||||
title = "شماره ثابت";
|
||||
break;
|
||||
case "mobile":
|
||||
title = "شماره همراه";
|
||||
break;
|
||||
case "telegramLink":
|
||||
title = "لینک تلگرام";
|
||||
break;
|
||||
case "whatsappNumber":
|
||||
title = "شماره واتساپ";
|
||||
break;
|
||||
case "instagramLink":
|
||||
title = "لینک اینستاگرام";
|
||||
break;
|
||||
case "saveInfoForNextAds":
|
||||
title = "ذخیره اطلاعات برای آگهیهای بعدی";
|
||||
break;
|
||||
default:
|
||||
title = key;
|
||||
}
|
||||
return { title, value: vitrineDetails.contactInfo[key] };
|
||||
}) : [];
|
||||
|
||||
return (<>
|
||||
{loading && <Loading />}
|
||||
{!loading && vitrineDetails && (
|
||||
<>
|
||||
|
||||
<div className="min-h-screen mx-auto">
|
||||
<Modal
|
||||
isOpen={modalIsOpen}
|
||||
onRequestClose={closeModal}
|
||||
className="post-image-modal"
|
||||
style={customStyles}
|
||||
>
|
||||
{/* استفاده از ImageMagnify */}
|
||||
<img src={modalImage} alt="Post" className="post-modal-image" />
|
||||
</Modal>
|
||||
<div className="flex h-full w-full">
|
||||
<div className=" w-64"><Sidebar /></div>
|
||||
<div className="w-full h-full p-14 mb-11 mx-auto bg-[#252525] text-white ">
|
||||
<div className="w-full flex items-center justify-between">
|
||||
<h4 className="font-medium font-KalamehMed text-lg">
|
||||
اطلاعات ویترین:
|
||||
</h4>
|
||||
<button
|
||||
onClick={() => navigate(-1)}
|
||||
className="flex gap-1 bg-[#1f5f79] font-semibold font-KalamehSemi rounded-lg hover:bg-[#E0E3E3] transition-colors duration-500 p-[10px]"
|
||||
>
|
||||
بازگشت
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-5 pr-5 mt-5">
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
عنوان ویترین:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{vitrineDetails?.title}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
نام و نام خانوادگی سازنده:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{vitrineDetails?.creator_id?.first_name} {vitrineDetails?.creator_id?.last_name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
نام کاربری کاربر:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{vitrineDetails?.creator_id?.user_name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
دسته بندی
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{vitrineDetails?.category}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
استان
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{vitrineDetails?.province?.name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
شهر
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{vitrineDetails?.city?.name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
محله
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{vitrineDetails?.neighbourhood}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
لیست خدمات و کالا
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4 py-4">
|
||||
<div>
|
||||
{vitrineDetails?.services && vitrineDetails?.services?.map(item => {
|
||||
return (<div className='mt-5'>
|
||||
<img
|
||||
onClick={() => { openModal(ImageBaseUrl + item?.image) }}
|
||||
src={ImageBaseUrl + item?.image} className='w-[100px] h-[100px] rounded-sm' alt="" />
|
||||
<p className='mt-2'>نام : {item?.title}</p>
|
||||
<p className='mt-2'>درصد تخفیف : {item?.discountPercentage}</p>
|
||||
<p className='mt-2'>قیمت اصلی : {item?.originalPrice}</p>
|
||||
<p className='mt-2'>قیمت تخفیف خورده : {item?.discountPrice}</p>
|
||||
</div>)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
لیست امکانات
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4 py-4">
|
||||
<div>
|
||||
{vitrineDetails?.features && vitrineDetails?.features?.map(item => {
|
||||
return (<div className='mt-5 flex'>
|
||||
<p className='mt-2'> {item?.title} : </p>
|
||||
<p className='mt-2 mr-5'>{item?.value == true ? "دارد" : "ندارد"}</p>
|
||||
</div>)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
لیست تماس
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4 py-4">
|
||||
<div>
|
||||
{contactInfoArray.map((item, index) => (
|
||||
<div className='mt-5 flex' key={index}>
|
||||
<p className='mt-2'> {item.title} : </p>
|
||||
<p className='mt-2 mr-5'>{item.value}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
تصاویر
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4 py-4">
|
||||
{vitrineDetails?.images && vitrineDetails?.images?.map(item => {
|
||||
return (<div>
|
||||
<img
|
||||
onClick={() => { openModal(ImageBaseUrl + item) }}
|
||||
src={ImageBaseUrl + item} className='w-[100px] h-[100px] rounded-sm mx-4' alt="" />
|
||||
</div>)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
توضیحات
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{vitrineDetails?.description}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
وضعیت:
|
||||
</div>
|
||||
<div className={`flex items-center font-medium min-w-[500px] font-KalamehMed text-sm rounded-lg bg-[#111] min-h-14 px-4 ${vitrineDetails?.status
|
||||
=== "pending" ?
|
||||
"text-yellow-400" :
|
||||
vitrineDetails?.status == "accepted" ?
|
||||
"text-green-400" :
|
||||
"text-red-400"
|
||||
}`}>
|
||||
<p className={`font-KalamehMed text-sm font-medium ${vitrineDetails?.status
|
||||
=== "pre_payment" ?
|
||||
"text-yellow-400" :
|
||||
vitrineDetails?.status == "paid" ?
|
||||
"text-green-400" : vitrineDetails?.status == "accepted" ?
|
||||
"text-green-400" :
|
||||
"text-red-400"}
|
||||
`}>
|
||||
{vitrineDetails?.status
|
||||
=== "pre_payment" ?
|
||||
"در انتظار پرداخت" :
|
||||
vitrineDetails?.status == "paid" ?
|
||||
"پرداخت شده" : vitrineDetails?.status == "accepted" ?
|
||||
"تایید شده" : vitrineDetails?.status == "rejected" ?
|
||||
"رد شده" :
|
||||
"منقضی شده"
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{vitrineDetails?.reject_reason &&
|
||||
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
علت رد
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{vitrineDetails?.reject_reason}
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
<div className="flex items-center gap-4 mt-5">
|
||||
{!editingVitrine ? (
|
||||
<>
|
||||
{/* <button
|
||||
onClick={handleEditVitrine}
|
||||
className="bg-green-500 text-white px-3 py-1 rounded-lg"
|
||||
>
|
||||
ویرایش نظر
|
||||
</button> */}
|
||||
<button
|
||||
onClick={acceptVitrine}
|
||||
className="bg-blue-500 text-white px-3 py-1 rounded-lg"
|
||||
>
|
||||
قبول ویترین
|
||||
</button>
|
||||
<div className="flex flex-col mr-[100px] items-center">
|
||||
<textarea
|
||||
className="bg-gray-100 rounded-lg p-2 w-[500px] text-black"
|
||||
placeholder='دلیل رد ویترین'
|
||||
name="" id="" onChange={e => setRejectReason(e.target.value)} value={rejectReason} ></textarea>
|
||||
<button
|
||||
onClick={rejectVitrine}
|
||||
className="bg-red-500 mt-5 w-[120px] text-white px-3 py-1 rounded-lg"
|
||||
>
|
||||
رد ویترین
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<textarea
|
||||
value={newVitrine}
|
||||
onChange={(e) => setNewVitrine(e.target.value)}
|
||||
className="bg-gray-100 rounded-lg p-2"
|
||||
/>
|
||||
<button
|
||||
onClick={() => setEditingVitrine(false)}
|
||||
className="bg-green-500 text-white px-3 py-1 rounded-lg"
|
||||
>
|
||||
ذخیره تغییرات
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setEditingVitrine(false)}
|
||||
className="bg-red-500 text-white px-3 py-1 rounded-lg"
|
||||
>
|
||||
انصراف
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default VitrineDetail
|
||||
259
src/pages/vitrine/vitrine.jsx
Normal file
259
src/pages/vitrine/vitrine.jsx
Normal file
@@ -0,0 +1,259 @@
|
||||
import React, { useEffect, useState, useCallback } from "react";
|
||||
import AdminHeader from "../../components/admin-header";
|
||||
import Sidebar from "../../components/sidebar";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { BaseUrl } from "../../components/BaseUrl";
|
||||
import axios from "axios";
|
||||
import Loading from "../../components/elements/loading";
|
||||
import Pagination from "../../components/Pagination";
|
||||
import Filters from "../../components/Filters";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
const Vitrine = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation(); // برای گرفتن URL
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [vitrineList, setVitrineList] = useState([]);
|
||||
const [totalPages, setTotalPages] = useState(0);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
||||
const [filters, setFilters] = useState({
|
||||
startDate: "",
|
||||
endDate: "",
|
||||
status: "",
|
||||
});
|
||||
|
||||
// گرفتن لیست تبلیغات
|
||||
const getVitrineList = useCallback(
|
||||
(page = 1) => {
|
||||
setLoading(true);
|
||||
|
||||
// چک کردن پارامتر URL
|
||||
const urlParams = new URLSearchParams(location.search);
|
||||
let statusParam = filters.status;
|
||||
|
||||
if (urlParams.get("status") === "pending") {
|
||||
// اگر ?status=pending بود، فقط pre_payment نمایش بده
|
||||
statusParam = "pre_payment";
|
||||
}
|
||||
|
||||
const { startDate, endDate } = filters;
|
||||
|
||||
axios
|
||||
.get(
|
||||
`${BaseUrl}/advertising?limit=20&page=${page}${
|
||||
startDate ? "&startDate=" + startDate : ""
|
||||
}${endDate ? "&endDate=" + endDate : ""}${
|
||||
statusParam ? "&status=" + statusParam : ""
|
||||
}`
|
||||
)
|
||||
.then((res) => {
|
||||
setVitrineList(res?.data?.advertisings || []);
|
||||
setTotalPages(res?.data?.totalPages || 0);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("خطا در دریافت اطلاعات");
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
},
|
||||
[filters, location.search]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
getVitrineList(currentPage);
|
||||
}, [currentPage, filters, getVitrineList]);
|
||||
|
||||
// تغییر وضعیت تبلیغ
|
||||
const toggleVitrineStatus = (id, currentStatus) => {
|
||||
const newStatus = currentStatus === "accepted" ? "rejected" : "accepted";
|
||||
setLoading(true);
|
||||
|
||||
axios
|
||||
.post(
|
||||
`${BaseUrl}/advertising/accept`,
|
||||
{ advertisingId: id, status: newStatus },
|
||||
{ headers: { "Content-Type": "application/json" } }
|
||||
)
|
||||
.then((res) => {
|
||||
toast.success(
|
||||
res?.data?.message ||
|
||||
`تبلیغ ${newStatus === "accepted" ? "تأیید" : "رد"} شد`
|
||||
);
|
||||
getVitrineList(currentPage); // ریفرش لیست
|
||||
})
|
||||
.catch((error) => {
|
||||
const errorMessage =
|
||||
error.response?.data?.message || "خطا در تغییر وضعیت تبلیغ";
|
||||
toast.error(errorMessage);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
const rejectAdvertisingWithReason = async (id) => {
|
||||
const rejectReason = prompt("لطفاً دلیل رد را وارد کنید:");
|
||||
if (!rejectReason) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
|
||||
const response = await axios.post(
|
||||
`${BaseUrl}/advertising/reject`,
|
||||
{ advertisingId: id, rejectReason },
|
||||
{ headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
|
||||
toast.success(response.data.message || "تبلیغ رد شد");
|
||||
getVitrineList(currentPage);
|
||||
} catch (error) {
|
||||
toast.error(error.response?.data?.message || "خطا در رد تبلیغ");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// console.log(vitrineList);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="min-h-screen mx-auto bg-gray-900">
|
||||
<div className="flex h-full w-full">
|
||||
<div className="w-64">
|
||||
<Sidebar />
|
||||
</div>
|
||||
<div className="w-full h-full p-14 mb-11 mx-auto text-white">
|
||||
{/* فیلتر */}
|
||||
<Filters
|
||||
statusOptions={[
|
||||
{ key: "", label: "همه" },
|
||||
{ key: "pre_payment", label: "در انتظار پرداخت" },
|
||||
{ key: "paid", label: "پرداخت شده" },
|
||||
{ key: "accepted", label: "تأیید شده" },
|
||||
{ key: "rejected", label: "رد شده" },
|
||||
{ key: "expired", label: "منقضی شده" },
|
||||
]}
|
||||
provinceOptions={[{ value: "", label: "همه استانها" }]}
|
||||
cityOptions={[{ value: "", label: "همه شهرها" }]}
|
||||
photographyModelOptions={[{ value: "", label: "همه مدلها" }]}
|
||||
blockStatusOptions={[{ value: "", label: "همه کاربران" }]}
|
||||
onFilterChange={setFilters}
|
||||
/>
|
||||
|
||||
{/* هدر جدول */}
|
||||
<div className="w-full mt-4 flex items-center border-b">
|
||||
<div className="flex-1 w-full flex items-center h-11 px-5">
|
||||
<div className="w-[13%]">نام و نام خانوادگی</div>
|
||||
<div className="w-[13%]">دستهبندی</div>
|
||||
<div className="w-[13%]">عنوان تبلیغ</div>
|
||||
<div className="w-[13%]">استان</div>
|
||||
<div className="w-[13%]">شهر</div>
|
||||
<div className="w-[13%]">محله</div>
|
||||
<div className="w-[10%]">وضعیت</div>
|
||||
<div className="w-[12%] text-center">عملیات</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* بدنه جدول */}
|
||||
{loading ? (
|
||||
<div className="w-full flex items-center justify-center mt-5">
|
||||
<Loading className="w-14 h-14 text-blacklead animate-pulse" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-y-scroll hide-scrollbar w-full">
|
||||
{vitrineList?.map((vitrine, index) => (
|
||||
<div
|
||||
key={vitrine?._id}
|
||||
className="w-full mt-2 flex items-center"
|
||||
>
|
||||
<div
|
||||
className={`flex-1 w-full flex items-center h-[80px] px-5 rounded-lg ${
|
||||
index % 2 === 0 ? "bg-[#c0e2f04b]" : "bg-[#678fa062]"
|
||||
}`}
|
||||
>
|
||||
<div className="w-[13%]">
|
||||
{vitrine?.creator_id?.first_name}{" "}
|
||||
{vitrine?.creator_id?.last_name}
|
||||
</div>
|
||||
<div className="w-[13%]">{vitrine?.category}</div>
|
||||
<div className="w-[13%]">{vitrine?.title}</div>
|
||||
<div className="w-[13%]">{vitrine?.province?.name}</div>
|
||||
<div className="w-[13%]">{vitrine?.city?.name}</div>
|
||||
<div className="w-[13%]">{vitrine?.neighbourhood}</div>
|
||||
<div className="w-[10%]">
|
||||
<span
|
||||
className={`${
|
||||
vitrine?.status === "pre_payment"
|
||||
? "text-yellow-400"
|
||||
: vitrine?.status === "paid"
|
||||
? "text-green-400"
|
||||
: vitrine?.status === "accepted"
|
||||
? "text-green-300"
|
||||
: vitrine?.status === "rejected"
|
||||
? "text-red-400"
|
||||
: "text-gray-400"
|
||||
}`}
|
||||
>
|
||||
{vitrine?.status === "pre_payment"
|
||||
? "در انتظار پرداخت"
|
||||
: vitrine?.status === "paid"
|
||||
? "پرداخت شده"
|
||||
: vitrine?.status === "accepted"
|
||||
? "تأیید شده"
|
||||
: vitrine?.status === "rejected"
|
||||
? "رد شده"
|
||||
: "منقضی شده"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* دکمه عملیات */}
|
||||
<div className="w-[12%] flex justify-center gap-2">
|
||||
<button
|
||||
onClick={() => navigate(`${vitrine?._id}`)}
|
||||
className="bg-[#1f5f79] hover:bg-[#247392] px-4 h-[35px] rounded-md duration-300"
|
||||
>
|
||||
جزئیات
|
||||
</button>
|
||||
{vitrine?.status !== "expired" && (
|
||||
<>
|
||||
<button
|
||||
onClick={() =>
|
||||
toggleVitrineStatus(vitrine?._id, vitrine?.status)
|
||||
}
|
||||
className={`${
|
||||
vitrine?.status === "accepted"
|
||||
? "bg-red-600 hover:bg-red-700 hidden"
|
||||
: "bg-green-600 hover:bg-green-700"
|
||||
} px-4 h-[35px] rounded-md duration-300`}
|
||||
>
|
||||
{vitrine?.status === "accepted" ? "رد" : "تأیید"}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => rejectAdvertisingWithReason(vitrine?._id)}
|
||||
className={`${vitrine?.status === "accepted" ? "" : "hidden"} bg-red-600 hover:bg-red-700 px-4 h-[35px] rounded-md duration-300`}
|
||||
>
|
||||
رد با دلیل
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* صفحهبندی */}
|
||||
<Pagination
|
||||
totalItems={vitrineList.length}
|
||||
totalPages={totalPages}
|
||||
currentPage={currentPage}
|
||||
onPageChange={(page) => setCurrentPage(page)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Vitrine;
|
||||
278
src/pages/vitrineComments/VitrineCommentDetail.jsx
Normal file
278
src/pages/vitrineComments/VitrineCommentDetail.jsx
Normal file
@@ -0,0 +1,278 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { BaseUrl } from '../../components/BaseUrl';
|
||||
import axios from 'axios';
|
||||
import AdminHeader from '../../components/admin-header';
|
||||
import Sidebar from '../../components/sidebar';
|
||||
import Loading from '../../components/elements/loading';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const VitrineCommentDetail = () => {
|
||||
const navigate = useNavigate();
|
||||
const param = useParams();
|
||||
const id = param?.id;
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [commentDetails, setCommentDetails] = useState([]);
|
||||
const [commentStatus, setCommentStatus] = useState('');
|
||||
const [editingComment, setEditingComment] = useState(false);
|
||||
const [newComment, setNewComment] = useState('');
|
||||
const getCommentDetails = (page = 1) => {
|
||||
setLoading(true);
|
||||
const config = {
|
||||
url: `${BaseUrl}/advertising/comments/detail?comment_id=${id}`,
|
||||
method: "GET",
|
||||
};
|
||||
axios(config)
|
||||
.then((res) => {
|
||||
setCommentDetails(res?.data?.comment);
|
||||
setLoading(false);
|
||||
setNewComment(res?.data?.comment?.text)
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
getCommentDetails();
|
||||
}, [id]);
|
||||
|
||||
const acceptComment = () => {
|
||||
const data = {
|
||||
id: commentDetails?._id,
|
||||
status: "accepted",
|
||||
text: newComment
|
||||
};
|
||||
axios
|
||||
.post(`${BaseUrl}/advertising/comments/accept`, data, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(async (response) => {
|
||||
try {
|
||||
toast.success(response?.data?.message)
|
||||
getCommentDetails()
|
||||
} catch (error) {
|
||||
if (
|
||||
error?.response?.data?.error
|
||||
) {
|
||||
toast.error(error.response.data.error);
|
||||
} else {
|
||||
// نمایش پیام خطا برای سایر موارد
|
||||
toast.error("خطای ناشناخته");
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
toast.show(error?.response?.data?.message, {
|
||||
type: "danger",
|
||||
placement: "top",
|
||||
duration: 4000,
|
||||
offset: 30,
|
||||
animationType: "slide-in",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const rejectComment = () => {
|
||||
const data = {
|
||||
id: commentDetails?._id,
|
||||
status: "rejected"
|
||||
};
|
||||
axios
|
||||
.post(`${BaseUrl}/advertising/comments/accept`, data, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(async (response) => {
|
||||
try {
|
||||
toast.success(response?.data?.message)
|
||||
getCommentDetails()
|
||||
} catch (error) {
|
||||
if (
|
||||
error?.response?.data?.error
|
||||
) {
|
||||
toast.error(error.response.data.error);
|
||||
} else {
|
||||
// نمایش پیام خطا برای سایر موارد
|
||||
toast.error("خطای ناشناخته");
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
toast.show(error?.response?.data?.message, {
|
||||
type: "danger",
|
||||
placement: "top",
|
||||
duration: 4000,
|
||||
offset: 30,
|
||||
animationType: "slide-in",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleEditComment = () => {
|
||||
// ارسال درخواست برای ویرایش کامنت به سرور
|
||||
// سپس بروزرسانی متغیر مربوط به متن کامنت
|
||||
setEditingComment(true);
|
||||
};
|
||||
return (<>
|
||||
{loading && <Loading />}
|
||||
{!loading && commentDetails && (
|
||||
<>
|
||||
|
||||
<div className="min-h-screen mx-auto">
|
||||
<div className="flex h-full w-full">
|
||||
<div className=" w-64"><Sidebar /></div>
|
||||
<div className="w-full h-full p-14 mb-11 mx-auto bg-[#252525] text-white ">
|
||||
<div className="w-full flex items-center justify-between">
|
||||
<h4 className="font-medium font-KalamehMed text-lg">
|
||||
اطلاعات کامنت:
|
||||
</h4>
|
||||
<button
|
||||
onClick={() => navigate(-1)}
|
||||
className="flex gap-1 bg-[#1f5f79] font-semibold font-KalamehSemi rounded-lg hover:bg-[#E0E3E3] transition-colors duration-500 p-[10px]"
|
||||
>
|
||||
بازگشت
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-5 pr-5 mt-5">
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
عنوان ویترین:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{commentDetails?.advertising}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
نام کاربری سازنده:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{commentDetails?.creator?.user_name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
نام و نام خانوادگی سازنده:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{commentDetails?.creator?.first_name} {commentDetails?.creator?.last_name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
نام کاربری کاربر:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{commentDetails?.user?.user_name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
نام و نام خانوادگی کاربر:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{commentDetails?.user?.first_name} {commentDetails?.user?.last_name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
تاریخ ثبت کامنت:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{commentDetails?.createdAt}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
متن کامنت:
|
||||
</div>
|
||||
<div className="flex items-center font-medium min-w-[500px] font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
{commentDetails?.text}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-[180px] ml-3 flex items-center font-medium font-KalamehMed text-sm text-primary rounded-lg bg-[#111] min-h-14 px-4">
|
||||
وضعیت:
|
||||
</div>
|
||||
<div className={`flex items-center font-medium min-w-[500px] font-KalamehMed text-sm rounded-lg bg-[#111] min-h-14 px-4 ${commentDetails?.status
|
||||
=== "pending" ?
|
||||
"text-yellow-400" :
|
||||
commentDetails?.status == "accepted" ?
|
||||
"text-green-400" :
|
||||
"text-red-400"
|
||||
}`}>
|
||||
{commentDetails?.status
|
||||
=== "pending" ?
|
||||
"در انتظار تایید" :
|
||||
commentDetails?.status == "accepted" ?
|
||||
"تایید شده" :
|
||||
"رد شده"
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 mt-5">
|
||||
{!editingComment ? (
|
||||
<>
|
||||
<button
|
||||
onClick={handleEditComment}
|
||||
className="bg-green-500 text-white px-3 py-1 rounded-lg"
|
||||
>
|
||||
ویرایش نظر
|
||||
</button>
|
||||
<button
|
||||
onClick={acceptComment}
|
||||
className="bg-blue-500 text-white px-3 py-1 rounded-lg"
|
||||
>
|
||||
قبول کامنت
|
||||
</button>
|
||||
<button
|
||||
onClick={rejectComment}
|
||||
className="bg-red-500 text-white px-3 py-1 rounded-lg"
|
||||
>
|
||||
رد کامنت
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className='flex flex-col'>
|
||||
<textarea
|
||||
value={newComment}
|
||||
onChange={(e) => setNewComment(e.target.value)}
|
||||
className="bg-gray-100 text-black rounded-lg p-2 w-[300px] h-[200px] mb-3"
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditingComment(false)
|
||||
acceptComment()
|
||||
}} className="bg-green-500 text-white px-3 py-1 rounded-lg"
|
||||
>
|
||||
ذخیره تغییرات
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setEditingComment(false)}
|
||||
className="bg-red-500 text-white px-3 py-1 rounded-lg"
|
||||
>
|
||||
انصراف
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default VitrineCommentDetail
|
||||
217
src/pages/vitrineComments/VitrineComments.jsx
Normal file
217
src/pages/vitrineComments/VitrineComments.jsx
Normal file
@@ -0,0 +1,217 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import AdminHeader from "../../components/admin-header";
|
||||
import Sidebar from "../../components/sidebar";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { BaseUrl } from "../../components/BaseUrl";
|
||||
import axios from "axios";
|
||||
import Loading from "../../components/elements/loading";
|
||||
import Pagination from "../../components/Pagination";
|
||||
import Filters from "../../components/Filters";
|
||||
|
||||
const VitrineComments = () => {
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [commentList, setCommentList] = useState([]);
|
||||
const [totalPages, setTotalPages] = useState(0);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
//
|
||||
const [filters, setFilters] = useState({
|
||||
startDate: "",
|
||||
endDate: "",
|
||||
status: "",
|
||||
});
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
const statusParam = searchParams.get("status"); // مثلا "pending"
|
||||
const token = document.cookie
|
||||
.split('; ')
|
||||
.find(row => row.startsWith('token='))
|
||||
?.split('=')[1];
|
||||
|
||||
const getCommentList = (page = 1) => {
|
||||
setLoading(true);
|
||||
//
|
||||
const { startDate, endDate, status } = filters;
|
||||
const config = {
|
||||
url: `${BaseUrl}/advertising/comments?limit=20&page=${page}${
|
||||
startDate ? "&startDate=" + startDate : ""
|
||||
}${endDate ? "&endDate=" + endDate : ""}&status=${statusParam? "pending" : status}`,
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}` // اضافه کردن توکن به هدر
|
||||
}
|
||||
};
|
||||
axios(config)
|
||||
.then((res) => {
|
||||
setCommentList(res?.data?.comments);
|
||||
setTotalPages(res?.data?.totalPages); // دریافت تعداد صفحات از API
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
getCommentList(currentPage);
|
||||
}, [currentPage, filters]);
|
||||
return (
|
||||
<>
|
||||
<div className="min-h-screen mx-auto bg-gray-900">
|
||||
<div className="flex h-full w-full">
|
||||
<div className=" w-64">
|
||||
<Sidebar />
|
||||
</div>
|
||||
<div className="w-full h-full p-14 mb-11 mx-auto text-white ">
|
||||
<Filters
|
||||
statusOptions={[
|
||||
{ key: "", label: "همه" },
|
||||
{ key: "pending", label: "در انتظار تایید" },
|
||||
{ key: "accepted", label: "تایید شده" },
|
||||
{ key: "rejected", label: "رد شده" },
|
||||
]}
|
||||
provinceOptions={[{ value: "", label: "همه استانها" }]}
|
||||
cityOptions={[{ value: "", label: "همه شهرها" }]}
|
||||
photographyModelOptions={[{ value: "", label: "همه مدلها" }]}
|
||||
blockStatusOptions={[{ value: "", label: "همه کاربران" }]}
|
||||
onFilterChange={setFilters}
|
||||
/>
|
||||
<div className="w-full mt-4 flex items-center border-b ">
|
||||
<div className="flex-1 w-full flex items-center h-11 px-5">
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
عنوان ویترین
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
نام کاربری سازنده
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
نام و نام خانوادگی سازنده
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
نام کاربری یوزر
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
نام و نام خانوادگی یوزر
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
تاریخ ثبت کامنت
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[12%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">وضعیت</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[11%]">
|
||||
<p className="font-KalamehMed text-sm font-medium"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{loading ? (
|
||||
<div className="w-full flex items-center justify-center mt-5">
|
||||
<Loading className="w-14 h-14 text-blacklead animate-pulse" />
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="overflow-y-scroll hide-scrollbar w-full"
|
||||
// style={{ maxHeight: maxHeightTable }}
|
||||
>
|
||||
{commentList?.map((comment, index) => (
|
||||
<div className="w-full mt-4 flex items-center bg-[#252525]">
|
||||
<div
|
||||
className={`flex-1 w-full flex items-center h-[90px] px-5 rounded-lg py-8 ${
|
||||
index % 2 == 0 ? "bg-[#c0e2f04b]" : "bg-[#678fa062]"
|
||||
}`}
|
||||
>
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium mr-2">
|
||||
{comment?.advertising}
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium mr-2">
|
||||
{comment?.creator?.user_name}
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium mr-2">
|
||||
{comment?.creator?.first_name}{" "}
|
||||
{comment?.creator?.last_name}
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium mr-2">
|
||||
{comment?.user?.user_name}
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium mr-2">
|
||||
{comment?.user?.first_name} {comment?.user?.last_name}
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[13%]">
|
||||
<p className="font-KalamehMed text-sm font-medium mr-2">
|
||||
{comment?.createdAt}
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 w-[12%]">
|
||||
<div className="flex items-center">
|
||||
<p
|
||||
className={`font-KalamehMed text-sm font-medium ${
|
||||
comment?.status === "pending"
|
||||
? "text-yellow-400"
|
||||
: comment?.status == "accepted"
|
||||
? "text-green-400"
|
||||
: "text-red-400"
|
||||
}
|
||||
`}
|
||||
>
|
||||
{comment?.status === "pending"
|
||||
? "در انتظار تایید"
|
||||
: comment?.status == "accepted"
|
||||
? "تایید شده"
|
||||
: "رد شده"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-1 w-[11%]">
|
||||
<p className="font-KalamehMed text-sm font-medium">
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate(`${comment?._id}`);
|
||||
}}
|
||||
className="bg-[#1f5f79] hover:bg-[#247392] px-8 h-[35px] rounded-md duration-300 mr-5"
|
||||
>
|
||||
جزئیات
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<Pagination
|
||||
totalItems={commentList.length}
|
||||
totalPages={totalPages}
|
||||
currentPage={currentPage}
|
||||
onPageChange={(page) => setCurrentPage(page)} // تغییر صفحه
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default VitrineComments;
|
||||
287
src/routers/appRoutes.jsx
Normal file
287
src/routers/appRoutes.jsx
Normal file
@@ -0,0 +1,287 @@
|
||||
import React from "react";
|
||||
import { createBrowserRouter } from "react-router-dom";
|
||||
import ProtectedRoute from "../utils/ProtectedRoute";
|
||||
import Dashboard from "../pages/Dashboard";
|
||||
import Users from "../pages/users/Users";
|
||||
import Posts from "../pages/posts/Posts";
|
||||
import Projects from "../pages/projects/Projects";
|
||||
import Login from "../pages/Login";
|
||||
import UserDetail from "../pages/users/UserDetail";
|
||||
import PostDetail from "../pages/posts/PostDetail";
|
||||
import ProjectDetail from "../pages/projects/ProjectDetail";
|
||||
import Tickets from "../pages/tickets/Tickets";
|
||||
import TicketDetail from "../pages/tickets/TicketDetail";
|
||||
import FinancialDetail from "../pages/financials/FinancialDetail";
|
||||
import Financials from "../pages/financials/Financials";
|
||||
import Comments from "../pages/comments/Comments";
|
||||
import CommentDetail from "../pages/comments/CommentDetail";
|
||||
import Vitrine from "../pages/vitrine/vitrine";
|
||||
import VitrineDetail from "../pages/vitrine/VitrineDetail";
|
||||
import VitrineComments from "../pages/vitrineComments/VitrineComments";
|
||||
import VitrineCommentDetail from "../pages/vitrineComments/VitrineCommentDetail";
|
||||
import Shops from "../pages/shops/Shops";
|
||||
import ShopDetail from "../pages/shops/ShopDetail";
|
||||
import TermsAndConditionsEditor from "../pages/terms/TermsAndConditionsEditor";
|
||||
import Version from "../pages/version/Version"
|
||||
import Expertise from "../pages/expertise/Expertise";
|
||||
import License from "../pages/license/License";
|
||||
import Academy from "../pages/academy/academy";
|
||||
import Payments from "../pages/payments/payments";
|
||||
import Courses from "../pages/courses/courses";
|
||||
import Academies from "../pages/academies/academies";
|
||||
import Purchased from "../pages/purchased/purchased";
|
||||
import Settings from "../pages/settings/settings";
|
||||
import SystemSettings from "../pages/system-settings/SystemSettings";
|
||||
|
||||
export const router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
element: (
|
||||
<ProtectedRoute>
|
||||
<Dashboard />
|
||||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/login",
|
||||
element: <Login />,
|
||||
},
|
||||
{
|
||||
path: "/users",
|
||||
element: (
|
||||
<ProtectedRoute>
|
||||
<Users />
|
||||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "users/:id",
|
||||
element: (
|
||||
<ProtectedRoute>
|
||||
<UserDetail />
|
||||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/posts",
|
||||
element: (
|
||||
<ProtectedRoute>
|
||||
<Posts />
|
||||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "posts/:id",
|
||||
element: (
|
||||
<ProtectedRoute>
|
||||
<PostDetail />
|
||||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/projects",
|
||||
element: (
|
||||
<ProtectedRoute>
|
||||
<Projects />
|
||||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "projects/:id",
|
||||
element: (
|
||||
<ProtectedRoute>
|
||||
<ProjectDetail />
|
||||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/tickets",
|
||||
element: (
|
||||
<ProtectedRoute>
|
||||
<Tickets />
|
||||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "tickets/:id/:status",
|
||||
element: (
|
||||
<ProtectedRoute>
|
||||
<TicketDetail />
|
||||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/financials",
|
||||
element: (
|
||||
<ProtectedRoute>
|
||||
<Financials />
|
||||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/academy",
|
||||
element: (
|
||||
<ProtectedRoute>
|
||||
<Academy />
|
||||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "financials/:id",
|
||||
element: (
|
||||
<ProtectedRoute>
|
||||
<FinancialDetail />
|
||||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/comments",
|
||||
element: (
|
||||
<ProtectedRoute>
|
||||
<Comments />
|
||||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "comments/:id",
|
||||
element: (
|
||||
<ProtectedRoute>
|
||||
<CommentDetail />
|
||||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/vitrine",
|
||||
element: (
|
||||
<ProtectedRoute>
|
||||
<Vitrine />
|
||||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "vitrine/:id",
|
||||
element: (
|
||||
<ProtectedRoute>
|
||||
<VitrineDetail />
|
||||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/vitrine-comments",
|
||||
element: (
|
||||
<ProtectedRoute>
|
||||
<VitrineComments />
|
||||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "vitrine-comments/:id",
|
||||
element: (
|
||||
<ProtectedRoute>
|
||||
<VitrineCommentDetail />
|
||||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/shops",
|
||||
element: (
|
||||
<ProtectedRoute>
|
||||
<Shops />
|
||||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "shops/:id",
|
||||
element: (
|
||||
<ProtectedRoute>
|
||||
<ShopDetail />
|
||||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "license",
|
||||
element: (
|
||||
<ProtectedRoute>
|
||||
<License/>
|
||||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/terms",
|
||||
element: (
|
||||
<ProtectedRoute>
|
||||
<TermsAndConditionsEditor />
|
||||
</ProtectedRoute>
|
||||
),
|
||||
}, {
|
||||
path: "/version",
|
||||
element: (
|
||||
<ProtectedRoute>
|
||||
<Version />
|
||||
</ProtectedRoute>
|
||||
),
|
||||
}, {
|
||||
path: "/expertise",
|
||||
element: (
|
||||
<ProtectedRoute>
|
||||
<Expertise />
|
||||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/payments",
|
||||
element: (
|
||||
<ProtectedRoute>
|
||||
<Payments/>
|
||||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/courses",
|
||||
element: (
|
||||
<ProtectedRoute>
|
||||
<Courses/>
|
||||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/academies",
|
||||
element: (
|
||||
<ProtectedRoute>
|
||||
<Academies/>
|
||||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/academy",
|
||||
element: (
|
||||
<ProtectedRoute>
|
||||
<Academy/>
|
||||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/purchased",
|
||||
element: (
|
||||
<ProtectedRoute>
|
||||
<Purchased/>
|
||||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/settings",
|
||||
element: (
|
||||
<ProtectedRoute>
|
||||
<Settings/>
|
||||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/system-settings",
|
||||
element: (
|
||||
<ProtectedRoute>
|
||||
<SystemSettings />
|
||||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
]);
|
||||
22
src/utils/AuthProvider.jsx
Normal file
22
src/utils/AuthProvider.jsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { createContext, useContext, useState } from "react";
|
||||
|
||||
const AuthContext = createContext(null);
|
||||
|
||||
const AuthProvider = ({ children, isSignedIn }) => {
|
||||
const token = localStorage.getItem("token");
|
||||
return (
|
||||
<AuthContext.Provider value={token}>{children}</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthProvider;
|
||||
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
|
||||
if (context === undefined) {
|
||||
throw new Error("useAuth must be used within an AuthProvider");
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
19
src/utils/ProtectedRoute.jsx
Normal file
19
src/utils/ProtectedRoute.jsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useAuth } from './AuthProvider';
|
||||
|
||||
const ProtectedRoute = ({ children }) => {
|
||||
const token = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) {
|
||||
navigate('/login', { replace: true });
|
||||
}
|
||||
}, [navigate, token]);
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
export default ProtectedRoute;
|
||||
10
tailwind.config.js
Normal file
10
tailwind.config.js
Normal file
@@ -0,0 +1,10 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./src/**/*.{js,jsx,ts,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
Reference in New Issue
Block a user