Init(Core): add project to new repo

This commit is contained in:
2026-04-24 18:56:52 +03:30
commit b75246ac0f
195 changed files with 10747 additions and 0 deletions

4
.env Normal file
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

60
package.json Normal file
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

8
public/manifest.json Normal file
View File

@@ -0,0 +1,8 @@
{
"short_name": "نیلی پنل",
"name": "نیلی پنل",
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

3
public/robots.txt Normal file
View File

@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

19
src/App.js Normal file
View 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
View 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
View 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>
);
}

View 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>
);
}

View 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;

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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]'}>&nbsp;&nbsp;{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>
);
}

View 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
}
/>
);
}

View 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>
)
}

View 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>
);
}

View 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>
)
}

View 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>
)
}

View 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>
);
}

View 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;
}

View 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>
);
}

View 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>
)
}

View 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>
);
}

View 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">&laquo;</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">&raquo;</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>
);
}

View 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>
);
}

View 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>
)
}

View 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>
);
}

View 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>
)
}

View 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>
)
}

View 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>
);
}

View 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}
/>
);
});

View 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>
)
}

View 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>
);
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
);
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
);
}

View 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
View File

View 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>
)
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,6 @@
import adminApi from "./AdminApi";
export const userToken = async () => await adminApi.get('token')
.then((data) => {
return data
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More