Init(Core): add project to new repo
This commit is contained in:
4
.env
Normal file
4
.env
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
REACT_APP_FRONT_BASE_URL=https://mypanel.liara.run/
|
||||||
|
REACT_APP_PUBLIC_BASE_URL=https://axicon-pim.iran.liara.run/api/public/
|
||||||
|
REACT_APP_BACK_BASE_URL=https://axicon-portal.iran.liara.run/
|
||||||
|
REACT_APP_PRIVATE_BASE_URL=https://axicon-portal.iran.liara.run/api/
|
||||||
22
.github/workflows/liara.yaml
vendored
Normal file
22
.github/workflows/liara.yaml
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
name: CD-Liara
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: "18"
|
||||||
|
- name: update-liara
|
||||||
|
env:
|
||||||
|
LIARA_TOKEN: ${{ secrets.LIARA_API_TOKEN }}
|
||||||
|
run: |
|
||||||
|
npm i -g @liara/cli@4
|
||||||
|
liara deploy --app="axicon-portal-front" --api-token="$LIARA_TOKEN" --detach
|
||||||
|
|
||||||
|
|
||||||
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# 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.developement
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
.idea
|
||||||
|
|
||||||
|
/public/tinymce/
|
||||||
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)
|
||||||
5
app/postinstall.js
Normal file
5
app/postinstall.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
const fse = require('fs-extra');
|
||||||
|
const path = require('path');
|
||||||
|
const topDir = __dirname;
|
||||||
|
fse.emptyDirSync(path.join(topDir, 'public', 'tinymce'));
|
||||||
|
fse.copySync(path.join(topDir, 'node_modules', 'tinymce'), path.join(topDir, 'public', 'tinymce'), { overwrite: true });
|
||||||
5131
package-lock.json
generated
Normal file
5131
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
60
package.json
Normal file
60
package.json
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
{
|
||||||
|
"name": "dash-front",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@ckeditor/ckeditor5-build-classic": "^28.0.0",
|
||||||
|
"@ckeditor/ckeditor5-react": "^6.1.0",
|
||||||
|
"@emotion/react": "^11.11.1",
|
||||||
|
"@emotion/styled": "^11.11.0",
|
||||||
|
"@floating-ui/dom": "^1.5.1",
|
||||||
|
"@headlessui/react": "^1.7.16",
|
||||||
|
"@mui/material": "^5.14.2",
|
||||||
|
"@tailwindcss/forms": "^0.5.4",
|
||||||
|
"@testing-library/jest-dom": "^5.17.0",
|
||||||
|
"@testing-library/react": "^13.4.0",
|
||||||
|
"@testing-library/user-event": "^13.5.0",
|
||||||
|
"@tinymce/tinymce-react": "^6.3.0",
|
||||||
|
"axios": "^1.4.0",
|
||||||
|
"bootstrap": "^5.3.1",
|
||||||
|
"env-cmd": "^10.1.0",
|
||||||
|
"fs-extra": "^11.1.1",
|
||||||
|
"js-cookie": "^3.0.5",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-bootstrap": "^2.8.0",
|
||||||
|
"react-cookie": "^8.1.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-router-dom": "^6.14.2",
|
||||||
|
"react-scripts": "^0.0.0",
|
||||||
|
"react-select": "^5.7.4",
|
||||||
|
"tailwindcss": "^3.3.3",
|
||||||
|
"tinymce": "^8.4.0",
|
||||||
|
"web-vitals": "^2.1.4",
|
||||||
|
"yup": "^1.2.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start:dev": "env-cmd -f .env.developement react-scripts start",
|
||||||
|
"start:prod": "env-cmd -f .env.production react-scripts start",
|
||||||
|
"build": "env-cmd -f .env 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
5
postinstall.js
Normal file
5
postinstall.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
const fse = require('fs-extra');
|
||||||
|
const path = require('path');
|
||||||
|
const topDir = __dirname;
|
||||||
|
fse.emptyDirSync(path.join(topDir, 'public', 'tinymce'));
|
||||||
|
fse.copySync(path.join(topDir, 'node_modules', 'tinymce'), path.join(topDir, 'public', 'tinymce'), { overwrite: true });
|
||||||
45
public/index.html
Normal file
45
public/index.html
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%PUBLIC_URL%/logo.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content=""
|
||||||
|
/>
|
||||||
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo.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>
|
||||||
|
<script src="https://cdn.tiny.cloud/1/raa757cai6ev1hnseovcby3du747r6f0ketni4ptuceesyr4/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script>
|
||||||
|
|
||||||
|
</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/logo.png
Normal file
BIN
public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
8
public/manifest.json
Normal file
8
public/manifest.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"short_name": "نیلی پنل",
|
||||||
|
"name": "نیلی پنل",
|
||||||
|
"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:
|
||||||
19
src/App.js
Normal file
19
src/App.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import './index.css';
|
||||||
|
import Login from "./views/Auth/Login";
|
||||||
|
import {BrowserRouter, Route, Routes} from "react-router-dom";
|
||||||
|
import PanelLayout from "./Layouts/PanelLayout";
|
||||||
|
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<BrowserRouter>
|
||||||
|
<Routes>
|
||||||
|
<Route exact path="/" element={(<Login/>)}/>
|
||||||
|
<Route path='/panel/*' element={<PanelLayout/>}/>
|
||||||
|
{/*<Route path="*" element={(<NotFound/>)}/>*/}
|
||||||
|
</Routes>
|
||||||
|
</BrowserRouter>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
8
src/App.test.js
Normal file
8
src/App.test.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
test('renders learn react link', () => {
|
||||||
|
render(<App />);
|
||||||
|
const linkElement = screen.getByText(/learn react/i);
|
||||||
|
expect(linkElement).toBeInTheDocument();
|
||||||
|
});
|
||||||
11
src/Components/Button.jsx
Normal file
11
src/Components/Button.jsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export default function Button({ onClick, children, className, disabled }) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
disabled={ disabled }
|
||||||
|
onClick={ onClick }
|
||||||
|
className={"rounded-lg p-2 active:outline active:animate-pulse " + className}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
15
src/Components/DangerButton.jsx
Normal file
15
src/Components/DangerButton.jsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export default function DangerButton({ className = '', disabled, children, ...props }) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
{...props}
|
||||||
|
className={
|
||||||
|
`inline-flex items-center px-4 py-2 bg-gray-600 border border-transparent rounded-md font-semibold text-white uppercase tracking-widest hover:bg-gray-500 active:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 transition ease-in-out duration-150 ${
|
||||||
|
disabled && 'opacity-25'
|
||||||
|
} ` + className
|
||||||
|
}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
91
src/Components/Dropdown.jsx
Normal file
91
src/Components/Dropdown.jsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { useState, createContext, useContext, Fragment } from 'react';
|
||||||
|
import { Transition } from '@headlessui/react';
|
||||||
|
import {Link} from "react-router-dom";
|
||||||
|
|
||||||
|
const DropDownContext = createContext();
|
||||||
|
|
||||||
|
const Dropdown = ({ children }) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const toggleOpen = () => {
|
||||||
|
setOpen((previousState) => !previousState);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropDownContext.Provider value={{ open, setOpen, toggleOpen }}>
|
||||||
|
<div className="relative">{children}</div>
|
||||||
|
</DropDownContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Trigger = ({ children }) => {
|
||||||
|
const { open, setOpen, toggleOpen } = useContext(DropDownContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div onClick={toggleOpen}>{children}</div>
|
||||||
|
|
||||||
|
{open && <div className="fixed inset-0 z-40" onClick={() => setOpen(false)}></div>}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Content = ({ align = 'right', width = '48', contentClasses = 'py-1 bg-white', children }) => {
|
||||||
|
const { open, setOpen } = useContext(DropDownContext);
|
||||||
|
|
||||||
|
let alignmentClasses = 'origin-top';
|
||||||
|
|
||||||
|
if (align === 'left') {
|
||||||
|
alignmentClasses = 'origin-top-left left-0';
|
||||||
|
} else if (align === 'right') {
|
||||||
|
alignmentClasses = 'origin-top-right right-0';
|
||||||
|
}
|
||||||
|
|
||||||
|
let widthClasses = '';
|
||||||
|
|
||||||
|
if (width === '48') {
|
||||||
|
widthClasses = 'w-48';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Transition
|
||||||
|
as={Fragment}
|
||||||
|
show={open}
|
||||||
|
enter="transition ease-out duration-200"
|
||||||
|
enterFrom="opacity-0 scale-95"
|
||||||
|
enterTo="opacity-100 scale-100"
|
||||||
|
leave="transition ease-in duration-75"
|
||||||
|
leaveFrom="opacity-100 scale-100"
|
||||||
|
leaveTo="opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`absolute z-50 mt-2 rounded-md shadow-lg ${alignmentClasses} ${widthClasses}`}
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
<div className={`rounded-md ring-1 ring-black ring-opacity-5 ` + contentClasses}>{children}</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const DropdownLink = ({ className = '', children, ...props }) => {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
{...props}
|
||||||
|
className={
|
||||||
|
'block w-full px-4 py-2 text-left text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 transition duration-150 ease-in-out ' +
|
||||||
|
className
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Dropdown.Trigger = Trigger;
|
||||||
|
Dropdown.Content = Content;
|
||||||
|
Dropdown.Link = DropdownLink;
|
||||||
|
|
||||||
|
export default Dropdown;
|
||||||
22
src/Components/ImageInput.jsx
Normal file
22
src/Components/ImageInput.jsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
export default function ImageInput({ image, onChange, id, label, className, placeholder=null }) {
|
||||||
|
return (
|
||||||
|
<div className={"flex flex-col h-full" + " " + className}>
|
||||||
|
<label className="pr-3 pb-0.5">{ label }</label>
|
||||||
|
<div class="flex items-center justify-center w-full h-full">
|
||||||
|
<label for="dropzone-file" class="flex flex-col items-center justify-center w-full h-full border border-gray-300 rounded-lg cursor-pointer">
|
||||||
|
<div class="w-full h-full flex flex-col items-center justify-center p-6">
|
||||||
|
{ image ? image : (
|
||||||
|
<div className="flex justify-center items-center bg-gray-200 rounded-lg w-full h-52">
|
||||||
|
<svg class="w-8 h-8 mb-4 text-gray-500 dark:text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 16">
|
||||||
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 13h3a3 3 0 0 0 0-6h-.025A5.56 5.56 0 0 0 16 6.5 5.5 5.5 0 0 0 5.207 5.021C5.137 5.017 5.071 5 5 5a4 4 0 0 0 0 8h2.167M10 15V6m0 0L8 8m2-2 2 2"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<span className="pt-4">برای آپلود عکس کلیک کنید</span>
|
||||||
|
</div>
|
||||||
|
<input onChange={ onChange } id="dropzone-file" type="file" class="hidden" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
85
src/Components/ProductForm.jsx
Normal file
85
src/Components/ProductForm.jsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import Input from './../Components/common/Input'
|
||||||
|
import TextArea from './../Components/common/TextArea'
|
||||||
|
import Button from './../Components/Button'
|
||||||
|
import ImageInput from './../Components/ImageInput'
|
||||||
|
// import SelectInput from './../Components/SelectInput'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
|
||||||
|
export default function ProductForm({ onCancel, onSubmit, categories, preselectedCategory=null }) {
|
||||||
|
|
||||||
|
categories = categories.map((category) => ({value: category.id, label: category.title}));
|
||||||
|
|
||||||
|
let [title, setTitle] = useState();
|
||||||
|
let [description, setDescription] = useState();
|
||||||
|
let [price, setPrice] = useState(0);
|
||||||
|
let [inventory, setInventory] = useState(-1);
|
||||||
|
|
||||||
|
let [selectedImage, setSelectedImage] = useState(null);
|
||||||
|
|
||||||
|
let [createCategoryOpen, setCreateCategoryOpen] = useState(false);
|
||||||
|
let [selectedCategory, setSelectedCategory] = useState(preselectedCategory ?? (categories[0] ?? null));
|
||||||
|
|
||||||
|
function handleSubmit(){
|
||||||
|
let data = {
|
||||||
|
title: title,
|
||||||
|
description: description,
|
||||||
|
price: price,
|
||||||
|
inventory: inventory,
|
||||||
|
image: selectedImage,
|
||||||
|
}
|
||||||
|
onCancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCreateCategoryOpen(inputValue){
|
||||||
|
// setCategoryTitle(inputValue);
|
||||||
|
setCreateCategoryOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function handleModalCancel(){
|
||||||
|
setCreateCategoryOpen(false);
|
||||||
|
// setCreateTagOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCategoryModalSubmit(){
|
||||||
|
setCreateCategoryOpen(false);
|
||||||
|
// setData('category', {value: categoryTitle, label: categoryTitle});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col bg-white rounded-lg w-full max-w-4xl h-fit p-8 space-y-8">
|
||||||
|
<span className="pb-4 text-lg font-semibold">محصول جدید</span>
|
||||||
|
{/*<SelectInput value={ selectedCategory } setValue={e => setSelectedCategory(e) } options={ categories } onCreateOption={ handleCreateCategoryOpen } label="دسته بندی" className="w-full"/>*/}
|
||||||
|
<div className="flex flex-col lg:flex-row">
|
||||||
|
<div className="w-full pl-4 flex flex-col space-y-4">
|
||||||
|
<Input label="عنوان" className="w-full ml-6" onChange={(e) => {setTitle(e.target.value)}} />
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<Input label="قیمت" className="w-full ml-6" onChange={(e) => {setPrice(e.target.value)}} />
|
||||||
|
<Input label="موجودی" className="w-full" onChange={(e) => {setInventory(e.target.value)}} />
|
||||||
|
</div>
|
||||||
|
<TextArea label="توضیحات (اختیاری)" className="" onChange={(e) => {setDescription(e.target.value)}}/>
|
||||||
|
</div>
|
||||||
|
<div className="w-full pr-4 h-full">
|
||||||
|
<ImageInput id="image" image={selectedImage && (
|
||||||
|
<div className="flex flex-col justify-center w-full h-52 rounded-2xl">
|
||||||
|
<img
|
||||||
|
alt="not found"
|
||||||
|
className="h-52 object-scale-down"
|
||||||
|
src={URL.createObjectURL(selectedImage)}
|
||||||
|
/>
|
||||||
|
<button class="z-10" onClick={() => setSelectedImage(null)}>حذف</button>
|
||||||
|
</div>
|
||||||
|
)} onChange={(event) => {
|
||||||
|
setSelectedImage(event.target.files[0]);
|
||||||
|
}} label="تصویر" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<Button onClick={ onCancel } className="bg-white text-black border-gray-300 border-2 w-full transition hover:bg-zinc-100 ml-4 shadow-none">لغو</Button>
|
||||||
|
<Button onClick={ handleSubmit } className="w-full text-white bg-zinc-800 transition hover:bg-zinc-700">ایجاد</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
16
src/Components/ResponsiveNavLink.jsx
Normal file
16
src/Components/ResponsiveNavLink.jsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { Link } from '@inertiajs/react';
|
||||||
|
|
||||||
|
export default function ResponsiveNavLink({ active = false, className = '', children, ...props }) {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
{...props}
|
||||||
|
className={`w-full flex items-start pl-3 pr-4 py-2 border-l-4 ${
|
||||||
|
active
|
||||||
|
? 'border-indigo-400 text-indigo-700 bg-indigo-50 focus:text-indigo-800 focus:bg-indigo-100 focus:border-indigo-700'
|
||||||
|
: 'border-transparent text-gray-600 hover:text-gray-800 hover:bg-gray-50 hover:border-gray-300 focus:text-gray-800 focus:bg-gray-50 focus:border-gray-300'
|
||||||
|
} text-base font-medium focus:outline-none transition duration-150 ease-in-out ${className}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
16
src/Components/SecondaryButton.jsx
Normal file
16
src/Components/SecondaryButton.jsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
export default function SecondaryButton({ type = 'button', className = '', disabled, children, ...props }) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
{...props}
|
||||||
|
type={type}
|
||||||
|
className={
|
||||||
|
`inline-flex items-center px-4 py-2 bg-white border border-gray-300 rounded-md font-semibold text-gray-700 uppercase tracking-widest shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-25 transition ease-in-out duration-150 ${
|
||||||
|
disabled && 'opacity-25'
|
||||||
|
} ` + className
|
||||||
|
}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
20
src/Components/articles/ArticleCard.jsx
Normal file
20
src/Components/articles/ArticleCard.jsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
|
||||||
|
import categoryIcon from "./../../assets/images/CategoryTagIcon.svg"
|
||||||
|
import viewIcon from "./../../assets/images/EyeIcon.svg"
|
||||||
|
import {EnglishToPersian} from "../../helpers/EnglishToPersian";
|
||||||
|
import {Link} from "react-router-dom";
|
||||||
|
export default function ArticleCard({article, asd, className}) {
|
||||||
|
return (
|
||||||
|
<Link to={`edit/${article.id}`} className={'rounded-xl border-1 flex flex-col justify-between bg-1 ' + className}>
|
||||||
|
<img className={'rounded-top-3 product-card-image'} src={article.image_thumbnail} alt={article.title}/>
|
||||||
|
<div className={'mx-3 flex flex-col pt-3 w-[20vw]'}> <span className={'truncate font-[Vazirmatn] font-medium text-lg'}>{article.title}</span>
|
||||||
|
<div className={'flex flex-col md:flex-row items-center justify-between my-5'}>
|
||||||
|
<div className={'px-3 bg-[#E6E6E6] rounded-xl flex item-center flex-row justify-between'}>
|
||||||
|
<img src={viewIcon} alt={''}/>
|
||||||
|
<span className={'mr-1 text-[#333333] font-[Vazirmatn]'}>{EnglishToPersian(article.viewCount ? article.viewCount.toString() : '0')}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
13
src/Components/articles/ArticleGrid.jsx
Normal file
13
src/Components/articles/ArticleGrid.jsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import ArticleCard from "./ArticleCard";
|
||||||
|
|
||||||
|
export default function ArticlesGrid({articles}) {
|
||||||
|
return (
|
||||||
|
<div className={'flex grid grid-cols-3 gap-7'}>
|
||||||
|
{
|
||||||
|
articles.map((article, index) => (
|
||||||
|
<ArticleCard asd={index%2} key={index} article={article} />
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
35
src/Components/categories/CategoryCard.jsx
Normal file
35
src/Components/categories/CategoryCard.jsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import CategoryItemCard from "./CategoryItemCard"
|
||||||
|
import ArrowRight from "../../assets/images/ArrowRight.svg"
|
||||||
|
import {Link} from "react-router-dom";
|
||||||
|
import Button from "../Button"
|
||||||
|
|
||||||
|
export default function CategoryCard({category, index, onDelete}) {
|
||||||
|
const previewProducts = category.products.length <= 4 ? category.products : category.products.slice(0, 4)
|
||||||
|
return (
|
||||||
|
<div key={index} className="flex flex-col bg-1 col-span-6 lg:col-span-3 xl:col-span-2 rounded-lg p-4 justify-between">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-xl mb-2 font-[Vazirmatn]">{category.title}</span>
|
||||||
|
<span className="my-2 font-[Dana] text-[0.9rem]"> {category.products.length} محصول</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row w-1/2 justify-start">
|
||||||
|
{
|
||||||
|
previewProducts.map((product, index) => (
|
||||||
|
<CategoryItemCard index={index} item={product}/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col lg:grid lg:grid-cols-3 pt-6 items-center">
|
||||||
|
<span onClick={() => onDelete(category.id)} className="text-4 cursor-pointer hover:opacity-80 underline w-full h-fit text-center items-center justify-center">
|
||||||
|
حذف
|
||||||
|
</span>
|
||||||
|
<Link className="w-full col-span-2" to={`${category.category ? category.id : `/panel/categories/${category.id}`}`}>
|
||||||
|
<Button className="w-full font-[Vazirmatn] bg-gradient-to-r from-4 to-purp text-white transition duration-300 hover:opacity-80">
|
||||||
|
<span className="">ویرایش</span>
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
6
src/Components/categories/CategoryItemCard.jsx
Normal file
6
src/Components/categories/CategoryItemCard.jsx
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default function CategoryCard({ item, index }){
|
||||||
|
return (
|
||||||
|
<div key={index} style={{backgroundImage : `url(${item.images[0]?.path})`}} className={"mx-1 shadow bg-cover bg-center relative h-full w-full rounded-lg transition duration-600"}>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
18
src/Components/categories/CreateCategoryModal.jsx
Normal file
18
src/Components/categories/CreateCategoryModal.jsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import CreateCategoryStepper from "./CreateCategoryStepper";
|
||||||
|
import {Modal} from "react-bootstrap";
|
||||||
|
|
||||||
|
export default function CreateCategoryModal({showCreateModal, handleCloseCreateModal, callback}) {
|
||||||
|
return (
|
||||||
|
<Modal size={'lg'} centered show={showCreateModal} onHide={handleCloseCreateModal}>
|
||||||
|
<Modal.Header closeButton={true} className={'flex flex-row-reverse'}>
|
||||||
|
<Modal.Title className={'font-[Vazirmatn] text-[1.5rem] font-bold'}>افزودن دسته بندی</Modal.Title>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body style={{direction: 'rtl'}}>
|
||||||
|
<CreateCategoryStepper onCancel={() => {
|
||||||
|
handleCloseCreateModal()
|
||||||
|
callback()
|
||||||
|
}}/>
|
||||||
|
</Modal.Body>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
35
src/Components/categories/CreateCategoryStep1.jsx
Normal file
35
src/Components/categories/CreateCategoryStep1.jsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import InputLabel from "../common/InputLabel";
|
||||||
|
import TextInput from "../common/TextInput";
|
||||||
|
import TextArea from "../common/TextArea";
|
||||||
|
|
||||||
|
export default function CreateCategoryStep1({onChange}) {
|
||||||
|
return (
|
||||||
|
<form className={'pt-10 min-h-[50vh] mx-5'}>
|
||||||
|
<div className={'flex flex-col'}>
|
||||||
|
<InputLabel className={"justify-start font-[Vazirmatn] font-normal mb-2 mr-2 "} htmlFor="title" value="نام" />
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
id="title"
|
||||||
|
type="text"
|
||||||
|
name="title"
|
||||||
|
className="py-3 font-[Vazirmatn] flex text-right w-full border-gray-500 placeholder:text-sm placeholder-gray-400 "
|
||||||
|
placeholder={'مثال: محصولات ورزشی'}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col mt-5">
|
||||||
|
<InputLabel className={"justify-start font-normal font-[Vazirmatn] mb-2 mr-2 "} htmlFor="description" value="توضیحات" />
|
||||||
|
|
||||||
|
<TextArea
|
||||||
|
id="description"
|
||||||
|
type="text"
|
||||||
|
rows={3}
|
||||||
|
name="description"
|
||||||
|
placeholder={'توضیح درباره این دسته بندی'}
|
||||||
|
className="py-3 font-[Vazirmatn] flex text-right w-full placeholder:text-sm placeholder-gray-400 "
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
82
src/Components/categories/CreateCategoryStep2.jsx
Normal file
82
src/Components/categories/CreateCategoryStep2.jsx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import InputLabel from "../common/InputLabel";
|
||||||
|
import AddIcon from './../../assets/images/PlusGreen.svg'
|
||||||
|
import FloatingItems from "../common/FloatingItems";
|
||||||
|
import React, {useEffect, useState} from "react";
|
||||||
|
import AddProperty from "../properties/AddProperty";
|
||||||
|
import PublicApi from "../../api/PublicAPi";
|
||||||
|
import SelectProperty from "../properties/SelectProperty";
|
||||||
|
import AdminApi from "../../api/AdminApi";
|
||||||
|
import {userToken} from "../../api/TokenApi";
|
||||||
|
|
||||||
|
export default function CreateCategoryStep2({categoryId}) {
|
||||||
|
|
||||||
|
const [selectedProperties, setSelectedProperties] = useState([])
|
||||||
|
const [properties, setProperties] = useState([])
|
||||||
|
|
||||||
|
const attachProperty = async (propertyId, categoryId) => {
|
||||||
|
await AdminApi.post(`properties/${propertyId}/attachCategory/${categoryId}`, {})
|
||||||
|
}
|
||||||
|
const detachProperty = async (propertyId, categoryId) => {
|
||||||
|
await AdminApi.post(`properties/${propertyId}/detachCategory/${categoryId}`, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getProperties = async () => {
|
||||||
|
const fetchedProperties = await PublicApi.get(`properties/index/${await userToken()}`)
|
||||||
|
setProperties(fetchedProperties)
|
||||||
|
}
|
||||||
|
const getCategoryProperties = async () => {
|
||||||
|
const fetchedCategory = await PublicApi.get(`categories/show/${categoryId}`)
|
||||||
|
if (fetchedCategory) {
|
||||||
|
setSelectedProperties(fetchedCategory.properties)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
useEffect(() => {
|
||||||
|
getProperties()
|
||||||
|
getCategoryProperties()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
|
||||||
|
const handleDeleteProperty = (title) => {
|
||||||
|
const deletedProperty = selectedProperties.find(prop => prop.title === title)
|
||||||
|
detachProperty(deletedProperty.id, categoryId).then(() =>{
|
||||||
|
getCategoryProperties()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const handleChangePropertyStatus = (property) => {
|
||||||
|
if (selectedProperties.find(prop => prop.id === property.id)) {
|
||||||
|
detachProperty(property.id, categoryId).then(() => {
|
||||||
|
getCategoryProperties()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
attachProperty(property.id, categoryId).then(() => {
|
||||||
|
getCategoryProperties()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={'flex flex-col pt-10 min-h-[50vh] mx-5'}>
|
||||||
|
<SelectProperty
|
||||||
|
properties={properties}
|
||||||
|
selectedProperties={selectedProperties}
|
||||||
|
handleChangePropertyStatus={handleChangePropertyStatus}
|
||||||
|
/>
|
||||||
|
<FloatingItems
|
||||||
|
containerClassName={'flex flex-wrap items-center justify-start mt-5'}
|
||||||
|
itemClassName={'my-2'}
|
||||||
|
onDelete={handleDeleteProperty}
|
||||||
|
itemsList={selectedProperties.map(prop => prop.title)}
|
||||||
|
/>
|
||||||
|
<InputLabel className={"justify-start font-[Vazirmatn] font-normal mt-5 mb-2 mr-2"}>
|
||||||
|
<div className={'flex flex-row items-center justify-center'}>
|
||||||
|
ویژگی جدید
|
||||||
|
<img alt={''} src={AddIcon}/>
|
||||||
|
</div>
|
||||||
|
</InputLabel>
|
||||||
|
<AddProperty categoryId={categoryId} callback={() => {
|
||||||
|
getProperties().then()
|
||||||
|
getCategoryProperties().then()
|
||||||
|
}}/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
149
src/Components/categories/CreateCategoryStepper.jsx
Normal file
149
src/Components/categories/CreateCategoryStepper.jsx
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import React, {useState} from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Stepper from '@mui/material/Stepper';
|
||||||
|
import Step from '@mui/material/Step';
|
||||||
|
import StepButton from '@mui/material/StepButton';
|
||||||
|
import step1 from '../../assets/images/step1.svg'
|
||||||
|
import step2 from '../../assets/images/step2.svg'
|
||||||
|
import step1complete from '../../assets/images/step1black.svg'
|
||||||
|
import step2complete from '../../assets/images/step2black.svg'
|
||||||
|
import CreateCategoryStep1 from "./CreateCategoryStep1";
|
||||||
|
import CreateCategoryStep2 from "./CreateCategoryStep2";
|
||||||
|
import LoadingTemplate from "../common/LoadingTemplate";
|
||||||
|
import AdminApi from "../../api/AdminApi";
|
||||||
|
import InputError from "../common/InputError";
|
||||||
|
|
||||||
|
const steps = ['مشخصات دسته بندی', 'ویژگی دسته بندی'];
|
||||||
|
|
||||||
|
export default function CreateCategoryStepper({onCancel}) {
|
||||||
|
const [data, setData] = useState({
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
});
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [categoryId, setCategoryId] = useState(null);
|
||||||
|
const [errorInPostCategory, setErrorInPostCategory] = useState(false);
|
||||||
|
const [activeStep, setActiveStep] = useState(0);
|
||||||
|
const [completed, setCompleted] = useState({});
|
||||||
|
const [icons, _] = useState({
|
||||||
|
active: [
|
||||||
|
<img key={1} alt={'step1'} src={step1}/>,
|
||||||
|
<img key={2} alt={'step2'} src={step2}/>,
|
||||||
|
],
|
||||||
|
inactive: [
|
||||||
|
<img key={1} className={'opacity-50'} alt={'step1'} src={step1}/>,
|
||||||
|
<img key={2} className={'opacity-50'} alt={'step2'} src={step2}/>,
|
||||||
|
],
|
||||||
|
completed: [
|
||||||
|
<img key={1} alt={'step1'} src={step1complete}/>,
|
||||||
|
<img key={2} alt={'step2'} src={step2complete}/>,
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleChange = (e) => {
|
||||||
|
setData({
|
||||||
|
...data,
|
||||||
|
[e.target.name]: e.target.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const totalSteps = () => {
|
||||||
|
return steps.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
const completedSteps = () => {
|
||||||
|
return Object.keys(completed).length;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isLastStep = () => {
|
||||||
|
return activeStep === totalSteps() - 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const allStepsCompleted = () => {
|
||||||
|
return completedSteps() === totalSteps();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNext = () => {
|
||||||
|
const newActiveStep =
|
||||||
|
isLastStep() && !allStepsCompleted()
|
||||||
|
? // It's the last step, but not all steps have been completed,
|
||||||
|
// find the first step that has been completed
|
||||||
|
steps.findIndex((step, i) => !(i in completed))
|
||||||
|
: activeStep + 1;
|
||||||
|
setActiveStep(newActiveStep);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleComplete = async () => {
|
||||||
|
if (activeStep === totalSteps() - 1) {
|
||||||
|
onCancel()
|
||||||
|
} else {
|
||||||
|
const newCompleted = completed;
|
||||||
|
newCompleted[activeStep] = true;
|
||||||
|
setCompleted(newCompleted);
|
||||||
|
setLoading(true)
|
||||||
|
const categoryPostRes = await AdminApi.post('categories/store', data)
|
||||||
|
if (categoryPostRes === undefined) {
|
||||||
|
setErrorInPostCategory(true)
|
||||||
|
} else {
|
||||||
|
setCategoryId(categoryPostRes.data.id)
|
||||||
|
setErrorInPostCategory(false)
|
||||||
|
handleNext();
|
||||||
|
}
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<div className={'w-full flex justify-center items-center'}>
|
||||||
|
<Stepper nonLinear activeStep={activeStep} className={'w-50'}>
|
||||||
|
{steps.map((label, index) => (
|
||||||
|
<Step key={index} completed={completed[index]}>
|
||||||
|
<StepButton
|
||||||
|
icon={
|
||||||
|
activeStep === index ? icons.active[index]
|
||||||
|
: completed[index] ? icons.completed[index]
|
||||||
|
: icons.inactive[index]}>
|
||||||
|
<p className={'font-[Vazirmatn]'}> {label}</p>
|
||||||
|
</StepButton>
|
||||||
|
</Step>
|
||||||
|
))}
|
||||||
|
</Stepper>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<React.Fragment>
|
||||||
|
{
|
||||||
|
activeStep === 0 ?
|
||||||
|
<CreateCategoryStep1 onChange={handleChange}/>
|
||||||
|
: activeStep === 1 ?
|
||||||
|
<CreateCategoryStep2 onChange={handleChange} categoryId={categoryId}/>
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
{
|
||||||
|
errorInPostCategory ?
|
||||||
|
<InputError message={'خطا! لطفا فیلد هارا به درستی پر کنید و دوباره تلاش کنید. در غیر این صورت با پشتیبانی تماس بفرمایید'} className="mt-3 mx-3 font-[dana]" />
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
<Box sx={{display: 'flex', flexDirection: 'row', pt: 2}}>
|
||||||
|
<button
|
||||||
|
disabled={loading}
|
||||||
|
className={'font-[Vazirmatn] bg-white text-black border-1 border-black rounded-lg h-full mx-1 ml-4 py-2 px-6 text-center w-full'}
|
||||||
|
onClick={onCancel}
|
||||||
|
>
|
||||||
|
لغو
|
||||||
|
</button>
|
||||||
|
<LoadingTemplate loading={loading} className={'ml-5'} marginLeft={'-22px'}>
|
||||||
|
<button
|
||||||
|
disabled={loading}
|
||||||
|
className={'font-[Vazirmatn] bg-zinc-900 text-white rounded-lg h-full mx-1 mr-4 py-2 px-6 text-center w-full hover:bg-zinc-700'}
|
||||||
|
onClick={handleComplete}>
|
||||||
|
{completedSteps() === totalSteps() - 1
|
||||||
|
? 'ثبت'
|
||||||
|
: 'بعدی'}
|
||||||
|
</button>
|
||||||
|
</LoadingTemplate>
|
||||||
|
</Box>
|
||||||
|
</React.Fragment>
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
src/Components/common/Checkbox.jsx
Normal file
12
src/Components/common/Checkbox.jsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export default function Checkbox({ className = '', ...props }) {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
{...props}
|
||||||
|
type="checkbox"
|
||||||
|
className={
|
||||||
|
'rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-white ' +
|
||||||
|
className
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
src/Components/common/CreateButton.jsx
Normal file
11
src/Components/common/CreateButton.jsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export default function CreateButton({ onClick, title }){
|
||||||
|
return (
|
||||||
|
|
||||||
|
<button onClick={ onClick } className="bg-gradient-to-r from-4 to-purp hover:opacity-80 transition duration-600 font-[Vazirmatn] text-white rounded-lg w-fit h-full py-2 px-6 flex items-center justify-center">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="w-6 h-6">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6v12m6-6H6" />
|
||||||
|
</svg>
|
||||||
|
<span className="pr-4 leading-none">{ title }</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
50
src/Components/common/FilterBy.jsx
Normal file
50
src/Components/common/FilterBy.jsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import React, {useState} from 'react';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Menu from '@mui/material/Menu';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
import FilterByIcon from '../../assets/images/FilterByIcon.svg'
|
||||||
|
export default function FilterBy({onAddFilter}) {
|
||||||
|
const [anchorEl, setAnchorEl] = useState(null);
|
||||||
|
const open = Boolean(anchorEl);
|
||||||
|
const handleClick = (event) => {
|
||||||
|
setAnchorEl(event.currentTarget);
|
||||||
|
};
|
||||||
|
const handleClose = () => {
|
||||||
|
setAnchorEl(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="hidden">
|
||||||
|
<Button
|
||||||
|
variant={'outlined'}
|
||||||
|
sx={{fontWeight: '300', letterSpacing: '-0.025em', height: '2.6rem', borderRadius: '8px', fontFamily: 'Vazirmatn', borderColor: '#1e1e1e', color: '#1e1e1e', ':hover':{backgroundColor: 'white', borderColor: '#1e1e1e'}}}
|
||||||
|
aria-controls={open ? 'filterMenu' : undefined}
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-expanded={open ? 'true' : undefined}
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<img alt={''} src={FilterByIcon} className={'ml-3'}/>
|
||||||
|
فیلتر
|
||||||
|
</Button>
|
||||||
|
<Menu
|
||||||
|
id="filterMenu"
|
||||||
|
aria-labelledby="filterMenu"
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
open={open}
|
||||||
|
onClose={handleClose}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'bottom',
|
||||||
|
horizontal: 'left',
|
||||||
|
}}
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItem sx={{fontFamily: 'Vazirmatn', color: '#1e1e1e', display: 'flex', justifyContent: 'end'}} onClick={() => onAddFilter('پرفروش ترین')}>پرفروش ترین</MenuItem>
|
||||||
|
<MenuItem sx={{fontFamily: 'Vazirmatn', color: '#1e1e1e', display: 'flex', justifyContent: 'end'}} onClick={() => onAddFilter('پربازدید ترین')}>پربازدید ترین</MenuItem>
|
||||||
|
<MenuItem sx={{fontFamily: 'Vazirmatn', color: '#1e1e1e', display: 'flex', justifyContent: 'end'}} onClick={() => onAddFilter('پرمحصول ترین')}>پرمحصول ترین</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
19
src/Components/common/FloatingItems.jsx
Normal file
19
src/Components/common/FloatingItems.jsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import CloseIcon from "../../assets/images/CloseIcon.svg";
|
||||||
|
|
||||||
|
export default function FloatingItems({itemsList, containerClassName, itemClassName, onDelete}) {
|
||||||
|
return (
|
||||||
|
<div className={'' + containerClassName}>
|
||||||
|
{
|
||||||
|
itemsList.map((name, index) => (
|
||||||
|
<div key={index}
|
||||||
|
className={'h-[2.6rem] flex flex-row items-center bg-gray-100 font-[Vazirmatn] text-sm rounded-lg p-2 mx-3 ' + itemClassName}>
|
||||||
|
<img alt={'close'} className={'ml-1'} src={CloseIcon} onClick={() => onDelete(name)}/>
|
||||||
|
<div className={'mb-1'}>
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
17
src/Components/common/Header.jsx
Normal file
17
src/Components/common/Header.jsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import bgTexture from '../../assets/images/DashboardHeaderTexture.png'
|
||||||
|
import avatar from '../../assets/images/avatar.png'
|
||||||
|
import bellIcon from '../../assets/images/DashboardHeaderBell.svg'
|
||||||
|
|
||||||
|
|
||||||
|
export default function Header({ sidebarOpen ,setSidebarOpen }){
|
||||||
|
return (
|
||||||
|
<div className={"sticky flex z-40 top-0 w-full bg-1 p-4 px-12 justify-between " + (sidebarOpen ? " pr-64" : "")} >
|
||||||
|
<button onClick={() => setSidebarOpen(!sidebarOpen)}>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<img src={bellIcon} className={'w-6 h-6 opacity-80'}/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
9
src/Components/common/Input.jsx
Normal file
9
src/Components/common/Input.jsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export default function Input({ error, onChange, id, label, className, placeholder=null, value }) {
|
||||||
|
return (
|
||||||
|
<div className={"flex flex-col" + " " + className}>
|
||||||
|
<label className="pr-3 pb-0.5">{ label }</label>
|
||||||
|
<input onChange={ onChange } id={id} type="text" className="w-full border-gray-300 rounded-lg" placeholder={placeholder} value={value}/>
|
||||||
|
{error && <span className="text-sm text-gray-600 pt-0.5 pr-3">{error}</span>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
7
src/Components/common/InputError.jsx
Normal file
7
src/Components/common/InputError.jsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export default function InputError({ message, className = '', ...props }) {
|
||||||
|
return message ? (
|
||||||
|
<p {...props} className={'text-sm text-right text-gray-600 ' + className}>
|
||||||
|
{message}
|
||||||
|
</p>
|
||||||
|
) : null;
|
||||||
|
}
|
||||||
7
src/Components/common/InputLabel.jsx
Normal file
7
src/Components/common/InputLabel.jsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export default function InputLabel({ value, className = '', children, ...props }) {
|
||||||
|
return (
|
||||||
|
<label {...props} className={`flex font-medium text-sm ` + className}>
|
||||||
|
{value ? value : children}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
22
src/Components/common/LoadingTemplate.jsx
Normal file
22
src/Components/common/LoadingTemplate.jsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import {Box, CircularProgress} from "@mui/material";
|
||||||
|
|
||||||
|
export default function LoadingTemplate({loading, marginLeft, children, className}) {
|
||||||
|
return (
|
||||||
|
<Box className={`w-full ` + className} sx={{ position: 'relative' }}>
|
||||||
|
{children}
|
||||||
|
{loading && (
|
||||||
|
<CircularProgress
|
||||||
|
size={24}
|
||||||
|
sx={{
|
||||||
|
color: "#fff",
|
||||||
|
position: 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
marginTop: '-12px',
|
||||||
|
marginLeft: marginLeft,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
31
src/Components/common/OptionsMenu.jsx
Normal file
31
src/Components/common/OptionsMenu.jsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import {FormControl, MenuItem, Select} from "@mui/material";
|
||||||
|
|
||||||
|
export default function OptionsMenu({options, onChangeOrder, orderedBy}) {
|
||||||
|
return (
|
||||||
|
<div className="w-full mx-3">
|
||||||
|
<FormControl className={"w-40 shadow rounded-lg hover:ring-red-200"}>
|
||||||
|
<Select
|
||||||
|
sx={{backgroundColor: 'white', height: '2.6rem', borderRadius: '8px', fontFamily: 'Vazirmatn', color: '#1e1e1e'}}
|
||||||
|
value={orderedBy}
|
||||||
|
defaultValue={'default'}
|
||||||
|
onChange={(e) => onChangeOrder(e)}
|
||||||
|
className="text-gray-500"
|
||||||
|
>
|
||||||
|
<option className={''} value="default" disabled hidden>مرتب سازی</option>
|
||||||
|
{
|
||||||
|
options.map((option, index) => (
|
||||||
|
<MenuItem
|
||||||
|
divider={true}
|
||||||
|
sx={{fontFamily: 'Vazirmatn', color: '#1e1e1e', display: 'flex', justifyContent: 'end'}}
|
||||||
|
key={index}
|
||||||
|
value={option.value}
|
||||||
|
>
|
||||||
|
{option.name}
|
||||||
|
</MenuItem>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
152
src/Components/common/Pagination.jsx
Normal file
152
src/Components/common/Pagination.jsx
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import {Fragment, useEffect, useState} from "react";
|
||||||
|
|
||||||
|
export default function Pagination({totalPages, onNewPage}) {
|
||||||
|
const LEFT_PAGE = 'LEFT';
|
||||||
|
const RIGHT_PAGE = 'RIGHT';
|
||||||
|
|
||||||
|
const range = (from, to, step = 1) => {
|
||||||
|
let i = from;
|
||||||
|
const range = [];
|
||||||
|
|
||||||
|
while (i <= to) {
|
||||||
|
range.push(i);
|
||||||
|
i += step;
|
||||||
|
}
|
||||||
|
|
||||||
|
return range;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [currentPage, setCurrentPage] = useState(1)
|
||||||
|
useEffect(() => {
|
||||||
|
gotoPage(1);
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const gotoPage = page => {
|
||||||
|
setCurrentPage(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClick = page => evt => {
|
||||||
|
evt.preventDefault();
|
||||||
|
gotoPage(page);
|
||||||
|
onNewPage(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMoveLeft = evt => {
|
||||||
|
evt.preventDefault();
|
||||||
|
gotoPage(currentPage - 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMoveRight = evt => {
|
||||||
|
evt.preventDefault();
|
||||||
|
gotoPage(currentPage + 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Let's say we have 10 pages and we set pageNeighbours to 2
|
||||||
|
* Given that the current page is 6
|
||||||
|
* The pagination control will look like the following:
|
||||||
|
*
|
||||||
|
* (1) < {4 5} [6] {7 8} > (10)
|
||||||
|
*
|
||||||
|
* (x) => terminal pages: first and last page(always visible)
|
||||||
|
* [x] => represents current page
|
||||||
|
* {...x} => represents page neighbours
|
||||||
|
*/
|
||||||
|
const fetchPageNumbers = () => {
|
||||||
|
|
||||||
|
const pageNeighbours = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* totalNumbers: the total page numbers to show on the control
|
||||||
|
* totalBlocks: totalNumbers + 2 to cover for the left(<) and right(>) controls
|
||||||
|
*/
|
||||||
|
const totalNumbers = 7;
|
||||||
|
const totalBlocks = totalNumbers + 2;
|
||||||
|
|
||||||
|
if (totalPages > totalBlocks) {
|
||||||
|
|
||||||
|
const startPage = Math.max(2, currentPage - pageNeighbours);
|
||||||
|
const endPage = Math.min(totalPages - 1, currentPage + pageNeighbours);
|
||||||
|
|
||||||
|
let pages = range(startPage, endPage);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* hasLeftSpill: has hidden pages to the left
|
||||||
|
* hasRightSpill: has hidden pages to the right
|
||||||
|
* spillOffset: number of hidden pages either to the left or to the right
|
||||||
|
*/
|
||||||
|
const hasLeftSpill = startPage > 2;
|
||||||
|
const hasRightSpill = (totalPages - endPage) > 1;
|
||||||
|
const spillOffset = totalNumbers - (pages.length + 1);
|
||||||
|
|
||||||
|
switch (true) {
|
||||||
|
// handle: (1) < {5 6} [7] {8 9} (10)
|
||||||
|
case (hasLeftSpill && !hasRightSpill): {
|
||||||
|
const extraPages = range(startPage - spillOffset, startPage - 1);
|
||||||
|
pages = [LEFT_PAGE, ...extraPages, ...pages];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle: (1) {2 3} [4] {5 6} > (10)
|
||||||
|
case (!hasLeftSpill && hasRightSpill): {
|
||||||
|
const extraPages = range(endPage + 1, endPage + spillOffset);
|
||||||
|
pages = [...pages, ...extraPages, RIGHT_PAGE];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle: (1) < {4 5} [6] {7 8} > (10)
|
||||||
|
case (hasLeftSpill && hasRightSpill):
|
||||||
|
default: {
|
||||||
|
pages = [LEFT_PAGE, ...pages, RIGHT_PAGE];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [1, ...pages, totalPages];
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return range(1, totalPages);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalPages === 1) return null;
|
||||||
|
const pages = fetchPageNumbers();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<nav aria-label="Pagination">
|
||||||
|
<ul className="pagination">
|
||||||
|
{ pages.map((page, index) => {
|
||||||
|
|
||||||
|
if (page === LEFT_PAGE) return (
|
||||||
|
<li key={index} className="page-item">
|
||||||
|
<a className="page-link" href="#" aria-label="Previous" onClick={handleMoveLeft}>
|
||||||
|
<span aria-hidden="true">«</span>
|
||||||
|
<span className="sr-only">قبلی</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (page === RIGHT_PAGE) return (
|
||||||
|
<li key={index} className="page-item">
|
||||||
|
<a className="page-link" href="#" aria-label="Next" onClick={handleMoveRight}>
|
||||||
|
<span className="sr-only">بعدی</span>
|
||||||
|
<span aria-hidden="true">»</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li key={index} className={`page-item${ currentPage === page ? ' active' : ''}`}>
|
||||||
|
<a className="page-link" href="#" onClick={ handleClick(page) }>{ page }</a>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
|
||||||
|
}) }
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
15
src/Components/common/PrimaryButton.jsx
Normal file
15
src/Components/common/PrimaryButton.jsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export default function PrimaryButton({ className = '', disabled, children, ...props }) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
{...props}
|
||||||
|
className={
|
||||||
|
`inline-flex items-center px-4 py-2 bg-black border border-transparent rounded-xl text-white tracking-widest hover:bg-gray-700 transition ease-in-out duration-150 ${
|
||||||
|
disabled && 'opacity-25'
|
||||||
|
} ` + className
|
||||||
|
}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
src/Components/common/ProfitState.jsx
Normal file
11
src/Components/common/ProfitState.jsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import {EnglishToPersian} from "../../helpers/EnglishToPersian";
|
||||||
|
import downProfit from './../../assets/images/downProfit.svg'
|
||||||
|
import upProfit from './../../assets/images/upProfit.svg'
|
||||||
|
import zeroProfit from './../../assets/images/zeroProfit.svg'
|
||||||
|
export default function ({number}) {
|
||||||
|
return (
|
||||||
|
<p className={`text-sm font-[Vazirmatn] flex ${number > 0 ? 'text-3' : number === 0 ? 'text-gray-700' : 'text-[#fc0303]'}`}>
|
||||||
|
({number > 0 ? <img width={'30%'} src={upProfit}/> : number < 0 ? <img width={'30%'} src={downProfit}/> : <img width={'20%'} src={zeroProfit}/>}{EnglishToPersian(Math.abs(number).toString())}٪)
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
14
src/Components/common/Searchbar.jsx
Normal file
14
src/Components/common/Searchbar.jsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import searchIcon from '../../assets/images/SearchIcon.svg'
|
||||||
|
|
||||||
|
export default function Searchbar({ onChange }) {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative h-fit">
|
||||||
|
<div className="absolute inset-y-0 left-2 flex items-center pointer-events-none">
|
||||||
|
<img alt={''} src={searchIcon}/>
|
||||||
|
</div>
|
||||||
|
<input onChange={onChange} id="default-search" className="block border-none shadow transition duration-600 focus:shadow-red-300 focus:ring-red-300 w-80 p-2 pr-5 font-[Vazirmatn] pl-2 rounded-lg placeholder:text-gray-500" placeholder="جستجو" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
57
src/Components/common/Sidebar.jsx
Normal file
57
src/Components/common/Sidebar.jsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import SidebarItem from './SidebarItem'
|
||||||
|
import dashboardIcon from '../../assets/images/SideItemDashboardIcon.svg'
|
||||||
|
import usersIcon from '../../assets/images/SideItemUsersIcon.svg'
|
||||||
|
import productsIcon from '../../assets/images/SideItemProductsIcon.svg'
|
||||||
|
import categoriesIcon from '../../assets/images/SideItemCategoriesIcon.svg'
|
||||||
|
import ordersIcon from '../../assets/images/SideItemOrdersIcon.svg'
|
||||||
|
import ArticleIcon from '../../assets/images/ArticleIcon.svg'
|
||||||
|
import ContentsIcon from '../../assets/images/ContentsIcon.svg'
|
||||||
|
import topLogo from '../../assets/images/SidebarTopLogo.png'
|
||||||
|
import {Divider} from "@mui/material";
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
export default function Sidebar({ sidebarOpen, setSidebarOpen }) {
|
||||||
|
const [active, setActive] = useState({
|
||||||
|
dashboard: true,
|
||||||
|
users: false,
|
||||||
|
products: false,
|
||||||
|
categories: false,
|
||||||
|
orders: false,
|
||||||
|
article: false,
|
||||||
|
contents: false
|
||||||
|
})
|
||||||
|
const [activeItem, setActiveItem] = useState('dashboard')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleActiveSideItem(window.location.pathname.slice(7).replace('/', ''))
|
||||||
|
}, [])
|
||||||
|
const handleActiveSideItem = (name) => {
|
||||||
|
if (activeItem !== name) {
|
||||||
|
setActive({
|
||||||
|
...active,
|
||||||
|
[activeItem]: false,
|
||||||
|
[name]: true
|
||||||
|
})
|
||||||
|
setActiveItem(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={(sidebarOpen ? "fixed lg:relative right-0 z-50" : "hidden ") + " flex flex-col w-60 h-full bg-1"}>
|
||||||
|
<div className="p-6">
|
||||||
|
<div className={'flex items-center mb-3'}>
|
||||||
|
<img src={topLogo} width={'20%'}/>
|
||||||
|
<span className={'mr-2 text-2xl font-[dana] text-gray-700 font-bold'}>نیلی پنل</span>
|
||||||
|
</div>
|
||||||
|
<span className={'font-[dana] text-gray-800 text-xl'}>خوش آمدید</span>
|
||||||
|
</div>
|
||||||
|
<Divider variant="middle" />
|
||||||
|
<div className={'pl-6'}>
|
||||||
|
<SidebarItem onClick={() => handleActiveSideItem('categories')} activeState={active.categories} icon={categoriesIcon} title="دسته بندی ها" href="/panel/categories" />
|
||||||
|
<SidebarItem onClick={() => handleActiveSideItem('products')} activeState={active.products} icon={productsIcon} title="محصولات" href="/panel/products" />
|
||||||
|
<SidebarItem onClick={() => handleActiveSideItem('article')} activeState={active.article} icon={ArticleIcon} title="مقالات" href="/panel/articles" />
|
||||||
|
<SidebarItem onClick={() => handleActiveSideItem('contents')} activeState={active.contents} icon={ContentsIcon} title="محتوا" href="/panel/contents" />
|
||||||
|
<SidebarItem onClick={() => handleActiveSideItem('orders')} activeState={active.orders} icon={ordersIcon} title="سفارشات" href="/panel/orders" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
18
src/Components/common/SidebarItem.jsx
Normal file
18
src/Components/common/SidebarItem.jsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import {Link} from "react-router-dom";
|
||||||
|
|
||||||
|
export default function SidebarItem({icon, title, href, onClick, activeState}) {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
onClick={onClick}
|
||||||
|
className={`mr-3 flex my-2 rounded-lg transition duration-600 ${activeState ? 'bg-gradient-to-r from-transparent to-gray-300 border-r-[#2F2F2F] pr-2' : 'hover:pr-[0.05rem] hover:bg-[#CDCDCD]'}`}
|
||||||
|
to={href}
|
||||||
|
>
|
||||||
|
<img alt={''} className={`mr-3 ${activeState ? '' : 'opacity-40'}`} src={icon}/>
|
||||||
|
<div
|
||||||
|
className={`w-full flex h-full px-2 py-2 font-[dana] ${activeState ? 'text-dark' : 'text-gray-500'}`}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
5
src/Components/common/TextArea.jsx
Normal file
5
src/Components/common/TextArea.jsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export default function TextArea({ onChange, id, className, index=null, placeholder=null , ...props}) {
|
||||||
|
return (
|
||||||
|
<textarea key={index} {...props} onChange={onChange} id={id} maxLength={2047} placeholder={placeholder} className={`focus:border-indigo-500 focus:ring-indigo-500 w-full rounded-lg ` + className}></textarea>
|
||||||
|
);
|
||||||
|
}
|
||||||
23
src/Components/common/TextInput.jsx
Normal file
23
src/Components/common/TextInput.jsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { forwardRef, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
export default forwardRef(function TextInput({ type = 'text', className = '', isFocused = false, ...props }, ref) {
|
||||||
|
const input = useRef();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isFocused) {
|
||||||
|
input.current.focus();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
{...props}
|
||||||
|
type={type}
|
||||||
|
className={
|
||||||
|
'focus:border-indigo-500 focus:ring-indigo-500 rounded-lg ' +
|
||||||
|
className
|
||||||
|
}
|
||||||
|
ref={input}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
7
src/Components/orders/FailedTag.jsx
Normal file
7
src/Components/orders/FailedTag.jsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export default function FailedTag() {
|
||||||
|
return (
|
||||||
|
<div className={'flex justify-center items-center bg-[rgb(229, 76, 76)] rounded-lg p-1'}>
|
||||||
|
<span className={'text-[#f37272]'}>رد شده</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
81
src/Components/orders/OrdersTable.jsx
Normal file
81
src/Components/orders/OrdersTable.jsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import SentTag from "./SentTag";
|
||||||
|
import FailedTag from "./FailedTag";
|
||||||
|
import PendingTag from "./PendingTag";
|
||||||
|
import ArrowDownOpen from './../../assets/images/ArrowDownOpen.svg'
|
||||||
|
import ArrowUpClose from './../../assets/images/ArrowUpClose.svg'
|
||||||
|
|
||||||
|
function timeSince(date) {
|
||||||
|
|
||||||
|
var seconds = Math.floor((new Date() - date) / 1000);
|
||||||
|
|
||||||
|
var interval = seconds / 31536000;
|
||||||
|
|
||||||
|
if (interval > 1) {
|
||||||
|
return Math.floor(interval) + " سال";
|
||||||
|
}
|
||||||
|
interval = seconds / 2592000;
|
||||||
|
if (interval > 1) {
|
||||||
|
return Math.floor(interval) + " ماه";
|
||||||
|
}
|
||||||
|
interval = seconds / 86400;
|
||||||
|
if (interval > 1) {
|
||||||
|
return Math.floor(interval) + " روز";
|
||||||
|
}
|
||||||
|
interval = seconds / 3600;
|
||||||
|
if (interval > 1) {
|
||||||
|
return Math.floor(interval) + " ساعت";
|
||||||
|
}
|
||||||
|
interval = seconds / 60;
|
||||||
|
if (interval > 1) {
|
||||||
|
return Math.floor(interval) + " دقیقه";
|
||||||
|
}
|
||||||
|
return Math.floor(seconds) + " ثانیه";
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function OrdersTable({orders}) {
|
||||||
|
function handleClick(id) {
|
||||||
|
window.location.href = window.location.href + `/${id}`
|
||||||
|
}
|
||||||
|
const _orders = orders <= 10 ? orders : orders.slice(0, 10)
|
||||||
|
const orders_list = _orders.map(order => {
|
||||||
|
return (
|
||||||
|
<tr onClick={()=>handleClick(order.id)} key={order.id} className="border-y text-right font-normal">
|
||||||
|
<td className="py-4 pr-2 ">{order.id}</td>
|
||||||
|
<td className="py-4 pr-2 ">{timeSince(new Date(order.created_at))} پیش</td>
|
||||||
|
<td className="py-4 flex items-center">{order.total} تومان</td>
|
||||||
|
<td className="py-4">{order.customer.phone}</td>
|
||||||
|
<td className="py-4 truncate">{order.address.address}</td>
|
||||||
|
<td className="py-4 flex items-center justify-between pl-2">
|
||||||
|
{
|
||||||
|
order.status === 'shipped' ?
|
||||||
|
<p>ارسال شده</p>
|
||||||
|
: order.status === 'pre' ?
|
||||||
|
<p>پرداخت نشده</p>
|
||||||
|
: <p>پرداخت شده</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="text-xs md:text-base w-full flex flex-col bg-1 font-[Vazirmatn] rounded-top-2xl">
|
||||||
|
<table className="w-full text-right">
|
||||||
|
<thead className={'text-white font-medium bg-dark w-full'}>
|
||||||
|
<tr>
|
||||||
|
<th className={'p-3'} style={{borderTopRightRadius: '15px'}}>کد</th>
|
||||||
|
<th>تاریخ</th>
|
||||||
|
<th>مبلغ</th>
|
||||||
|
<th>تلفن مشتری</th>
|
||||||
|
<th>آدرس</th>
|
||||||
|
<th style={{borderTopLeftRadius: '15px'}}>وضعیت</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
{orders_list}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
7
src/Components/orders/PendingTag.jsx
Normal file
7
src/Components/orders/PendingTag.jsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export default function PendingTag() {
|
||||||
|
return (
|
||||||
|
<div className={'flex justify-center items-center bg-[rgb(229, 76, 76)] rounded-lg p-1'}>
|
||||||
|
<span className={'text-[#f37272]'}>در انتظار</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
7
src/Components/orders/SentTag.jsx
Normal file
7
src/Components/orders/SentTag.jsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export default function SentTag() {
|
||||||
|
return (
|
||||||
|
<div className={'flex justify-center items-center bg-[#F6FFED] rounded-lg p-1'}>
|
||||||
|
<span className={'text-3'}>ارسال شده</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
75
src/Components/products/AddProductStep1.jsx
Normal file
75
src/Components/products/AddProductStep1.jsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import InputLabel from "../common/InputLabel";
|
||||||
|
import TextInput from "../common/TextInput";
|
||||||
|
import TextArea from "../common/TextArea";
|
||||||
|
import PlusIcon from './../../assets/images/PlusGray.svg'
|
||||||
|
import Input from "../common/Input";
|
||||||
|
|
||||||
|
export default function AddProductStep1({images, onChange, onChangeImage, onChangeAlt}) {
|
||||||
|
return (
|
||||||
|
<form className={'pt-10 min-h-[50vh] mx-5'}>
|
||||||
|
<div className={'flex flex-col'}>
|
||||||
|
<InputLabel className={"justify-start font-[Vazirmatn] font-normal mb-2 mr-2 "} htmlFor="title"
|
||||||
|
value="نام"/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
id="title"
|
||||||
|
type="text"
|
||||||
|
name="title"
|
||||||
|
className="py-3 font-[Vazirmatn] flex text-right w-full placeholder:text-sm placeholder-[#828282] border-[#B3B3B3] bg-1"
|
||||||
|
placeholder={'مثال: محصولات ورزشی'}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={'flex flex-col'}>
|
||||||
|
<InputLabel className={"justify-start font-[Vazirmatn] font-normal mb-2 mr-2 "} htmlFor="inventory"
|
||||||
|
value="موجودی"/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
id="inventory"
|
||||||
|
type="number"
|
||||||
|
name="inventory"
|
||||||
|
className="py-3 font-[Vazirmatn] flex text-right w-full placeholder:text-sm placeholder-[#828282] border-[#B3B3B3] bg-1"
|
||||||
|
placeholder={100}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col mt-5">
|
||||||
|
<InputLabel className={"justify-start font-normal font-[Vazirmatn] mb-2 mr-2 "} htmlFor="description"
|
||||||
|
value="توضیحات"/>
|
||||||
|
|
||||||
|
<TextArea
|
||||||
|
id="description"
|
||||||
|
type="text"
|
||||||
|
rows={3}
|
||||||
|
name="description"
|
||||||
|
placeholder={'توضیح درباره این محصول'}
|
||||||
|
className="py-3 font-[Vazirmatn] flex text-right w-full placeholder:text-sm placeholder-[#828282] border-[#B3B3B3] bg-1"
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col mt-5">
|
||||||
|
<InputLabel className={"justify-start font-normal font-[Vazirmatn] mb-2 mr-2 "} value="تصاویر"/>
|
||||||
|
<div className="flex items-start justify-start mb-16 overflow-x-scroll">
|
||||||
|
<label
|
||||||
|
htmlFor="image-input"
|
||||||
|
className="flex items-center justify-center rounded-lg ml-3 cursor-pointer bg-[#CDCDCD] hover:bg-[#ABABAB]">
|
||||||
|
<div className="flex items-center justify-center w-[10vw] h-[10vw]">
|
||||||
|
<img alt={'click to upload'} src={PlusIcon}/>
|
||||||
|
</div>
|
||||||
|
<input id="image-input" accept={'image/*'} multiple type="file" className="hidden"
|
||||||
|
onChange={onChangeImage}/>
|
||||||
|
</label>
|
||||||
|
{
|
||||||
|
images.map((image, index) => (
|
||||||
|
<div key={index} className="flex flex-col items-center justify-center">
|
||||||
|
<img className={'preview-image mx-3 rounded-lg shadow-xl'} src={image} alt={image.name}/>
|
||||||
|
<Input onChange={(e) =>{onChangeAlt(index, e.target.value)}} className={'m-3 rounded-lg w-[10vw] text-sm font-light font-[Vazirmatn]'} placeholder={'alt value'}/>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
82
src/Components/products/AddProductStep2.jsx
Normal file
82
src/Components/products/AddProductStep2.jsx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import InputLabel from "../common/InputLabel";
|
||||||
|
import AddIcon from "../../assets/images/PlusGreen.svg";
|
||||||
|
import PenIcon from './../../assets/images/BluePen.svg'
|
||||||
|
import {FormControl, MenuItem, Select} from "@mui/material";
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
import PublicApi from "../../api/PublicAPi";
|
||||||
|
import adminApi, {goToLoginPage} from "../../api/AdminApi";
|
||||||
|
import {userToken} from "../../api/TokenApi";
|
||||||
|
|
||||||
|
|
||||||
|
export default function AddProductStep2({onChangeCategory, selectedCategory, onChangeProperty}) {
|
||||||
|
const [categories, setCategories] = useState([])
|
||||||
|
const [properties, setProperties] = useState([])
|
||||||
|
|
||||||
|
// fetch all categories and set
|
||||||
|
const getCategories = async () => {
|
||||||
|
const fetchedCategories = await PublicApi.get(`categories/index/${await userToken()}`)
|
||||||
|
setCategories(fetchedCategories)
|
||||||
|
}
|
||||||
|
useEffect(() => {
|
||||||
|
getCategories()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// set properties based on selected category
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedCategory) {
|
||||||
|
const selectedCategoryObject = categories.filter((category) => category.id === selectedCategory)[0]
|
||||||
|
setProperties(selectedCategoryObject.properties)
|
||||||
|
}
|
||||||
|
}, [selectedCategory])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={'flex flex-col pt-10 min-h-[50vh] mx-5'}>
|
||||||
|
<InputLabel className={"justify-start font-[Vazirmatn] font-normal mb-2 mr-2 "} htmlFor="title">
|
||||||
|
<div className={'flex flex-row items-center justify-center'}>
|
||||||
|
انتخاب دسته بندی
|
||||||
|
<img className={'mx-1'} alt={''} src={PenIcon}/>
|
||||||
|
<img className={'mx-1'} alt={''} src={AddIcon}/>
|
||||||
|
</div>
|
||||||
|
</InputLabel>
|
||||||
|
<FormControl className={"w-full"}>
|
||||||
|
<Select
|
||||||
|
sx={{height: '2.6rem', borderRadius: '8px', fontFamily: 'Vazirmatn', color: '#1e1e1e'}}
|
||||||
|
value={selectedCategory}
|
||||||
|
onChange={(e) => onChangeCategory(e)}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
categories.map((category, index) => (
|
||||||
|
<MenuItem
|
||||||
|
divider={true}
|
||||||
|
sx={{fontFamily: 'Vazirmatn', color: '#1e1e1e', display: 'flex', justifyContent: 'end'}}
|
||||||
|
key={index}
|
||||||
|
value={category.id}
|
||||||
|
>
|
||||||
|
{category.title}
|
||||||
|
</MenuItem>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<table className="w-full text-right mt-8">
|
||||||
|
<thead className={'text-white font-[Vazirmatn] bg-dark w-full'}>
|
||||||
|
<tr className={''}>
|
||||||
|
<th className={'border-1 border-[#B3B3B3] p-3'}>ویژگی</th>
|
||||||
|
<th className={'border-1 border-[#B3B3B3] pr-3'}>مقدار</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{
|
||||||
|
properties.map((property, index) => (
|
||||||
|
<tr key={index} className="text-right font-normal">
|
||||||
|
<td className="border-1 border-[#B3B3B3]"><span className={'mr-2 w-full border-0'}>{property.title}</span></td>
|
||||||
|
<td className="border-1 border-[#B3B3B3]"><input onChange={(e) => onChangeProperty(property.slug, e.target.value)} className={'w-full border-0'}/></td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
44
src/Components/products/ProductCard.jsx
Normal file
44
src/Components/products/ProductCard.jsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import viewIcon from "./../../assets/images/EyeIcon.svg"
|
||||||
|
import categoryIcon from "./../../assets/images/CategoryTagIcon.svg"
|
||||||
|
import sellsIcon from "./../../assets/images/ProductSellsIcon.svg"
|
||||||
|
import availableIcon from "./../../assets/images/ProductAvailableIcon.svg"
|
||||||
|
import priceIcon from "./../../assets/images/ProductPriceIcon.svg"
|
||||||
|
import Button from "../Button"
|
||||||
|
import {EnglishToPersian} from "../../helpers/EnglishToPersian";
|
||||||
|
import {Link} from "react-router-dom";
|
||||||
|
export default function ProductCard({product, category, onDelete}) {
|
||||||
|
return (
|
||||||
|
<div className={'shadow transition duration-600 hover:shadow-sm col-span-12 md:col-span-6 lg:col-span-4 xl:col-span-3 rounded-xl flex flex-col justify-between bg-1'}>
|
||||||
|
<img className={'rounded-top-3 shadow-sm product-card-image'} src={product.images[0]?.path} alt={product.title}/>
|
||||||
|
<div className={'p-4 flex flex-col'}>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<div className={'flex rounded-2xl pb-2 w-fit'}>
|
||||||
|
<img className="hue-rotate-180" src={categoryIcon} alt={''}/>
|
||||||
|
<span className={'mr-1 text-sm min-w-fit text-3 font-[Vazirmatn]'}>{product.category ? product.category.title : category}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className={'font-[Vazirmatn] font-base text-lg'}>{product.title}</span>
|
||||||
|
{product.detail ? (
|
||||||
|
<div className={'grid grid-cols-2 px-2 pt-6'}>
|
||||||
|
<div className={'flex col-span-2 lg:col-span-1 font-[Vazirmatn]'}>
|
||||||
|
{EnglishToPersian(product.inventory? product.inventory.toString() : '0')} موجود
|
||||||
|
</div>
|
||||||
|
<div className={'flex justify-end col-span-2 lg:col-span-1 justify-end font-medium font-[Vazirmatn]'}>
|
||||||
|
{EnglishToPersian(product.details.filter((detail) => detail.slug == "price")[0]? product.details.filter((detail) => detail.slug == "price")[0].value : '0')} تومان
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div className="flex flex-col lg:grid lg:grid-cols-3 pt-6 items-center">
|
||||||
|
<span onClick={() => onDelete(product.id)} className="text-4 cursor-pointer hover:opacity-80 underline w-full h-fit text-center items-center justify-center">
|
||||||
|
حذف
|
||||||
|
</span>
|
||||||
|
<Link className="w-full col-span-2" to={`${product.category ? product.id : `/panel/products/${product.id}`}`}>
|
||||||
|
<Button className="w-full font-[Vazirmatn] bg-gradient-to-r from-4 to-purp text-white transition duration-300 hover:opacity-80">
|
||||||
|
<span className="">ویرایش</span>
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
48
src/Components/products/ProductTable.jsx
Normal file
48
src/Components/products/ProductTable.jsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import categoryIcon from "./../../assets/images/CategoryTagIcon.svg"
|
||||||
|
import {Link} from "react-router-dom";
|
||||||
|
|
||||||
|
export default function ProductTable({products}) {
|
||||||
|
const products_list = products.map(product => {
|
||||||
|
return (
|
||||||
|
<tr key={product.id} className="border-y text-right font-normal">
|
||||||
|
<td className="py-4 pr-2 ">
|
||||||
|
<Link to={`${product.id}`}>
|
||||||
|
{product.title}
|
||||||
|
</Link>
|
||||||
|
</td>
|
||||||
|
<td className="py-4 text-lg">{product.id}</td>
|
||||||
|
<td className="py-4 flex items-center">
|
||||||
|
<div className={'bg-[#F6FFED] flex rounded-2xl w-fit px-4 py-1'}>
|
||||||
|
<img src={categoryIcon} alt={''}/>
|
||||||
|
<span className={'mr-1 text-3 font-[Vazirmatn]'}>{product.category.title}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="py-4">{product.viewCount ? product.viewCount : 0} بازدید</td>
|
||||||
|
<td className="py-4">{product.sells ? product.sells : 0} عدد</td>
|
||||||
|
<td className="py-4">{product.available ? product.available : 0} عدد</td>
|
||||||
|
<td className="py-4 pl-2">{product.price ? product.price : 0} تومان</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full flex flex-col bg-1 font-[Vazirmatn]">
|
||||||
|
<table className="w-full text-right">
|
||||||
|
<thead className={'text-white font-medium bg-dark w-full'}>
|
||||||
|
<tr>
|
||||||
|
<th className={'p-3'} style={{borderTopRightRadius: '15px'}}>نام محصول</th>
|
||||||
|
<th>کد محصول</th>
|
||||||
|
<th>دسته بندی</th>
|
||||||
|
<th>تعداد بازدید</th>
|
||||||
|
<th>فروش</th>
|
||||||
|
<th>موجودی</th>
|
||||||
|
<th style={{borderTopLeftRadius: '15px'}}>قیمت</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
{products_list}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
47
src/Components/products/ProductsGrid.jsx
Normal file
47
src/Components/products/ProductsGrid.jsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import ProductCard from "./ProductCard";
|
||||||
|
import {Modal} from "react-bootstrap";
|
||||||
|
import React, {useState} from "react";
|
||||||
|
import AdminApi from "../../api/AdminApi";
|
||||||
|
import LoadingTemplate from "../../Components/common/LoadingTemplate";
|
||||||
|
import DangerButton from "../../Components/DangerButton";
|
||||||
|
import SecondaryButton from "../../Components/SecondaryButton";
|
||||||
|
import Button from "../Button"
|
||||||
|
|
||||||
|
export default function ProductsGrid({products, category=''}) {
|
||||||
|
const [deleteLoading, setDeleteLoading] = useState(false)
|
||||||
|
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
||||||
|
const [deleteId, SetDeleteId] = useState(-1);
|
||||||
|
const handleDeleteModal = (id) => {
|
||||||
|
SetDeleteId(id);
|
||||||
|
setShowDeleteModal(!showDeleteModal)
|
||||||
|
}
|
||||||
|
const handleDelete = async () => {
|
||||||
|
setDeleteLoading(true)
|
||||||
|
await AdminApi.delete(`products/delete/${deleteId}`).then(() => {
|
||||||
|
window.location = '/panel/products'
|
||||||
|
})
|
||||||
|
setDeleteLoading(false)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={'flex grid grid-cols-12 gap-7'}>
|
||||||
|
{
|
||||||
|
products.map((product, index) => (
|
||||||
|
<ProductCard category={category} key={index} product={product} onDelete={handleDeleteModal} />
|
||||||
|
))
|
||||||
|
}
|
||||||
|
<Modal size={'md'} centered show={showDeleteModal} onHide={handleDeleteModal}>
|
||||||
|
<Modal.Header closeButton={true} className={'flex flex-row-reverse'}>
|
||||||
|
<Modal.Title className={'font-[Vazirmatn] text-lg'}>آیا از حذف این محصول اطمینان دارید؟</Modal.Title>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body>
|
||||||
|
<div className={'w-full flex flex-row items-center justify-between px-5'}>
|
||||||
|
<LoadingTemplate loading={deleteLoading} className={'mx-2'} marginLeft={'-12px'}>
|
||||||
|
<Button className={'font-[Vazirmatn] w-full flex justify-center items-center tracking-tight bg-3'} onClick={handleDelete}>حذف</Button>
|
||||||
|
</LoadingTemplate>
|
||||||
|
<Button disabled={deleteLoading} className={'font-[Vazirmatn] w-full flex justify-center items-center mx-2 tracking-tight border'} onClick={handleDeleteModal}>لغو</Button>
|
||||||
|
</div>
|
||||||
|
</Modal.Body>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
150
src/Components/properties/AddProperty.jsx
Normal file
150
src/Components/properties/AddProperty.jsx
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import InputLabel from "../common/InputLabel";
|
||||||
|
import TextInput from "../common/TextInput";
|
||||||
|
import {FormControl, MenuItem, Select} from "@mui/material";
|
||||||
|
import Checkbox from "../common/Checkbox";
|
||||||
|
import AdminApi from "../../api/AdminApi";
|
||||||
|
import React, {useState} from "react";
|
||||||
|
import LoadingTemplate from "../common/LoadingTemplate";
|
||||||
|
import InputError from "../common/InputError";
|
||||||
|
|
||||||
|
export default function AddProperty({callback, categoryId}) {
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState(false)
|
||||||
|
const [data, setData] = useState({
|
||||||
|
title: '',
|
||||||
|
slug: '',
|
||||||
|
main: 0,
|
||||||
|
isVariable: 0,
|
||||||
|
type: 'default',
|
||||||
|
validation_rules: "{}"
|
||||||
|
})
|
||||||
|
const handlePostProperty = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
const PostPropertyRes = await AdminApi.post(`properties/store/${categoryId}`, data)
|
||||||
|
if (PostPropertyRes === undefined) {
|
||||||
|
setError(true)
|
||||||
|
} else {
|
||||||
|
await callback()
|
||||||
|
setError(false)
|
||||||
|
}
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChange = (name, value) => {
|
||||||
|
setData({
|
||||||
|
...data,
|
||||||
|
[name]: value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={'flex flex-col'}>
|
||||||
|
<div className={'flex flex-row items-center justify-center'}>
|
||||||
|
<div className={'flex flex-col w-50'}>
|
||||||
|
<InputLabel className={"justify-start font-[Vazirmatn] font-normal mt-3 mb-2 mr-2"} htmlFor={'property-title'}>
|
||||||
|
<div className={'flex flex-row items-center justify-center'}>
|
||||||
|
نام
|
||||||
|
</div>
|
||||||
|
</InputLabel>
|
||||||
|
<TextInput
|
||||||
|
id="property-title"
|
||||||
|
type="text"
|
||||||
|
name="title"
|
||||||
|
value={data.title}
|
||||||
|
className="py-3 ml-1 w-full font-[Vazirmatn] flex text-right border-gray-500 placeholder:text-sm placeholder-gray-400 "
|
||||||
|
placeholder={'مثال: وزن'}
|
||||||
|
onChange={(e) => {handleChange(e.target.name, e.target.value)}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={"flex flex-col w-50"}>
|
||||||
|
<InputLabel className={"justify-start font-[Vazirmatn] font-normal mt-3 mb-2 mr-2"} htmlFor={'property-title'}>
|
||||||
|
<div className={'flex flex-row items-center justify-center'}>
|
||||||
|
اسلاگ
|
||||||
|
</div>
|
||||||
|
</InputLabel>
|
||||||
|
<TextInput
|
||||||
|
id="property-slug"
|
||||||
|
type="text"
|
||||||
|
name="slug"
|
||||||
|
value={data.slug}
|
||||||
|
className="py-3 mr-1 w-full font-[Vazirmatn] flex text-right border-gray-500 placeholder:text-sm placeholder-gray-400 "
|
||||||
|
placeholder={'مثال: weight'}
|
||||||
|
onChange={(e) => {handleChange(e.target.name, e.target.value)}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={'flex flex-col items-start justify-center my-3'}>
|
||||||
|
<div className={'flex flex-row items-center justify-center'}>
|
||||||
|
<FormControl className={"w-40"}>
|
||||||
|
<Select
|
||||||
|
name={'type'}
|
||||||
|
defaultValue={'default'}
|
||||||
|
sx={{height: '2.6rem', borderRadius: '8px', fontFamily: 'Vazirmatn', color: '#1e1e1e'}}
|
||||||
|
value={data.type}
|
||||||
|
onChange={(e) => {handleChange(e.target.name, e.target.value)}}
|
||||||
|
>
|
||||||
|
<option value={'default'} disabled hidden>نوع</option>
|
||||||
|
<MenuItem
|
||||||
|
invalid={true}
|
||||||
|
divider={true}
|
||||||
|
sx={{fontFamily: 'Vazirmatn', color: '#1e1e1e', display: 'flex', justifyContent: 'end'}}
|
||||||
|
value={'integer'}
|
||||||
|
>
|
||||||
|
عددی
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
divider={true}
|
||||||
|
sx={{fontFamily: 'Vazirmatn', color: '#1e1e1e', display: 'flex', justifyContent: 'end'}}
|
||||||
|
value={'string'}
|
||||||
|
>
|
||||||
|
متنی
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
divider={true}
|
||||||
|
sx={{fontFamily: 'Vazirmatn', color: '#1e1e1e', display: 'flex', justifyContent: 'end'}}
|
||||||
|
value={'date'}
|
||||||
|
>
|
||||||
|
تاریخ
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
divider={true}
|
||||||
|
sx={{fontFamily: 'Vazirmatn', color: '#1e1e1e', display: 'flex', justifyContent: 'end'}}
|
||||||
|
value={'list'}
|
||||||
|
>
|
||||||
|
لیست
|
||||||
|
</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<label className="flex items-center">
|
||||||
|
<span className="mx-2 text-sm font-[Vazirmatn]">ویژگی اصلی</span>
|
||||||
|
<Checkbox
|
||||||
|
onChange={(e) => {handleChange(e.target.name, Number(e.target.checked))}}
|
||||||
|
name="main"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center">
|
||||||
|
<span className="mx-2 text-sm font-[Vazirmatn]">چند مقدار</span>
|
||||||
|
<Checkbox
|
||||||
|
onChange={(e) => {handleChange(e.target.name, Number(e.target.checked))}}
|
||||||
|
name="isVariable"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
error ?
|
||||||
|
<InputError message={'خطا! لطفا فیلد هارا به درستی پر کنید و دوباره تلاش کنید. در غیر این صورت با پشتیبانی تماس بفرمایید'} className="mt-3 mx-3 font-[dana]" />
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
<LoadingTemplate loading={loading} marginLeft={'-22px'}>
|
||||||
|
<button
|
||||||
|
disabled={loading}
|
||||||
|
className={'font-[Vazirmatn] bg-zinc-900 text-white rounded-lg mb-5 mt-4 py-2 text-center w-full hover:bg-zinc-700'}
|
||||||
|
onClick={handlePostProperty}>
|
||||||
|
ثبت ویژگی
|
||||||
|
</button>
|
||||||
|
</LoadingTemplate>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
86
src/Components/properties/SelectProperty.jsx
Normal file
86
src/Components/properties/SelectProperty.jsx
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import FilterByIcon from "../../assets/images/FilterByIcon.svg";
|
||||||
|
import Menu from "@mui/material/Menu";
|
||||||
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
|
import CheckIcon from "../../assets/images/CheckIcon.svg";
|
||||||
|
import React, {useState} from "react";
|
||||||
|
|
||||||
|
export default function SelectProperty({properties, selectedProperties, handleChangePropertyStatus}) {
|
||||||
|
|
||||||
|
const [anchorEl, setAnchorEl] = useState(null);
|
||||||
|
const open = Boolean(anchorEl);
|
||||||
|
const handleClick = (event) => {
|
||||||
|
setAnchorEl(event.currentTarget);
|
||||||
|
};
|
||||||
|
const handleClose = () => {
|
||||||
|
setAnchorEl(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
variant={'outlined'}
|
||||||
|
sx={{
|
||||||
|
fontWeight: '300',
|
||||||
|
letterSpacing: '-0.025em',
|
||||||
|
height: '2.6rem',
|
||||||
|
borderRadius: '8px',
|
||||||
|
fontFamily: 'Vazirmatn',
|
||||||
|
borderColor: '#1e1e1e',
|
||||||
|
color: '#1e1e1e',
|
||||||
|
':hover': {backgroundColor: 'white', borderColor: '#1e1e1e'}
|
||||||
|
}}
|
||||||
|
aria-controls={open ? 'propertyMenu' : undefined}
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-expanded={open ? 'true' : undefined}
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<img alt={''} src={FilterByIcon} className={'ml-3'}/>
|
||||||
|
انتخاب ویژگی
|
||||||
|
</Button>
|
||||||
|
<Menu
|
||||||
|
id="propertyMenu"
|
||||||
|
aria-labelledby="propertyMenu"
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
open={open}
|
||||||
|
onClose={handleClose}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'bottom',
|
||||||
|
horizontal: 'left',
|
||||||
|
}}
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
properties.map((property) => (
|
||||||
|
<MenuItem
|
||||||
|
divider={true}
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row-reverse',
|
||||||
|
justifyContent: 'end',
|
||||||
|
minWidth: '10rem'
|
||||||
|
}}
|
||||||
|
key={property.id}
|
||||||
|
value={property.id}
|
||||||
|
onClick={() => handleChangePropertyStatus(property)}
|
||||||
|
>
|
||||||
|
<span style={{
|
||||||
|
textAlign: 'right',
|
||||||
|
fontFamily: 'Vazirmatn',
|
||||||
|
color: '#1e1e1e'
|
||||||
|
}}>
|
||||||
|
<div className={'flex flex-row-reverse items-center justify-center'}>
|
||||||
|
{selectedProperties.filter((prop) => prop.id === property.id).length === 1 ? <img className={'ml-2'} alt={''} src={CheckIcon}/> : null}
|
||||||
|
{property.title}
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</MenuItem>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
125
src/Layouts/AuthenticatedLayout.jsx
Normal file
125
src/Layouts/AuthenticatedLayout.jsx
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import ApplicationLogo from '@/Components/ApplicationLogo';
|
||||||
|
import Dropdown from '@/Components/Dropdown';
|
||||||
|
import NavLink from '@/Components/NavLink';
|
||||||
|
import ResponsiveNavLink from '@/Components/ResponsiveNavLink';
|
||||||
|
import { Link } from '@inertiajs/react';
|
||||||
|
|
||||||
|
export default function Authenticated({ user, header, children }) {
|
||||||
|
const [showingNavigationDropdown, setShowingNavigationDropdown] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-100">
|
||||||
|
<nav className="bg-white border-b border-gray-100">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="flex justify-between h-16">
|
||||||
|
<div className="flex">
|
||||||
|
<div className="shrink-0 flex items-center">
|
||||||
|
<Link href="/">
|
||||||
|
<ApplicationLogo className="block h-9 w-auto fill-current text-gray-800" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
|
||||||
|
<NavLink href={route('dashboard')} active={route().current('dashboard')}>
|
||||||
|
Dashboard
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="hidden sm:flex sm:items-center sm:ml-6">
|
||||||
|
<div className="ml-3 relative">
|
||||||
|
<Dropdown>
|
||||||
|
<Dropdown.Trigger>
|
||||||
|
<span className="inline-flex rounded-md">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none transition ease-in-out duration-150"
|
||||||
|
>
|
||||||
|
{user.name}
|
||||||
|
|
||||||
|
<svg
|
||||||
|
className="ml-2 -mr-0.5 h-4 w-4"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</Dropdown.Trigger>
|
||||||
|
|
||||||
|
<Dropdown.Content>
|
||||||
|
<Dropdown.Link href={route('profile.edit')}>Profile</Dropdown.Link>
|
||||||
|
<Dropdown.Link href={route('logout')} method="post" as="button">
|
||||||
|
Log Out
|
||||||
|
</Dropdown.Link>
|
||||||
|
</Dropdown.Content>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="-mr-2 flex items-center sm:hidden">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowingNavigationDropdown((previousState) => !previousState)}
|
||||||
|
className="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 transition duration-150 ease-in-out"
|
||||||
|
>
|
||||||
|
<svg className="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
className={!showingNavigationDropdown ? 'inline-flex' : 'hidden'}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
d="M4 6h16M4 12h16M4 18h16"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className={showingNavigationDropdown ? 'inline-flex' : 'hidden'}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={(showingNavigationDropdown ? 'block' : 'hidden') + ' sm:hidden'}>
|
||||||
|
<div className="pt-2 pb-3 space-y-1">
|
||||||
|
<ResponsiveNavLink href={route('dashboard')} active={route().current('dashboard')}>
|
||||||
|
Dashboard
|
||||||
|
</ResponsiveNavLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-4 pb-1 border-t border-gray-200">
|
||||||
|
<div className="px-4">
|
||||||
|
<div className="font-medium text-base text-gray-800">{user.name}</div>
|
||||||
|
<div className="font-medium text-sm text-gray-500">{user.email}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-3 space-y-1">
|
||||||
|
<ResponsiveNavLink href={route('profile.edit')}>Profile</ResponsiveNavLink>
|
||||||
|
<ResponsiveNavLink method="post" href={route('logout')} as="button">
|
||||||
|
Log Out
|
||||||
|
</ResponsiveNavLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{header && (
|
||||||
|
<header className="bg-white shadow">
|
||||||
|
<div className="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">{header}</div>
|
||||||
|
</header>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<main>{children}</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
src/Layouts/GuestLayout.jsx
Normal file
11
src/Layouts/GuestLayout.jsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import {Link} from "react-router-dom";
|
||||||
|
|
||||||
|
export default function Guest({ children }) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100">
|
||||||
|
<div className="w-full sm:max-w-md mt-6 px-6 py-4 bg-white shadow-sm overflow-hidden sm:rounded-xl">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
0
src/Layouts/Layout.jsx
Normal file
0
src/Layouts/Layout.jsx
Normal file
36
src/Layouts/MainSection.jsx
Normal file
36
src/Layouts/MainSection.jsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import {Route, Routes} from "react-router-dom";
|
||||||
|
import Dashboard from "../views/Panel/Dashboard";
|
||||||
|
import UsersList from "../Pages/Profile/UsersList";
|
||||||
|
import Products from "../views/Panel/Products";
|
||||||
|
import AddProduct from "../views/Panel/AddProduct";
|
||||||
|
import Categories from "../views/Panel/Categories";
|
||||||
|
import Orders from "../views/Panel/Orders";
|
||||||
|
import OrderDetail from "../views/Panel/OrderDetail"
|
||||||
|
import CategoryDetail from "../views/Panel/CategoryDetail";
|
||||||
|
import Articles from "../views/Panel/Articles";
|
||||||
|
import Contents from "../views/Panel/Contents";
|
||||||
|
import AddArticle from "../views/Panel/AddArticle";
|
||||||
|
import EditArticle from "../views/Panel/EditArticle";
|
||||||
|
import ProductDetail from "../views/Panel/ProductDetail";
|
||||||
|
|
||||||
|
export default function MainSection() {
|
||||||
|
return (
|
||||||
|
<div className="pt-12 px-0 sm:px-8 md:px-12 lg:px-20 xl:px-28">
|
||||||
|
<Routes>
|
||||||
|
<Route path="/dashboard" element={<Dashboard/>}/>
|
||||||
|
<Route path="/users" element={<UsersList/>}/>
|
||||||
|
<Route path="/products" element={<Products/>}/>
|
||||||
|
<Route path="/products/add" element={<AddProduct/>}/>
|
||||||
|
<Route path="/products/:id" element={<ProductDetail/>}/>
|
||||||
|
<Route path="/categories" element={<Categories/>}/>
|
||||||
|
<Route path="/categories/:id" element={<CategoryDetail/>}/>
|
||||||
|
<Route path="/orders" element={<Orders/>}/>
|
||||||
|
<Route path="/orders/:id" element={<OrderDetail/>}/>
|
||||||
|
<Route path="/articles" element={<Articles/>}/>
|
||||||
|
<Route path="/articles/add" element={<AddArticle/>}/>
|
||||||
|
<Route path="/articles/edit/:id" element={<EditArticle/>}/>
|
||||||
|
<Route path="/contents" element={<Contents/>}/>
|
||||||
|
</Routes>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
20
src/Layouts/PanelLayout.jsx
Normal file
20
src/Layouts/PanelLayout.jsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import Header from '../Components/common/Header'
|
||||||
|
import Sidebar from '../Components/common/Sidebar'
|
||||||
|
import MainSection from "./MainSection";
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
export default function PanelLayout() {
|
||||||
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||||
|
return (
|
||||||
|
<div dir="rtl" className="w-screen h-screen flex flex-row bg-gray-200">
|
||||||
|
<div className="relative h-full border-[#E6E6E6] border-l-2 md:flex">
|
||||||
|
<Sidebar sidebarOpen={sidebarOpen} setSidebarOpen={setSidebarOpen}/>
|
||||||
|
</div>
|
||||||
|
<div className="relative flex flex-col bg-2 h-full w-full overflow-auto" >
|
||||||
|
<Header sidebarOpen={sidebarOpen} setSidebarOpen={setSidebarOpen}/>
|
||||||
|
<MainSection/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
36
src/Pages/Profile/Edit.jsx
Normal file
36
src/Pages/Profile/Edit.jsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import AuthenticatedLayout from './../../Layouts/AuthenticatedLayout';
|
||||||
|
import DeleteUserForm from './Partials/DeleteUserForm';
|
||||||
|
import UpdatePasswordForm from './Partials/UpdatePasswordForm';
|
||||||
|
import UpdateProfileInformationForm from './Partials/UpdateProfileInformationForm';
|
||||||
|
import { Head } from '@inertiajs/react';
|
||||||
|
|
||||||
|
export default function Edit({ auth, mustVerifyEmail, status }) {
|
||||||
|
return (
|
||||||
|
<AuthenticatedLayout
|
||||||
|
user={auth.user}
|
||||||
|
header={<h2 className="font-semibold text-xl text-gray-800 leading-tight">Profile</h2>}
|
||||||
|
>
|
||||||
|
<Head title="Profile" />
|
||||||
|
|
||||||
|
<div className="py-12">
|
||||||
|
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
|
||||||
|
<div className="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
|
||||||
|
<UpdateProfileInformationForm
|
||||||
|
mustVerifyEmail={mustVerifyEmail}
|
||||||
|
status={status}
|
||||||
|
className="max-w-xl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
|
||||||
|
<UpdatePasswordForm className="max-w-xl" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
|
||||||
|
<DeleteUserForm className="max-w-xl" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AuthenticatedLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
99
src/Pages/Profile/Partials/DeleteUserForm.jsx
Normal file
99
src/Pages/Profile/Partials/DeleteUserForm.jsx
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import DangerButton from '@/Components/DangerButton';
|
||||||
|
import InputError from '@/Components/InputError';
|
||||||
|
import InputLabel from '@/Components/InputLabel';
|
||||||
|
import Modal from '@/Components/Modal';
|
||||||
|
import SecondaryButton from '@/Components/SecondaryButton';
|
||||||
|
import TextInput from '@/Components/TextInput';
|
||||||
|
import { useForm } from '@inertiajs/react';
|
||||||
|
|
||||||
|
export default function DeleteUserForm({ className = '' }) {
|
||||||
|
const [confirmingUserDeletion, setConfirmingUserDeletion] = useState(false);
|
||||||
|
const passwordInput = useRef();
|
||||||
|
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
setData,
|
||||||
|
delete: destroy,
|
||||||
|
processing,
|
||||||
|
reset,
|
||||||
|
errors,
|
||||||
|
} = useForm({
|
||||||
|
password: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const confirmUserDeletion = () => {
|
||||||
|
setConfirmingUserDeletion(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteUser = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
destroy(route('profile.destroy'), {
|
||||||
|
preserveScroll: true,
|
||||||
|
onSuccess: () => closeModal(),
|
||||||
|
onError: () => passwordInput.current.focus(),
|
||||||
|
onFinish: () => reset(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
setConfirmingUserDeletion(false);
|
||||||
|
|
||||||
|
reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={`space-y-6 ${className}`}>
|
||||||
|
<header>
|
||||||
|
<h2 className="text-lg font-medium text-gray-900">Delete Account</h2>
|
||||||
|
|
||||||
|
<p className="mt-1 text-sm text-gray-600">
|
||||||
|
Once your account is deleted, all of its resources and data will be permanently deleted. Before
|
||||||
|
deleting your account, please download any data or information that you wish to retain.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<DangerButton onClick={confirmUserDeletion}>Delete Account</DangerButton>
|
||||||
|
|
||||||
|
<Modal show={confirmingUserDeletion} onClose={closeModal}>
|
||||||
|
<form onSubmit={deleteUser} className="p-6">
|
||||||
|
<h2 className="text-lg font-medium text-gray-900">
|
||||||
|
Are you sure you want to delete your account?
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p className="mt-1 text-sm text-gray-600">
|
||||||
|
Once your account is deleted, all of its resources and data will be permanently deleted. Please
|
||||||
|
enter your password to confirm you would like to permanently delete your account.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-6">
|
||||||
|
<InputLabel htmlFor="password" value="Password" className="sr-only" />
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
ref={passwordInput}
|
||||||
|
value={data.password}
|
||||||
|
onChange={(e) => setData('password', e.target.value)}
|
||||||
|
className="mt-1 block w-3/4"
|
||||||
|
isFocused
|
||||||
|
placeholder="Password"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<InputError message={errors.password} className="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 flex justify-end">
|
||||||
|
<SecondaryButton onClick={closeModal}>Cancel</SecondaryButton>
|
||||||
|
|
||||||
|
<DangerButton className="ml-3" disabled={processing}>
|
||||||
|
Delete Account
|
||||||
|
</DangerButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
113
src/Pages/Profile/Partials/UpdatePasswordForm.jsx
Normal file
113
src/Pages/Profile/Partials/UpdatePasswordForm.jsx
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import { useRef } from 'react';
|
||||||
|
import InputError from '@/Components/InputError';
|
||||||
|
import InputLabel from '@/Components/InputLabel';
|
||||||
|
import PrimaryButton from '@/Components/PrimaryButton';
|
||||||
|
import TextInput from '@/Components/TextInput';
|
||||||
|
import { useForm } from '@inertiajs/react';
|
||||||
|
import { Transition } from '@headlessui/react';
|
||||||
|
|
||||||
|
export default function UpdatePasswordForm({ className = '' }) {
|
||||||
|
const passwordInput = useRef();
|
||||||
|
const currentPasswordInput = useRef();
|
||||||
|
|
||||||
|
const { data, setData, errors, put, reset, processing, recentlySuccessful } = useForm({
|
||||||
|
current_password: '',
|
||||||
|
password: '',
|
||||||
|
password_confirmation: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatePassword = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
put(route('password.update'), {
|
||||||
|
preserveScroll: true,
|
||||||
|
onSuccess: () => reset(),
|
||||||
|
onError: (errors) => {
|
||||||
|
if (errors.password) {
|
||||||
|
reset('password', 'password_confirmation');
|
||||||
|
passwordInput.current.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.current_password) {
|
||||||
|
reset('current_password');
|
||||||
|
currentPasswordInput.current.focus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={className}>
|
||||||
|
<header>
|
||||||
|
<h2 className="text-lg font-medium text-gray-900">Update Password</h2>
|
||||||
|
|
||||||
|
<p className="mt-1 text-sm text-gray-600">
|
||||||
|
Ensure your account is using a long, random password to stay secure.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<form onSubmit={updatePassword} className="mt-6 space-y-6">
|
||||||
|
<div>
|
||||||
|
<InputLabel htmlFor="current_password" value="Current Password" />
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
id="current_password"
|
||||||
|
ref={currentPasswordInput}
|
||||||
|
value={data.current_password}
|
||||||
|
onChange={(e) => setData('current_password', e.target.value)}
|
||||||
|
type="password"
|
||||||
|
className="mt-1 block w-full"
|
||||||
|
autoComplete="current-password"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<InputError message={errors.current_password} className="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<InputLabel htmlFor="password" value="New Password" />
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
id="password"
|
||||||
|
ref={passwordInput}
|
||||||
|
value={data.password}
|
||||||
|
onChange={(e) => setData('password', e.target.value)}
|
||||||
|
type="password"
|
||||||
|
className="mt-1 block w-full"
|
||||||
|
autoComplete="new-password"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<InputError message={errors.password} className="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<InputLabel htmlFor="password_confirmation" value="Confirm Password" />
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
id="password_confirmation"
|
||||||
|
value={data.password_confirmation}
|
||||||
|
onChange={(e) => setData('password_confirmation', e.target.value)}
|
||||||
|
type="password"
|
||||||
|
className="mt-1 block w-full"
|
||||||
|
autoComplete="new-password"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<InputError message={errors.password_confirmation} className="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<PrimaryButton disabled={processing}>Save</PrimaryButton>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
show={recentlySuccessful}
|
||||||
|
enter="transition ease-in-out"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
leave="transition ease-in-out"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<p className="text-sm text-gray-600">Saved.</p>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
103
src/Pages/Profile/Partials/UpdateProfileInformationForm.jsx
Normal file
103
src/Pages/Profile/Partials/UpdateProfileInformationForm.jsx
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import InputError from '@/Components/InputError';
|
||||||
|
import InputLabel from '@/Components/InputLabel';
|
||||||
|
import PrimaryButton from '@/Components/PrimaryButton';
|
||||||
|
import TextInput from '@/Components/TextInput';
|
||||||
|
import { Link, useForm, usePage } from '@inertiajs/react';
|
||||||
|
import { Transition } from '@headlessui/react';
|
||||||
|
|
||||||
|
export default function UpdateProfileInformation({ mustVerifyEmail, status, className = '' }) {
|
||||||
|
const user = usePage().props.auth.user;
|
||||||
|
|
||||||
|
const { data, setData, patch, errors, processing, recentlySuccessful } = useForm({
|
||||||
|
name: user.name,
|
||||||
|
email: user.email,
|
||||||
|
});
|
||||||
|
|
||||||
|
const submit = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
patch(route('profile.update'));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={className}>
|
||||||
|
<header>
|
||||||
|
<h2 className="text-lg font-medium text-gray-900">Profile Information</h2>
|
||||||
|
|
||||||
|
<p className="mt-1 text-sm text-gray-600">
|
||||||
|
Update your account's profile information and email address.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<form onSubmit={submit} className="mt-6 space-y-6">
|
||||||
|
<div>
|
||||||
|
<InputLabel htmlFor="name" value="Name" />
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
id="name"
|
||||||
|
className="mt-1 block w-full"
|
||||||
|
value={data.name}
|
||||||
|
onChange={(e) => setData('name', e.target.value)}
|
||||||
|
required
|
||||||
|
isFocused
|
||||||
|
autoComplete="name"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<InputError className="mt-2" message={errors.name} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<InputLabel htmlFor="email" value="Email" />
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
className="mt-1 block w-full"
|
||||||
|
value={data.email}
|
||||||
|
onChange={(e) => setData('email', e.target.value)}
|
||||||
|
required
|
||||||
|
autoComplete="username"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<InputError className="mt-2" message={errors.email} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{mustVerifyEmail && user.email_verified_at === null && (
|
||||||
|
<div>
|
||||||
|
<p className="text-sm mt-2 text-gray-800">
|
||||||
|
Your email address is unverified.
|
||||||
|
<Link
|
||||||
|
href={route('verification.send')}
|
||||||
|
method="post"
|
||||||
|
as="button"
|
||||||
|
className="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||||
|
>
|
||||||
|
Click here to re-send the verification email.
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{status === 'verification-link-sent' && (
|
||||||
|
<div className="mt-2 font-medium text-sm text-green-600">
|
||||||
|
A new verification link has been sent to your email address.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<PrimaryButton disabled={processing}>Save</PrimaryButton>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
show={recentlySuccessful}
|
||||||
|
enter="transition ease-in-out"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
leave="transition ease-in-out"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<p className="text-sm text-gray-600">Saved.</p>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
8
src/Pages/Profile/UsersList.jsx
Normal file
8
src/Pages/Profile/UsersList.jsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export default function UsersList() {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative flex flex-col h-full w-full space-y-6">
|
||||||
|
hello
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
81
src/api/AdminApi.jsx
Normal file
81
src/api/AdminApi.jsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
const FRONT_BASE_URL = "https://app.axicon.ir/"
|
||||||
|
const PRIVATE_BASE_URL = "https://api.axicon.ir/api/"
|
||||||
|
|
||||||
|
const axiosParams = {
|
||||||
|
withCredentials: true,
|
||||||
|
baseURL: PRIVATE_BASE_URL,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': FRONT_BASE_URL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
axios.defaults.withXSRFToken = true
|
||||||
|
const axiosInstance = axios.create(axiosParams);
|
||||||
|
|
||||||
|
export const goToLoginPage = () => {
|
||||||
|
window.location.href = FRONT_BASE_URL
|
||||||
|
}
|
||||||
|
const AdminApi = {
|
||||||
|
get: async (url) => {
|
||||||
|
try {
|
||||||
|
const response = await axiosInstance.get(url);
|
||||||
|
return response.data
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response?.status === 419 || error.response?.status === 401) {
|
||||||
|
goToLoginPage()
|
||||||
|
} else {
|
||||||
|
console.log("error in admin get api")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
delete: async (url) => {
|
||||||
|
try {
|
||||||
|
const response = await axiosInstance.delete(url);
|
||||||
|
return response.data
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response?.status === 419) {
|
||||||
|
goToLoginPage()
|
||||||
|
} else {
|
||||||
|
console.log("error in admin delete api")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
post: async (url, body, config={}) => {
|
||||||
|
try {
|
||||||
|
const response = await axiosInstance.post(url, body, config);
|
||||||
|
return response.data
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response?.status === 419) {
|
||||||
|
goToLoginPage()
|
||||||
|
} else {
|
||||||
|
console.log("error in admin post api")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
imagePost: async (url, body) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(PRIVATE_BASE_URL + url, body, {
|
||||||
|
withCredentials: true,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
'Access-Control-Allow-Origin': FRONT_BASE_URL,
|
||||||
|
'Accept' : 'application/json',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return response.data
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response?.status === 419) {
|
||||||
|
goToLoginPage()
|
||||||
|
} else {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AdminApi;
|
||||||
33
src/api/CmsAPi.jsx
Normal file
33
src/api/CmsAPi.jsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import {goToLoginPage} from "./AdminApi";
|
||||||
|
|
||||||
|
const FRONT_BASE_URL = "https://app.axicon.ir/"
|
||||||
|
const PUBLIC_BASE_URL = "https://cms2.liara.run/api/public"
|
||||||
|
|
||||||
|
const axiosParams = {
|
||||||
|
withCredentials: true,
|
||||||
|
baseURL: PUBLIC_BASE_URL,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': FRONT_BASE_URL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
axios.defaults.withXSRFToken = true
|
||||||
|
const axiosInstance = axios.create(axiosParams);
|
||||||
|
|
||||||
|
const PublicApi = {
|
||||||
|
get: async (url) => {
|
||||||
|
try {
|
||||||
|
const response = await axiosInstance.get(url);
|
||||||
|
return response.data
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response?.status === 419) {
|
||||||
|
goToLoginPage()
|
||||||
|
} else {
|
||||||
|
console.log("error in public get api")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PublicApi;
|
||||||
20
src/api/GetCSRF.jsx
Normal file
20
src/api/GetCSRF.jsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
const BACK_BASE_URL = "https://api.axicon.ir/"
|
||||||
|
|
||||||
|
const axiosParams = {
|
||||||
|
withCredentials: true,
|
||||||
|
baseURL: BACK_BASE_URL
|
||||||
|
}
|
||||||
|
axios.defaults.withXSRFToken = true
|
||||||
|
const axiosInstance = axios.create(axiosParams);
|
||||||
|
|
||||||
|
export default function GetCSRF() {
|
||||||
|
return (
|
||||||
|
axiosInstance.get("sanctum/csrf-cookie").then((response) => {
|
||||||
|
return response
|
||||||
|
}).catch((error) => {
|
||||||
|
return error.response;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
34
src/api/LoginApi.jsx
Normal file
34
src/api/LoginApi.jsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import GetCSRF from "./GetCSRF";
|
||||||
|
|
||||||
|
const FRONT_BASE_URL = "https://app.axicon.ir/"
|
||||||
|
const BACK_BASE_URL = "https://api.axicon.ir/"
|
||||||
|
|
||||||
|
const axiosParams = {
|
||||||
|
withCredentials: true,
|
||||||
|
baseURL: BACK_BASE_URL,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': FRONT_BASE_URL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
axios.defaults.withXSRFToken = true
|
||||||
|
|
||||||
|
const axiosInstance = axios.create(axiosParams);
|
||||||
|
|
||||||
|
export default async function LoginApi(email, password) {
|
||||||
|
|
||||||
|
await GetCSRF()
|
||||||
|
return (
|
||||||
|
axiosInstance.post("login",
|
||||||
|
{
|
||||||
|
email: email,
|
||||||
|
password: password
|
||||||
|
}
|
||||||
|
).then((response) => {
|
||||||
|
return response
|
||||||
|
}).catch((error) => {
|
||||||
|
return error.response;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
33
src/api/PublicAPi.jsx
Normal file
33
src/api/PublicAPi.jsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import {goToLoginPage} from "./AdminApi";
|
||||||
|
|
||||||
|
const FRONT_BASE_URL = "https://app.axicon.ir/"
|
||||||
|
const PUBLIC_BASE_URL = "https://pim.liara.run/api/public/"
|
||||||
|
|
||||||
|
const axiosParams = {
|
||||||
|
withCredentials: true,
|
||||||
|
baseURL: PUBLIC_BASE_URL,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': FRONT_BASE_URL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
axios.defaults.withXSRFToken = true
|
||||||
|
const axiosInstance = axios.create(axiosParams);
|
||||||
|
|
||||||
|
const PublicApi = {
|
||||||
|
get: async (url) => {
|
||||||
|
try {
|
||||||
|
const response = await axiosInstance.get(url);
|
||||||
|
return response.data
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response?.status === 419) {
|
||||||
|
goToLoginPage()
|
||||||
|
} else {
|
||||||
|
console.log("error in public get api")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PublicApi;
|
||||||
6
src/api/TokenApi.jsx
Normal file
6
src/api/TokenApi.jsx
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import adminApi from "./AdminApi";
|
||||||
|
|
||||||
|
export const userToken = async () => await adminApi.get('token')
|
||||||
|
.then((data) => {
|
||||||
|
return data
|
||||||
|
})
|
||||||
BIN
src/assets/0jF1GcYgKK4FQ8CkIjW4jXx20m2pC3Q2_cover_580.jpg
Normal file
BIN
src/assets/0jF1GcYgKK4FQ8CkIjW4jXx20m2pC3Q2_cover_580.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
BIN
src/assets/Card4_399895799.jpg
Normal file
BIN
src/assets/Card4_399895799.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
src/assets/fonts/eot/dana-bold.eot
Normal file
BIN
src/assets/fonts/eot/dana-bold.eot
Normal file
Binary file not shown.
BIN
src/assets/fonts/eot/dana-bolditalic.eot
Normal file
BIN
src/assets/fonts/eot/dana-bolditalic.eot
Normal file
Binary file not shown.
BIN
src/assets/fonts/eot/dana-regular.eot
Normal file
BIN
src/assets/fonts/eot/dana-regular.eot
Normal file
Binary file not shown.
BIN
src/assets/fonts/eot/dana-regularitalic.eot
Normal file
BIN
src/assets/fonts/eot/dana-regularitalic.eot
Normal file
Binary file not shown.
BIN
src/assets/fonts/woff/KalamehWebFaNum-Black.woff
Normal file
BIN
src/assets/fonts/woff/KalamehWebFaNum-Black.woff
Normal file
Binary file not shown.
BIN
src/assets/fonts/woff/KalamehWebFaNum-Bold.woff
Normal file
BIN
src/assets/fonts/woff/KalamehWebFaNum-Bold.woff
Normal file
Binary file not shown.
BIN
src/assets/fonts/woff/KalamehWebFaNum-ExtraBold.woff
Normal file
BIN
src/assets/fonts/woff/KalamehWebFaNum-ExtraBold.woff
Normal file
Binary file not shown.
BIN
src/assets/fonts/woff/KalamehWebFaNum-ExtraLight.woff
Normal file
BIN
src/assets/fonts/woff/KalamehWebFaNum-ExtraLight.woff
Normal file
Binary file not shown.
BIN
src/assets/fonts/woff/KalamehWebFaNum-Light.woff
Normal file
BIN
src/assets/fonts/woff/KalamehWebFaNum-Light.woff
Normal file
Binary file not shown.
BIN
src/assets/fonts/woff/KalamehWebFaNum-Medium.woff
Normal file
BIN
src/assets/fonts/woff/KalamehWebFaNum-Medium.woff
Normal file
Binary file not shown.
BIN
src/assets/fonts/woff/KalamehWebFaNum-Regular.woff
Normal file
BIN
src/assets/fonts/woff/KalamehWebFaNum-Regular.woff
Normal file
Binary file not shown.
BIN
src/assets/fonts/woff/KalamehWebFaNum-SemiBold.woff
Normal file
BIN
src/assets/fonts/woff/KalamehWebFaNum-SemiBold.woff
Normal file
Binary file not shown.
BIN
src/assets/fonts/woff/KalamehWebFaNum-Thin.woff
Normal file
BIN
src/assets/fonts/woff/KalamehWebFaNum-Thin.woff
Normal file
Binary file not shown.
BIN
src/assets/fonts/woff/dana-bold.woff
Normal file
BIN
src/assets/fonts/woff/dana-bold.woff
Normal file
Binary file not shown.
BIN
src/assets/fonts/woff/dana-bolditalic.woff
Normal file
BIN
src/assets/fonts/woff/dana-bolditalic.woff
Normal file
Binary file not shown.
BIN
src/assets/fonts/woff/dana-regular.woff
Normal file
BIN
src/assets/fonts/woff/dana-regular.woff
Normal file
Binary file not shown.
BIN
src/assets/fonts/woff/dana-regularitalic.woff
Normal file
BIN
src/assets/fonts/woff/dana-regularitalic.woff
Normal file
Binary file not shown.
BIN
src/assets/fonts/woff2/KalamehWebFaNum-Black.woff2
Normal file
BIN
src/assets/fonts/woff2/KalamehWebFaNum-Black.woff2
Normal file
Binary file not shown.
BIN
src/assets/fonts/woff2/KalamehWebFaNum-Bold.woff2
Normal file
BIN
src/assets/fonts/woff2/KalamehWebFaNum-Bold.woff2
Normal file
Binary file not shown.
BIN
src/assets/fonts/woff2/KalamehWebFaNum-ExtraBold.woff2
Normal file
BIN
src/assets/fonts/woff2/KalamehWebFaNum-ExtraBold.woff2
Normal file
Binary file not shown.
BIN
src/assets/fonts/woff2/KalamehWebFaNum-ExtraLight.woff2
Normal file
BIN
src/assets/fonts/woff2/KalamehWebFaNum-ExtraLight.woff2
Normal file
Binary file not shown.
BIN
src/assets/fonts/woff2/KalamehWebFaNum-Light.woff2
Normal file
BIN
src/assets/fonts/woff2/KalamehWebFaNum-Light.woff2
Normal file
Binary file not shown.
BIN
src/assets/fonts/woff2/KalamehWebFaNum-Medium.woff2
Normal file
BIN
src/assets/fonts/woff2/KalamehWebFaNum-Medium.woff2
Normal file
Binary file not shown.
BIN
src/assets/fonts/woff2/KalamehWebFaNum-Regular.woff2
Normal file
BIN
src/assets/fonts/woff2/KalamehWebFaNum-Regular.woff2
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user