first commit
This commit is contained in:
1 parent
b27f4f2f23
commit
db10146ecd
58 files changed
+3684
-202
No files matched your search
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "default",
|
||||||
|
"rsc": true,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.ts",
|
||||||
|
"css": "src/app/globals.css",
|
||||||
|
"baseColor": "slate",
|
||||||
|
"cssVariables": true
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils"
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
-1
@@ -1,4 +1,13 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {}
|
const nextConfig = {
|
||||||
|
images: {
|
||||||
|
remotePatterns: [
|
||||||
|
{
|
||||||
|
hostname: '127.0.0.1',
|
||||||
|
port: '8000'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = nextConfig
|
module.exports = nextConfig
|
||||||
Generated
+999
-6
File diff suppressed because it is too large.
Load diff
@@ -9,19 +9,33 @@
|
|||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@radix-ui/react-checkbox": "^1.0.4",
|
||||||
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
|
"@radix-ui/react-separator": "^1.0.3",
|
||||||
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
|
"@radix-ui/react-toast": "^1.1.5",
|
||||||
"@reduxjs/toolkit": "^1.9.7",
|
"@reduxjs/toolkit": "^1.9.7",
|
||||||
|
"@tanstack/react-query": "^4.36.1",
|
||||||
"@types/node": "20.5.1",
|
"@types/node": "20.5.1",
|
||||||
"@types/react": "18.2.20",
|
"@types/react": "18.2.20",
|
||||||
"@types/react-dom": "18.2.7",
|
"@types/react-dom": "18.2.7",
|
||||||
"autoprefixer": "10.4.15",
|
"autoprefixer": "10.4.15",
|
||||||
|
"axios": "^1.5.1",
|
||||||
|
"class-variance-authority": "^0.7.0",
|
||||||
|
"clsx": "^2.0.0",
|
||||||
"eslint": "8.47.0",
|
"eslint": "8.47.0",
|
||||||
"eslint-config-next": "^13.5.4",
|
"eslint-config-next": "^13.5.4",
|
||||||
|
"lucide-react": "^0.298.0",
|
||||||
|
"moment-jalaali": "^0.10.0",
|
||||||
"next": "^13.5.4",
|
"next": "^13.5.4",
|
||||||
"postcss": "8.4.28",
|
"postcss": "8.4.28",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-icons": "^4.12.0",
|
||||||
"react-redux": "^8.1.3",
|
"react-redux": "^8.1.3",
|
||||||
|
"tailwind-merge": "^2.1.0",
|
||||||
"tailwindcss": "3.3.3",
|
"tailwindcss": "3.3.3",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"typescript": "5.1.6"
|
"typescript": "5.1.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
|
||||||
|
import { cookies, headers } from 'next/headers'
|
||||||
|
|
||||||
|
const api_url = 'http://127.0.0.1:8000/';
|
||||||
|
|
||||||
|
const rewrite_url = async (context:any, request:any)=>{
|
||||||
|
|
||||||
|
const { slug } = await context.params
|
||||||
|
const { searchParams } = await new URL(request.url)
|
||||||
|
|
||||||
|
const path = await slug.join("/")
|
||||||
|
|
||||||
|
const url_params = await searchParams.toString()
|
||||||
|
|
||||||
|
const full_url = url_params ? api_url+path+"?"+url_params : api_url+path+"/"
|
||||||
|
|
||||||
|
return full_url
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(request: Request, context: { params: any }) {
|
||||||
|
|
||||||
|
const new_api_url = await rewrite_url(context, request)
|
||||||
|
|
||||||
|
const cookiesList = cookies()
|
||||||
|
const refreshToken = cookiesList.get('refreshToken')
|
||||||
|
const accsessToken = cookiesList.get('accsessToken')
|
||||||
|
|
||||||
|
|
||||||
|
if (!accsessToken || !refreshToken ){ return Response.json('nextjs token not found'); }
|
||||||
|
|
||||||
|
const res = await fetch( new_api_url, {
|
||||||
|
cache: 'no-store',
|
||||||
|
method: request.method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': "Bearer "+accsessToken.value.toString()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await res.json()
|
||||||
|
|
||||||
|
return new Response(JSON.stringify(data),{
|
||||||
|
status: res.status,
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: Request, context: { params: any }) {
|
||||||
|
|
||||||
|
const cookiesList = await cookies()
|
||||||
|
const refreshToken = await cookiesList.get('refreshToken')
|
||||||
|
const accsessToken = await cookiesList.get('accsessToken')
|
||||||
|
|
||||||
|
if (!accsessToken || !refreshToken ){ return Response.json('token not found'); }
|
||||||
|
|
||||||
|
const new_api_url = await rewrite_url(context, request)
|
||||||
|
|
||||||
|
|
||||||
|
const payload = await request.json()
|
||||||
|
|
||||||
|
const res = await fetch( new_api_url, {
|
||||||
|
method: "POST",
|
||||||
|
cache: "no-store",
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': "Bearer "+accsessToken.value.toString()
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await res.json()
|
||||||
|
|
||||||
|
return new Response(JSON.stringify(data),{
|
||||||
|
status: res.status,
|
||||||
|
headers: res.headers
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function PUT(request: Request, context: { params: any }) {
|
||||||
|
|
||||||
|
const new_api_url = await rewrite_url(context, request)
|
||||||
|
|
||||||
|
const cookiesList = await cookies()
|
||||||
|
const refreshToken = await cookiesList.get('refreshToken')
|
||||||
|
const accsessToken = await cookiesList.get('accsessToken')
|
||||||
|
|
||||||
|
if (!accsessToken || !refreshToken ){ return Response.json('token not found'); }
|
||||||
|
|
||||||
|
const payload = await request.json()
|
||||||
|
|
||||||
|
const res = await fetch( new_api_url, {
|
||||||
|
method: request.method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': accsessToken.toString()
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
})
|
||||||
|
|
||||||
|
return new Response(res.body,{
|
||||||
|
status: res.status,
|
||||||
|
headers: res.headers
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function DELETE(request: Request, context: { params: any }) {
|
||||||
|
|
||||||
|
|
||||||
|
console.log("DELETE METHODDDD!!!!!!!!!!")
|
||||||
|
|
||||||
|
const new_api_url = await rewrite_url(context, request)
|
||||||
|
|
||||||
|
const cookiesList = await cookies()
|
||||||
|
const refreshToken = await cookiesList.get('refreshToken')
|
||||||
|
const accsessToken = await cookiesList.get('accsessToken')
|
||||||
|
|
||||||
|
if (!accsessToken || !refreshToken ){ return Response.json('token not found'); }
|
||||||
|
|
||||||
|
const res = await fetch( new_api_url, {
|
||||||
|
cache: 'no-store',
|
||||||
|
method: request.method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': "Bearer "+accsessToken.value.toString()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return new Response(res.body,{
|
||||||
|
status: res.status,
|
||||||
|
headers: res.headers
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function PATCH(request: Request, context: { params: any }) {
|
||||||
|
|
||||||
|
const cookiesList = await cookies()
|
||||||
|
const refreshToken = await cookiesList.get('refreshToken')
|
||||||
|
const accsessToken = await cookiesList.get('accsessToken')
|
||||||
|
|
||||||
|
if (!accsessToken || !refreshToken ){ return Response.json('token not found'); }
|
||||||
|
|
||||||
|
const new_api_url = await rewrite_url(context, request)
|
||||||
|
|
||||||
|
const payload = await request.json()
|
||||||
|
|
||||||
|
const res = await fetch( new_api_url, {
|
||||||
|
method: request.method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': accsessToken.toString()
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
})
|
||||||
|
|
||||||
|
return new Response(res.body,{
|
||||||
|
status: res.status,
|
||||||
|
headers: res.headers
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
export async function POST(request: Request, context: { params: any }) {
|
||||||
|
|
||||||
|
const payload = await request.json()
|
||||||
|
|
||||||
|
const res = await fetch( "http://127.0.0.1:8000/auth/login/mobile/", {
|
||||||
|
method: "POST",
|
||||||
|
cache: "no-store",
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await res.json()
|
||||||
|
|
||||||
|
return new Response(JSON.stringify(data),{
|
||||||
|
status: res.status,
|
||||||
|
headers: res.headers
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
export async function POST(request: Request, context: { params: any }) {
|
||||||
|
|
||||||
|
const payload = await request.json()
|
||||||
|
|
||||||
|
console.log("verifyyyyy 11")
|
||||||
|
|
||||||
|
const res = await fetch( "http://127.0.0.1:8000/auth/login/mobile/verify/", {
|
||||||
|
method: "POST",
|
||||||
|
cache: "no-store",
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log("verifyyyyy 22")
|
||||||
|
|
||||||
|
const data = await res.json()
|
||||||
|
|
||||||
|
console.log(res)
|
||||||
|
|
||||||
|
console.log(data)
|
||||||
|
|
||||||
|
return new Response(JSON.stringify(data),{
|
||||||
|
status: res.status,
|
||||||
|
headers: res.headers
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { cookies } from 'next/headers'
|
||||||
|
|
||||||
|
|
||||||
|
export async function GET(request: Request, context: { params: any }) {
|
||||||
|
|
||||||
|
|
||||||
|
//return Response.json("aaaa")
|
||||||
|
|
||||||
|
const cookiesList = await cookies()
|
||||||
|
const refreshToken = await cookiesList.get('refreshToken')
|
||||||
|
|
||||||
|
|
||||||
|
const res = await fetch( "http://localhost:8000/auth/token/refresh/", {
|
||||||
|
method: "POST",
|
||||||
|
cache: "no-store",
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
refresh: refreshToken?.value
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const data = await res.json()
|
||||||
|
|
||||||
|
if ('access' in data && 'refresh' in data){
|
||||||
|
|
||||||
|
await cookies().set({
|
||||||
|
name: 'accsessToken',
|
||||||
|
value:data.access,
|
||||||
|
httpOnly: true,
|
||||||
|
path: '/',
|
||||||
|
})
|
||||||
|
|
||||||
|
cookies().set({
|
||||||
|
name: 'refreshToken',
|
||||||
|
value:data.refresh,
|
||||||
|
httpOnly: true,
|
||||||
|
path: '/',
|
||||||
|
})
|
||||||
|
|
||||||
|
cookies().set({
|
||||||
|
name: 'isAuth',
|
||||||
|
value:'true',
|
||||||
|
path: '/',
|
||||||
|
})
|
||||||
|
|
||||||
|
return new Response(JSON.stringify(data),{
|
||||||
|
status: res.status,
|
||||||
|
headers: res.headers
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
|
||||||
|
/*cookies().set({
|
||||||
|
name: 'isAuth',
|
||||||
|
value:'false',
|
||||||
|
path: '/',
|
||||||
|
})*/
|
||||||
|
|
||||||
|
return new Response(JSON.stringify(data),{
|
||||||
|
status: res.status,
|
||||||
|
headers: res.headers
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { cookies } from 'next/headers'
|
||||||
|
|
||||||
|
|
||||||
|
export async function POST(request: Request, context: { params: any }) {
|
||||||
|
|
||||||
|
const payload = await request.json()
|
||||||
|
|
||||||
|
const res = await fetch( "http://127.0.0.1:8000/auth/token/", {
|
||||||
|
method: "POST",
|
||||||
|
cache: "no-store",
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await res.json()
|
||||||
|
|
||||||
|
if ('access' in data && 'refresh' in data){
|
||||||
|
|
||||||
|
cookies().set({
|
||||||
|
name: 'accsessToken',
|
||||||
|
value:data.access,
|
||||||
|
httpOnly: true,
|
||||||
|
path: '/',
|
||||||
|
})
|
||||||
|
|
||||||
|
cookies().set({
|
||||||
|
name: 'refreshToken',
|
||||||
|
value:data.refresh,
|
||||||
|
httpOnly: true,
|
||||||
|
path: '/',
|
||||||
|
})
|
||||||
|
|
||||||
|
cookies().set({
|
||||||
|
name: 'isAuth',
|
||||||
|
value:'true',
|
||||||
|
path: '/',
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(JSON.stringify(data),{
|
||||||
|
status: res.status,
|
||||||
|
headers: res.headers
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { cookies } from 'next/headers'
|
||||||
|
|
||||||
|
|
||||||
|
export async function GET(request: Request, context: { params: any }) {
|
||||||
|
|
||||||
|
cookies().set('accsessToken', '78541514545')
|
||||||
|
cookies().set('refreshToken', '545647565')
|
||||||
|
return Response.json("set shod")
|
||||||
|
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+80
-28
@@ -3,63 +3,115 @@ import type { Metadata } from 'next'
|
|||||||
import { Inter } from 'next/font/google'
|
import { Inter } from 'next/font/google'
|
||||||
import HamberMenu from '../components/layout/hamber-menu'
|
import HamberMenu from '../components/layout/hamber-menu'
|
||||||
import { headers } from 'next/headers';
|
import { headers } from 'next/headers';
|
||||||
import OrderContextProvider from '@/context/OrderContextProvider';
|
|
||||||
import ReduxProvider from '../redux/provider';
|
import ReduxProvider from '../redux/provider';
|
||||||
|
import ReactQueryProvider from '../react_query/provider';
|
||||||
|
import FetchCart from '../components/layout/shop/fetch-cart';
|
||||||
|
import FetchCartProvider from '../components/layout/shop/fetch-cart';
|
||||||
|
import Link from 'next/link'
|
||||||
|
import MainLayoutUtil from '../components/layout/main-layout-util';
|
||||||
|
import localFont from 'next/font/local'
|
||||||
|
import { Toaster } from "@/components/ui/toaster"
|
||||||
|
import { FaBasketShopping, FaClipboardList } from "react-icons/fa6";
|
||||||
|
import { SiGooglemessages } from "react-icons/si";
|
||||||
|
|
||||||
|
|
||||||
const inter = Inter({ subsets: ['latin'] })
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Fesahat CRM',
|
title: 'Fesahat CRM',
|
||||||
description: 'Perfect CRM software',
|
description: 'Perfect CRM software',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const myFont = localFont({
|
||||||
|
src: './fonts/woff2/IRANSansX-Regular.woff2',
|
||||||
|
display: 'swap',
|
||||||
|
})
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
|
|
||||||
const headersList = headers();
|
|
||||||
const activePath = headersList.get('x-invoke-path');
|
|
||||||
//const navigation = use
|
//const navigation = use
|
||||||
return (
|
return (
|
||||||
<html lang="fa" dir='rtl'>
|
<html lang="fa" dir='rtl' className={myFont.className}>
|
||||||
<body>
|
<body className='bg-slate-200'>
|
||||||
<div className='flex md:flex-row'>
|
<div className='flex md:flex-row'>
|
||||||
|
|
||||||
<div className='hidden md:block basis-[9rem] h-screen fixed top-0 right-0 bg-slate-700 text-white'>
|
<div className='hidden md:block basis-[9rem] h-screen fixed top-0 right-0 bg-slate-700 text-white'>
|
||||||
|
|
||||||
<ul className='flex flex-col list-none text-center'>
|
<ul className='flex flex-col list-none text-center'>
|
||||||
<li className='border-b-[1px] border-slate-800 p-4'>صفحه اصلی</li>
|
|
||||||
<li className='border-b-[1px] border-slate-800 p-4'>سفارشات</li>
|
<li className='border-b-[1px] border-slate-800 p-4'>
|
||||||
<li className='border-b-[1px] border-slate-800 p-4'>تیکت ها</li>
|
<Link href="/shop/">
|
||||||
|
|
||||||
|
<div className='flex flex-row gap-x-2'>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<FaBasketShopping size={25} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
ثبت سفارش
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li className='border-b-[1px] border-slate-800 p-4'>
|
||||||
|
|
||||||
|
<Link href="/orders">
|
||||||
|
|
||||||
|
<div className='flex flex-row gap-x-2'>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<FaClipboardList size={25} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
سفارشات
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li className='border-b-[1px] border-slate-800 p-4'>
|
||||||
|
<Link href={"/tickets/"}>
|
||||||
|
|
||||||
|
<div className='flex flex-row gap-x-2'>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<SiGooglemessages size={25} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
تیکت ها
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
{/*}
|
||||||
<li className='border-b-[1px] border-slate-800 p-4'>چک ها</li>
|
<li className='border-b-[1px] border-slate-800 p-4'>چک ها</li>
|
||||||
<li className='border-b-[1px] border-slate-800 p-4'>واریزی ها</li>
|
<li className='border-b-[1px] border-slate-800 p-4'>واریزی ها</li>
|
||||||
<li className='border-b-[1px] border-slate-800 p-4'>صورت حساب</li>
|
<li className='border-b-[1px] border-slate-800 p-4'>صورت حساب</li>
|
||||||
|
*/}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='basis-full md:mr-[7.6rem] lg:ml-[20rem]'>
|
<MainLayoutUtil>
|
||||||
<div className='w-full h-[2.5rem] flex flex-row bg-gray-800 p-2 text-white'>
|
{children}
|
||||||
|
</MainLayoutUtil>
|
||||||
<HamberMenu />
|
|
||||||
|
|
||||||
<div className='pr-4'>
|
|
||||||
محمد جواد فصاحت
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<OrderContextProvider>
|
|
||||||
<ReduxProvider>
|
|
||||||
{children}
|
|
||||||
</ReduxProvider>
|
|
||||||
</OrderContextProvider>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Toaster />
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import axiosInstance from '@/axios/axiosInstance'
|
||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
interface OrderItemsInterface {
|
||||||
|
id: number,
|
||||||
|
created_at: string,
|
||||||
|
updated_at: string,
|
||||||
|
product_name: string,
|
||||||
|
price: string,
|
||||||
|
amount: number,
|
||||||
|
order: number,
|
||||||
|
product: number,
|
||||||
|
category_name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const Page = ({params}:{params: { id: number }})=>{
|
||||||
|
|
||||||
|
const [orderItems, setOrderItems] = useState<[OrderItemsInterface] | [] >([])
|
||||||
|
|
||||||
|
const getOrderIteams = useQuery({ queryKey:['order-'+params.id.toString()], queryFn:()=>{
|
||||||
|
return axiosInstance.get(`shop/order/iteams/list/${params.id.toString()}/`)
|
||||||
|
|
||||||
|
},onSuccess:(result)=>{
|
||||||
|
setOrderItems(result.data)
|
||||||
|
},})
|
||||||
|
|
||||||
|
if (getOrderIteams.isLoading){
|
||||||
|
return (
|
||||||
|
<p className='text-center'>در حال دریافت اطلاعات</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getOrderIteams.isError){
|
||||||
|
return (
|
||||||
|
<p className='text-center'>
|
||||||
|
خطا در دریافت اطلاعات
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="flex bg-white flex-col w-full md:w-6/12 border-black border-2 mx-auto mt-4 rounded-2xl p-2 gap-y-3">
|
||||||
|
<div className="basis-full"> شماره سفارش {params.id}</div>
|
||||||
|
|
||||||
|
<div className='overflow-x-auto basis-full'>
|
||||||
|
<table className="border-collapse table-auto border border-slate-400 text-center">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="border border-slate-300 w-full md:min-w-[400px]">نام محصول</th>
|
||||||
|
<th className="border border-slate-300 md:w-[150px]">سایز</th>
|
||||||
|
<th className="border border-slate-300 md:w-[150px]">تعداد</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
{getOrderIteams.isSuccess ? orderItems.map((item, key)=>{
|
||||||
|
return (
|
||||||
|
|
||||||
|
<tr key={key}>
|
||||||
|
<td className="border border-slate-300">{item.category_name}</td>
|
||||||
|
<td className="border border-slate-300">{item.product_name}</td>
|
||||||
|
<td className="border border-slate-300">{item.amount}</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
)
|
||||||
|
}): null}
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Page
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import axiosInstance from "@/axios/axiosInstance"
|
||||||
|
import { useQuery } from "@tanstack/react-query"
|
||||||
|
import { useState } from 'react'
|
||||||
|
import moment from "moment-jalaali";
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
const Page = ()=>{
|
||||||
|
|
||||||
|
const [orders, setOrders] = useState<[] | null>(null)
|
||||||
|
|
||||||
|
const ordersFetch = useQuery({queryKey:['ordersData',], queryFn: ()=>{
|
||||||
|
return axiosInstance.get('shop/order/list/')
|
||||||
|
}, onSuccess:(result)=>{
|
||||||
|
if(result.status == 200){
|
||||||
|
console.log(result)
|
||||||
|
setOrders(result.data);
|
||||||
|
}
|
||||||
|
}})
|
||||||
|
|
||||||
|
if(ordersFetch.isLoading) {
|
||||||
|
return (
|
||||||
|
<p className="text-center mt-4">درحال دریافت اطلاعات</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(ordersFetch.isError) {
|
||||||
|
<p className="text-center mt-4">خطا در دریافت اطلاعات</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="flex bg-white flex-col w-full md:w-6/12 border-black border-2 mx-auto mt-4 rounded-2xl p-2 gap-y-3">
|
||||||
|
<div className="basis-full">سوابق سفارشات</div>
|
||||||
|
|
||||||
|
{orders ? orders.map((iteam, key)=>{
|
||||||
|
return (
|
||||||
|
<Link href={`/orders/${iteam.id}/`}>
|
||||||
|
<div key={key} className="basis-full border-gray-100 border-2 p-2 text-center rounded-xl bg-slate-200">
|
||||||
|
|
||||||
|
<span className="ml-2">تاریخ</span>
|
||||||
|
|
||||||
|
{ moment(iteam.created_at).format('jYYYY/jM/jD') }
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
)
|
||||||
|
}) : null}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Page
|
||||||
@@ -13,28 +13,14 @@ export default function ShopLayout({
|
|||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<div className='flex flex-row'>
|
<div className='flex flex-row'>
|
||||||
<div className='basis-full pt-2 pr-2 pl-2'>
|
<div className='basis-full pt-2 pr-2 pl-2'>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
<div className='hidden bg-slate-200 lg:block w-[20rem] h-screen border-r-[1px] fixed top-0 left-0'>
|
|
||||||
<h3 className='text-center bg-slate-600 p-4 text-white'>سبد خرید شما</h3>
|
|
||||||
|
|
||||||
<div className='w-full flex flex-col gap-y-2 pt-2 pr-1 pl-1 h-screen overflow-auto pb-44'>
|
<OrdersList />
|
||||||
<OrdersList />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div className='absolute bg-slate-200 bottom-0 right-0 w-full p-4'>
|
|
||||||
|
|
||||||
<p className='text-center mb-2 text-gray-500'>مشاهده کامل سبد خرید</p>
|
|
||||||
<div className='w-full bg-green-900 text-white mb-2 rounded text-center pt-2 pb-2'>
|
|
||||||
ثبت نهایی
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,43 +1,45 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
import ProductCategoryBox from '@/app/components/layout/shop/products-cat'
|
import ProductCategoryBox from '@/app/components/layout/shop/products-cat'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
|
import axiosInstance from '@/axios/axiosInstance'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useQuery,useQueryClient } from '@tanstack/react-query'
|
||||||
|
|
||||||
|
const Page = ()=>{
|
||||||
|
|
||||||
|
const [category, setCategory] = useState([])
|
||||||
|
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
const query = useQuery({ queryKey: ['categoryProducts'], queryFn: async ()=>{
|
||||||
|
return await axiosInstance.get("shop/v2/product-category/list/")
|
||||||
|
},refetchOnWindowFocus:false, onSuccess(result:any) {
|
||||||
|
setCategory(result.data)
|
||||||
|
}, })
|
||||||
|
|
||||||
|
if(query.isLoading){
|
||||||
|
return (
|
||||||
|
<p className='text-center'>درحال دریافت اطلاعات</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(query.isError){
|
||||||
|
return (
|
||||||
|
<p className='text-center'>
|
||||||
|
خطا در دریافت اطلاعات
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const Page = ()=>{
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="grid grid-cols-2 min-[1350px]:grid-cols-5 min-[767px]:grid-cols-3 min-[1100px]:grid-cols-4 gap-x-2 gap-y-2">
|
<div className="grid grid-cols-2 min-[1350px]:grid-cols-5 min-[767px]:grid-cols-3 min-[1100px]:grid-cols-4 gap-x-2 gap-y-2">
|
||||||
|
|
||||||
<ProductCategoryBox />
|
{query.isSuccess?category.map((iteam:any)=>{
|
||||||
<ProductCategoryBox />
|
return <ProductCategoryBox key={iteam.id} iteam={iteam} />
|
||||||
<ProductCategoryBox />
|
}):null}
|
||||||
<ProductCategoryBox />
|
|
||||||
<ProductCategoryBox />
|
|
||||||
<ProductCategoryBox />
|
|
||||||
<ProductCategoryBox />
|
|
||||||
<ProductCategoryBox />
|
|
||||||
<ProductCategoryBox />
|
|
||||||
<ProductCategoryBox />
|
|
||||||
<ProductCategoryBox />
|
|
||||||
|
|
||||||
<ProductCategoryBox />
|
|
||||||
<ProductCategoryBox />
|
|
||||||
<ProductCategoryBox />
|
|
||||||
<ProductCategoryBox />
|
|
||||||
<ProductCategoryBox />
|
|
||||||
<ProductCategoryBox />
|
|
||||||
<ProductCategoryBox />
|
|
||||||
<ProductCategoryBox />
|
|
||||||
<ProductCategoryBox />
|
|
||||||
<ProductCategoryBox />
|
|
||||||
<ProductCategoryBox />
|
|
||||||
<ProductCategoryBox />
|
|
||||||
<ProductCategoryBox />
|
|
||||||
<ProductCategoryBox />
|
|
||||||
<ProductCategoryBox />
|
|
||||||
<ProductCategoryBox />
|
|
||||||
<ProductCategoryBox />
|
|
||||||
<ProductCategoryBox />
|
|
||||||
<ProductCategoryBox />
|
|
||||||
<ProductCategoryBox />
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,46 +3,72 @@ import Products from '@/app/components/layout/shop/products'
|
|||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
|
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import axiosInstance from '@/axios/axiosInstance'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
|
||||||
const Page = ()=>{
|
const Page = ({ params }: { params: { id: string } })=>{
|
||||||
const router = useRouter()
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="mx-auto w-full md:w-[35rem] border-[1px] rounded p-2 shadow">
|
|
||||||
<div className='flex justify-between border-b-[1px] pb-2'>
|
|
||||||
|
|
||||||
<span>
|
|
||||||
سیفون پایه دار سیفون پایه دار
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span onClick={()=>{ router.back() }} className='cursor-pointer bg-cyan-700 pr-1 pl-1 text-white text-[0.9rem] rounded'>بازگشت</span>
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [products, setProducts] = useState<{category_name: string, category_img: string, products:[{id:number, name:string, price: number}]}>()
|
||||||
|
|
||||||
|
const query = useQuery({ queryKey: ['productListByCat'], queryFn: async ()=>{
|
||||||
|
return await axiosInstance.get(`shop/product/list/${params.id}/`)
|
||||||
|
}, onSuccess(data:any) {
|
||||||
|
setProducts(data.data)
|
||||||
|
}, });
|
||||||
|
|
||||||
|
if(query.isLoading){
|
||||||
|
return (
|
||||||
|
<p className='text-center'>درحال دریافت اطلاعات</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if(query.isError){
|
||||||
|
return(<p className='text-center'>خطا در دریافت اطلاعات</p>)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(query.isSuccess){
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="mx-auto bg-white w-full md:w-[35rem] border-[1px] rounded p-2 shadow">
|
||||||
|
<div className='flex justify-between border-b-[1px] pb-2'>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
{products?.category_name}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span onClick={()=>{ router.back() }} className='cursor-pointer bg-cyan-700 pr-1 pl-1 text-white text-[0.9rem] rounded'>بازگشت</span>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div className='w-full'>
|
||||||
|
<Image className='mx-auto' alt="sdf" src={`http://127.0.0.1:8000/${products?.category_img}`} height={100} width={100} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='grid grid-cols-2 gap-2 mt-2'>
|
||||||
|
|
||||||
|
{products?.products.map((iteam)=>{
|
||||||
|
return (<Products key={iteam.id} iteam={iteam} />)
|
||||||
|
})}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div onClick={()=>{ router.back() }} className='w-full text-center bg-gray-800 text-white rounded mt-2 p-2'>
|
||||||
|
<span>بازگشت به صفحه قبل</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
<div className='w-full'>
|
|
||||||
<Image className='mx-auto' alt="sdf" src={"/zano45-9.png"} height={100} width={100} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='grid grid-cols-2 gap-2 mt-2'>
|
|
||||||
|
|
||||||
<Products />
|
|
||||||
<Products />
|
|
||||||
<Products />
|
|
||||||
<Products />
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div onClick={()=>{ router.back() }} className='w-full text-center bg-gray-800 text-white rounded mt-2 p-2'>
|
|
||||||
<span>بازگشت به صفحه قبل</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Page
|
export default Page
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { Input, } from "@/components/ui/input"
|
||||||
|
import { Label } from "@/components/ui/label"
|
||||||
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import axiosInstance from '@/axios/axiosInstance'
|
||||||
|
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { useState, useRef } from 'react'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { IoMdAddCircle } from "react-icons/io";
|
||||||
|
import moment from "moment-jalaali";
|
||||||
|
|
||||||
|
|
||||||
|
const TicketIDPage = ({params}:{params: {id: number} })=>{
|
||||||
|
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
const desc = useRef()
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const [ticketComments, setTicketComments] = useState<[]>([])
|
||||||
|
|
||||||
|
const [ufiles, setUFiles] = useState<any>([])
|
||||||
|
const [uploadCounter, setUploadCounter] = useState<any>(['1', '1', '1'])
|
||||||
|
|
||||||
|
|
||||||
|
const fileHandeler = async (e:any)=>{
|
||||||
|
const file = await e.target.files[0];
|
||||||
|
setUFiles((old:any)=>[...old, file])
|
||||||
|
}
|
||||||
|
|
||||||
|
const addUpload = ()=>{
|
||||||
|
setUploadCounter((old:any)=>[...old, '1'])
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = useQuery({ queryKey:['ticket-'+params.id.toString()], queryFn:()=>{
|
||||||
|
return axiosInstance.get(`ticket/${params.id.toString()}/`)
|
||||||
|
|
||||||
|
},onSuccess:(result)=>{
|
||||||
|
setTicketComments(result.data)
|
||||||
|
},})
|
||||||
|
|
||||||
|
|
||||||
|
const addComment = async ()=>{
|
||||||
|
|
||||||
|
const data = new FormData()
|
||||||
|
data.append('desc', desc.current ? desc.current.value : null)
|
||||||
|
|
||||||
|
await ufiles.map( async (val:any, key:any)=>{
|
||||||
|
await data.append('files', val ? val : null)
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await axiosInstance.post(`ticket/${params.id.toString()}/new/`, data , {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if(result.status == 201){
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['ticket-'+params.id.toString()] })
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const goBack = ()=>{
|
||||||
|
router.push(`/tickets/`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.isLoading){
|
||||||
|
return (
|
||||||
|
<p className='text-center'>در حال دریافت اطلاعات</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.isError){
|
||||||
|
return (
|
||||||
|
<p className='text-center'>
|
||||||
|
خطا در دریافت اطلاعات
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
|
||||||
|
<div className="w-full md:w-[60%] lg:w-[50%] m-auto rounded-md" dir="rtl">
|
||||||
|
|
||||||
|
<div className="mt-7 bg-white p-4 rounded-md">
|
||||||
|
|
||||||
|
<div className="mb-4 w-full">
|
||||||
|
<Label htmlFor="title">عنوان</Label>
|
||||||
|
<span className="bg-sky-950 text-white w-full block p-2 rounded-md">{ticketComments[0] ? ticketComments[0].ticket.title : null}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{ticketComments.map((item, key)=>{
|
||||||
|
return (
|
||||||
|
<div key={key} className="mb-4 w-full">
|
||||||
|
<Label htmlFor="title">
|
||||||
|
{item.cuser_role.name_family}
|
||||||
|
</Label>
|
||||||
|
<div className={`${item.cuser_role.role_type == 3 ? 'bg-gray-100': 'bg-green-100'} w-full block p-2 rounded-r-lg rounded-tl-lg`}>
|
||||||
|
<div className="pb-2">
|
||||||
|
{item.desc}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{item.comment_files.length >= 1 ? (
|
||||||
|
|
||||||
|
<div className="border-t-2 mt-3 pt-2">
|
||||||
|
<p className="text-sm">عکس و فایل ها:</p>
|
||||||
|
<div className="flex flex-row gap-x-2">
|
||||||
|
{item.comment_files.map((file:any, key:any)=>{
|
||||||
|
return <Link target="_blank" key={key} className="bg-green-900 rounded-lg text-white pr-2 pl-2 pt-1 pb-0" href={`http://127.0.0.1:8000/${file.file}`}>{`${ decodeURIComponent(file.file.split('/').slice(-1)) }`}</Link>
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
): null}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="datetime flex flex-row justify-end">
|
||||||
|
<span className="bg-slate-100 rounded-b-lg p-1 text-[0.8rem]">{moment(item.created_at).format('jYYYY/jM/jD H:m')}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div className="border-t-2 border-black mb-8 mt-8"></div>
|
||||||
|
<Label htmlFor="title">پاسخ</Label>
|
||||||
|
|
||||||
|
<Textarea ref={desc} id="matn" />
|
||||||
|
|
||||||
|
<div className="mb-2 mt-4">
|
||||||
|
<Label>عکس یا فایل</Label>
|
||||||
|
{uploadCounter.map((value:any, key:any)=>{
|
||||||
|
return (
|
||||||
|
<div className="flex flex-row justify-between gap-x-4 mb-2">
|
||||||
|
<Input onChange={(e)=>{fileHandeler(e)}} type="file" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div onClick={addUpload} className="">
|
||||||
|
<IoMdAddCircle size="2rem" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row justify-end mt-2">
|
||||||
|
<Button onClick={addComment} className="bg-slate-900 text-white hover:bg-slate-800 hover:text-white" variant="outline">ثبت</Button>
|
||||||
|
<Button onClick={goBack} className="bg-slate-600 text-white hover:bg-slate-800 hover:text-white" variant="outline">بازگشت</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TicketIDPage
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
'use client'
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { Label } from "@/components/ui/label"
|
||||||
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Separator } from "@/components/ui/separator"
|
||||||
|
import Link from 'next/link'
|
||||||
|
import axiosInstance from "@/axios/axiosInstance"
|
||||||
|
import { useRef, useState } from "react"
|
||||||
|
import { useToast } from "@/components/ui/use-toast"
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
import { IoMdAddCircle } from "react-icons/io";
|
||||||
|
|
||||||
|
|
||||||
|
const NewTicketPage = ()=>{
|
||||||
|
|
||||||
|
const title = useRef('')
|
||||||
|
const desc = useRef('')
|
||||||
|
const { toast } = useToast()
|
||||||
|
const router = useRouter()
|
||||||
|
const [ufiles, setUFiles] = useState<any>([])
|
||||||
|
const [uploadCounter, setUploadCounter] = useState<any>(['1', '1', '1'])
|
||||||
|
|
||||||
|
|
||||||
|
const fileHandeler = async (e:any)=>{
|
||||||
|
const file = await e.target.files[0];
|
||||||
|
setUFiles((old:any)=>[...old, file])
|
||||||
|
}
|
||||||
|
|
||||||
|
const addUpload = ()=>{
|
||||||
|
setUploadCounter((old:any)=>[...old, '1'])
|
||||||
|
}
|
||||||
|
const submitTicket = async ()=>{
|
||||||
|
|
||||||
|
|
||||||
|
const data = new FormData()
|
||||||
|
data.append('title', title.current ? title.current.value : null)
|
||||||
|
data.append('desc', desc.current ? desc.current.value : null)
|
||||||
|
|
||||||
|
await ufiles.map( async (val:any, key:any)=>{
|
||||||
|
await data.append('files', val ? val : null)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const result = await axiosInstance.post('ticket/create/', data, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if(result && result.status == 201){
|
||||||
|
|
||||||
|
alert("با موفقیت ثبت شد")
|
||||||
|
|
||||||
|
router.push(`/tickets/${result.data.id}`)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
alert("خطا در ثبت")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
|
||||||
|
<>
|
||||||
|
<div className="w-full md:w-[60%] lg:w-[50%] m-auto bg-white rounded-md" dir="rtl">
|
||||||
|
|
||||||
|
<div className="mt-7 p-4">
|
||||||
|
|
||||||
|
<p className="font-bold text-center" >ثبت تیکت جدید</p>
|
||||||
|
<p className="text-sm text-center">خواهشمند است هرگونه، درخواست، سوال، پیگیری، انتقاد یا پیشنهاد را از طریق تیکت ثبت کنید</p>
|
||||||
|
|
||||||
|
<Separator className="mt-4 mb-4" />
|
||||||
|
|
||||||
|
<div className="mb-10">
|
||||||
|
<Label>عنوان</Label>
|
||||||
|
<Input ref={title} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-8">
|
||||||
|
<Label>توضیحات</Label>
|
||||||
|
<Textarea ref={desc} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-2">
|
||||||
|
<Label>عکس یا فایل</Label>
|
||||||
|
{uploadCounter.map((value:any, key:any)=>{
|
||||||
|
return (
|
||||||
|
<div className="flex flex-row justify-between gap-x-4 mb-2">
|
||||||
|
<Input onChange={(e)=>{fileHandeler(e)}} type="file" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div onClick={addUpload} className="">
|
||||||
|
<IoMdAddCircle size="2rem" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div className="text-center flex flex-row justify-center gap-x-2">
|
||||||
|
|
||||||
|
<Button onClick={submitTicket}>ثبت تیکت</Button>
|
||||||
|
<Link href={"/tickets/"}>
|
||||||
|
<Button className="bg-gray-700">لغو</Button>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NewTicketPage
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCaption,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table"
|
||||||
|
|
||||||
|
import { BiSolidShow, } from "react-icons/bi";
|
||||||
|
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
import axiosInstance from "@/axios/axiosInstance"
|
||||||
|
import { useQuery } from "@tanstack/react-query"
|
||||||
|
import { useState } from 'react'
|
||||||
|
import moment from "moment-jalaali";
|
||||||
|
|
||||||
|
|
||||||
|
const TicketPage = ()=>{
|
||||||
|
|
||||||
|
const [tickets, setTickets] = useState([])
|
||||||
|
|
||||||
|
const ticketsFetch = useQuery({queryKey:['ticketListData',], queryFn: ()=>{
|
||||||
|
return axiosInstance.get('ticket/list/')
|
||||||
|
|
||||||
|
}, onSuccess:(result)=>{
|
||||||
|
if(result.status == 200){
|
||||||
|
console.log(result.data)
|
||||||
|
setTickets(result.data);
|
||||||
|
}
|
||||||
|
}})
|
||||||
|
|
||||||
|
if(ticketsFetch.isLoading) {
|
||||||
|
return (
|
||||||
|
<p className="text-center mt-4">درحال دریافت اطلاعات</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(ticketsFetch.isError) {
|
||||||
|
<p className="text-center mt-4">خطا در دریافت اطلاعات</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
|
||||||
|
<>
|
||||||
|
<div className="w-full md:w-[60%] lg:w-[50%] m-auto bg-white rounded-md">
|
||||||
|
<div className="mt-7 flex flex-row justify-between p-2 items-center">
|
||||||
|
<p className="text-sm">
|
||||||
|
هرگونه سوال و یا درخواست را فقط از طریق تیکت مطرح کنید تا قابل پیگیری و استناد باشد
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Link href={"/tickets/new/"}>
|
||||||
|
<Button className="bg-green-800">ثبت تیکت جدید</Button>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full md:w-[60%] lg:w-[50%] m-auto bg-white rounded-md" dir="rtl">
|
||||||
|
|
||||||
|
<div className="mt-4">
|
||||||
|
<Table>
|
||||||
|
<TableCaption>لیست تیکت ها</TableCaption>
|
||||||
|
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="w-[100px] text-center">شماره</TableHead>
|
||||||
|
<TableHead className="text-center">عنوان</TableHead>
|
||||||
|
<TableHead className="text-center">وضعیت</TableHead>
|
||||||
|
<TableHead className="text-center">تاریخ</TableHead>
|
||||||
|
<TableHead className="text-center">مشاهده</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
|
||||||
|
<TableBody>
|
||||||
|
|
||||||
|
{tickets ? tickets.map((iteam, key)=>{
|
||||||
|
return (
|
||||||
|
<TableRow className={`${ ( (key+1) % 2) == 0 ? 'bg-gray-100' : ''}`}>
|
||||||
|
|
||||||
|
<TableCell className="font-medium text-center">{key+1}</TableCell>
|
||||||
|
<TableCell className="text-center">
|
||||||
|
{iteam.title}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-center">
|
||||||
|
{iteam.status == 0 ? 'در انتظار پاسخ': null}
|
||||||
|
{iteam.status == 1 ? 'پاسخ مشتری': null}
|
||||||
|
{iteam.status == 2 ? 'پاسخ کارشناس': null}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-center">{moment(iteam.created_at).format('jYYYY/jM/jD')}</TableCell>
|
||||||
|
|
||||||
|
<TableCell className="text-center flex justify-center">
|
||||||
|
<Link href={"/tickets/"+iteam.id}>
|
||||||
|
<BiSolidShow size={25} color={'black'} />
|
||||||
|
</Link>
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
</TableRow>
|
||||||
|
)
|
||||||
|
}): null }
|
||||||
|
|
||||||
|
|
||||||
|
</TableBody>
|
||||||
|
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TicketPage
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,6 +1,8 @@
|
|||||||
import '../globals.css'
|
import '../globals.css'
|
||||||
import type { Metadata } from 'next'
|
import type { Metadata } from 'next'
|
||||||
import { Inter } from 'next/font/google'
|
import { Inter } from 'next/font/google'
|
||||||
|
import ReduxProvider from '../redux/provider'
|
||||||
|
import localFont from 'next/font/local'
|
||||||
|
|
||||||
const inter = Inter({ subsets: ['latin'] })
|
const inter = Inter({ subsets: ['latin'] })
|
||||||
|
|
||||||
@@ -9,16 +11,23 @@ export const metadata: Metadata = {
|
|||||||
description: 'Perfect CRM software',
|
description: 'Perfect CRM software',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const myFont = localFont({
|
||||||
|
src: './fonts/woff2/IRANSansX-Regular.woff2',
|
||||||
|
display: 'swap',
|
||||||
|
})
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<html lang="fa" dir='rtl'>
|
<html lang="fa" dir='rtl' className={myFont.className}>
|
||||||
<body>
|
<body>
|
||||||
<div className="flex bg-slate-200 h-screen items-center">
|
<div className="flex bg-slate-200 h-screen items-center">
|
||||||
{children}
|
<ReduxProvider>
|
||||||
|
{children}
|
||||||
|
</ReduxProvider>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,6 +1,48 @@
|
|||||||
import { Fragment } from "react"
|
'use client'
|
||||||
|
|
||||||
|
import { Fragment, useState } from "react"
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
import { useDispatch } from 'react-redux'
|
||||||
|
import { addMobile } from "@/app/redux/features/mobileoptSlice"
|
||||||
|
|
||||||
|
|
||||||
const Page = ()=>{
|
const Page = ()=>{
|
||||||
|
const [username, setUserName] = useState()
|
||||||
|
const [password, setPassword] = useState()
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
|
||||||
|
const onChangeMobile = (e:any)=>{
|
||||||
|
setUserName(e.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const login = async ()=>{
|
||||||
|
|
||||||
|
const res = await fetch('http://127.0.0.1:8000/auth/login/mobile/', {
|
||||||
|
method: "POST",
|
||||||
|
cache: 'no-store',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
mobile: username
|
||||||
|
})
|
||||||
|
},)
|
||||||
|
|
||||||
|
console.log(res)
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
console.log(data)
|
||||||
|
|
||||||
|
dispatch( addMobile(data.result.mobile) )
|
||||||
|
|
||||||
|
if (data.result.message == 'opt code send'){
|
||||||
|
router.push('/login/verify/')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div className="mx-auto w-full sm:w-[30rem] mt-[-10rem]">
|
<div className="mx-auto w-full sm:w-[30rem] mt-[-10rem]">
|
||||||
@@ -17,11 +59,12 @@ const Page = ()=>{
|
|||||||
type="email"
|
type="email"
|
||||||
id="UserEmail"
|
id="UserEmail"
|
||||||
placeholder="شماره موبایل را وارد کنید"
|
placeholder="شماره موبایل را وارد کنید"
|
||||||
|
onChange={(e)=>{ onChangeMobile(e) }}
|
||||||
className="mt-1 w-full h-[45px] p-5 rounded-md border-gray-200 shadow-sm sm:text-sm"
|
className="mt-1 w-full h-[45px] p-5 rounded-md border-gray-200 shadow-sm sm:text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input type="submit" className="bg-slate-900 text-white rounded p-2" value={"ورود"} />
|
<input onClick={()=>{ login() }} type="submit" className="bg-slate-900 text-white rounded p-2" value={"ورود"} />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { Fragment, useState } from "react"
|
||||||
|
import { useSelector, useDispatch } from 'react-redux'
|
||||||
|
import { RootState } from "@/app/redux/store"
|
||||||
|
import { addUuidOpt } from "@/app/redux/features/mobileoptSlice"
|
||||||
|
import { useRouter } from "next/navigation"
|
||||||
|
|
||||||
|
function setCookieFn(name:string, value:string, daysToExpire:number) {
|
||||||
|
const expires = new Date();
|
||||||
|
expires.setTime(expires.getTime() + (daysToExpire * 24 * 60 * 60 * 1000));
|
||||||
|
document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Page = ()=>{
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const [optCode, setOptCode]: [any, any] = useState();
|
||||||
|
|
||||||
|
const mobileOpt = useSelector((state: RootState)=> state.mobileopt.mobile )
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
|
||||||
|
const inputOpt = (event: any)=>{
|
||||||
|
setOptCode(event.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const veifyLogin = async (optcode: string)=>{
|
||||||
|
const res = await fetch('http://127.0.0.1:8000/auth/login/mobile/verify/', {
|
||||||
|
cache: 'no-store',
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
mobile: mobileOpt,
|
||||||
|
opt: optCode
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
console.log(data)
|
||||||
|
|
||||||
|
if ('error' in data){
|
||||||
|
router.push('/login/')
|
||||||
|
|
||||||
|
} else {
|
||||||
|
const roles: Array<{cuser_role:any, expire: any, uuid: string}> = await data
|
||||||
|
|
||||||
|
if (roles.length==1) {
|
||||||
|
const uuidOpt = await roles[0].uuid
|
||||||
|
|
||||||
|
const getTokenRes = await fetch('http://127.0.0.1:8000/auth/token/', {
|
||||||
|
cache: 'no-store',
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
uuid_opt: uuidOpt,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const tokenData = await getTokenRes.json();
|
||||||
|
|
||||||
|
console.log(tokenData)
|
||||||
|
|
||||||
|
|
||||||
|
if('refresh' in tokenData){
|
||||||
|
setCookieFn('isAuth', 'true', 365)
|
||||||
|
localStorage.setItem('access', tokenData.access)
|
||||||
|
localStorage.setItem('refresh', tokenData.refresh)
|
||||||
|
|
||||||
|
router.push("/shop")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//await dispatch(addUuidOpt(uuid_opt))
|
||||||
|
|
||||||
|
/*const getTokenRes = await fetch('http://localhost:3000/api/auth/token/', {
|
||||||
|
cache: 'no-store',
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
uuid_opt: uuid_opt,
|
||||||
|
})
|
||||||
|
})*/
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return(
|
||||||
|
<>
|
||||||
|
<div className="mx-auto w-full sm:w-[30rem] mt-[-10rem]">
|
||||||
|
<div className="flex flex-col gap-y-2 pr-8 pl-8">
|
||||||
|
|
||||||
|
<h1 className="text-center border-b-2 border-black pb-4 text-2xl">ورود به سامانه مشتریان</h1>
|
||||||
|
<p className="text-center text-red-600">
|
||||||
|
کد تایید پیامک شده را وارد کنید
|
||||||
|
</p>
|
||||||
|
<div className="mt-5">
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="UserEmail"
|
||||||
|
onChange={(event)=>{ inputOpt(event) }}
|
||||||
|
placeholder="کد تایید را وارد کنید"
|
||||||
|
className="mt-1 w-full h-[45px] p-5 rounded-md border-gray-200 shadow-sm sm:text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input onClick={()=>{ veifyLogin(optCode) }} type="submit" className="bg-slate-900 text-white rounded p-2" value={"ورود به سامانه"} />
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Page
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import axiosInstance from "@/axios/axiosInstance"
|
||||||
|
import { useQuery } from "@tanstack/react-query"
|
||||||
|
import { useState } from "react"
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
|
||||||
|
const CustomerInfo = ()=>{
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [nameFamily, setNameFamily] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const onLogout = ()=>{
|
||||||
|
localStorage.removeItem('access');
|
||||||
|
localStorage.removeItem('refresh');
|
||||||
|
document.cookie = "isAuth=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
||||||
|
setTimeout(()=>{
|
||||||
|
router.push('/login')
|
||||||
|
}, 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = useQuery({ queryKey: ['cuser-info-x1'], queryFn: async ()=>{
|
||||||
|
return await axiosInstance.get("cuser/info/")
|
||||||
|
},refetchOnWindowFocus: false,onSuccess(result:any) {
|
||||||
|
setNameFamily(result.data.name_family)
|
||||||
|
}, })
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-row gap-x-2">
|
||||||
|
<div>{nameFamily}</div>
|
||||||
|
<div onClick={onLogout} className="bg-slate-950 rounded-md pr-1 pl-1 cursor-pointer">خروج</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CustomerInfo
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import ReduxProvider from "@/app/redux/provider"
|
||||||
|
import HamberMenu from "./hamber-menu"
|
||||||
|
import ReactQueryProvider from "@/app/react_query/provider"
|
||||||
|
import { usePathname } from 'next/navigation'
|
||||||
|
import CustomerInfo from "./customer-info"
|
||||||
|
|
||||||
|
|
||||||
|
const MainLayoutUtil = ({children,}: {children: React.ReactNode})=>{
|
||||||
|
const pathname = usePathname()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`basis-full md:mr-[8.9rem] ${pathname.startsWith('/shop') ? 'lg:ml-[20rem]': ''} `}>
|
||||||
|
|
||||||
|
<ReduxProvider>
|
||||||
|
<ReactQueryProvider>
|
||||||
|
<div className='w-full h-[2.5rem] flex flex-row bg-gray-800 p-2 text-white'>
|
||||||
|
|
||||||
|
<HamberMenu />
|
||||||
|
|
||||||
|
<div className='pr-4'>
|
||||||
|
<CustomerInfo />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{children}
|
||||||
|
|
||||||
|
</ReactQueryProvider>
|
||||||
|
</ReduxProvider>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default MainLayoutUtil
|
||||||
@@ -1,46 +1,74 @@
|
|||||||
interface OrderItemProps {
|
'use client'
|
||||||
|
|
||||||
}
|
import axiosInstance from "@/axios/axiosInstance"
|
||||||
|
import { useQueryClient } from "@tanstack/react-query"
|
||||||
|
import { MdDelete } from "react-icons/md";
|
||||||
|
const formatCurrency = (number:any, symbol = '') => {
|
||||||
|
// Add thousands separator
|
||||||
|
const formattedNumber = number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||||
|
|
||||||
|
// Format the number as a currency string
|
||||||
|
return `${symbol}${formattedNumber}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
const OrderItem = ()=>{
|
const OrderItem = (props:any)=>{
|
||||||
|
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
const deleteOrderIteam = async (orderIteamID:number)=>{
|
||||||
|
const deleteOrderRes = await axiosInstance.delete(`shop/cart/delete/${orderIteamID}/`);
|
||||||
|
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ['cartList1'] });
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='iteam border-[1px] border-gray-300 rounded p-2 bg-white shadow'>
|
<div className='iteam border-[1px] border-gray-300 rounded p-2 bg-white shadow'>
|
||||||
<p className='text-center mb-2'>
|
<p className='text-center mb-2 bg-green-700 text-white rounded-md'>
|
||||||
سه راهي دريچه بازديد 90 درجه
|
{props.iteam.product.category.name}
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className='flex flex-row justify-between justify-items-center gap-x-2'>
|
|
||||||
|
|
||||||
<span>
|
|
||||||
سایز
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span>
|
<div className='flex flex-row gap-x-2 text-center border-b-[1px]'>
|
||||||
تعداد
|
|
||||||
</span>
|
<span className="pb-2">
|
||||||
<span>
|
{props.iteam.product.name}
|
||||||
قیمت
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex flex-row justify-between gap-x-2 text-center'>
|
<div className='flex flex-row justify-between gap-x-2 text-center pt-2'>
|
||||||
|
|
||||||
|
<div className="">
|
||||||
|
<span>تعداد: </span>
|
||||||
|
<span>{props.iteam.quantity}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div className="">
|
||||||
|
{formatCurrency(props.iteam.product.price)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div onClick={()=>{ deleteOrderIteam(props.iteam.id) }} className=" text-black rounded-md p-1">
|
||||||
|
|
||||||
|
<MdDelete size={25} />
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<span>
|
|
||||||
110*3.2
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span>
|
|
||||||
24 کارتن
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
5.598.058
|
|
||||||
</span>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import axiosInstance from '@/axios/axiosInstance';
|
||||||
|
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { useSelector, useDispatch } from 'react-redux'
|
||||||
|
import { RootState } from "@/app/redux/store"
|
||||||
|
import { addAllToCars } from '@/app/redux/features/cartSlice';
|
||||||
|
|
||||||
|
const FetchCartProvider = ({children} : {children: React.ReactNode})=>{
|
||||||
|
|
||||||
|
const cartData = useSelector((state: RootState) => state.cart)
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
|
||||||
|
const query = useQuery({ queryKey: ['categoryProducts'], queryFn: async ()=>{
|
||||||
|
|
||||||
|
if(cartData.length == 0){
|
||||||
|
return await axiosInstance.get(`shop/cart/`)
|
||||||
|
}
|
||||||
|
|
||||||
|
}, onSuccess(data:any) {
|
||||||
|
dispatch(addAllToCars(data.data))
|
||||||
|
}, });
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
{children}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FetchCartProvider
|
||||||
@@ -1,28 +1,88 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useOrderContext } from "@/context/OrderContext"
|
import { useOrderContext } from "@/context/OrderContext"
|
||||||
|
import {
|
||||||
|
useQuery,
|
||||||
|
} from '@tanstack/react-query'
|
||||||
import OrderItem from "../order-item"
|
import OrderItem from "../order-item"
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector, useDispatch } from 'react-redux'
|
||||||
import { RootState } from "@/app/redux/store"
|
import { RootState } from "@/app/redux/store"
|
||||||
|
import axiosInstance from "@/axios/axiosInstance"
|
||||||
|
import { addAllToCars, removeAllFromCart } from '@/app/redux/features/cartSlice';
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
const OrdersList = ()=>{
|
const OrdersList = ()=>{
|
||||||
|
|
||||||
const {orderList} = useOrderContext()
|
const {orderList} = useOrderContext()
|
||||||
const count = useSelector((state: RootState) => state.counter.counter_value )
|
const cartData = useSelector((state: RootState) => state.cart)
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
|
||||||
|
const query = useQuery({ queryKey: ['cartList1'], queryFn: async ()=>{
|
||||||
|
return await axiosInstance.get(`shop/cart/`)
|
||||||
|
}, onSuccess(data:any) {
|
||||||
|
dispatch(addAllToCars(data.data))
|
||||||
|
}, });
|
||||||
|
|
||||||
|
const [cartIsOpen, setCartIsOpen ] = useState<boolean>(false)
|
||||||
|
|
||||||
|
const submitOrder = async ()=>{
|
||||||
|
const orderData = await cartData.map((item)=>{
|
||||||
|
return {
|
||||||
|
product_id: item.product.id,
|
||||||
|
amount: item.quantity
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const newOrder = await axiosInstance.post('shop/order-and-item/create/', {
|
||||||
|
items: JSON.stringify(orderData)
|
||||||
|
})
|
||||||
|
|
||||||
|
if(newOrder.status == 201){
|
||||||
|
alert("ثبت سفارش انجام شد")
|
||||||
|
dispatch(removeAllFromCart())
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<p>{count}</p>
|
|
||||||
{orderList.map((iteamo: any)=>(
|
<div className={`${cartIsOpen ? null : 'hidden' } bg-white lg:block w-full md:w-[20rem] h-screen border-r-[1px] fixed top-0 left-0`}>
|
||||||
<p>{iteamo}</p>
|
<div className='text-center bg-slate-600 p-4 text-white flex flex-row items-center justify-between md:justify-center'>
|
||||||
))}
|
<span>سبد خرید شما</span>
|
||||||
<OrderItem />
|
<span onClick={()=>{ setCartIsOpen(false) }} className="md:hidden bg-slate-900 rounded-lg pr-2 pl-2 pt-1 pb-1">بستن</span>
|
||||||
<OrderItem />
|
</div>
|
||||||
<OrderItem />
|
|
||||||
<OrderItem />
|
<div className='w-full flex flex-col gap-y-2 pt-2 pr-1 pl-1 h-screen overflow-auto pb-44'>
|
||||||
<OrderItem />
|
{cartData?cartData.map((iteam, key)=>{
|
||||||
<OrderItem />
|
return <OrderItem key={key} iteam={iteam} />
|
||||||
<OrderItem />
|
}):null }
|
||||||
<OrderItem />
|
</div>
|
||||||
<OrderItem />
|
|
||||||
|
|
||||||
|
|
||||||
|
<div className='absolute bg-white bottom-0 right-0 w-full p-4'>
|
||||||
|
|
||||||
|
<p className='text-center mb-2 text-gray-500'>مشاهده کامل سبد خرید</p>
|
||||||
|
<div onClick={submitOrder} className='w-full bg-green-900 text-white mb-2 rounded text-center pt-2 pb-2'>
|
||||||
|
ثبت نهایی
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{cartData.length > 0 ? (
|
||||||
|
<div className={`${cartIsOpen? 'hidden': null } md:hidden fixed bottom-0 right-0 pb-4 flex w-full flex-row items-center justify-center`}>
|
||||||
|
<div onClick={()=>{ setCartIsOpen(true) }} className='bg-cyan-800 text-white rounded-lg pt-2 pb-2 pr-4 pl-4'>
|
||||||
|
<span className="pr-1 pl-1" >مشاهده سبد خرید</span>
|
||||||
|
<span className="rounded bg-cyan-950 inline-block text-center h-[30px] w-[30px]">{cartData ? cartData.length : null}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,17 +2,19 @@ import Image from "next/image"
|
|||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
|
|
||||||
|
|
||||||
const ProductCategoryBox = ()=>{
|
const ProductCategoryBox: any = (props:any)=>{
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col text-center rounded overflow-hidden text-white border-solid border-[1px] border-slate-700">
|
<div className="flex flex-col bg-white text-center rounded overflow-hidden text-white border-solid border-[1px] border-slate-700">
|
||||||
<h2 className="bg-slate-600 mb-2">سیفون پایه دار</h2>
|
<h2 className="bg-slate-600 mb-2">{props.iteam.name}</h2>
|
||||||
<Image className="p-1 mx-auto" alt="a" width={"100"} height={"100"} src={"/zano45-9.png"} />
|
<Image className="p-1 mx-auto" alt="a" width={"100"} height={"100"} src={`http://127.0.0.1:8000/${props.iteam.image}`} />
|
||||||
|
|
||||||
<div className="flex justify-between text-black border-t-[1px] mt-2 p-1">
|
<div className="flex justify-between text-black border-t-[1px] mt-2 p-1">
|
||||||
|
|
||||||
<span >23 نوع</span>
|
<span >
|
||||||
|
{`${props.iteam.product_count} نوع`}
|
||||||
|
</span>
|
||||||
|
|
||||||
<Link href={"shop/product/1/"} className="bg-emerald-800 rounded text-white text-[0.8rem] pr-2 pl-2">
|
<Link href={`shop/product/${props.iteam.id}/`} className="bg-emerald-800 rounded text-white text-[0.8rem] pr-2 pl-2">
|
||||||
<span>مشاهده</span>
|
<span>مشاهده</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,39 +1,97 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useOrderContext } from "@/context/OrderContext"
|
import { useOrderContext } from "@/context/OrderContext"
|
||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { useDispatch } from "react-redux"
|
import { useDispatch, useSelector } from "react-redux"
|
||||||
import { incr } from "@/app/redux/features/couterSlice"
|
import { RootState } from "@/app/redux/store"
|
||||||
|
import { addToCart } from "@/app/redux/features/cartSlice"
|
||||||
|
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import axiosInstance from "@/axios/axiosInstance"
|
||||||
|
import { useRef, useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
const formatCurrency = (number:any, symbol = '') => {
|
||||||
|
// Add thousands separator
|
||||||
|
const formattedNumber = number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||||
|
|
||||||
|
// Format the number as a currency string
|
||||||
|
return `${symbol}${formattedNumber}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Products = (props:any)=>{
|
||||||
|
|
||||||
const Products = ()=>{
|
|
||||||
const {addToOrderList} = useOrderContext()
|
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
|
|
||||||
|
const cartData = useSelector((state: RootState) => state.cart)
|
||||||
|
|
||||||
|
const [isInCart, setIsInCart] = useState<boolean>(false)
|
||||||
|
|
||||||
|
const quantityInput = useRef<any|null>(null);
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
|
||||||
|
|
||||||
|
const checkSomeCartData = cartData.some((iteam)=> iteam.product.id == props.iteam.id )
|
||||||
|
setIsInCart( checkSomeCartData )
|
||||||
|
|
||||||
|
cartData.map((iteam, key)=>{
|
||||||
|
if(iteam.product.id == props.iteam.id){
|
||||||
|
quantityInput.current.value = iteam.quantity
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if(!checkSomeCartData){
|
||||||
|
quantityInput.current.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
},[cartData,])
|
||||||
|
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
|
||||||
|
const addIteamToCart = async (iteam:any)=>{
|
||||||
|
|
||||||
|
const addToCartRes = await axiosInstance.post('shop/cart/add/', {
|
||||||
|
product_id: iteam.id,
|
||||||
|
quantity: quantityInput.current.value
|
||||||
|
})
|
||||||
|
|
||||||
|
if(addToCartRes.status==201){
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ['cartList1'] });
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
||||||
<div className='border-[1px] rounded p-2 shadow'>
|
<div className={`flex flex-col justify-between border-[1px] rounded p-4 shadow ${isInCart ? 'bg-slate-100' : ''} `}>
|
||||||
|
|
||||||
<div className='flex justify-center mb-2'>
|
<div className='flex flex-row justify-center'>
|
||||||
<span>سایز</span>
|
<span className="text-justify font-[0.8rem]">{props.iteam.name}</span>
|
||||||
<span>110*3.2</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex justify-center mb-2'>
|
<div>
|
||||||
<span>قیمت</span>
|
<div className='flex flex-row gap-x-2 justify-center mb-2 border-t-[1px] pt-2 mt-2'>
|
||||||
<span>1060000</span>
|
<span>{formatCurrency(props.iteam.price)}</span>
|
||||||
</div>
|
<span>تومان</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='flex justify-center mb-2'>
|
<div className='flex justify-center mb-2'>
|
||||||
<input placeholder='تعداد را وارد کنید' className='text-center border-[1px] rounded w-full' type='number' />
|
<input ref={quantityInput} placeholder='تعداد را وارد کنید' className='text-center border-[1px] rounded w-full' type='tel' pattern="[0-9]*" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div className='flex justify-center mb-2'>
|
||||||
|
<span onClick={()=>{ addIteamToCart(props.iteam) }} className='cursor-pointer bg-lime-600 text-white rounded pt-1 pb-1 w-full text-center'>
|
||||||
|
{isInCart? "ویرایش":"ثبت"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div className='flex justify-center mb-2'>
|
|
||||||
<span onClick={()=> { dispatch ( incr() ) } }>inc</span>
|
|
||||||
<span onClick={()=>{ addToOrderList('salam') }} className='cursor-pointer bg-lime-600 text-white rounded pt-1 pb-1 w-full text-center'>ثبت</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,3 +1,76 @@
|
|||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
|
--primary: 222.2 47.4% 11.2%;
|
||||||
|
--primary-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--secondary: 210 40% 96.1%;
|
||||||
|
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
|
--muted: 210 40% 96.1%;
|
||||||
|
--muted-foreground: 215.4 16.3% 46.9%;
|
||||||
|
|
||||||
|
--accent: 210 40% 96.1%;
|
||||||
|
--accent-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--border: 214.3 31.8% 91.4%;
|
||||||
|
--input: 214.3 31.8% 91.4%;
|
||||||
|
--ring: 222.2 84% 4.9%;
|
||||||
|
|
||||||
|
--radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: 222.2 84% 4.9%;
|
||||||
|
--foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--card: 222.2 84% 4.9%;
|
||||||
|
--card-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--popover: 222.2 84% 4.9%;
|
||||||
|
--popover-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--primary: 210 40% 98%;
|
||||||
|
--primary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
|
--secondary: 217.2 32.6% 17.5%;
|
||||||
|
--secondary-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--muted: 217.2 32.6% 17.5%;
|
||||||
|
--muted-foreground: 215 20.2% 65.1%;
|
||||||
|
|
||||||
|
--accent: 217.2 32.6% 17.5%;
|
||||||
|
--accent-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--border: 217.2 32.6% 17.5%;
|
||||||
|
--input: 217.2 32.6% 17.5%;
|
||||||
|
--ring: 212.7 26.8% 83.9%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import {
|
||||||
|
useQuery,
|
||||||
|
useMutation,
|
||||||
|
useQueryClient,
|
||||||
|
QueryClient,
|
||||||
|
QueryClientProvider,
|
||||||
|
} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import axiosInstance from '@/axios/axiosInstance';
|
||||||
|
import { useSelector, useDispatch } from 'react-redux'
|
||||||
|
import { RootState } from "@/app/redux/store"
|
||||||
|
import { addAllToCars } from '@/app/redux/features/cartSlice';
|
||||||
|
|
||||||
|
const queryClient = new QueryClient()
|
||||||
|
|
||||||
|
const ReactQueryProvider = ({children} : {children: React.ReactNode})=>{
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
{ children }
|
||||||
|
</QueryClientProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ReactQueryProvider
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { createSlice } from "@reduxjs/toolkit"
|
||||||
|
import type { PayloadAction } from '@reduxjs/toolkit'
|
||||||
|
|
||||||
|
|
||||||
|
interface CartState {
|
||||||
|
id: number,
|
||||||
|
product: {
|
||||||
|
category: {
|
||||||
|
name: string,
|
||||||
|
},
|
||||||
|
id: number,
|
||||||
|
name: string,
|
||||||
|
price: string,
|
||||||
|
},
|
||||||
|
quantity: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: CartState[] | null = []
|
||||||
|
|
||||||
|
export const cartSlice = createSlice({
|
||||||
|
name: 'cart',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
addAllToCars: (state, action: PayloadAction<CartState[]>)=>{
|
||||||
|
return [...action.payload]
|
||||||
|
},
|
||||||
|
addToCart: (state, action: PayloadAction<CartState>)=>{
|
||||||
|
state.push(action.payload)
|
||||||
|
},
|
||||||
|
removeAllFromCart: (state)=>{
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const { addAllToCars, removeAllFromCart, addToCart } = cartSlice.actions;
|
||||||
|
export default cartSlice.reducer;
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { createSlice } from "@reduxjs/toolkit"
|
||||||
|
import type { PayloadAction } from '@reduxjs/toolkit'
|
||||||
|
|
||||||
|
|
||||||
|
interface MobileOptState {
|
||||||
|
mobile: number,
|
||||||
|
uuid_opt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: MobileOptState = {
|
||||||
|
mobile: 0,
|
||||||
|
uuid_opt: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mobileoptSlice = createSlice({
|
||||||
|
name: 'mobileOpt',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
addMobile: (state, action: PayloadAction<number>)=>{
|
||||||
|
state.mobile = action.payload
|
||||||
|
},
|
||||||
|
addUuidOpt: (state, action: PayloadAction<string>)=>{
|
||||||
|
state.uuid_opt = action.payload
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const { addMobile, addUuidOpt } = mobileoptSlice.actions;
|
||||||
|
export default mobileoptSlice.reducer;
|
||||||
@@ -2,11 +2,14 @@
|
|||||||
|
|
||||||
import { configureStore } from "@reduxjs/toolkit"
|
import { configureStore } from "@reduxjs/toolkit"
|
||||||
import couterReducer from "./features/couterSlice"
|
import couterReducer from "./features/couterSlice"
|
||||||
|
import mobileoptReducer from "./features/mobileoptSlice"
|
||||||
|
import cartReducer from "./features/cartSlice"
|
||||||
|
|
||||||
export const store = configureStore({
|
export const store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
counter: couterReducer
|
counter: couterReducer,
|
||||||
|
mobileopt: mobileoptReducer,
|
||||||
|
cart: cartReducer,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import axios from 'axios'
|
||||||
|
import { error } from 'console';
|
||||||
|
import { cookies, headers } from 'next/headers'
|
||||||
|
|
||||||
|
const axiosInstance = axios.create({
|
||||||
|
baseURL: 'http://localhost:3000/api/',
|
||||||
|
timeout: 2000,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
axiosInstance.interceptors.response.use((response)=> {
|
||||||
|
return response
|
||||||
|
}, async (error) =>{
|
||||||
|
|
||||||
|
const originalRequest = error.config;
|
||||||
|
|
||||||
|
if (error.response.status === 401 && !originalRequest._retry) {
|
||||||
|
|
||||||
|
originalRequest._retry = true; // Prevent infinite loops
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
const res = await fetch("http://localhost:3000/api/auth/token/refresh/",{
|
||||||
|
method: "GET",
|
||||||
|
cache: "no-store",
|
||||||
|
});
|
||||||
|
console.log(res);
|
||||||
|
|
||||||
|
return axiosInstance(originalRequest)
|
||||||
|
|
||||||
|
} catch(refreshError) {
|
||||||
|
throw refreshError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default axiosInstance
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import axios from 'axios'
|
||||||
|
import { error } from 'console';
|
||||||
|
import { cookies, headers } from 'next/headers'
|
||||||
|
|
||||||
|
//let accessToken = localStorage.getItem('access') ? localStorage.getItem('access') : null
|
||||||
|
function setCookieFn(name:string, value:string, daysToExpire:number) {
|
||||||
|
const expires = new Date();
|
||||||
|
expires.setTime(expires.getTime() + (daysToExpire * 24 * 60 * 60 * 1000));
|
||||||
|
document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const axiosInstance = axios.create({
|
||||||
|
baseURL: 'http://127.0.0.1:8000/',
|
||||||
|
timeout: 2000,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
//'Authorization': `Bearer ${accessToken}`,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
axiosInstance.interceptors.request.use(async req => {
|
||||||
|
const accessT = await localStorage.getItem('access')
|
||||||
|
req.headers.Authorization = `Bearer ${accessT}`
|
||||||
|
return req
|
||||||
|
})
|
||||||
|
|
||||||
|
axiosInstance.interceptors.response.use((response)=> {
|
||||||
|
return response
|
||||||
|
}, async (error) =>{
|
||||||
|
|
||||||
|
const originalRequest = error.config;
|
||||||
|
|
||||||
|
if (error.response.status === 401 && !originalRequest._retry) {
|
||||||
|
|
||||||
|
originalRequest._retry = true; // Prevent infinite loops
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
const res = await fetch("http://127.0.0.1:8000/auth/token/refresh/", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json; charset=UTF-8',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
'refresh': localStorage.getItem('refresh')
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if(res.status==200){
|
||||||
|
setCookieFn('isAuth', 'true', 365)
|
||||||
|
const tokenData = await res.json()
|
||||||
|
localStorage.setItem('access', tokenData.access)
|
||||||
|
localStorage.setItem('refresh',tokenData.refresh)
|
||||||
|
originalRequest.headers.Authorization = `Bearer ${tokenData.access}`
|
||||||
|
return axiosInstance(originalRequest)
|
||||||
|
}else{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
} catch(refreshError) {
|
||||||
|
|
||||||
|
throw refreshError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default axiosInstance
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||||
|
outline:
|
||||||
|
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-10 px-4 py-2",
|
||||||
|
sm: "h-9 rounded-md px-3",
|
||||||
|
lg: "h-11 rounded-md px-8",
|
||||||
|
icon: "h-10 w-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
|
asChild?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : "button"
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Button.displayName = "Button"
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||||
|
import { Check } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Checkbox = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CheckboxPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<CheckboxPrimitive.Indicator
|
||||||
|
className={cn("flex items-center justify-center text-current")}
|
||||||
|
>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</CheckboxPrimitive.Indicator>
|
||||||
|
</CheckboxPrimitive.Root>
|
||||||
|
))
|
||||||
|
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
||||||
|
|
||||||
|
export { Checkbox }
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
export interface InputProps
|
||||||
|
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||||
|
|
||||||
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
|
({ className, type, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
className={cn(
|
||||||
|
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Input.displayName = "Input"
|
||||||
|
|
||||||
|
export { Input }
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const labelVariants = cva(
|
||||||
|
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
|
)
|
||||||
|
|
||||||
|
const Label = React.forwardRef<
|
||||||
|
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||||
|
VariantProps<typeof labelVariants>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<LabelPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(labelVariants(), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
Label.displayName = LabelPrimitive.Root.displayName
|
||||||
|
|
||||||
|
export { Label }
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Separator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||||
|
>(
|
||||||
|
(
|
||||||
|
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||||
|
ref
|
||||||
|
) => (
|
||||||
|
<SeparatorPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
decorative={decorative}
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
"shrink-0 bg-border",
|
||||||
|
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||||
|
|
||||||
|
export { Separator }
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Skeleton({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn("animate-pulse rounded-md bg-muted", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Skeleton }
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Table = React.forwardRef<
|
||||||
|
HTMLTableElement,
|
||||||
|
React.HTMLAttributes<HTMLTableElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div className="relative w-full overflow-auto">
|
||||||
|
<table
|
||||||
|
ref={ref}
|
||||||
|
className={cn("w-full caption-bottom text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
Table.displayName = "Table"
|
||||||
|
|
||||||
|
const TableHeader = React.forwardRef<
|
||||||
|
HTMLTableSectionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
||||||
|
))
|
||||||
|
TableHeader.displayName = "TableHeader"
|
||||||
|
|
||||||
|
const TableBody = React.forwardRef<
|
||||||
|
HTMLTableSectionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<tbody
|
||||||
|
ref={ref}
|
||||||
|
className={cn("[&_tr:last-child]:border-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TableBody.displayName = "TableBody"
|
||||||
|
|
||||||
|
const TableFooter = React.forwardRef<
|
||||||
|
HTMLTableSectionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<tfoot
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TableFooter.displayName = "TableFooter"
|
||||||
|
|
||||||
|
const TableRow = React.forwardRef<
|
||||||
|
HTMLTableRowElement,
|
||||||
|
React.HTMLAttributes<HTMLTableRowElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<tr
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TableRow.displayName = "TableRow"
|
||||||
|
|
||||||
|
const TableHead = React.forwardRef<
|
||||||
|
HTMLTableCellElement,
|
||||||
|
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<th
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TableHead.displayName = "TableHead"
|
||||||
|
|
||||||
|
const TableCell = React.forwardRef<
|
||||||
|
HTMLTableCellElement,
|
||||||
|
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<td
|
||||||
|
ref={ref}
|
||||||
|
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TableCell.displayName = "TableCell"
|
||||||
|
|
||||||
|
const TableCaption = React.forwardRef<
|
||||||
|
HTMLTableCaptionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<caption
|
||||||
|
ref={ref}
|
||||||
|
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TableCaption.displayName = "TableCaption"
|
||||||
|
|
||||||
|
export {
|
||||||
|
Table,
|
||||||
|
TableHeader,
|
||||||
|
TableBody,
|
||||||
|
TableFooter,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
TableCaption,
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
export interface TextareaProps
|
||||||
|
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||||
|
|
||||||
|
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||||
|
({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
className={cn(
|
||||||
|
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Textarea.displayName = "Textarea"
|
||||||
|
|
||||||
|
export { Textarea }
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as ToastPrimitives from "@radix-ui/react-toast"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
import { X } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const ToastProvider = ToastPrimitives.Provider
|
||||||
|
|
||||||
|
const ToastViewport = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Viewport
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
||||||
|
|
||||||
|
const toastVariants = cva(
|
||||||
|
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "border bg-background text-foreground",
|
||||||
|
destructive:
|
||||||
|
"destructive group border-destructive bg-destructive text-destructive-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const Toast = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
||||||
|
VariantProps<typeof toastVariants>
|
||||||
|
>(({ className, variant, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<ToastPrimitives.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(toastVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
Toast.displayName = ToastPrimitives.Root.displayName
|
||||||
|
|
||||||
|
const ToastAction = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Action
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
ToastAction.displayName = ToastPrimitives.Action.displayName
|
||||||
|
|
||||||
|
const ToastClose = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Close
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
toast-close=""
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</ToastPrimitives.Close>
|
||||||
|
))
|
||||||
|
ToastClose.displayName = ToastPrimitives.Close.displayName
|
||||||
|
|
||||||
|
const ToastTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
ToastTitle.displayName = ToastPrimitives.Title.displayName
|
||||||
|
|
||||||
|
const ToastDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm opacity-90", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
ToastDescription.displayName = ToastPrimitives.Description.displayName
|
||||||
|
|
||||||
|
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
|
||||||
|
|
||||||
|
type ToastActionElement = React.ReactElement<typeof ToastAction>
|
||||||
|
|
||||||
|
export {
|
||||||
|
type ToastProps,
|
||||||
|
type ToastActionElement,
|
||||||
|
ToastProvider,
|
||||||
|
ToastViewport,
|
||||||
|
Toast,
|
||||||
|
ToastTitle,
|
||||||
|
ToastDescription,
|
||||||
|
ToastClose,
|
||||||
|
ToastAction,
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import {
|
||||||
|
Toast,
|
||||||
|
ToastClose,
|
||||||
|
ToastDescription,
|
||||||
|
ToastProvider,
|
||||||
|
ToastTitle,
|
||||||
|
ToastViewport,
|
||||||
|
} from "@/components/ui/toast"
|
||||||
|
import { useToast } from "@/components/ui/use-toast"
|
||||||
|
|
||||||
|
export function Toaster() {
|
||||||
|
const { toasts } = useToast()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToastProvider>
|
||||||
|
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||||
|
return (
|
||||||
|
<Toast key={id} {...props}>
|
||||||
|
<div className="grid gap-1">
|
||||||
|
{title && <ToastTitle>{title}</ToastTitle>}
|
||||||
|
{description && (
|
||||||
|
<ToastDescription>{description}</ToastDescription>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{action}
|
||||||
|
<ToastClose />
|
||||||
|
</Toast>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
<ToastViewport />
|
||||||
|
</ToastProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
// Inspired by react-hot-toast library
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ToastActionElement,
|
||||||
|
ToastProps,
|
||||||
|
} from "@/components/ui/toast"
|
||||||
|
|
||||||
|
const TOAST_LIMIT = 1
|
||||||
|
const TOAST_REMOVE_DELAY = 1000000
|
||||||
|
|
||||||
|
type ToasterToast = ToastProps & {
|
||||||
|
id: string
|
||||||
|
title?: React.ReactNode
|
||||||
|
description?: React.ReactNode
|
||||||
|
action?: ToastActionElement
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionTypes = {
|
||||||
|
ADD_TOAST: "ADD_TOAST",
|
||||||
|
UPDATE_TOAST: "UPDATE_TOAST",
|
||||||
|
DISMISS_TOAST: "DISMISS_TOAST",
|
||||||
|
REMOVE_TOAST: "REMOVE_TOAST",
|
||||||
|
} as const
|
||||||
|
|
||||||
|
let count = 0
|
||||||
|
|
||||||
|
function genId() {
|
||||||
|
count = (count + 1) % Number.MAX_SAFE_INTEGER
|
||||||
|
return count.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionType = typeof actionTypes
|
||||||
|
|
||||||
|
type Action =
|
||||||
|
| {
|
||||||
|
type: ActionType["ADD_TOAST"]
|
||||||
|
toast: ToasterToast
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ActionType["UPDATE_TOAST"]
|
||||||
|
toast: Partial<ToasterToast>
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ActionType["DISMISS_TOAST"]
|
||||||
|
toastId?: ToasterToast["id"]
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ActionType["REMOVE_TOAST"]
|
||||||
|
toastId?: ToasterToast["id"]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
toasts: ToasterToast[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
||||||
|
|
||||||
|
const addToRemoveQueue = (toastId: string) => {
|
||||||
|
if (toastTimeouts.has(toastId)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
toastTimeouts.delete(toastId)
|
||||||
|
dispatch({
|
||||||
|
type: "REMOVE_TOAST",
|
||||||
|
toastId: toastId,
|
||||||
|
})
|
||||||
|
}, TOAST_REMOVE_DELAY)
|
||||||
|
|
||||||
|
toastTimeouts.set(toastId, timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const reducer = (state: State, action: Action): State => {
|
||||||
|
switch (action.type) {
|
||||||
|
case "ADD_TOAST":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||||
|
}
|
||||||
|
|
||||||
|
case "UPDATE_TOAST":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: state.toasts.map((t) =>
|
||||||
|
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
case "DISMISS_TOAST": {
|
||||||
|
const { toastId } = action
|
||||||
|
|
||||||
|
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||||
|
// but I'll keep it here for simplicity
|
||||||
|
if (toastId) {
|
||||||
|
addToRemoveQueue(toastId)
|
||||||
|
} else {
|
||||||
|
state.toasts.forEach((toast) => {
|
||||||
|
addToRemoveQueue(toast.id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: state.toasts.map((t) =>
|
||||||
|
t.id === toastId || toastId === undefined
|
||||||
|
? {
|
||||||
|
...t,
|
||||||
|
open: false,
|
||||||
|
}
|
||||||
|
: t
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "REMOVE_TOAST":
|
||||||
|
if (action.toastId === undefined) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const listeners: Array<(state: State) => void> = []
|
||||||
|
|
||||||
|
let memoryState: State = { toasts: [] }
|
||||||
|
|
||||||
|
function dispatch(action: Action) {
|
||||||
|
memoryState = reducer(memoryState, action)
|
||||||
|
listeners.forEach((listener) => {
|
||||||
|
listener(memoryState)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type Toast = Omit<ToasterToast, "id">
|
||||||
|
|
||||||
|
function toast({ ...props }: Toast) {
|
||||||
|
const id = genId()
|
||||||
|
|
||||||
|
const update = (props: ToasterToast) =>
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_TOAST",
|
||||||
|
toast: { ...props, id },
|
||||||
|
})
|
||||||
|
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: "ADD_TOAST",
|
||||||
|
toast: {
|
||||||
|
...props,
|
||||||
|
id,
|
||||||
|
open: true,
|
||||||
|
onOpenChange: (open) => {
|
||||||
|
if (!open) dismiss()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: id,
|
||||||
|
dismiss,
|
||||||
|
update,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function useToast() {
|
||||||
|
const [state, setState] = React.useState<State>(memoryState)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
listeners.push(setState)
|
||||||
|
return () => {
|
||||||
|
const index = listeners.indexOf(setState)
|
||||||
|
if (index > -1) {
|
||||||
|
listeners.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [state])
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toast,
|
||||||
|
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useToast, toast }
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { type ClassValue, clsx } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
import type { NextRequest } from 'next/server'
|
||||||
|
|
||||||
|
// This function can be marked `async` if using `await` inside
|
||||||
|
export function middleware(request: NextRequest) {
|
||||||
|
|
||||||
|
let isAuth:any = request.cookies.get('isAuth')
|
||||||
|
|
||||||
|
try {
|
||||||
|
if(isAuth.value != "true"){
|
||||||
|
return NextResponse.redirect(new URL('/login', request.url))
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return NextResponse.redirect(new URL('/login', request.url))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
matcher: [
|
||||||
|
/*
|
||||||
|
* Match all request paths except for the ones starting with:
|
||||||
|
* - api (API routes)
|
||||||
|
* - _next/static (static files)
|
||||||
|
* - _next/image (image optimization files)
|
||||||
|
* - favicon.ico (favicon file)
|
||||||
|
*/
|
||||||
|
'/((?!api|_next/static|_next/image|favicon.ico|login|login/verify).*)',
|
||||||
|
],
|
||||||
|
}
|
||||||
+70
-14
@@ -1,20 +1,76 @@
|
|||||||
import type { Config } from 'tailwindcss'
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
const config: Config = {
|
darkMode: ["class"],
|
||||||
content: [
|
content: [
|
||||||
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
|
'./pages/**/*.{ts,tsx}',
|
||||||
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
|
'./components/**/*.{ts,tsx}',
|
||||||
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
|
'./app/**/*.{ts,tsx}',
|
||||||
],
|
'./src/**/*.{ts,tsx}',
|
||||||
|
],
|
||||||
theme: {
|
theme: {
|
||||||
|
container: {
|
||||||
|
center: true,
|
||||||
|
padding: "2rem",
|
||||||
|
screens: {
|
||||||
|
"2xl": "1400px",
|
||||||
|
},
|
||||||
|
},
|
||||||
extend: {
|
extend: {
|
||||||
backgroundImage: {
|
colors: {
|
||||||
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
|
border: "hsl(var(--border))",
|
||||||
'gradient-conic':
|
input: "hsl(var(--input))",
|
||||||
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
|
ring: "hsl(var(--ring))",
|
||||||
|
background: "hsl(var(--background))",
|
||||||
|
foreground: "hsl(var(--foreground))",
|
||||||
|
primary: {
|
||||||
|
DEFAULT: "hsl(var(--primary))",
|
||||||
|
foreground: "hsl(var(--primary-foreground))",
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: "hsl(var(--secondary))",
|
||||||
|
foreground: "hsl(var(--secondary-foreground))",
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: "hsl(var(--destructive))",
|
||||||
|
foreground: "hsl(var(--destructive-foreground))",
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: "hsl(var(--muted))",
|
||||||
|
foreground: "hsl(var(--muted-foreground))",
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: "hsl(var(--accent))",
|
||||||
|
foreground: "hsl(var(--accent-foreground))",
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
DEFAULT: "hsl(var(--popover))",
|
||||||
|
foreground: "hsl(var(--popover-foreground))",
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
DEFAULT: "hsl(var(--card))",
|
||||||
|
foreground: "hsl(var(--card-foreground))",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
lg: "var(--radius)",
|
||||||
|
md: "calc(var(--radius) - 2px)",
|
||||||
|
sm: "calc(var(--radius) - 4px)",
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
"accordion-down": {
|
||||||
|
from: { height: 0 },
|
||||||
|
to: { height: "var(--radix-accordion-content-height)" },
|
||||||
|
},
|
||||||
|
"accordion-up": {
|
||||||
|
from: { height: "var(--radix-accordion-content-height)" },
|
||||||
|
to: { height: 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
"accordion-down": "accordion-down 0.2s ease-out",
|
||||||
|
"accordion-up": "accordion-up 0.2s ease-out",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [require("tailwindcss-animate")],
|
||||||
}
|
}
|
||||||
export default config
|
|
||||||
Reference in new issue
Block a user