Browse Source

feat(API) : shop , products , orders connected to API

master
sina_sajjadi 4 weeks ago
parent
commit
480c1289b4
  1. 45
      .env.template
  2. 0
      README.md
  3. 2
      next.config.js
  4. 2
      package.json
  5. 3
      public/locales/en/common.json
  6. 17
      public/locales/en/form.json
  7. 45
      src/components/auth/forget-password/enter-email-view.tsx
  8. 17
      src/components/auth/forget-password/enter-new-password-view.tsx
  9. 191
      src/components/auth/forget-password/forget-password.tsx
  10. 72
      src/components/auth/login-form.tsx
  11. 320
      src/components/auth/registration-form.tsx
  12. 39
      src/components/common/get-file-url.tsx
  13. 48
      src/components/common/uploader.tsx
  14. 50
      src/components/dashboard/admin.tsx
  15. 42
      src/components/dashboard/widgets/box/widget-order-by-status.tsx
  16. 15
      src/components/filters/category-type-filter.tsx
  17. 12
      src/components/icons/social/facebook.tsx
  18. 5
      src/components/icons/social/index.tsx
  19. 10
      src/components/icons/social/twitter.tsx
  20. 2
      src/components/layouts/app.tsx
  21. 22
      src/components/layouts/navigation/authorized-menu.tsx
  22. 4
      src/components/layouts/navigation/sidebar-item.tsx
  23. 12
      src/components/layouts/navigation/top-navbar.tsx
  24. 3
      src/components/layouts/shop/index.tsx
  25. 36
      src/components/layouts/topbar/visit-store.tsx
  26. 26
      src/components/order/order-list.tsx
  27. 93
      src/components/product/form-utils.ts
  28. 5
      src/components/product/product-category-input.tsx
  29. 105
      src/components/product/product-form.tsx
  30. 80
      src/components/product/product-list.tsx
  31. 26
      src/components/product/product-simple-form.tsx
  32. 2
      src/components/product/product-tag-input.tsx
  33. 150
      src/components/product/product-validation-schema.ts
  34. 1
      src/components/shop/approve-shop-view.tsx
  35. 272
      src/components/shop/shop-form.tsx
  36. 117
      src/components/shop/shop-validation-schema.ts
  37. 3
      src/components/ui/button.tsx
  38. 194
      src/components/ui/field-array.tsx
  39. 2
      src/components/ui/input.tsx
  40. 2
      src/components/ui/lang-action/action.tsx
  41. 76
      src/components/ui/switch-input.tsx
  42. 2
      src/components/ui/text-area.tsx
  43. 4
      src/components/user/user-details.tsx
  44. 65
      src/components/validations/shop-validation-schema .tsx
  45. 2
      src/components/widgets/sticker-card.tsx
  46. 4
      src/config/routes.ts
  47. 5
      src/contexts/settings.context.tsx
  48. 5
      src/data/category.ts
  49. 30
      src/data/client/api-endpoints.ts
  50. 1
      src/data/client/category.ts
  51. 18
      src/data/client/curd-factory.ts
  52. 25
      src/data/client/http-client.ts
  53. 11
      src/data/client/product.ts
  54. 18
      src/data/client/settings.ts
  55. 4
      src/data/client/shop.ts
  56. 48
      src/data/client/upload.ts
  57. 6
      src/data/client/user.ts
  58. 2
      src/data/dashboard.ts
  59. 2
      src/data/order.ts
  60. 17
      src/data/product.ts
  61. 147
      src/data/settings.ts
  62. 32
      src/data/shop.ts
  63. 2
      src/data/tag.ts
  64. 18
      src/data/upload.ts
  65. 50
      src/data/user.ts
  66. 4
      src/pages/_app.tsx
  67. 6
      src/pages/_document.tsx
  68. 28
      src/pages/index.tsx
  69. 7
      src/pages/logout.tsx
  70. 22
      src/pages/orders/index.tsx
  71. 3
      src/pages/products/[productSlug]/[action].tsx
  72. 45
      src/pages/products/create.tsx
  73. 9
      src/pages/products/index.tsx
  74. 6
      src/pages/register.tsx
  75. 0
      src/pages/shop/attributes/[attributeId]/[action].tsx
  76. 0
      src/pages/shop/attributes/create.tsx
  77. 0
      src/pages/shop/attributes/index.tsx
  78. 0
      src/pages/shop/authors/create.tsx
  79. 0
      src/pages/shop/authors/index.tsx
  80. 0
      src/pages/shop/coupons/[couponSlug]/[action].tsx
  81. 0
      src/pages/shop/coupons/create.tsx
  82. 0
      src/pages/shop/coupons/index.tsx
  83. 6
      src/pages/shop/create.tsx
  84. 13
      src/pages/shop/edit.tsx
  85. 0
      src/pages/shop/faqs/[id]/[action].tsx
  86. 0
      src/pages/shop/faqs/create.tsx
  87. 0
      src/pages/shop/faqs/index.tsx
  88. 0
      src/pages/shop/flash-sale/[slug]/index.tsx
  89. 0
      src/pages/shop/flash-sale/index.tsx
  90. 0
      src/pages/shop/flash-sale/my-products.tsx
  91. 0
      src/pages/shop/flash-sale/vendor-request/[id]/[action].tsx
  92. 0
      src/pages/shop/flash-sale/vendor-request/[id]/index.tsx
  93. 0
      src/pages/shop/flash-sale/vendor-request/create.tsx
  94. 0
      src/pages/shop/flash-sale/vendor-request/index.tsx
  95. 19
      src/pages/shop/index.tsx
  96. 0
      src/pages/shop/manufacturers/create.tsx
  97. 0
      src/pages/shop/manufacturers/index.tsx
  98. 0
      src/pages/shop/orders/[orderId]/index.tsx
  99. 0
      src/pages/shop/orders/index.tsx
  100. 0
      src/pages/shop/orders/transaction.tsx

45
.env.template

@ -1,45 +0,0 @@
# Don't add `/` after the URL
NEXT_PUBLIC_REST_API_ENDPOINT="http://localhost"
NEXT_PUBLIC_SHOP_URL="http://localhost:3003"
# Application Mode and Authentication key
# development or production
APPLICATION_MODE=production
NEXT_PUBLIC_AUTH_TOKEN_KEY=AUTH_CRED
# Default Language
NEXT_PUBLIC_DEFAULT_LANGUAGE=en
# Multilang
# If you want to enable multilang then follow this doc -> https://chawkbazar-doc.vercel.app/multilingual
NEXT_PUBLIC_ENABLE_MULTI_LANG=false
NEXT_PUBLIC_AVAILABLE_LANGUAGES=en,de
# API Key for third party service
NEXT_PUBLIC_GOOGLE_MAP_API_KEY=
# pusher config
# 'log', 'pusher', 'null', 'redis'
NEXT_PUBLIC_API_BROADCAST_DRIVER=pusher
# true or false
NEXT_PUBLIC_PUSHER_DEV_MOOD=true
NEXT_PUBLIC_PUSHER_APP_KEY='4f67daf566b719c6f606'
NEXT_PUBLIC_PUSHER_APP_CLUSTER='ap2'
NEXT_PUBLIC_BROADCAST_AUTH_URL="${NEXT_PUBLIC_REST_API_ENDPOINT}/broadcasting/auth"
# Channel name from PHP
NEXT_PUBLIC_STORE_NOTICE_CREATED_CHANNEL_PRIVATE=private-store_notice.created
NEXT_PUBLIC_ORDER_CREATED_CHANNEL_PRIVATE=private-order.created
NEXT_PUBLIC_MESSAGE_CHANNEL_PRIVATE=private-message.created
# Event name from PHP
NEXT_PUBLIC_STORE_NOTICE_CREATED_EVENT=store.notice.event
NEXT_PUBLIC_ORDER_CREATED_EVENT=order.create.event
NEXT_PUBLIC_MESSAGE_EVENT=message.event
# App version
NEXT_PUBLIC_VERSION="6.6.0"

0
README.md

2
next.config.js

@ -27,6 +27,8 @@ const nextConfig = {
'lh3.googleusercontent.com', 'lh3.googleusercontent.com',
'chawkbazarlaravel.s3.ap-southeast-1.amazonaws.com', 'chawkbazarlaravel.s3.ap-southeast-1.amazonaws.com',
'127.0.0.1:8000', '127.0.0.1:8000',
"fastly.picsum.photos",
"mesbahi.nwhco.ir"
], ],
}, },
...(process.env.APPLICATION_MODE === 'production' && { ...(process.env.APPLICATION_MODE === 'production' && {

2
package.json

@ -1,5 +1,5 @@
{ {
"name": "@marvel/admin-rest",
"name": "@chawkbazar/admin-rest",
"version": "6.6.0", "version": "6.6.0",
"private": true, "private": true,
"scripts": { "scripts": {

3
public/locales/en/common.json

@ -62,7 +62,7 @@
"sidebar-nav-item-settings": "Settings", "sidebar-nav-item-settings": "Settings",
"sidebar-nav-item-store-notice": "Store Notice", "sidebar-nav-item-store-notice": "Store Notice",
"sidebar-nav-item-message": "Message", "sidebar-nav-item-message": "Message",
"sidebar-nav-item-shops": "Shops",
"sidebar-nav-item-shops": "Shop",
"sidebar-nav-item-my-shops": "My Shops", "sidebar-nav-item-my-shops": "My Shops",
"sidebar-nav-item-my-shops-dashboard": "Shops Dashboard", "sidebar-nav-item-my-shops-dashboard": "Shops Dashboard",
"sidebar-nav-item-tags": "Tags", "sidebar-nav-item-tags": "Tags",
@ -229,6 +229,7 @@
"text-shop-approve-description": "Are you sure?", "text-shop-approve-description": "Are you sure?",
"text-approve-shop": "Approve Shop", "text-approve-shop": "Approve Shop",
"filter-by-group": "Filter By Brand", "filter-by-group": "Filter By Brand",
"filter-by-tag": "Filter By Tag",
"filter-by-group-placeholder": "Filter by Brand", "filter-by-group-placeholder": "Filter by Brand",
"text-disapprove-shop": "Disapprove Shop ?", "text-disapprove-shop": "Disapprove Shop ?",
"filter-by-category": "Filter By Category", "filter-by-category": "Filter By Category",

17
public/locales/en/form.json

@ -63,8 +63,9 @@
"input-label-disable-variant": "Disable This Variant", "input-label-disable-variant": "Disable This Variant",
"input-label-logo": "Logo", "input-label-logo": "Logo",
"input-label-collapse-logo": "Collapse Logo", "input-label-collapse-logo": "Collapse Logo",
"input-label-email": "Email",
"input-label-token": "Put your token you got from email", "input-label-token": "Put your token you got from email",
"input-label-email": "Email",
"input-label-number": "Phone Number",
"input-label-password": "Password", "input-label-password": "Password",
"input-label-bio": "Bio", "input-label-bio": "Bio",
"input-label-contact": "Contact Number", "input-label-contact": "Contact Number",
@ -79,11 +80,13 @@
"input-label-orders": "Orders", "input-label-orders": "Orders",
"input-label-order-id": "Order ID", "input-label-order-id": "Order ID",
"input-label-sku": "SKU", "input-label-sku": "SKU",
"input-label-stock": "Stock",
"input-note-multilang-sku": "Make sure SKU is identical for all languages.", "input-note-multilang-sku": "Make sure SKU is identical for all languages.",
"input-label-name": "Name", "input-label-name": "Name",
"input-label-description": "Description", "input-label-description": "Description",
"input-label-price": "Price", "input-label-price": "Price",
"input-label-sale-price": "Sale Price", "input-label-sale-price": "Sale Price",
"input-label-discount": "Discount",
"input-label-quantity": "Quantity", "input-label-quantity": "Quantity",
"input-label-unit": "Unit", "input-label-unit": "Unit",
"input-label-width": "Width", "input-label-width": "Width",
@ -91,7 +94,7 @@
"input-label-length": "Length", "input-label-length": "Length",
"input-label-status": "Status", "input-label-status": "Status",
"input-label-under-review": "Under Review", "input-label-under-review": "Under Review",
"input-label-published": "Published",
"input-label-published": "Publish",
"input-label-unpublish": "Unpublish", "input-label-unpublish": "Unpublish",
"input-label-draft": "Draft", "input-label-draft": "Draft",
"input-label-approved": "Approved", "input-label-approved": "Approved",
@ -226,6 +229,7 @@
"edit-attribute": "Edit Attribute", "edit-attribute": "Edit Attribute",
"error-message-required": "Message is required", "error-message-required": "Message is required",
"error-name-required": "Name is required", "error-name-required": "Name is required",
"error-slug-required": "Slug is required",
"error-value-required": "Value is required", "error-value-required": "Value is required",
"error-meta-required": "Meta is required", "error-meta-required": "Meta is required",
"error-author-name-required": "Author name is required", "error-author-name-required": "Author name is required",
@ -267,6 +271,7 @@
"input-label-account-holder-email": "Account Holder Email", "input-label-account-holder-email": "Account Holder Email",
"input-label-bank-name": "Bank Name", "input-label-bank-name": "Bank Name",
"input-label-account-number": "Account Number", "input-label-account-number": "Account Number",
"input-label-iban-number": "International Bank Account Number",
"shop-address": "Shop Address", "shop-address": "Shop Address",
"shop-address-helper-text": "Add your physical shop address from here", "shop-address-helper-text": "Add your physical shop address from here",
"footer-address": "Address", "footer-address": "Address",
@ -336,12 +341,20 @@
"error-color-required": "Color is required", "error-color-required": "Color is required",
"error-product-type-required": "Product type is required", "error-product-type-required": "Product type is required",
"error-sku-required": "SKU is required", "error-sku-required": "SKU is required",
"error-stock-must-number" : "Stock must be a number",
"error-stock-must-positive" :"Stock must be positive",
"error-stock-required": "Stock is required",
"error-stock-must-integer": "Stock must be integer",
"error-price-must-number": "Price must be a number", "error-price-must-number": "Price must be a number",
"error-account-must-number": "Account must be a number", "error-account-must-number": "Account must be a number",
"error-price-must-positive": "Price must be positive", "error-price-must-positive": "Price must be positive",
"error-price-required": "Price is required", "error-price-required": "Price is required",
"error-sale-price-less-number": "Sale Price should be less than", "error-sale-price-less-number": "Sale Price should be less than",
"error-sale-price-must-positive": "Sale price must be positive", "error-sale-price-must-positive": "Sale price must be positive",
"error-discount-must-number" : "The discount must be a valid number.",
"error-discount-minimum" : "The discount cannot be less than 0",
"error-discount-maximum" : "The discount cannot be more than 100.",
"error-discount-required" : "The discount field is required.",
"error-free-shipping-amount-must-positive": "Free shipping amount must be positive", "error-free-shipping-amount-must-positive": "Free shipping amount must be positive",
"error-quantity-must-number": "Quantity must be a number", "error-quantity-must-number": "Quantity must be a number",
"error-quantity-must-positive": "Quantity must be positive", "error-quantity-must-positive": "Quantity must be positive",

45
src/components/auth/forget-password/enter-email-view.tsx

@ -6,38 +6,49 @@ import * as yup from 'yup';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
interface Props { interface Props {
onSubmit: (values: { email: string }) => void;
onSubmit: (values: { phone_number: string , range_phone : string }) => void;
loading: boolean; loading: boolean;
} }
const schema = yup.object().shape({ const schema = yup.object().shape({
email: yup
.string()
.email('form:error-email-format')
.required('form:error-email-required'),
phone_number: yup.string().required('Phone number is required'),
range_phone: yup.string().required('Country code is required'),
}); });
const EnterEmailView = ({ onSubmit, loading }: Props) => {
const EnterEmailView = ({ onSubmit, loading }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { const {
register, register,
handleSubmit, handleSubmit,
formState: { errors }, formState: { errors },
} = useForm<{ email: string }>({ resolver: yupResolver(schema) });
} = useForm<{ phone_number: string , range_phone : string }>({ resolver: yupResolver(schema) });
return ( return (
<form onSubmit={handleSubmit(onSubmit)} noValidate> <form onSubmit={handleSubmit(onSubmit)} noValidate>
<Input
label={t('form:input-label-email')}
{...register('email')}
type="email"
variant="outline"
className="mb-5"
placeholder="demo@demo.com"
error={t(errors.email?.message!)}
/>
<Button className="h-11 w-full" loading={loading} disabled={loading}>
<label className="block">
<span className="text-gray-600 font-semibold text-sm leading-none mb-3">
Phone Number
</span>
<div className="py-1.5 px-4 md:px-5 w-full appearance-none border text-input text-xs lg:text-sm font-body placeholder-body min-h-12 transition duration-200 ease-in-out border-gray-300 focus:shadow focus:bg-white focus:border-primary flex items-center rounded-md">
<span className="mr-[-10px] text-gray-700">+</span>
<input
type="number"
placeholder="000"
maxLength={3}
{...register('range_phone')}
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none w-[50px] bg-gray-100 text-center bg-transparent text-gray-800 border-none outline-none"
/>
<span className="px-2 text-gray-500">|</span>
<input
{...register('phone_number')}
type="number"
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none flex-1 bg-gray-100 border-none outline-none bg-transparent text-gray-800 p-2"
/>
</div>
</label>
<Button type='submit' className="h-11 w-full" loading={loading} disabled={loading}>
{t('form:text-submit-email')} {t('form:text-submit-email')}
</Button> </Button>
</form> </form>

17
src/components/auth/forget-password/enter-new-password-view.tsx

@ -6,12 +6,16 @@ import * as yup from 'yup';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
interface Props { interface Props {
onSubmit: (values: { password: string }) => void;
onSubmit: (values: { password: string , password_confirmation : string }) => void;
loading: boolean; loading: boolean;
} }
const schema = yup.object().shape({ const schema = yup.object().shape({
password: yup.string().required('form:error-password-required'), password: yup.string().required('form:error-password-required'),
password_confirmation: yup
.string()
.oneOf([yup.ref('password')], 'Passwords must match')
.required('Confirm password is required'),
}); });
const EnterNewPasswordView = ({ onSubmit, loading }: Props) => { const EnterNewPasswordView = ({ onSubmit, loading }: Props) => {
@ -20,9 +24,10 @@ const EnterNewPasswordView = ({ onSubmit, loading }: Props) => {
register, register,
handleSubmit, handleSubmit,
formState: { errors }, formState: { errors },
} = useForm<{ password: string }>({ resolver: yupResolver(schema) });
} = useForm<{ password: string , password_confirmation : string }>({ resolver: yupResolver(schema) });
return ( return (
<form onSubmit={handleSubmit(onSubmit)} noValidate> <form onSubmit={handleSubmit(onSubmit)} noValidate>
<PasswordInput <PasswordInput
label={t('form:input-label-password')} label={t('form:input-label-password')}
@ -31,7 +36,13 @@ const EnterNewPasswordView = ({ onSubmit, loading }: Props) => {
variant="outline" variant="outline"
className="mb-5" className="mb-5"
/> />
<PasswordInput
label={t('form:input-label-confirm-password')}
{...register('password_confirmation')}
error={t(errors.password?.message!)}
variant="outline"
className="mb-5"
/>
<Button className="h-11 w-full" loading={loading} disabled={loading}> <Button className="h-11 w-full" loading={loading} disabled={loading}>
{t('form:text-reset-password')} {t('form:text-reset-password')}
</Button> </Button>

191
src/components/auth/forget-password/forget-password.tsx

@ -1,5 +1,6 @@
import { useState } from 'react';
import { useState, useRef, useEffect } from 'react';
import Alert from '@/components/ui/alert'; import Alert from '@/components/ui/alert';
import Button from '@/components/ui/button';
import { import {
useForgetPasswordMutation, useForgetPasswordMutation,
useVerifyForgetPasswordTokenMutation, useVerifyForgetPasswordTokenMutation,
@ -8,9 +9,11 @@ import {
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import Router from 'next/router'; import Router from 'next/router';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import Cookies from 'js-cookie';
import { setAuthCredentials } from '@/utils/auth-utils';
import { Routes } from '@/config/routes';
const EnterEmailView = dynamic(() => import('./enter-email-view')); const EnterEmailView = dynamic(() => import('./enter-email-view'));
const EnterTokenView = dynamic(() => import('./enter-token-view'));
const EnterNewPasswordView = dynamic(() => import('./enter-new-password-view')); const EnterNewPasswordView = dynamic(() => import('./enter-new-password-view'));
const ForgotPassword = () => { const ForgotPassword = () => {
@ -20,64 +23,196 @@ const ForgotPassword = () => {
useVerifyForgetPasswordTokenMutation(); useVerifyForgetPasswordTokenMutation();
const { mutate: resetPassword, isLoading: resetting } = const { mutate: resetPassword, isLoading: resetting } =
useResetPasswordMutation(); useResetPasswordMutation();
const [errorMsg, setErrorMsg] = useState<string | null | undefined>('');
const [stage, setStage] = useState('email'); // Stages: email, otp, password
const [otp, setOtp] = useState(['', '', '', '', '']); // OTP state
const [time, setTime] = useState(30); // Timer for OTP resend
const otpRefs = useRef<(HTMLInputElement | null)[]>([]);
const firstOtpRef = useRef<HTMLInputElement | null>(null);
const [verifiedEmail, setVerifiedEmail] = useState(''); const [verifiedEmail, setVerifiedEmail] = useState('');
const [verifiedToken, setVerifiedToken] = useState(''); const [verifiedToken, setVerifiedToken] = useState('');
const [errorMsg, setErrorMsg] = useState<string | null | undefined>('');
const AUTH_TOKEN_KEY = process.env.NEXT_PUBLIC_AUTH_TOKEN_KEY ?? 'authToken';
useEffect(() => {
if (time > 0 && stage === 'otp') {
const timer = setInterval(
() => setTime((prevTime) => prevTime - 1),
1000,
);
return () => clearInterval(timer);
}
}, [time, stage]);
function handleEmailSubmit({ email }: { email: string }) {
useEffect(() => {
if (stage === 'otp' && firstOtpRef.current) {
firstOtpRef.current.focus();
}
}, [stage]);
function handleEmailSubmit({
phone_number,
range_phone,
}: {
phone_number: string;
range_phone: string;
}) {
setVerifiedEmail('+' + range_phone + phone_number);
forgetPassword( forgetPassword(
{ {
email,
phone_number: '+' + range_phone + phone_number,
range_phone,
user_type: 'user',
}, },
{ {
onSuccess: (data) => { onSuccess: (data) => {
if (data?.success) {
setVerifiedEmail(email);
} else {
setErrorMsg(data?.message);
}
setStage('otp');
// if (data?.success) {
// setVerifiedEmail(`${range_phone}${phone_number}`);
// setStage('otp');
// } else {
// setErrorMsg(data?.message);
// }
}, },
}
onError: (error) => {
setErrorMsg(error?.response?.data?.message || 'An error occurred.');
},
},
); );
} }
function handleTokenSubmit({ token }: { token: string }) {
function handleOTPSubmit() {
if (!otp.join('').trim()) {
setErrorMsg('Please enter the OTP');
return;
}
verifyToken( verifyToken(
{ {
email: verifiedEmail,
token,
method: 'reset',
phone_number: verifiedEmail,
code: otp.join(''),
user_type: 'merchant',
}, },
{ {
onSuccess: (data) => { onSuccess: (data) => {
if (data?.success) {
setVerifiedToken(token);
} else {
setErrorMsg(data?.message);
}
setAuthCredentials(data.token);
setVerifiedToken(data.token);
setStage('password');
}, },
}
onError: (error: any) => {
setErrorMsg(error?.response?.data?.message || 'Invalid OTP.');
},
},
); );
} }
function handleResetPassword({ password }: { password: string }) {
function handleResetPassword({
password,
password_confirmation,
}: {
password: string;
password_confirmation: string;
}) {
resetPassword( resetPassword(
{ {
email: verifiedEmail,
phone_number: verifiedEmail,
password_confirmation,
token: verifiedToken, token: verifiedToken,
password, password,
}, },
{ {
onSuccess: (data) => { onSuccess: (data) => {
if (data?.success) { if (data?.success) {
Router.push('/');
Router.push(Routes.logout);
} else { } else {
setErrorMsg(data?.message); setErrorMsg(data?.message);
} }
}, },
}
onError: (error: any) => {
setErrorMsg(
error?.response?.data?.message || 'Password reset failed.',
);
},
},
); );
} }
const handleOtpChange = (value: string, index: number) => {
if (/^[0-9]?$/.test(value)) {
const newOtp = [...otp];
newOtp[index] = value;
setOtp(newOtp);
if (value && index < otpRefs.current.length - 1) {
otpRefs.current[index + 1]?.focus();
}
}
};
const handleKeyDown = (
e: React.KeyboardEvent<HTMLInputElement>,
index: number,
) => {
if (e.key === 'Backspace' && !otp[index] && index > 0) {
otpRefs.current[index - 1]?.focus();
}
};
const renderOTP = () => (
<div className="nc-PageSignUp">
<div className="flex gap-4 flex-col">
<h2 className="my-10 text-center text-2xl font-semibold text-neutral-900">
Verification Code
</h2>
<p className="text-center text-sm text-neutral-500 mb-4">
Enter the 5-digit code we sent to your phone.
</p>
<div className="max-w-sm mx-auto space-y-6">
<div className="flex justify-center space-x-2 mb-4">
{otp.map((value, index) => (
<input
key={index}
ref={(el) => {
otpRefs.current[index] = el;
if (index === 0) firstOtpRef.current = el;
}}
type="text"
maxLength={1}
value={value}
onChange={(e) => handleOtpChange(e.target.value, index)}
onKeyDown={(e) => handleKeyDown(e, index)}
className="w-12 h-12 border rounded-lg text-center text-lg font-semibold border-neutral-200 focus:outline-none focus:ring-2 focus:ring-primary-500"
/>
))}
</div>
<p className="text-center text-sm text-neutral-500 mb-4">
Didnt receive the code?{' '}
<button
onClick={() => setTime(30)} // Example resend logic
className={`text-primary-600 hover:underline ${
time > 0 ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'
}`}
disabled={time > 0}
>
Resend
</button>
{time > 0 && (
<span className="text-xs text-neutral-400">({time}s)</span>
)}
</p>
<Button
onClick={handleOTPSubmit}
className="h-11 md:h-12 w-full mt-2"
>
Confirm
</Button>
</div>
</div>
</div>
);
return ( return (
<> <>
{errorMsg && ( {errorMsg && (
@ -89,13 +224,11 @@ const ForgotPassword = () => {
onClose={() => setErrorMsg('')} onClose={() => setErrorMsg('')}
/> />
)} )}
{!verifiedEmail && (
{stage === 'email' && (
<EnterEmailView loading={isLoading} onSubmit={handleEmailSubmit} /> <EnterEmailView loading={isLoading} onSubmit={handleEmailSubmit} />
)} )}
{verifiedEmail && !verifiedToken && (
<EnterTokenView loading={verifying} onSubmit={handleTokenSubmit} />
)}
{verifiedEmail && verifiedToken && (
{stage === 'otp' && renderOTP()}
{stage === 'password' && (
<EnterNewPasswordView <EnterNewPasswordView
loading={resetting} loading={resetting}
onSubmit={handleResetPassword} onSubmit={handleResetPassword}

72
src/components/auth/login-form.tsx

@ -18,10 +18,8 @@ import {
} from '@/utils/auth-utils'; } from '@/utils/auth-utils';
const loginFormSchema = yup.object().shape({ const loginFormSchema = yup.object().shape({
email: yup
.string()
.email('form:error-email-format')
.required('form:error-email-required'),
phone_number: yup.string().required('Phone number is required'),
range_phone: yup.string().required('Country code is required'),
password: yup.string().required('form:error-password-required'), password: yup.string().required('form:error-password-required'),
}); });
@ -30,27 +28,33 @@ const LoginForm = () => {
const [errorMessage, setErrorMessage] = useState<string | null>(null); const [errorMessage, setErrorMessage] = useState<string | null>(null);
const { mutate: login, isLoading, error } = useLogin(); const { mutate: login, isLoading, error } = useLogin();
function onSubmit({ email, password }: LoginInput) {
function onSubmit({ phone_number, range_phone, password }: LoginInput) {
login( login(
{ {
email,
phone_number: range_phone + phone_number,
password, password,
user_type: 'merchant',
}, },
{ {
onSuccess: (data) => { onSuccess: (data) => {
if (data?.token) {
if (hasAccess(allowedRoles, data?.permissions)) {
setAuthCredentials(data?.token, data?.permissions, data?.role);
Router.push(Routes.dashboard);
return;
}
setErrorMessage('form:error-enough-permission');
} else {
setErrorMessage('form:error-credential-wrong');
}
setAuthCredentials(data.token)
Router.push(Routes.dashboard);
// if (data?.token) {
// if (hasAccess(allowedRoles, data?.permissions)) {
// setAuthCredentials(data?.token, data?.permissions, data?.role);
// return;
// }
// setErrorMessage('form:error-enough-permission');
// } else {
// setErrorMessage('form:error-credential-wrong');
// }
},
onError: (err) => {
console.log(err);
}, },
onError: () => {},
}
},
); );
} }
@ -59,14 +63,28 @@ const LoginForm = () => {
<Form<LoginInput> validationSchema={loginFormSchema} onSubmit={onSubmit}> <Form<LoginInput> validationSchema={loginFormSchema} onSubmit={onSubmit}>
{({ register, formState: { errors } }) => ( {({ register, formState: { errors } }) => (
<> <>
<Input
label={t('form:input-label-email')}
{...register('email')}
type="email"
variant="outline"
className="mb-4"
error={t(errors?.email?.message!)}
/>
<label className="block">
<span className="text-gray-600 font-semibold text-sm leading-none mb-3">
Phone Number
</span>
<div className="py-1.5 px-4 md:px-5 w-full appearance-none border text-input text-xs lg:text-sm font-body placeholder-body min-h-12 transition duration-200 ease-in-out border-gray-300 focus:shadow focus:bg-white focus:border-primary flex items-center rounded-md">
<span className="mr-[-10px] text-gray-700">+</span>
<input
type="number"
placeholder="000"
maxLength={3}
{...register('range_phone')}
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none w-[50px] bg-gray-100 text-center bg-transparent text-gray-800 border-none outline-none"
/>
<span className="px-2 text-gray-500">|</span>
<input
{...register('phone_number')}
type="number"
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none flex-1 bg-gray-100 border-none outline-none bg-transparent text-gray-800 p-2"
/>
</div>
</label>
<PasswordInput <PasswordInput
label={t('form:input-label-password')} label={t('form:input-label-password')}
forgotPassHelpText={t('form:input-forgot-password-label')} forgotPassHelpText={t('form:input-forgot-password-label')}
@ -76,7 +94,7 @@ const LoginForm = () => {
className="mb-4" className="mb-4"
forgotPageLink={Routes.forgotPassword} forgotPageLink={Routes.forgotPassword}
/> />
<Button className="w-full" loading={isLoading} disabled={isLoading}>
<Button type='submit' className="w-full" loading={isLoading} disabled={isLoading}>
{t('form:button-label-login')} {t('form:button-label-login')}
</Button> </Button>

320
src/components/auth/registration-form.tsx

@ -16,26 +16,39 @@ import {
setAuthCredentials, setAuthCredentials,
} from '@/utils/auth-utils'; } from '@/utils/auth-utils';
import { Permission } from '@/types'; import { Permission } from '@/types';
import { useRegisterMutation } from '@/data/user';
import { useOTPMutation, useRegisterMutation } from '@/data/user';
import { useRef } from 'react';
import { useEffect } from 'react';
type FormValues = { type FormValues = {
name: string;
email: string;
fullname: string;
phone_number: string;
range_phone: string;
password: string; password: string;
permission: Permission;
password_confirmation: string;
user_type: 'merchant';
}; };
const registrationFormSchema = yup.object().shape({ const registrationFormSchema = yup.object().shape({
name: yup.string().required('form:error-name-required'),
email: yup
fullname: yup.string().required('Full name is required'),
phone_number: yup.string().required('Phone number is required'),
range_phone: yup.string().required('Country code is required'),
password: yup.string().required('Password is required'),
password_confirmation: yup
.string() .string()
.email('form:error-email-format')
.required('form:error-email-required'),
password: yup.string().required('form:error-password-required'),
.oneOf([yup.ref('password')], 'Passwords must match')
.required('Confirm password is required'),
permission: yup.string().default('store_owner').oneOf(['store_owner']), permission: yup.string().default('store_owner').oneOf(['store_owner']),
}); });
const RegistrationForm = () => { const RegistrationForm = () => {
const [errorMessage, setErrorMessage] = useState<string | null>(null); const [errorMessage, setErrorMessage] = useState<string | null>(null);
const { mutate: registerUser, isLoading: loading } = useRegisterMutation(); const { mutate: registerUser, isLoading: loading } = useRegisterMutation();
const { mutate: confirmUser, isLoading: OTPloading } = useOTPMutation();
const [stage, setStage] = useState('signUp'); // Stage management
const [otp, setOtp] = useState(['', '', '', '', '']); // OTP state
const [time, setTime] = useState(3); // Timer for OTP resend
const otpRefs = useRef<(HTMLInputElement | null)[]>([]); // Refs for OTP input focus
const firstOtpRef = useRef<HTMLInputElement | null>(null);
const { const {
register, register,
@ -44,72 +57,161 @@ const RegistrationForm = () => {
setError, setError,
} = useForm({ } = useForm({
resolver: yupResolver(registrationFormSchema), resolver: yupResolver(registrationFormSchema),
defaultValues: {
permission: Permission.StoreOwner,
},
}); });
const router = useRouter(); const router = useRouter();
const { t } = useTranslation(); const { t } = useTranslation();
async function onSubmit({ name, email, password, permission }: FormValues) {
registerUser(
{
name,
email,
password,
//@ts-ignore
permission,
},
{
onSuccess: (data) => {
if (data?.token) {
if (hasAccess(allowedRoles, data?.permissions)) {
setAuthCredentials(data?.token, data?.permissions, data?.role);
router.push(Routes.dashboard);
return;
}
setErrorMessage('form:error-enough-permission');
} else {
setErrorMessage('form:error-credential-wrong');
}
useEffect(() => {
if (time > 0 && stage === 'OTP') {
const timer = setInterval(
() => setTime((prevTime) => prevTime - 1),
1000,
);
return () => clearInterval(timer);
}
}, [time, stage]);
useEffect(() => {
if (stage === 'OTP' && firstOtpRef.current) {
firstOtpRef.current.focus();
}
}, [stage]);
async function onSubmit({
fullname,
phone_number,
range_phone,
password,
password_confirmation,
}: FormValues) {
if (stage === 'signUp') {
registerUser(
{
fullname,
phone_number: '+' + range_phone + phone_number,
range_phone,
password,
password_confirmation,
verification_method: 'whatsapp',
user_type: 'merchant',
}, },
onError: (error: any) => {
Object.keys(error?.response?.data).forEach((field: any) => {
setError(field, {
type: 'manual',
message: error?.response?.data[field],
{
onSuccess: (data) => {
// if (data?.token) {
setStage('OTP'); // Transition to OTP stage on successful registration
// } else {
// setErrorMessage('form:error-credential-wrong');
// }
},
onError: (error: any) => {
Object.keys(error?.response?.data).forEach((field: any) => {
setError(field, {
type: 'manual',
message: error?.response?.data[field],
});
}); });
});
},
}, },
},
);
);
}
if (stage === 'OTP') {
confirmUser(
{
method: 'register',
phone_number: '+' + range_phone + phone_number,
code: otp.join(''),
user_type: 'merchant',
},
{
onSuccess: (data) => {
// if (data?.token) {
setAuthCredentials(data.token)
router.replace(Routes.dashboard);
// } else {
// setErrorMessage('form:error-credential-wrong');
// }
},
onError: (error: any) => {
console.log(error);
Object.keys(error?.response?.data).forEach((field: any) => {
setError(field, {
type: 'manual',
message: error?.response?.data[field],
});
});
},
},
);
}
} }
return (
<>
<form
onSubmit={handleSubmit(
//@ts-ignore
onSubmit,
)}
noValidate
>
const handleOtpChange = (value: string, index: number) => {
if (/^[0-9]?$/.test(value)) {
const newOtp = [...otp];
newOtp[index] = value;
setOtp(newOtp);
if (value && index < otpRefs.current.length - 1) {
otpRefs.current[index + 1]?.focus();
}
}
};
const handleKeyDown = (
e: React.KeyboardEvent<HTMLInputElement>,
index: number,
) => {
if (e.key === 'Backspace' && !otp[index] && index > 0) {
otpRefs.current[index - 1]?.focus();
}
};
const handleOTPSubmit = () => {
if (!otp.join('').trim()) {
setErrorMessage('Please enter the OTP');
return;
}
// Replace with your OTP verification logic
setErrorMessage(null);
// router.push(Routes.dashboard);
};
const renderSignIn = () => {
return (
<>
<Input <Input
label={t('form:input-label-name')} label={t('form:input-label-name')}
{...register('name')}
variant="outline"
className="mb-4"
error={t(errors?.name?.message!)}
/>
<Input
label={t('form:input-label-email')}
{...register('email')}
type="email"
{...register('fullname')}
variant="outline" variant="outline"
className="mb-4" className="mb-4"
error={t(errors?.email?.message!)}
error={t(errors?.fullname?.message!)}
/> />
<label className="block">
<span className="text-gray-600 font-semibold text-sm leading-none mb-3">
Phone Number
</span>
<div className="py-1.5 px-4 md:px-5 w-full appearance-none border text-input text-xs lg:text-sm font-body placeholder-body min-h-12 transition duration-200 ease-in-out border-gray-300 focus:shadow focus:bg-white focus:border-primary flex items-center rounded-md">
<span className="mr-[-10px] text-gray-700">+</span>
<input
type="number"
placeholder="000"
maxLength={3}
{...register('range_phone')}
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none w-[50px] bg-gray-100 text-center bg-transparent text-gray-800 border-none outline-none"
/>
<span className="px-2 text-gray-500">|</span>
<input
{...register('phone_number')}
type="number"
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none flex-1 bg-gray-100 border-none outline-none bg-transparent text-gray-800 p-2"
/>
</div>
</label>
<PasswordInput <PasswordInput
label={t('form:input-label-password')} label={t('form:input-label-password')}
{...register('password')} {...register('password')}
@ -117,7 +219,19 @@ const RegistrationForm = () => {
variant="outline" variant="outline"
className="mb-4" className="mb-4"
/> />
<Button className="w-full" loading={loading} disabled={loading}>
<PasswordInput
label={t('form:input-label-confirm-password')}
{...register('password_confirmation')}
error={t(errors?.password_confirmation?.message!)}
variant="outline"
className="mb-4"
/>
<Button
type="submit"
className="w-full"
loading={loading}
disabled={loading}
>
{t('form:text-register')} {t('form:text-register')}
</Button> </Button>
@ -130,23 +244,73 @@ const RegistrationForm = () => {
onClose={() => setErrorMessage(null)} onClose={() => setErrorMessage(null)}
/> />
) : null} ) : null}
</form>
<div className="relative flex flex-col items-center justify-center mt-8 mb-6 text-sm text-heading sm:mt-11 sm:mb-8">
<hr className="w-full" />
<span className="start-2/4 -ms-4 absolute -top-2.5 bg-light px-2">
{t('common:text-or')}
</span>
</div>
<div className="text-sm text-center text-body sm:text-base">
{t('form:text-already-account')}{' '}
<Link
href={Routes.login}
className="font-semibold underline transition-colors duration-200 ms-1 text-accent hover:text-accent-hover hover:no-underline focus:text-accent-700 focus:no-underline focus:outline-none"
>
{t('form:button-label-login')}
</Link>
</>
);
};
const renderOTP = () => (
<div className="nc-PageSignUp">
<div className="flex gap-4 flex-col">
<h2 className="my-10 text-center text-2xl font-semibold text-neutral-900">
Verification Code
</h2>
<p className="text-center text-sm text-neutral-500 mb-4">
Enter the 5-digit code we sent to your phone.
</p>
<div className="max-w-sm mx-auto space-y-6">
<div className="flex justify-center space-x-2 mb-4">
{otp.map((value, index) => (
<input
key={index}
ref={(el) => {
otpRefs.current[index] = el;
if (index === 0) firstOtpRef.current = el;
}}
type="text"
maxLength={1}
value={value}
onChange={(e) => handleOtpChange(e.target.value, index)}
onKeyDown={(e) => handleKeyDown(e, index)}
className="w-12 h-12 border rounded-lg text-center text-lg font-semibold border-neutral-200 focus:outline-none focus:ring-2 focus:ring-primary-500"
/>
))}
</div>
<p className="text-center text-sm text-neutral-500 mb-4">
Didnt receive the code?{' '}
<Button
onClick={() => {
setTime(30)
setStage("signUp")
}}
className={`text-primary-600 hover:underline p-0 bg-0 hover:bg-o disabled:border-none ${
time > 0 ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'
}`}
disabled={time > 0}
>
Resend
</Button>
{time > 0 && (
<span className="text-xs text-neutral-400">({time}s)</span>
)}
</p>
<Button
type="submit"
className="h-11 md:h-12 w-full mt-2"
>
Confirm
</Button>
</div>
</div> </div>
</>
</div>
);
return (
//@ts-ignore
<form onSubmit={handleSubmit(onSubmit)} noValidate>
{stage === 'signUp' && renderSignIn()}
{stage === 'OTP' && renderOTP()}
</form>
); );
}; };

39
src/components/common/get-file-url.tsx

@ -0,0 +1,39 @@
import { HttpClient } from "@/data/client/http-client";
const getFileURL = async (file: File) => {
let image;
if (file) {
const modifiedFileName = file.name.replaceAll(" ", "");
const modifiedFile = new File([file], modifiedFileName, {
type: file.type,
lastModified: file.lastModified,
});
const formData = new FormData();
formData.append("file", modifiedFile);
console.log("gfdsgfdsgfsd");
try {
const response = await HttpClient.post(
'https://mesbahi.nwhco.ir/api/upload-tmp-media/', // Full URL
formData,
{
headers: {
"Content-Type": "multipart/form-data",
"X-CSRFToken": "58B9R5uED9steCGYg4A4hOB2dt6BZ5lT52R08lk6cIcOhgWiMNRRdKgJ6WXhhQMb", // Include CSRF token
},
}
);
if (response.status === 200) {
image = response.data;
} else {
console.log("Something went wrong during file upload");
}
} catch (error: any) {
console.log(error.response?.data || error.message); // Log the actual error response
}
}
return image;
};
export default getFileURL;

48
src/components/common/uploader.tsx

@ -44,33 +44,39 @@ export default function Uploader({
: { ...ACCEPTED_FILE_TYPES }), : { ...ACCEPTED_FILE_TYPES }),
multiple, multiple,
onDrop: async (acceptedFiles) => { onDrop: async (acceptedFiles) => {
if (acceptedFiles.length) { if (acceptedFiles.length) {
upload( upload(
acceptedFiles, // it will be an array of uploaded attachments acceptedFiles, // it will be an array of uploaded attachments
{ {
onSuccess: (data: any) => { onSuccess: (data: any) => {
// Process Digital File Name section // Process Digital File Name section
data &&
data?.map((file: any, idx: any) => {
const splitArray = file?.original?.split('/');
let fileSplitName =
splitArray[splitArray?.length - 1]?.split('.');
const fileType = fileSplitName?.pop(); // it will pop the last item from the fileSplitName arr which is the file ext
const filename = fileSplitName?.join('.'); // it will join the array with dot, which restore the original filename
data[idx]['file_name'] = filename + '.' + fileType;
});
// data &&(
// data?.map((file: any, idx: any) => {
// const splitArray = file?.file?.split('/');
// let fileSplitName =
// splitArray[splitArray?.length - 1]?.split('.');
// const fileType = fileSplitName?.pop(); // it will pop the last item from the fileSplitName arr which is the file ext
// const filename = fileSplitName?.join('.'); // it will join the array with dot, which restore the original filename
// data[idx]['file_name'] = filename + '.' + fileType;
// }));
let mergedData; let mergedData;
if (multiple) { if (multiple) {
mergedData = files.concat(data);
setFiles(files.concat(data));
} else {
mergedData = data[0];
setFiles(data);
mergedData = files.concat({image : data.url , display_order : files.length + 1});
setFiles(files.concat({image : data.url , display_order : files.length + 1}));
}
else {
mergedData = data.url;
setFiles(files.concat(data.url));
} }
if (onChange) { if (onChange) {
onChange(mergedData); onChange(mergedData);
} }
}, },
}, },
); );
@ -92,7 +98,7 @@ export default function Uploader({
}); });
const handleDelete = (image: string) => { const handleDelete = (image: string) => {
const images = files.filter((file) => file.thumbnail !== image);
const images = files.filter((file) => file.display_order !== image);
setFiles(images); setFiles(images);
if (onChange) { if (onChange) {
onChange(images); onChange(images);
@ -112,14 +118,14 @@ export default function Uploader({
'raw', 'raw',
]; ];
// let filename, fileType, isImage; // let filename, fileType, isImage;
if (file && file.id) {
if (file) {
// const processedFile = processFileWithName(file); // const processedFile = processFileWithName(file);
const splitArray = file?.file_name
? file?.file_name.split('.')
const splitArray = file?.image
? file?.image.split('.')
: file?.thumbnail?.split('.'); : file?.thumbnail?.split('.');
const fileType = splitArray?.pop(); // it will pop the last item from the fileSplitName arr which is the file ext const fileType = splitArray?.pop(); // it will pop the last item from the fileSplitName arr which is the file ext
const filename = splitArray?.join('.'); // it will join the array with dot, which restore the original filename const filename = splitArray?.join('.'); // it will join the array with dot, which restore the original filename
const isImage = file?.thumbnail && imgTypes.includes(fileType); // check if the original filename has the img ext
const isImage = imgTypes.includes(fileType); // check if the original filename has the img ext
// Old Code ******* // Old Code *******
@ -150,7 +156,7 @@ export default function Uploader({
// </div> // </div>
<figure className="relative flex items-center justify-center h-16 w-28 aspect-square"> <figure className="relative flex items-center justify-center h-16 w-28 aspect-square">
<Image <Image
src={file.thumbnail}
src={file.image}
alt={filename} alt={filename}
fill fill
sizes="(max-width: 768px) 100vw" sizes="(max-width: 768px) 100vw"
@ -183,7 +189,7 @@ export default function Uploader({
{!disabled ? ( {!disabled ? (
<button <button
className="absolute flex items-center justify-center w-4 h-4 text-xs bg-red-600 rounded-full shadow-xl outline-none top-1 text-light end-1" className="absolute flex items-center justify-center w-4 h-4 text-xs bg-red-600 rounded-full shadow-xl outline-none top-1 text-light end-1"
onClick={() => handleDelete(file.thumbnail)}
onClick={() => handleDelete(file.display_order)}
> >
<CloseIcon width={10} height={10} /> <CloseIcon width={10} height={10} />
</button> </button>

50
src/components/dashboard/admin.tsx

@ -64,12 +64,16 @@ export default function Dashboard() {
const [orderDataRange, setOrderDataRange] = useState( const [orderDataRange, setOrderDataRange] = useState(
data?.todayTotalOrderByStatus, data?.todayTotalOrderByStatus,
); );
console.log(data);
const { price: total_revenue } = usePrice( const { price: total_revenue } = usePrice(
data && { data && {
amount: data?.totalRevenue!,
amount: data?.merchant_info?.summary.total_revenue!,
}, },
); );
console.log(total_revenue);
const { price: todays_revenue } = usePrice( const { price: todays_revenue } = usePrice(
data && { data && {
amount: data?.todaysRevenue!, amount: data?.todaysRevenue!,
@ -165,26 +169,26 @@ export default function Dashboard() {
} }
}); });
if (
loading ||
orderLoading ||
popularProductLoading ||
withdrawLoading ||
topRatedProductsLoading
) {
return <Loader text={t('common:text-loading')} />;
}
if (orderError || popularProductError || topRatedProductsError) {
return (
<ErrorMessage
message={
orderError?.message ||
popularProductError?.message ||
topRatedProductsError?.message
}
/>
);
}
// if (
// loading ||
// orderLoading ||
// popularProductLoading ||
// withdrawLoading ||
// topRatedProductsLoading
// ) {
// return <Loader text={t('common:text-loading')} />;
// }
// if (orderError || popularProductError || topRatedProductsError) {
// return (
// <ErrorMessage
// message={
// orderError?.message ||
// popularProductError?.message ||
// topRatedProductsError?.message
// }
// />
// );
// }
return ( return (
<div className="grid gap-7 md:gap-8 lg:grid-cols-2 2xl:grid-cols-12"> <div className="grid gap-7 md:gap-8 lg:grid-cols-2 2xl:grid-cols-12">
@ -201,14 +205,14 @@ export default function Dashboard() {
subtitleTransKey="sticker-card-subtitle-rev" subtitleTransKey="sticker-card-subtitle-rev"
icon={<EaringIcon className="h-8 w-8" />} icon={<EaringIcon className="h-8 w-8" />}
color="#1EAE98" color="#1EAE98"
price={total_revenue}
price={data?.merchant_info?.summary.total_revenue}
/> />
<StickerCard <StickerCard
titleTransKey="sticker-card-title-order" titleTransKey="sticker-card-title-order"
subtitleTransKey="sticker-card-subtitle-order" subtitleTransKey="sticker-card-subtitle-order"
icon={<ShoppingIcon className="h-8 w-8" />} icon={<ShoppingIcon className="h-8 w-8" />}
color="#865DFF" color="#865DFF"
price={data?.totalOrders}
price={data?.merchant_info?.summary.total_revenue}
/> />
<StickerCard <StickerCard
titleTransKey="sticker-card-title-vendor" titleTransKey="sticker-card-title-vendor"

42
src/components/dashboard/widgets/box/widget-order-by-status.tsx

@ -94,27 +94,27 @@ const WidgetOrderByStatus: React.FC<IProps> = ({
tempContent.push(items); tempContent.push(items);
} }
return (
<Fragment>
<div className="mt-5 grid w-full grid-cols-1 gap-5 sm:grid-cols-2 xl:grid-cols-4">
{tempContent && tempContent.length > 0
? tempContent.map((content) => {
return (
<div className="w-full" key={content?.key}>
<StickerCard
titleTransKey={content?.title}
subtitleTransKey={content?.subtitle}
icon={content?.icon}
color={content?.color}
price={content?.data}
/>
</div>
);
})
: ''}
</div>
</Fragment>
);
// return (
// <Fragment>
// <div className="mt-5 grid w-full grid-cols-1 gap-5 sm:grid-cols-2 xl:grid-cols-4">
// {tempContent && tempContent.length > 0
// ? tempContent.map((content) => {
// return (
// <div className="w-full" key={content?.key}>
// <StickerCard
// titleTransKey={content?.title}
// subtitleTransKey={content?.subtitle}
// icon={content?.icon}
// color={content?.color}
// price={content?.data}
// />
// </div>
// );
// })
// : ''}
// </div>
// </Fragment>
// );
}; };
export default WidgetOrderByStatus; export default WidgetOrderByStatus;

15
src/components/filters/category-type-filter.tsx

@ -3,6 +3,7 @@ import Select from '@/components/ui/select/select';
import { useAuthorsQuery } from '@/data/author'; import { useAuthorsQuery } from '@/data/author';
import { useCategoriesQuery } from '@/data/category'; import { useCategoriesQuery } from '@/data/category';
import { useManufacturersQuery } from '@/data/manufacturer'; import { useManufacturersQuery } from '@/data/manufacturer';
import { useTagsQuery } from '@/data/tag';
import { useTypesQuery } from '@/data/type'; import { useTypesQuery } from '@/data/type';
import { ProductType } from '@/types'; import { ProductType } from '@/types';
import cn from 'classnames'; import cn from 'classnames';
@ -24,7 +25,7 @@ type Props = {
) => void; ) => void;
className?: string; className?: string;
type?: string; type?: string;
enableType?: boolean;
enableTag?: boolean;
enableCategory?: boolean; enableCategory?: boolean;
enableAuthor?: boolean; enableAuthor?: boolean;
enableProductType?: boolean; enableProductType?: boolean;
@ -38,7 +39,7 @@ export default function CategoryTypeFilter({
onProductTypeFilter, onProductTypeFilter,
className, className,
type, type,
enableType,
enableTag,
enableCategory, enableCategory,
enableAuthor, enableAuthor,
enableProductType, enableProductType,
@ -48,7 +49,7 @@ export default function CategoryTypeFilter({
const { locale } = useRouter(); const { locale } = useRouter();
const { t } = useTranslation(); const { t } = useTranslation();
const { types, loading } = useTypesQuery({ language: locale });
const { tags, loading } = useTagsQuery({ language: locale });
const { categories, loading: categoryLoading } = useCategoriesQuery({ const { categories, loading: categoryLoading } = useCategoriesQuery({
limit: 999, limit: 999,
language: locale, language: locale,
@ -77,11 +78,11 @@ export default function CategoryTypeFilter({
className, className,
)} )}
> >
{enableType ? (
{enableTag ? (
<div className="w-full"> <div className="w-full">
<Label>{t('common:filter-by-group')}</Label>
<Label>{t('common:filter-by-tag')}</Label>
<Select <Select
options={types}
options={tags.results}
isLoading={loading} isLoading={loading}
getOptionLabel={(option: any) => option.name} getOptionLabel={(option: any) => option.name}
getOptionValue={(option: any) => option.slug} getOptionValue={(option: any) => option.slug}
@ -98,7 +99,7 @@ export default function CategoryTypeFilter({
<div className="w-full"> <div className="w-full">
<Label>{t('common:filter-by-category')}</Label> <Label>{t('common:filter-by-category')}</Label>
<Select <Select
options={categories}
options={categories.results}
getOptionLabel={(option: any) => option.name} getOptionLabel={(option: any) => option.name}
getOptionValue={(option: any) => option.slug} getOptionValue={(option: any) => option.slug}
placeholder={t('common:filter-by-category-placeholder')} placeholder={t('common:filter-by-category-placeholder')}

12
src/components/icons/social/facebook.tsx

@ -1,9 +1,5 @@
export const FacebookIcon: React.FC<React.SVGAttributes<{}>> = (props) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12" {...props}>
<path
data-name="_ionicons_svg_logo-facebook (6)"
d="M11.338 0H.662A.663.663 0 000 .663v10.674a.663.663 0 00.662.662H6V7.25H4.566V5.5H6V4.206a2.28 2.28 0 012.459-2.394c.662 0 1.375.05 1.541.072V3.5H8.9c-.753 0-.9.356-.9.881V5.5h1.794L9.56 7.25H8V12h3.338a.663.663 0 00.662-.663V.662A.663.663 0 0011.338 0z"
fill="currentColor"
/>
</svg>
export const TelegramIcon: React.FC<React.SVGAttributes<{}>> = (props) => (
<svg viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" fill="#b5b5b5" stroke="#b5b5b5"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <title>telegram_line</title> <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="Brand" transform="translate(-672.000000, -0.000000)"> <g id="telegram_line" transform="translate(672.000000, 0.000000)"> <path d="M24,0 L24,24 L0,24 L0,0 L24,0 Z M12.5934901,23.257841 L12.5819402,23.2595131 L12.5108777,23.2950439 L12.4918791,23.2987469 L12.4918791,23.2987469 L12.4767152,23.2950439 L12.4056548,23.2595131 C12.3958229,23.2563662 12.3870493,23.2590235 12.3821421,23.2649074 L12.3780323,23.275831 L12.360941,23.7031097 L12.3658947,23.7234994 L12.3769048,23.7357139 L12.4804777,23.8096931 L12.4953491,23.8136134 L12.4953491,23.8136134 L12.5071152,23.8096931 L12.6106902,23.7357139 L12.6232938,23.7196733 L12.6232938,23.7196733 L12.6266527,23.7031097 L12.609561,23.275831 C12.6075724,23.2657013 12.6010112,23.2592993 12.5934901,23.257841 L12.5934901,23.257841 Z M12.8583906,23.1452862 L12.8445485,23.1473072 L12.6598443,23.2396597 L12.6498822,23.2499052 L12.6498822,23.2499052 L12.6471943,23.2611114 L12.6650943,23.6906389 L12.6699349,23.7034178 L12.6699349,23.7034178 L12.678386,23.7104931 L12.8793402,23.8032389 C12.8914285,23.8068999 12.9022333,23.8029875 12.9078286,23.7952264 L12.9118235,23.7811639 L12.8776777,23.1665331 C12.8752882,23.1545897 12.8674102,23.1470016 12.8583906,23.1452862 L12.8583906,23.1452862 Z M12.1430473,23.1473072 C12.1332178,23.1423925 12.1221763,23.1452606 12.1156365,23.1525954 L12.1099173,23.1665331 L12.0757714,23.7811639 C12.0751323,23.7926639 12.0828099,23.8018602 12.0926481,23.8045676 L12.108256,23.8032389 L12.3092106,23.7104931 L12.3186497,23.7024347 L12.3186497,23.7024347 L12.3225043,23.6906389 L12.340401,23.2611114 L12.337245,23.2485176 L12.337245,23.2485176 L12.3277531,23.2396597 L12.1430473,23.1473072 Z" id="MingCute" fill-rule="nonzero"> </path> <path d="M21.8394,6.05639 C22.0315,4.8917 20.8652,3.97177 19.7773,4.42984 L2.67703,11.63 C1.48461,12.132 1.42351,13.8558 2.67788,14.3758 C3.60596,14.7605 5.02633,15.3246 6.45085,15.7943 C7.61932,16.1795 8.8931,16.5371 9.91353,16.6383 C10.1929,16.9725 10.5445,17.2935 10.9017,17.5872 C11.4487,18.0371 12.1074,18.5012 12.7873,18.9455 C14.1489,19.8352 15.6597,20.6865 16.678,21.2396 C17.8949,21.9006 19.3517,21.1395 19.5705,19.8131 L21.8394,6.05639 Z M4.59485,12.9925 L19.7186,6.62459 L17.6009,19.4649 C16.6024,18.9219 15.163,18.1087 13.8813,17.2713 C13.2329,16.8475 12.6407,16.4279 12.172,16.0425 C12.0051,15.9052 11.8638,15.7802 11.7461,15.6683 L15.7072,11.7071 C16.0977,11.3166 16.0977,10.6834 15.7072,10.2929 C15.3167,9.90237 14.6835,9.90237 14.293,10.2929 L9.95476,14.6311 C9.22132,14.5373 8.19888,14.2647 7.07709,13.8949 C6.21377,13.6102 5.34574,13.2869 4.59485,12.9925 Z" id="形状" fill="#09244B"> </path> </g> </g> </g> </g></svg>
); );

5
src/components/icons/social/index.tsx

@ -1,4 +1,3 @@
export { FacebookIcon } from './facebook';
export { TelegramIcon } from './facebook';
export { InstagramIcon } from './instagram'; export { InstagramIcon } from './instagram';
export { TwitterIcon } from './twitter';
export { YouTubeIcon } from './youtube';
export { WhatsappIcon } from './twitter';

10
src/components/icons/social/twitter.tsx

@ -1,9 +1,3 @@
export const TwitterIcon: React.FC<React.SVGAttributes<{}>> = (props) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14.747 12" {...props}>
<path
data-name="_ionicons_svg_logo-twitter (5)"
d="M14.747 1.422a6.117 6.117 0 01-1.737.478A3.036 3.036 0 0014.341.225a6.012 6.012 0 01-1.922.734 3.025 3.025 0 00-5.234 2.069 2.962 2.962 0 00.078.691A8.574 8.574 0 011.026.553a3.032 3.032 0 00.941 4.044 2.955 2.955 0 01-1.375-.378v.037A3.028 3.028 0 003.02 7.225a3.046 3.046 0 01-.8.106 2.854 2.854 0 01-.569-.056 3.03 3.03 0 002.828 2.1 6.066 6.066 0 01-3.759 1.3 6.135 6.135 0 01-.722-.044A8.457 8.457 0 004.631 12a8.557 8.557 0 008.616-8.619c0-.131 0-.262-.009-.391a6.159 6.159 0 001.509-1.568z"
fill="currentColor"
/>
</svg>
export const WhatsappIcon: React.FC<React.SVGAttributes<{}>> = (props) => (
<svg viewBox="0 0 24 24" id="meteor-icon-kit__solid-whatsapp" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path fill-rule="evenodd" clip-rule="evenodd" d="M20.5129 3.4866C18.2882 1.24722 15.2597 -0.00837473 12.1032 4.20445e-05C5.54964 4.20445e-05 0.216056 5.33306 0.213776 11.8883C0.210977 13.9746 0.75841 16.0247 1.80085 17.8319L0.114014 23.9932L6.41672 22.34C8.15975 23.2898 10.1131 23.7874 12.0981 23.7874H12.1032C18.6556 23.7874 23.9897 18.4538 23.992 11.8986C24.0022 8.74248 22.7494 5.71347 20.5129 3.4866ZM17.5234 14.3755C17.2264 14.2267 15.7659 13.5085 15.4934 13.4064C15.2209 13.3044 15.0231 13.2576 14.8253 13.5552C14.6275 13.8528 14.058 14.5215 13.8847 14.7199C13.7114 14.9182 13.5381 14.9427 13.241 14.794C12.944 14.6452 11.9869 14.3316 10.8519 13.3198C9.96884 12.5319 9.36969 11.5594 9.19867 11.2618C9.02765 10.9642 9.18043 10.8057 9.32922 10.6552C9.46261 10.5224 9.62622 10.3086 9.77444 10.1348C9.92266 9.9609 9.97283 9.83776 10.0714 9.63938C10.1701 9.44099 10.121 9.26769 10.0469 9.1189C9.97283 8.97011 9.37824 7.50788 9.13083 6.9133C8.88969 6.3341 8.64513 6.4122 8.46271 6.40023C8.29169 6.39168 8.09102 6.38997 7.89264 6.38997C7.58822 6.39793 7.30097 6.53267 7.10024 6.76166C6.82831 7.05923 6.061 7.77752 6.061 9.23976C6.061 10.702 7.12532 12.1146 7.27354 12.313C7.42176 12.5114 9.36855 15.5117 12.3472 16.7989C12.9004 17.0375 13.4657 17.2468 14.0409 17.426C14.7523 17.654 15.3999 17.6204 15.9118 17.544C16.4819 17.4585 17.6694 16.8251 17.9173 16.1313C18.1653 15.4376 18.1648 14.8424 18.0884 14.7187C18.012 14.595 17.8204 14.5266 17.5234 14.3778V14.3755Z" fill="#758CA3"></path></g></svg>
); );

2
src/components/layouts/app.tsx

@ -5,7 +5,7 @@ const AdminLayout = dynamic(() => import('@/components/layouts/admin'));
const OwnerLayout = dynamic(() => import('@/components/layouts/owner')); const OwnerLayout = dynamic(() => import('@/components/layouts/owner'));
export default function AppLayout({ export default function AppLayout({
userPermissions,
userPermissions = [SUPER_ADMIN],
...props ...props
}: { }: {
userPermissions: string[]; userPermissions: string[];

22
src/components/layouts/navigation/authorized-menu.tsx

@ -16,9 +16,10 @@ export default function AuthorizedMenu() {
const { t } = useTranslation('common'); const { t } = useTranslation('common');
const { pathname, query } = useRouter(); const { pathname, query } = useRouter();
const slug = (pathname === '/[shop]' && query?.shop) || ''; const slug = (pathname === '/[shop]' && query?.shop) || '';
const { role, permissions } = getAuthCredentials();
// const { role, permissions } = getAuthCredentials();
// Again, we're using framer-motion for the transition effe
// Again, we're using framer-motion for the transition effect
return ( return (
<Menu <Menu
as="div" as="div"
@ -27,7 +28,7 @@ export default function AuthorizedMenu() {
<Menu.Button className="flex max-w-[150px] items-center gap-2 focus:outline-none lg:py-0.5 xl:py-2.5"> <Menu.Button className="flex max-w-[150px] items-center gap-2 focus:outline-none lg:py-0.5 xl:py-2.5">
<Avatar <Avatar
src={ src={
data?.profile?.avatar?.thumbnail ??
data?.avatar ??
siteSettings?.avatar?.placeholder siteSettings?.avatar?.placeholder
} }
rounded="full" rounded="full"
@ -36,11 +37,11 @@ export default function AuthorizedMenu() {
/> />
<div className="hidden w-[calc(100%-48px)] flex-col items-start space-y-0.5 truncate text-sm ltr:text-left rtl:text-right xl:flex"> <div className="hidden w-[calc(100%-48px)] flex-col items-start space-y-0.5 truncate text-sm ltr:text-left rtl:text-right xl:flex">
<span className="w-full truncate font-semibold capitalize text-black"> <span className="w-full truncate font-semibold capitalize text-black">
{data?.name}
{data?.fullname}
</span> </span>
<span className="w-full truncate text-xs capitalize text-gray-400">
{/* <span className="w-full truncate text-xs capitalize text-gray-400">
{role ? role.split('_').join(' ') : data?.email} {role ? role.split('_').join(' ') : data?.email}
</span>
</span> */}
</div> </div>
</Menu.Button> </Menu.Button>
@ -62,7 +63,7 @@ export default function AuthorizedMenu() {
<div className="flex items-center gap-2 rounded-md bg-gray-100 px-3 py-2"> <div className="flex items-center gap-2 rounded-md bg-gray-100 px-3 py-2">
<Avatar <Avatar
src={ src={
data?.profile?.avatar?.thumbnail ??
data?.avatar ??
siteSettings?.avatar?.placeholder siteSettings?.avatar?.placeholder
} }
name="avatar" name="avatar"
@ -70,10 +71,10 @@ export default function AuthorizedMenu() {
/> />
<div className="flex w-[calc(100%-40px)] flex-col items-start space-y-0.5 text-sm"> <div className="flex w-[calc(100%-40px)] flex-col items-start space-y-0.5 text-sm">
<span className="w-full truncate font-semibold capitalize text-black"> <span className="w-full truncate font-semibold capitalize text-black">
{data?.name}
{data?.fullname}
</span> </span>
<span className="break-all text-xs text-gray-400"> <span className="break-all text-xs text-gray-400">
{data?.email}
{data?.phone_number}
</span> </span>
</div> </div>
</div> </div>
@ -82,7 +83,8 @@ export default function AuthorizedMenu() {
<div className="space-y-0.5 py-2"> <div className="space-y-0.5 py-2">
{siteSettings?.authorizedLinks?.map( {siteSettings?.authorizedLinks?.map(
({ href, labelTransKey, icon, permission }, index) => { ({ href, labelTransKey, icon, permission }, index) => {
const hasPermission = permission?.includes(role!);
const hasPermission = true
// permission?.includes(role!);
return ( return (
<Fragment key={index}> <Fragment key={index}>
{hasPermission && ( {hasPermission && (

4
src/components/layouts/navigation/sidebar-item.tsx

@ -40,8 +40,8 @@ function SidebarShortItem({
content={() => ( content={() => (
<> <>
{childMenu?.map((item: any, index: number) => { {childMenu?.map((item: any, index: number) => {
if (shop && !hasAccess(item?.permissions, currentUserPermissions))
return null;
// if (shop && !hasAccess(item?.permissions, currentUserPermissions))
// return null;
return ( return (
<div key={index}> <div key={index}>
<Link <Link

12
src/components/layouts/navigation/top-navbar.tsx

@ -22,6 +22,7 @@ import {
adminOnly, adminOnly,
getAuthCredentials, getAuthCredentials,
hasAccess, hasAccess,
useHasShop,
} from '@/utils/auth-utils'; } from '@/utils/auth-utils';
import { import {
RESPONSIVE_WIDTH, RESPONSIVE_WIDTH,
@ -151,7 +152,7 @@ const Navbar = () => {
]); ]);
if (loading || shopLoading) { if (loading || shopLoading) {
return <Loader showText={false} />;
// return <Loader showText={false} />;
} }
const { options } = settings!; const { options } = settings!;
@ -160,7 +161,6 @@ const Navbar = () => {
openModal('SEARCH_VIEW'); openModal('SEARCH_VIEW');
setSearchModal(true); setSearchModal(true);
} }
return ( return (
<header className="fixed top-0 z-40 w-full bg-white shadow"> <header className="fixed top-0 z-40 w-full bg-white shadow">
{width >= RESPONSIVE_WIDTH && isMaintenanceMode ? ( {width >= RESPONSIVE_WIDTH && isMaintenanceMode ? (
@ -252,8 +252,8 @@ const Navbar = () => {
</div> </div>
<div className="flex shrink-0 grow-0 basis-auto items-center"> <div className="flex shrink-0 grow-0 basis-auto items-center">
{hasAccess(adminAndOwnerOnly, permissions) && (
<> <>
{(!useHasShop()) ? (
<div className="hidden border-gray-200/80 px-6 py-5 border-e 2xl:block"> <div className="hidden border-gray-200/80 px-6 py-5 border-e 2xl:block">
<LinkButton <LinkButton
href={Routes.shop.create} href={Routes.shop.create}
@ -263,11 +263,14 @@ const Navbar = () => {
{t('common:text-create-shop')} {t('common:text-create-shop')}
</LinkButton> </LinkButton>
</div> </div>
) : (
<div className="hidden px-6 py-5 2xl:block"> <div className="hidden px-6 py-5 2xl:block">
<VisitStore /> <VisitStore />
</div> </div>
)}
{options?.pushNotification?.all?.order || {options?.pushNotification?.all?.order ||
options?.pushNotification?.all?.message || options?.pushNotification?.all?.message ||
options?.pushNotification?.all?.storeNotice ? ( options?.pushNotification?.all?.storeNotice ? (
@ -294,7 +297,6 @@ const Navbar = () => {
</div> </div>
) : null} ) : null}
</> </>
)}
</div> </div>
{enableMultiLang ? <LanguageSwitcher /> : null} {enableMultiLang ? <LanguageSwitcher /> : null}

3
src/components/layouts/shop/index.tsx

@ -107,7 +107,8 @@ const SidebarItemMap = ({ menuItems }: any) => {
const SideBarGroup = () => { const SideBarGroup = () => {
const [miniSidebar, _] = useAtom(miniSidebarInitialValue); const [miniSidebar, _] = useAtom(miniSidebarInitialValue);
const { role } = getAuthCredentials();
const { role } = 'staff'
// getAuthCredentials();
const menuItems: MenuItemsProps = const menuItems: MenuItemsProps =
role === 'staff' role === 'staff'
? siteSettings?.sidebarLinks?.staff ? siteSettings?.sidebarLinks?.staff

36
src/components/layouts/topbar/visit-store.tsx

@ -3,24 +3,36 @@ import { Routes } from '@/config/routes';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useState, useEffect } from 'react';
import { useHasShop } from '@/utils/auth-utils';
const VisitStore = () => { const VisitStore = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { pathname, query } = useRouter(); const { pathname, query } = useRouter();
const slug = (pathname === '/[shop]' && `shops/${query?.shop}`) || '/';
const slug = useHasShop(true);
// State to prevent hydration mismatch
const [mounted, setMounted] = useState(false);
useEffect(() => {
// Set mounted to true once the component is mounted on the client
setMounted(true);
}, []);
if (!mounted) {
return null; // Prevent rendering until the component is mounted on the client-side
}
return ( return (
<>
<Link
href={Routes.visitStore(slug as string)}
target="_blank"
className="inline-flex h-9 flex-shrink-0 items-center justify-center gap-2 rounded-full border border-gray-200 bg-gray-50 px-3.5 py-0 text-sm font-medium leading-none text-accent outline-none transition duration-300 ease-in-out hover:border-transparent hover:bg-accent-hover hover:text-white focus:shadow focus:outline-none"
rel="noreferrer"
>
<HomeIcon />
{slug === '/' ? t('text-visit-site') : t('text-visit-store')}
</Link>
</>
<Link
href={Routes.visitStore(slug as string)}
target="_blank"
className="inline-flex h-9 flex-shrink-0 items-center justify-center gap-2 rounded-full border border-gray-200 bg-gray-50 px-3.5 py-0 text-sm font-medium leading-none text-accent outline-none transition duration-300 ease-in-out hover:border-transparent hover:bg-accent-hover hover:text-white focus:shadow focus:outline-none"
rel="noreferrer"
>
<HomeIcon />
{slug === '/' ? t('text-visit-site') : t('text-visit-store')}
</Link>
); );
}; };

26
src/components/order/order-list.tsx

@ -77,7 +77,7 @@ const OrderList = ({
const columns = [ const columns = [
{ {
title: t('table:table-item-tracking-number'), title: t('table:table-item-tracking-number'),
dataIndex: 'tracking_number',
dataIndex: 'transaction_id',
key: 'tracking_number', key: 'tracking_number',
align: alignLeft, align: alignLeft,
width: 200, width: 200,
@ -92,7 +92,7 @@ const OrderList = ({
isActive={sortingObj.column === 'name'} isActive={sortingObj.column === 'name'}
/> />
), ),
dataIndex: 'customer',
dataIndex: 'user_info',
key: 'name', key: 'name',
align: alignLeft, align: alignLeft,
width: 250, width: 250,
@ -109,9 +109,9 @@ const OrderList = ({
render: (customer: any) => ( render: (customer: any) => (
<div className="flex items-center"> <div className="flex items-center">
{/* <Avatar name={customer.name} src={customer?.profile.avatar.thumbnail} /> */} {/* <Avatar name={customer.name} src={customer?.profile.avatar.thumbnail} /> */}
<Avatar name={customer?.name} />
<Avatar name={customer?.user} />
<div className="flex flex-col whitespace-nowrap font-medium ms-2"> <div className="flex flex-col whitespace-nowrap font-medium ms-2">
{customer?.name ? customer?.name : t('common:text-guest')}
{customer?.user ? customer?.user : t('common:text-guest')}
<span className="text-[13px] font-normal text-gray-500/80"> <span className="text-[13px] font-normal text-gray-500/80">
{customer?.email} {customer?.email}
</span> </span>
@ -121,10 +121,10 @@ const OrderList = ({
}, },
{ {
title: t('table:table-item-products'), title: t('table:table-item-products'),
dataIndex: 'products',
dataIndex: 'items',
key: 'products', key: 'products',
align: 'center', align: 'center',
render: (products: Product) => <span>{products.length}</span>,
render: (products: Product) => <span>{products?.length}</span>,
}, },
{ {
// title: t('table:table-item-order-date'), // title: t('table:table-item-order-date'),
@ -139,10 +139,10 @@ const OrderList = ({
className="cursor-pointer" className="cursor-pointer"
/> />
), ),
dataIndex: 'created_at',
key: 'created_at',
dataIndex: 'created',
key: 'created',
align: 'center', align: 'center',
onHeaderCell: () => onHeaderClick('created_at'),
onHeaderCell: () => onHeaderClick('created'),
render: (date: string) => { render: (date: string) => {
dayjs.extend(relativeTime); dayjs.extend(relativeTime);
dayjs.extend(utc); dayjs.extend(utc);
@ -179,11 +179,11 @@ const OrderList = ({
className="cursor-pointer" className="cursor-pointer"
/> />
), ),
dataIndex: 'total',
key: 'total',
dataIndex: 'total_price',
key: 'total_price',
align: 'center', align: 'center',
width: 120, width: 120,
onHeaderCell: () => onHeaderClick('total'),
onHeaderCell: () => onHeaderClick('total_price'),
render: function Render(value: any) { render: function Render(value: any) {
const { price } = usePrice({ const { price } = usePrice({
amount: value, amount: value,
@ -193,7 +193,7 @@ const OrderList = ({
}, },
{ {
title: t('table:table-item-status'), title: t('table:table-item-status'),
dataIndex: 'order_status',
dataIndex: 'status',
key: 'order_status', key: 'order_status',
align: 'center', align: 'center',
render: (order_status: string) => ( render: (order_status: string) => (

93
src/components/product/form-utils.ts

@ -78,22 +78,24 @@ export function processOptions(options: any) {
export function calculateMinMaxPrice(variationOptions: any) { export function calculateMinMaxPrice(variationOptions: any) {
if (!variationOptions || !variationOptions.length) { if (!variationOptions || !variationOptions.length) {
return {
min_price: null,
max_price: null,
};
return null
// {
// min_price: null,
// max_price: null,
// };
} }
const sortedVariationsByPrice = orderBy(variationOptions, ['price']); const sortedVariationsByPrice = orderBy(variationOptions, ['price']);
const sortedVariationsBySalePrice = orderBy(variationOptions, ['sale_price']); const sortedVariationsBySalePrice = orderBy(variationOptions, ['sale_price']);
return {
min_price:
sortedVariationsBySalePrice?.[0].sale_price <
sortedVariationsByPrice?.[0]?.price
? sortedVariationsBySalePrice?.[0].sale_price
: sortedVariationsByPrice?.[0]?.price,
max_price:
sortedVariationsByPrice?.[sortedVariationsByPrice?.length - 1]?.price,
};
return null
// {
// min_price:
// sortedVariationsBySalePrice?.[0].sale_price <
// sortedVariationsByPrice?.[0]?.price
// ? sortedVariationsBySalePrice?.[0].sale_price
// : sortedVariationsByPrice?.[0]?.price,
// max_price:
// sortedVariationsByPrice?.[sortedVariationsByPrice?.length - 1]?.price,
// };
} }
export function calculateQuantity(variationOptions: any) { export function calculateQuantity(variationOptions: any) {
@ -118,23 +120,29 @@ export function getProductDefaultValues(
image: [], image: [],
gallery: [], gallery: [],
video: [], video: [],
// isVariation: false,
variations: [], variations: [],
variation_options: [], variation_options: [],
}; };
} }
const { const {
variations, variations,
variation_options, variation_options,
product_type, product_type,
is_digital, is_digital,
digital_file, digital_file,
images,
} = product; } = product;
return cloneDeep({ return cloneDeep({
...product, ...product,
product_type: productTypeOptions.find( product_type: productTypeOptions.find(
(option) => product_type === option.value, (option) => product_type === option.value,
), ),
images: images.map((item, idx) => ({
image: item.image_url.lg,
display_order: idx + 1,
})),
...(product_type === ProductType.Simple && { ...(product_type === ProductType.Simple && {
...(is_digital && { ...(is_digital && {
digital_file_input: { digital_file_input: {
@ -148,22 +156,18 @@ export function getProductDefaultValues(
...(product_type === ProductType.Variable && { ...(product_type === ProductType.Variable && {
variations: getFormattedVariations(variations), variations: getFormattedVariations(variations),
variation_options: variation_options?.map(({ image, ...option }: any) => {
return {
...option,
...(!isEmpty(image) && { image: omitTypename(image) }),
...(option?.digital_file && {
digital_file_input: {
id: option?.digital_file?.attachment_id,
file_name: option?.digital_file?.file_name,
},
}),
};
}),
variation_options: variation_options?.map(({ image, ...option }: any) => ({
...option,
...(!isEmpty(image) && { image: omitTypename(image) }),
...(option?.digital_file && {
digital_file_input: {
id: option?.digital_file?.attachment_id,
file_name: option?.digital_file?.file_name,
},
}),
})),
}), }),
// isVariation: variations?.length && variation_options?.length ? true : false,
// Remove initial dependent value for new translation
...(isNewTranslation && { ...(isNewTranslation && {
type: null, type: null,
categories: [], categories: [],
@ -183,6 +187,7 @@ export function getProductDefaultValues(
}); });
} }
export function filterAttributes(attributes: any, variations: any) { export function filterAttributes(attributes: any, variations: any) {
let res = []; let res = [];
res = attributes?.filter((el: any) => { res = attributes?.filter((el: any) => {
@ -233,7 +238,7 @@ export function getProductInputValues(
quantity, quantity,
author, author,
manufacturer, manufacturer,
image,
images,
is_digital, is_digital,
categories, categories,
tags, tags,
@ -241,25 +246,23 @@ export function getProductInputValues(
variation_options, variation_options,
variations, variations,
in_flash_sale, in_flash_sale,
is_active,
discount,
...simpleValues ...simpleValues
} = values; } = values;
// const { locale } = useRouter(); // const { locale } = useRouter();
// const router = useRouter(); // const router = useRouter();
const processedFile = processFileWithName(digital_file_input); const processedFile = processFileWithName(digital_file_input);
console.log(values);
return { return {
...simpleValues, ...simpleValues,
is_digital,
in_flash_sale,
// language: router.locale, // language: router.locale,
author_id: author?.id,
manufacturer_id: manufacturer?.id,
type_id: type?.id,
product_type: product_type?.value,
categories: categories.map((category) => category?.id), categories: categories.map((category) => category?.id),
tags: tags.map((tag) => tag?.id), tags: tags.map((tag) => tag?.id),
image: omitTypename<any>(image),
gallery: values.gallery?.map((gi: any) => omitTypename(gi)),
images: omitTypename<any>(images),
is_active : values.is_active === "publish" ? true : false ,
discount : `${values.discount}` ,
...(product_type?.value === ProductType?.Simple && { ...(product_type?.value === ProductType?.Simple && {
quantity, quantity,
...(is_digital && { ...(is_digital && {
@ -271,13 +274,13 @@ export function getProductInputValues(
}, },
}), }),
}), }),
variations: [],
variation_options: {
upsert: [],
delete: initialValues?.variation_options?.map(
(variation: Variation) => variation?.id,
),
},
// variations: [],
// variation_options: {
// upsert: [],
// delete: initialValues?.variation_options?.map(
// (variation: Variation) => variation?.id,
// ),
// },
...(product_type?.value === ProductType?.Variable && { ...(product_type?.value === ProductType?.Variable && {
quantity: calculateQuantity(variation_options), quantity: calculateQuantity(variation_options),
variations: variations?.flatMap( variations: variations?.flatMap(

5
src/components/product/product-category-input.tsx

@ -27,10 +27,11 @@ const ProductCategoryInput = ({ control, setValue }: Props) => {
} }
}, [type?.slug]); }, [type?.slug]);
const { categories, loading } = useCategoriesQuery({
const { categories, loading , error} = useCategoriesQuery({
limit: 999, limit: 999,
type: type?.slug, type: type?.slug,
language: locale, language: locale,
}); });
return ( return (
@ -43,7 +44,7 @@ const ProductCategoryInput = ({ control, setValue }: Props) => {
getOptionLabel={(option: any) => option.name} getOptionLabel={(option: any) => option.name}
getOptionValue={(option: any) => option.id} getOptionValue={(option: any) => option.id}
// @ts-ignore // @ts-ignore
options={categories}
options={categories.results}
isLoading={loading} isLoading={loading}
/> />
</div> </div>

105
src/components/product/product-form.tsx

@ -103,15 +103,16 @@ export default function CreateOrUpdateProductForm({
const isNewTranslation = router?.query?.action === 'translate'; const isNewTranslation = router?.query?.action === 'translate';
const showPreviewButton = const showPreviewButton =
router?.query?.action === 'edit' && Boolean(initialValues?.slug); router?.query?.action === 'edit' && Boolean(initialValues?.slug);
const isSlugEditable =
router?.query?.action === 'edit' &&
router?.locale === Config.defaultLanguage;
const isSlugEditable = true
const methods = useForm<ProductFormValues>({ const methods = useForm<ProductFormValues>({
//@ts-ignore //@ts-ignore
resolver: yupResolver(productValidationSchema), resolver: yupResolver(productValidationSchema),
shouldUnregister: true, shouldUnregister: true,
// @ts-ignore // @ts-ignore
defaultValues: getProductDefaultValues(initialValues!, isNewTranslation),
defaultValues: {
...getProductDefaultValues(initialValues!, isNewTranslation),
is_active: initialValues?.is_active || ProductStatus.Draft, // Set default value for status
},
}); });
const { const {
register, register,
@ -130,24 +131,30 @@ export default function CreateOrUpdateProductForm({
const { mutate: updateProduct, isLoading: updating } = const { mutate: updateProduct, isLoading: updating } =
useUpdateProductMutation(); useUpdateProductMutation();
console.log(watch());
const onSubmit = async (values: ProductFormValues) => { const onSubmit = async (values: ProductFormValues) => {
const inputValues = { const inputValues = {
language: router.locale,
...getProductInputValues(values, initialValues), ...getProductInputValues(values, initialValues),
}; };
console.log("submit");
try { try {
if (
!initialValues ||
!initialValues.translated_languages.includes(router.locale!)
) {
if (!initialValues) {
console.log("createProduct");
//@ts-ignore //@ts-ignore
createProduct({ createProduct({
...inputValues, ...inputValues,
...(initialValues?.slug && { slug: initialValues.slug }),
shop_id: shopId || initialValues?.shop_id,
// ...(initialValues?.slug && { slug: initialValues.slug }),
// shop_id: shopId || initialValues?.shop_id,
}); });
} else { } else {
console.log("updateProduct");
//@ts-ignore //@ts-ignore
updateProduct({ updateProduct({
...inputValues, ...inputValues,
@ -169,10 +176,10 @@ export default function CreateOrUpdateProductForm({
const product_type = watch('product_type'); const product_type = watch('product_type');
const is_digital = watch('is_digital'); const is_digital = watch('is_digital');
const is_external = watch('is_external'); const is_external = watch('is_external');
const { fields, append, remove } = useFieldArray({
control,
name: 'video',
});
// const { fields, append, remove } = useFieldArray({
// control,
// name: 'video',
// });
const productName = watch('name'); const productName = watch('name');
const productDescriptionSuggestionLists = useMemo(() => { const productDescriptionSuggestionLists = useMemo(() => {
@ -200,19 +207,9 @@ export default function CreateOrUpdateProductForm({
value: ProductStatus.Publish, value: ProductStatus.Publish,
}, },
{ {
label: 'form:input-label-approved',
id: 'approved',
value: ProductStatus.Approved,
},
{
label: 'form:input-label-rejected',
id: 'rejected',
value: ProductStatus.Rejected,
},
{
label: 'form:input-label-soft-disabled',
id: 'unpublish',
value: ProductStatus.UnPublish,
label: 'form:input-label-draft',
id: 'draft',
value: ProductStatus.Draft,
}, },
]; ];
} else { } else {
@ -308,7 +305,7 @@ export default function CreateOrUpdateProductForm({
/> />
<Card className="w-full sm:w-8/12 md:w-2/3"> <Card className="w-full sm:w-8/12 md:w-2/3">
<FileInput name="image" control={control} multiple={false} />
<FileInput name="images" control={control} multiple />
{/* {errors.image?.message && ( {/* {errors.image?.message && (
<p className="my-2 text-xs text-red-500"> <p className="my-2 text-xs text-red-500">
{t(errors?.image?.message!)} {t(errors?.image?.message!)}
@ -317,7 +314,7 @@ export default function CreateOrUpdateProductForm({
</Card> </Card>
</div> </div>
<div className="flex flex-wrap pb-8 my-5 border-b border-dashed border-border-base sm:my-8">
{/* <div className="flex flex-wrap pb-8 my-5 border-b border-dashed border-border-base sm:my-8">
<Description <Description
title={t('form:gallery-title')} title={t('form:gallery-title')}
details={galleryImageInformation} details={galleryImageInformation}
@ -325,11 +322,11 @@ export default function CreateOrUpdateProductForm({
/> />
<Card className="w-full sm:w-8/12 md:w-2/3"> <Card className="w-full sm:w-8/12 md:w-2/3">
<FileInput name="gallery" control={control} />
<FileInput name="gallery" multiple control={control} />
</Card> </Card>
</div>
</div> */}
<div className="flex flex-wrap pb-8 my-5 border-b border-dashed border-border-base sm:my-8">
{/* <div className="flex flex-wrap pb-8 my-5 border-b border-dashed border-border-base sm:my-8">
<Description <Description
title={t('form:video-title')} title={t('form:video-title')}
details={t('form:video-help-text')} details={t('form:video-help-text')}
@ -337,7 +334,6 @@ export default function CreateOrUpdateProductForm({
/> />
<Card className="w-full sm:w-8/12 md:w-2/3"> <Card className="w-full sm:w-8/12 md:w-2/3">
{/* Video url picker */}
<div> <div>
{fields?.map((item: any, index: number) => ( {fields?.map((item: any, index: number) => (
<div <div
@ -382,7 +378,7 @@ export default function CreateOrUpdateProductForm({
{t('form:button-label-add-video')} {t('form:button-label-add-video')}
</Button> </Button>
</Card> </Card>
</div>
</div> */}
<div className="flex flex-wrap pb-8 my-5 border-b border-dashed border-border-base sm:my-8"> <div className="flex flex-wrap pb-8 my-5 border-b border-dashed border-border-base sm:my-8">
<Description <Description
@ -392,10 +388,10 @@ export default function CreateOrUpdateProductForm({
/> />
<Card className="w-full sm:w-8/12 md:w-2/3"> <Card className="w-full sm:w-8/12 md:w-2/3">
<ProductGroupInput
{/* <ProductGroupInput
control={control} control={control}
error={t((errors?.type as any)?.message)} error={t((errors?.type as any)?.message)}
/>
/> */}
<ProductCategoryInput control={control} setValue={setValue} /> <ProductCategoryInput control={control} setValue={setValue} />
{/* it's not needed in chawkbazar */} {/* it's not needed in chawkbazar */}
{/* <ProductAuthorInput control={control} /> */} {/* <ProductAuthorInput control={control} /> */}
@ -452,25 +448,20 @@ export default function CreateOrUpdateProductForm({
disabled disabled
/> />
)} )}
<Input
{/* <Input
label={`${t('form:input-label-unit')}*`} label={`${t('form:input-label-unit')}*`}
{...register('unit')} {...register('unit')}
error={t(errors.unit?.message!)} error={t(errors.unit?.message!)}
variant="outline" variant="outline"
className="mb-5" className="mb-5"
/>
/> */}
<div className="relative mb-5"> <div className="relative mb-5">
{options?.useAi && (
<OpenAIButton
title={t('form:button-label-description-ai')}
onClick={handleGenerateDescription}
/>
)}
<RichTextEditor
title={t('form:input-label-description')}
control={control}
name="description"
error={t(errors?.description?.message)}
<TextArea
label={`${t('form:input-label-description')}*`}
{...register('description')}
error={t(errors.description?.message!)}
variant="outline"
className="mb-5"
/> />
</div> </div>
@ -480,7 +471,7 @@ export default function CreateOrUpdateProductForm({
? statusList?.map((status: any, index: number) => ( ? statusList?.map((status: any, index: number) => (
<Radio <Radio
key={index} key={index}
{...register('status')}
{...register('is_active')}
label={t(status?.label)} label={t(status?.label)}
id={status?.id} id={status?.id}
value={status?.value} value={status?.value}
@ -534,7 +525,7 @@ export default function CreateOrUpdateProductForm({
</> </>
)} */} )} */}
<div className="flex flex-wrap pb-8 my-5 border-b border-dashed border-border-base sm:my-8">
{/* <div className="flex flex-wrap pb-8 my-5 border-b border-dashed border-border-base sm:my-8">
<Description <Description
title={t('form:form-title-product-type')} title={t('form:form-title-product-type')}
details={t('form:form-description-product-type')} details={t('form:form-description-product-type')}
@ -542,15 +533,10 @@ export default function CreateOrUpdateProductForm({
/> />
<ProductTypeInput /> <ProductTypeInput />
</div>
</div> */}
{/* Simple Type */} {/* Simple Type */}
{product_type?.value === ProductType.Simple && (
<ProductSimpleForm
initialValues={initialValues}
settings={options}
/>
)}
<ProductSimpleForm initialValues={initialValues} settings={options} />
{/* Variation Type */} {/* Variation Type */}
{product_type?.value === ProductType.Variable && ( {product_type?.value === ProductType.Variable && (
@ -596,6 +582,7 @@ export default function CreateOrUpdateProductForm({
</Link> </Link>
)} )}
<Button <Button
type="submit"
loading={updating || creating} loading={updating || creating}
disabled={updating || creating} disabled={updating || creating}
size="medium" size="medium"

80
src/components/product/product-list.tsx

@ -94,7 +94,7 @@ const ProductList = ({
width: 280, width: 280,
ellipsis: true, ellipsis: true,
onHeaderCell: () => onHeaderClick('name'), onHeaderCell: () => onHeaderClick('name'),
render: (name: string, { image, type }: { image: any; type: any }) => (
render: (name: string, {slug , image }: { image: any; slug: string }) => (
<div className="flex items-center"> <div className="flex items-center">
<div className="relative aspect-square h-10 w-10 shrink-0 overflow-hidden rounded border border-border-200/80 bg-gray-100 me-2.5"> <div className="relative aspect-square h-10 w-10 shrink-0 overflow-hidden rounded border border-border-200/80 bg-gray-100 me-2.5">
<Image <Image
@ -108,24 +108,24 @@ const ProductList = ({
<div className="flex flex-col"> <div className="flex flex-col">
<span className="truncate font-medium">{name}</span> <span className="truncate font-medium">{name}</span>
<span className="truncate whitespace-nowrap pt-1 pb-0.5 text-[13px] text-body/80"> <span className="truncate whitespace-nowrap pt-1 pb-0.5 text-[13px] text-body/80">
{type?.name}
{slug}
</span> </span>
</div> </div>
</div> </div>
), ),
}, },
{
title: t('table:table-item-product-type'),
dataIndex: 'product_type',
key: 'product_type',
width: 150,
align: alignLeft,
render: (product_type: string) => (
<span className="truncate whitespace-nowrap capitalize">
{product_type}
</span>
),
},
// {
// title: t('table:table-item-product-type'),
// dataIndex: 'product_type',
// key: 'product_type',
// width: 150,
// align: alignLeft,
// render: (product_type: string) => (
// <span className="truncate whitespace-nowrap capitalize">
// {product_type}
// </span>
// ),
// },
{ {
title: t('table:table-item-shop'), title: t('table:table-item-shop'),
dataIndex: 'shop', dataIndex: 'shop',
@ -133,12 +133,12 @@ const ProductList = ({
width: 170, width: 170,
align: alignLeft, align: alignLeft,
ellipsis: true, ellipsis: true,
render: (shop: Shop) => (
render: (merchant: Shop) => (
<div className="flex items-center font-medium"> <div className="flex items-center font-medium">
<div className="relative aspect-square h-9 w-9 shrink-0 overflow-hidden rounded-full border border-border-200/80 bg-gray-100 me-2"> <div className="relative aspect-square h-9 w-9 shrink-0 overflow-hidden rounded-full border border-border-200/80 bg-gray-100 me-2">
<Image <Image
src={shop?.logo?.thumbnail ?? siteSettings.product.placeholder}
alt={shop?.name ?? 'Shop Name'}
src={merchant?.logo_url ?? siteSettings.product.placeholder}
alt={merchant?.title ?? 'Shop Name'}
fill fill
priority={true} priority={true}
sizes="(max-width: 768px) 100vw" sizes="(max-width: 768px) 100vw"
@ -162,29 +162,29 @@ const ProductList = ({
className: 'cursor-pointer', className: 'cursor-pointer',
dataIndex: 'price', dataIndex: 'price',
key: 'price', key: 'price',
align: alignRight,
align: alignLeft,
width: 180, width: 180,
onHeaderCell: () => onHeaderClick('price'), onHeaderCell: () => onHeaderClick('price'),
render: function Render(value: number, record: Product) {
const { price: max_price } = usePrice({
amount: record?.max_price as number,
});
const { price: min_price } = usePrice({
amount: record?.min_price as number,
});
render: function Render(record: string ,) {
// const { price: max_price } = usePrice({
// amount: record?.max_price as number,
// });
// const { price: min_price } = usePrice({
// amount: record?.min_price as number,
// });
const { price } = usePrice({
amount: value,
});
// const { price } = usePrice({
// amount: value,
// });
const renderPrice =
record?.product_type === ProductType.Variable
? `${min_price} - ${max_price}`
: price;
// const renderPrice =
// record?.product_type === ProductType.Variable
// ? `${min_price} - ${max_price}`
// : price;
return ( return (
<span className="whitespace-nowrap" title={renderPrice}>
{renderPrice}
<span className="self-center" title={record}>
{record}
</span> </span>
); );
}, },
@ -206,7 +206,9 @@ const ProductList = ({
align: 'center', align: 'center',
width: 170, width: 170,
onHeaderCell: () => onHeaderClick('quantity'), onHeaderCell: () => onHeaderClick('quantity'),
render: (quantity: number) => {
render: (quantit: number , record) => {
const quantity = record.stock - record.purchased_quantity
if (quantity < 1) { if (quantity < 1) {
return ( return (
<Badge <Badge
@ -225,7 +227,7 @@ const ProductList = ({
key: 'status', key: 'status',
align: 'left', align: 'left',
width: 200, width: 200,
render: (status: string, record: any) => (
render: (is_active: boolean, record: any) => (
<div <div
className={`flex justify-start ${ className={`flex justify-start ${
record?.quantity > 0 && record?.quantity < 10 record?.quantity > 0 && record?.quantity < 10
@ -233,10 +235,11 @@ const ProductList = ({
: 'items-center space-x-2 rtl:space-x-reverse' : 'items-center space-x-2 rtl:space-x-reverse'
}`} }`}
> >
<Badge <Badge
text={status}
text={record.is_active ? "Published" : "Draft"}
color={ color={
status.toLocaleLowerCase() === 'draft'
!record.is_active
? 'bg-yellow-400/10 text-yellow-500' ? 'bg-yellow-400/10 text-yellow-500'
: 'bg-accent bg-opacity-10 !text-accent' : 'bg-accent bg-opacity-10 !text-accent'
} }
@ -260,6 +263,7 @@ const ProductList = ({
align: 'right', align: 'right',
width: 120, width: 120,
render: (slug: string, record: Product) => ( render: (slug: string, record: Product) => (
<LanguageSwitcher <LanguageSwitcher
slug={slug} slug={slug}
record={record} record={record}

26
src/components/product/product-simple-form.tsx

@ -53,13 +53,21 @@ export default function ProductSimpleForm({ initialValues, settings }: IProps) {
variant="outline" variant="outline"
className="mb-5" className="mb-5"
/> />
<Input
{/* <Input
label={t('form:input-label-sale-price')} label={t('form:input-label-sale-price')}
type="number" type="number"
{...register('sale_price')} {...register('sale_price')}
error={t(errors.sale_price?.message!)} error={t(errors.sale_price?.message!)}
variant="outline" variant="outline"
className="mb-5" className="mb-5"
/> */}
<Input
label={t('form:input-label-discount')}
type="number"
{...register('discount')}
error={t(errors.discount?.message!)}
variant="outline"
className="mb-5"
/> />
<Input <Input
@ -74,6 +82,16 @@ export default function ProductSimpleForm({ initialValues, settings }: IProps) {
/> />
<Input <Input
label={`${t('form:input-label-stock')}*`}
type="number"
{...register('stock')}
error={t(errors.stock?.message!)}
variant="outline"
className="mb-5"
inputClassName="uppercase"
disabled={isTranslateProduct}
/>
{/* <Input
label={`${t('form:input-label-sku')}*`} label={`${t('form:input-label-sku')}*`}
{...register('sku')} {...register('sku')}
note={ note={
@ -86,8 +104,8 @@ export default function ProductSimpleForm({ initialValues, settings }: IProps) {
className="mb-5" className="mb-5"
inputClassName="uppercase" inputClassName="uppercase"
disabled={isTranslateProduct} disabled={isTranslateProduct}
/>
/> */}
{/*
<Input <Input
label={t('form:input-label-width')} label={t('form:input-label-width')}
{...register('width')} {...register('width')}
@ -188,7 +206,7 @@ export default function ProductSimpleForm({ initialValues, settings }: IProps) {
className="mb-5" className="mb-5"
/> />
</div> </div>
) : null}
) : null} */}
</Card> </Card>
</div> </div>
); );

2
src/components/product/product-tag-input.tsx

@ -43,7 +43,7 @@ const ProductTagInput = ({ control, setValue }: Props) => {
getOptionLabel={(option: any) => option.name} getOptionLabel={(option: any) => option.name}
getOptionValue={(option: any) => option.id} getOptionValue={(option: any) => option.id}
// @ts-ignore // @ts-ignore
options={tags}
options={tags.results}
isLoading={loading} isLoading={loading}
/> />
</div> </div>

150
src/components/product/product-validation-schema.ts

@ -8,7 +8,6 @@ const SUPPORTED_IMAGE_FORMATS = ['image/jpg', 'image/jpeg', 'image/png'];
export const productValidationSchema = yup.object().shape({ export const productValidationSchema = yup.object().shape({
name: yup.string().required('form:error-name-required'), name: yup.string().required('form:error-name-required'),
product_type: yup.object().required('form:error-product-type-required'),
sku: yup.mixed().when('product_type', { sku: yup.mixed().when('product_type', {
is: (productType: { is: (productType: {
name: string; name: string;
@ -17,6 +16,12 @@ export const productValidationSchema = yup.object().shape({
}) => productType?.value === ProductType.Simple, }) => productType?.value === ProductType.Simple,
then: () => yup.string().nullable().required('form:error-sku-required'), then: () => yup.string().nullable().required('form:error-sku-required'),
}), }),
stock: yup
.number()
.typeError('form:error-stock-must-number') // Ensure it's a number
.positive('form:error-stock-must-positive') // Ensure it's positive
.integer('form:error-stock-must-integer') // Ensure it's an integer
.required('form:error-stock-required'), // Ensure it's required
price: yup.mixed().when('product_type', { price: yup.mixed().when('product_type', {
is: (productType: { is: (productType: {
name: string; name: string;
@ -36,6 +41,12 @@ export const productValidationSchema = yup.object().shape({
.lessThan(yup.ref('price'), 'Sale Price should be less than ${less}') .lessThan(yup.ref('price'), 'Sale Price should be less than ${less}')
.positive('form:error-sale-price-must-positive') .positive('form:error-sale-price-must-positive')
.nullable(), .nullable(),
discount: yup
.number()
.typeError('form:error-discount-must-number') // Ensure it's a number
.min(0, 'form:error-discount-minimum') // Discount cannot be less than 0
.max(100, 'form:error-discount-maximum') // Discount cannot be more than 100 (if it's a percentage)
.required('form:error-discount-required'), // Ensure it's required
quantity: yup.mixed().when('product_type', { quantity: yup.mixed().when('product_type', {
is: (productType: { is: (productType: {
name: string; name: string;
@ -50,76 +61,73 @@ export const productValidationSchema = yup.object().shape({
.integer('form:error-quantity-must-integer') .integer('form:error-quantity-must-integer')
.required('form:error-quantity-required'), .required('form:error-quantity-required'),
}), }),
unit: yup.string().required('form:error-unit-required'),
type: yup.object().nullable().required('form:error-type-required'),
status: yup.string().nullable().required('form:error-status-required'),
variation_options: yup.array().of(
yup.object().shape({
price: yup
.number()
.typeError('form:error-price-must-number')
.positive('form:error-price-must-positive')
.required('form:error-price-required'),
sale_price: yup
.number()
.transform((value) => (isNaN(value) ? undefined : value))
.lessThan(yup.ref('price'), 'Sale Price should be less than ${less}')
.positive('form:error-sale-price-must-positive')
.nullable(),
quantity: yup
.number()
.typeError('form:error-quantity-must-number')
.positive('form:error-quantity-must-positive')
.integer('form:error-quantity-must-integer')
.required('form:error-quantity-required'),
sku: yup.string().required('form:error-sku-required'),
is_digital: yup.boolean(),
digital_file_input: yup.object().when('is_digital', {
is: true,
then: () =>
yup
.object()
.shape({
id: yup.string().required(),
})
.required('form:error-digital-file-is-required'),
otherwise: () =>
yup
.object()
.shape({
id: yup.string().notRequired(),
original: yup.string().notRequired(),
})
.notRequired()
.nullable(),
}),
}),
),
is_digital: yup.boolean(),
digital_file_input: yup.object().when('is_digital', {
is: true,
then: () =>
yup
.object()
.shape({
id: yup.string().required(),
})
.required('form:error-digital-file-is-required'),
otherwise: () =>
yup
.object()
.shape({
id: yup.string().notRequired(),
original: yup.string().notRequired(),
})
.notRequired()
.nullable(),
}),
video: yup.array().of(
yup.object().shape({
url: yup.string().required('Video URL is required'),
}),
),
// variation_options: yup.array().of(
// yup.object().shape({
// price: yup
// .number()
// .typeError('form:error-price-must-number')
// .positive('form:error-price-must-positive')
// .required('form:error-price-required'),
// sale_price: yup
// .number()
// .transform((value) => (isNaN(value) ? undefined : value))
// .lessThan(yup.ref('price'), 'Sale Price should be less than ${less}')
// .positive('form:error-sale-price-must-positive')
// .nullable(),
// quantity: yup
// .number()
// .typeError('form:error-quantity-must-number')
// .positive('form:error-quantity-must-positive')
// .integer('form:error-quantity-must-integer')
// .required('form:error-quantity-required'),
// sku: yup.string().required('form:error-sku-required'),
// is_digital: yup.boolean(),
// digital_file_input: yup.object().when('is_digital', {
// is: true,
// then: () =>
// yup
// .object()
// .shape({
// id: yup.string().required(),
// })
// .required('form:error-digital-file-is-required'),
// otherwise: () =>
// yup
// .object()
// .shape({
// id: yup.string().notRequired(),
// original: yup.string().notRequired(),
// })
// .notRequired()
// .nullable(),
// }),
// }),
// ),
// is_digital: yup.boolean(),
// digital_file_input: yup.object().when('is_digital', {
// is: true,
// then: () =>
// yup
// .object()
// .shape({
// id: yup.string().required(),
// })
// .required('form:error-digital-file-is-required'),
// otherwise: () =>
// yup
// .object()
// .shape({
// id: yup.string().notRequired(),
// original: yup.string().notRequired(),
// })
// .notRequired()
// .nullable(),
// }),
// video: yup.array().of(
// yup.object().shape({
// url: yup.string().required('Video URL is required'),
// }),
// ),
description: yup description: yup
.string() .string()
.max( .max(

1
src/components/shop/approve-shop-view.tsx

@ -41,7 +41,6 @@ const ApproveShopView = () => {
via: 'admin', via: 'admin',
}); });
}, []); }, []);
console.log('data',data);
return data?.multiCommission ? ( return data?.multiCommission ? (
<MultiCommission <MultiCommission
data={data} data={data}

272
src/components/shop/shop-form.tsx

@ -46,6 +46,7 @@ import { ShopDescriptionSuggestion } from '@/components/shop/shop-ai-prompt';
import PhoneNumberInput from '@/components/ui/phone-input'; import PhoneNumberInput from '@/components/ui/phone-input';
import DatePicker from '@/components/ui/date-picker'; import DatePicker from '@/components/ui/date-picker';
import { addDays, addMinutes, isSameDay, isToday } from 'date-fns'; import { addDays, addMinutes, isSameDay, isToday } from 'date-fns';
import { useEffect } from 'react';
// const socialIcon = [ // const socialIcon = [
// { // {
@ -93,13 +94,26 @@ type FormValues = {
settings: ShopSettings; settings: ShopSettings;
isShopUnderMaintenance?: boolean; isShopUnderMaintenance?: boolean;
}; };
const socialMedias = ['telegram', 'whatsapp', 'instagram'];
const ShopForm = ({ initialValues }: { initialValues?: Shop }) => { const ShopForm = ({ initialValues }: { initialValues?: Shop }) => {
const [initialFormValues, setInitialFormValues] = useState({});
const [location] = useAtom(locationAtom); const [location] = useAtom(locationAtom);
const { mutate: createShop, isLoading: creating } = useCreateShopMutation(); const { mutate: createShop, isLoading: creating } = useCreateShopMutation();
const { mutate: updateShop, isLoading: updating } = useUpdateShopMutation(); const { mutate: updateShop, isLoading: updating } = useUpdateShopMutation();
// const { permissions } = getAuthCredentials(); // const { permissions } = getAuthCredentials();
// let permission = hasAccess(adminAndOwnerOnly, permissions); // let permission = hasAccess(adminAndOwnerOnly, permissions);
const { permissions } = getAuthCredentials(); const { permissions } = getAuthCredentials();
const defaultSocialMedias = {
telegram: '',
whatsapp: '',
instagram: '',
};
const formDefaults = {
...initialValues,
social_medias: { ...defaultSocialMedias, ...initialValues?.social_medias },
};
const { const {
register, register,
handleSubmit, handleSubmit,
@ -110,29 +124,10 @@ const ShopForm = ({ initialValues }: { initialValues?: Shop }) => {
control, control,
} = useForm<FormValues>({ } = useForm<FormValues>({
shouldUnregister: true, shouldUnregister: true,
...(initialValues
? {
defaultValues: {
...initialValues,
logo: getFormattedImage(initialValues?.logo as IImage),
cover_image: getFormattedImage(
initialValues?.cover_image as IImage,
),
settings: {
...initialValues?.settings,
socials: initialValues?.settings?.socials
? initialValues?.settings?.socials.map((social: any) => ({
icon: updatedIcons?.find(
(icon) => icon?.value === social?.icon,
),
url: social?.url,
}))
: [],
},
},
}
: {}),
// @ts-ignore
defaultValues: {
...formDefaults,
logo: formDefaults.logo_url,
},
resolver: yupResolver(shopValidationSchema), resolver: yupResolver(shopValidationSchema),
}); });
const router = useRouter(); const router = useRouter();
@ -146,9 +141,9 @@ const ShopForm = ({ initialValues }: { initialValues?: Shop }) => {
language: locale!, language: locale!,
}); });
const generateName = watch('name');
const generateName = watch('title');
const shopDescriptionSuggestionLists = useMemo(() => { const shopDescriptionSuggestionLists = useMemo(() => {
return ShopDescriptionSuggestion({ name: generateName ?? '' });
return ShopDescriptionSuggestion({ title: generateName ?? '' });
}, [generateName]); }, [generateName]);
const handleGenerateDescription = useCallback(() => { const handleGenerateDescription = useCallback(() => {
@ -161,7 +156,7 @@ const ShopForm = ({ initialValues }: { initialValues?: Shop }) => {
}); });
}, [generateName]); }, [generateName]);
const slugAutoSuggest = formatSlug(watch('name'));
const slugAutoSuggest = formatSlug(watch('title'));
const startDate = useWatch({ const startDate = useWatch({
name: 'settings.shopMaintenance.start', name: 'settings.shopMaintenance.start',
@ -171,56 +166,54 @@ const ShopForm = ({ initialValues }: { initialValues?: Shop }) => {
name: 'settings.shopMaintenance.until', name: 'settings.shopMaintenance.until',
control, control,
}); });
console.log({ startDate });
const isMaintenanceMode = watch('settings.isShopUnderMaintenance'); const isMaintenanceMode = watch('settings.isShopUnderMaintenance');
const today = new Date(); const today = new Date();
const { t } = useTranslation(); const { t } = useTranslation();
const { fields, append, remove } = useFieldArray({
control,
name: 'settings.socials',
});
// const { fields, append, remove } = useFieldArray({
// control,
// name: 'settings.socials',
// });
const [isSlugDisable, setIsSlugDisable] = useState<boolean>(true); const [isSlugDisable, setIsSlugDisable] = useState<boolean>(true);
const isSlugEditable =
(router?.query?.action === 'edit' || router?.pathname === '/[shop]/edit') &&
router?.locale === Config.defaultLanguage;
const isSlugEditable = router?.pathname === '/shop/create';
const isEdited =
JSON.stringify(initialFormValues) !== JSON.stringify(watch());
// &&
// router?.locale === Config.defaultLanguage;
function onSubmit(values: FormValues) { function onSubmit(values: FormValues) {
const settings = {
...values?.settings,
location: { ...omit(values?.settings?.location, '__typename') },
socials: values?.settings?.socials
? values?.settings?.socials?.map((social: any) => ({
icon: social?.icon?.value,
url: social?.url,
}))
: [],
shopMaintenance: values?.settings?.shopMaintenance,
};
// const settings = {
// ...values?.settings,
// location: { ...omit(values?.settings?.location, '__typename') },
// socials: values?.settings?.socials
// ? values?.settings?.socials?.map((social: any) => ({
// icon: social?.icon?.value,
// url: social?.url,
// }))
// : [],
// shopMaintenance: values?.settings?.shopMaintenance,
// };
console.log(values);
if (initialValues) { if (initialValues) {
const { ...restAddress } = values.address;
updateShop({
id: initialValues?.id as string,
...values,
address: restAddress,
settings,
balance: {
id: initialValues.balance?.id,
...values.balance,
},
});
if (isEdited) {
updateShop(values);
}
} else { } else {
createShop({
...values,
settings,
balance: {
...values.balance,
},
});
createShop(values);
} }
} }
useEffect(() => {
setInitialFormValues(watch());
}, []);
useEffect(() => {
if (slugAutoSuggest) {
setValue('slug', slugAutoSuggest); // Update form state with the slug
}
}, [slugAutoSuggest, setValue]);
const isGoogleMapActive = options?.useGoogleMap; const isGoogleMapActive = options?.useGoogleMap;
const askForAQuote = watch('settings.askForAQuote.enable'); const askForAQuote = watch('settings.askForAQuote.enable');
@ -280,7 +273,7 @@ const ShopForm = ({ initialValues }: { initialValues?: Shop }) => {
/> />
<Card className="w-full sm:w-8/12 md:w-2/3"> <Card className="w-full sm:w-8/12 md:w-2/3">
<FileInput name="cover_image" control={control} multiple={false} />
<FileInput name="images" control={control} multiple={true} />
</Card> </Card>
</div> </div>
<div className="flex flex-wrap pb-8 my-5 border-b border-dashed border-border-base sm:my-8"> <div className="flex flex-wrap pb-8 my-5 border-b border-dashed border-border-base sm:my-8">
@ -292,10 +285,10 @@ const ShopForm = ({ initialValues }: { initialValues?: Shop }) => {
<Card className="w-full sm:w-8/12 md:w-2/3"> <Card className="w-full sm:w-8/12 md:w-2/3">
<Input <Input
label={t('form:input-label-name')} label={t('form:input-label-name')}
{...register('name')}
{...register('title')}
variant="outline" variant="outline"
className="mb-5" className="mb-5"
error={t(errors.name?.message!)}
error={t(errors.title?.message!)}
required required
/> />
@ -303,8 +296,8 @@ const ShopForm = ({ initialValues }: { initialValues?: Shop }) => {
<div className="relative mb-5"> <div className="relative mb-5">
<Input <Input
label={t('form:input-label-slug')} label={t('form:input-label-slug')}
{...register('slug')}
error={t(errors.slug?.message!)}
{...register('slug')} // Register slug field for form handling
error={t(errors.slug?.message!)} // Display error message if any
variant="outline" variant="outline"
disabled={isSlugDisable} disabled={isSlugDisable}
/> />
@ -321,7 +314,6 @@ const ShopForm = ({ initialValues }: { initialValues?: Shop }) => {
<Input <Input
label={t('form:input-label-slug')} label={t('form:input-label-slug')}
{...register('slug')} {...register('slug')}
value={slugAutoSuggest}
variant="outline" variant="outline"
className="mb-5" className="mb-5"
disabled disabled
@ -354,33 +346,41 @@ const ShopForm = ({ initialValues }: { initialValues?: Shop }) => {
<Card className="w-full sm:w-8/12 md:w-2/3"> <Card className="w-full sm:w-8/12 md:w-2/3">
<Input <Input
label={t('form:input-label-account-holder-name')} label={t('form:input-label-account-holder-name')}
{...register('balance.payment_info.name')}
{...register('account_holder_name')}
variant="outline" variant="outline"
className="mb-5" className="mb-5"
error={t(errors.balance?.payment_info?.name?.message!)}
error={t(errors.account_holder_name?.message!)}
required required
/> />
<Input <Input
label={t('form:input-label-account-holder-email')} label={t('form:input-label-account-holder-email')}
{...register('balance.payment_info.email')}
{...register('account_holder_email')}
variant="outline" variant="outline"
className="mb-5" className="mb-5"
error={t(errors.balance?.payment_info?.email?.message!)}
error={t(errors.account_holder_email?.message!)}
required required
/> />
<Input <Input
label={t('form:input-label-bank-name')} label={t('form:input-label-bank-name')}
{...register('balance.payment_info.bank')}
{...register('bank_name')}
variant="outline" variant="outline"
className="mb-5" className="mb-5"
error={t(errors.balance?.payment_info?.bank?.message!)}
error={t(errors.bank_name?.message!)}
required required
/> />
<Input <Input
label={t('form:input-label-account-number')} label={t('form:input-label-account-number')}
{...register('balance.payment_info.account')}
{...register('account_number')}
variant="outline"
error={t(errors.account_number?.message!)}
className="mb-5"
required
/>
<Input
label={t('form:input-label-iban-number')}
{...register('iban_number')}
variant="outline" variant="outline"
error={t(errors.balance?.payment_info?.account?.message!)}
error={t(errors.iban_number?.message!)}
required required
/> />
</Card> </Card>
@ -393,7 +393,7 @@ const ShopForm = ({ initialValues }: { initialValues?: Shop }) => {
/> />
<Card className="w-full sm:w-8/12 md:w-2/3"> <Card className="w-full sm:w-8/12 md:w-2/3">
{isGoogleMapActive && (
{/* {isGoogleMapActive && (
<div className="mb-5"> <div className="mb-5">
<Label>{t('form:input-label-autocomplete')}</Label> <Label>{t('form:input-label-autocomplete')}</Label>
<Controller <Controller
@ -419,45 +419,45 @@ const ShopForm = ({ initialValues }: { initialValues?: Shop }) => {
)} )}
/> />
</div> </div>
)}
)} */}
<Input <Input
label={t('form:input-label-country')} label={t('form:input-label-country')}
{...register('address.country')}
{...register('country')}
variant="outline" variant="outline"
className="mb-5" className="mb-5"
error={t(errors.address?.country?.message!)}
error={t(errors.country?.message!)}
/> />
<Input <Input
label={t('form:input-label-city')} label={t('form:input-label-city')}
{...register('address.city')}
{...register('city')}
variant="outline" variant="outline"
className="mb-5" className="mb-5"
error={t(errors.address?.city?.message!)}
error={t(errors.city?.message!)}
/> />
<Input
{/* <Input
label={t('form:input-label-state')} label={t('form:input-label-state')}
{...register('address.state')}
{...register('address')}
variant="outline" variant="outline"
className="mb-5" className="mb-5"
error={t(errors.address?.state?.message!)}
/>
error={t(errors.address?.message!)}
/> */}
<Input <Input
label={t('form:input-label-zip')} label={t('form:input-label-zip')}
{...register('address.zip')}
{...register('zip_address')}
variant="outline" variant="outline"
className="mb-5" className="mb-5"
error={t(errors.address?.zip?.message!)}
error={t(errors.zip_address?.message!)}
/> />
<TextArea <TextArea
label={t('form:input-label-street-address')} label={t('form:input-label-street-address')}
{...register('address.street_address')}
{...register('address')}
variant="outline" variant="outline"
error={t(errors.address?.street_address?.message!)}
error={t(errors.address?.message!)}
/> />
</Card> </Card>
</div> </div>
{permissions?.includes(STORE_OWNER) ? (
{/* {permissions?.includes(STORE_OWNER) ? (
<div className="flex flex-wrap pb-8 my-5 border-b border-dashed border-border-base sm:my-8"> <div className="flex flex-wrap pb-8 my-5 border-b border-dashed border-border-base sm:my-8">
<Description <Description
title={t('form:form-notification-title')} title={t('form:form-notification-title')}
@ -489,7 +489,7 @@ const ShopForm = ({ initialValues }: { initialValues?: Shop }) => {
</div> </div>
) : ( ) : (
'' ''
)}
)} */}
<div className="flex flex-wrap pb-8 my-5 border-b border-gray-300 border-dashed sm:my-8"> <div className="flex flex-wrap pb-8 my-5 border-b border-gray-300 border-dashed sm:my-8">
<Description <Description
title={t('form:shop-settings')} title={t('form:shop-settings')}
@ -501,16 +501,16 @@ const ShopForm = ({ initialValues }: { initialValues?: Shop }) => {
<PhoneNumberInput <PhoneNumberInput
label={t('form:input-label-contact')} label={t('form:input-label-contact')}
required required
{...register('settings.contact')}
{...register('phone_number')}
control={control} control={control}
error={t(errors.settings?.contact?.message!)}
error={t(errors.phone_number?.message!)}
/> />
<Input <Input
label={t('form:input-label-website')} label={t('form:input-label-website')}
{...register('settings.website')}
{...register('website')}
variant="outline" variant="outline"
className="mb-5" className="mb-5"
error={t(errors.settings?.website?.message!)}
error={t(errors.website?.message!)}
required required
/> />
</Card> </Card>
@ -524,66 +524,21 @@ const ShopForm = ({ initialValues }: { initialValues?: Shop }) => {
/> />
<Card className="w-full sm:w-8/12 md:w-2/3"> <Card className="w-full sm:w-8/12 md:w-2/3">
<div>
{fields?.map(
(item: ShopSocialInput & { id: string }, index: number) => (
<div
className="py-5 border-b border-dashed border-border-200 first:mt-0 first:border-t-0 first:pt-0 last:border-b-0 md:py-8 md:first:mt-0"
key={item.id}
>
<div className="grid grid-cols-1 gap-5 sm:grid-cols-5">
<div className="sm:col-span-2">
<Label>{t('form:input-label-select-platform')}</Label>
<SelectInput
name={`settings.socials.${index}.icon` as const}
control={control}
options={updatedIcons}
isClearable={true}
defaultValue={item?.icon!}
/>
</div>
{/* <Input
className="sm:col-span-2"
label={t("form:input-label-icon")}
variant="outline"
{...register(`settings.socials.${index}.icon` as const)}
defaultValue={item?.icon!} // make sure to set up defaultValue
/> */}
<Input
className="sm:col-span-2"
label={t('form:input-label-url')}
variant="outline"
{...register(`settings.socials.${index}.url` as const)}
error={t(
errors?.settings?.socials?.[index]?.url?.message!,
)}
defaultValue={item.url!} // make sure to set up defaultValue
required
/>
<button
onClick={() => {
remove(index);
}}
type="button"
className="text-sm text-red-500 transition-colors duration-200 hover:text-red-700 focus:outline-none sm:col-span-1 sm:mt-4"
>
{t('form:button-label-remove')}
</button>
</div>
</div>
),
)}
</div>
<Button
type="button"
onClick={() => append({ icon: '', url: '' })}
className="w-full text-sm sm:w-auto md:text-base"
>
{t('form:button-label-add-social')}
</Button>
{socialMedias.map((platform) => (
<div key={platform} className="mb-5">
<Input
label={`${
platform.charAt(0).toUpperCase() + platform.slice(1)
} URL`}
{...register(`social_medias.${platform}` as const)}
variant="outline"
error={errors?.social_medias?.[platform]?.message}
/>
</div>
))}
</Card> </Card>
</div> </div>
{!permissions?.includes(SUPER_ADMIN) ? (
{/* {!permissions?.includes(SUPER_ADMIN) ? (
<div className="flex flex-wrap pb-8 my-5 border-b border-gray-300 border-dashed sm:my-8"> <div className="flex flex-wrap pb-8 my-5 border-b border-gray-300 border-dashed sm:my-8">
<Description <Description
title="Shop maintenance settings " title="Shop maintenance settings "
@ -691,8 +646,8 @@ const ShopForm = ({ initialValues }: { initialValues?: Shop }) => {
</div> </div>
) : ( ) : (
'' ''
)}
{!permissions?.includes(SUPER_ADMIN) &&
)} */}
{!permissions?.includes(SUPER_ADMIN) &&
!permissions?.includes(STAFF) && !permissions?.includes(STAFF) &&
!Boolean(initialValues?.is_active) && !Boolean(initialValues?.is_active) &&
Boolean(options?.isMultiCommissionRate) ? ( Boolean(options?.isMultiCommissionRate) ? (
@ -740,8 +695,9 @@ const ShopForm = ({ initialValues }: { initialValues?: Shop }) => {
<StickyFooterPanel className="z-0"> <StickyFooterPanel className="z-0">
<div className="mb-5 text-end"> <div className="mb-5 text-end">
<Button <Button
type="submit"
loading={creating || updating} loading={creating || updating}
disabled={creating || updating}
disabled={creating || updating || !isEdited}
> >
{initialValues {initialValues
? t('form:button-label-update') ? t('form:button-label-update')

117
src/components/shop/shop-validation-schema.ts

@ -3,70 +3,63 @@ import { phoneRegExp, URLRegExp } from '@/utils/constants';
const currentDate = new Date(); const currentDate = new Date();
const slugRegExp = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
export const shopValidationSchema = yup.object().shape({ export const shopValidationSchema = yup.object().shape({
name: yup.string().required('form:error-name-required'),
balance: yup.object().shape({
payment_info: yup.object().shape({
email: yup
.string()
.required('form:error-account-holder-email-required')
.typeError('form:error-email-string')
.email('form:error-email-format'),
name: yup.string().required('form:error-account-holder-name-required'),
bank: yup.string().required('form:error-bank-name-required'),
account: yup
.number()
.positive('form:error-account-number-positive-required')
.integer('form:error-account-number-integer-required')
.required('form:error-account-number-required')
.transform((value) => (isNaN(value) ? undefined : value)),
}),
}),
settings: yup.object().shape({
contact: yup
.string()
.required('form:error-contact-number-required')
.matches(phoneRegExp, 'form:error-contact-number-valid-required'),
website: yup
.string()
.required('form:error-website-required')
.matches(URLRegExp, 'form:error-url-valid-required'),
socials: yup.array().of(
yup.object().shape({
url: yup.string().when('icon', (data) => {
if (data) {
return yup.string().required('form:error-url-required');
}
return yup.string().nullable();
title: yup.string().required('form:error-name-required'),
slug: yup
.string()
.required('form:error-slug-required') // Slug is required
.matches(slugRegExp, 'form:error-slug-invalid') // Ensures it contains only lowercase English, numbers, and hyphens
,
account_holder_email: yup
.string()
.required('form:error-account-holder-email-required')
.typeError('form:error-email-string')
.email('form:error-email-format'),
account_holder_name: yup
.string()
.required('form:error-account-holder-name-required'),
bank_name: yup.string().required('form:error-bank-name-required'),
account_number: yup
.number()
.positive('form:error-account-number-positive-required')
.integer('form:error-account-number-integer-required')
.required('form:error-account-number-required')
.transform((value) => (isNaN(value) ? undefined : value)),
phone_number: yup
.string()
.required('form:error-contact-number-required')
.matches(phoneRegExp, 'form:error-contact-number-valid-required'),
website: yup
.string()
.required('form:error-website-required')
.matches(URLRegExp, 'form:error-url-valid-required'),
shopMaintenance: yup
.object()
.when('isShopUnderMaintenance', {
is: (data: boolean) => data,
then: () =>
yup.object().shape({
title: yup.string().required('Title is required'),
description: yup.string().required('Description is required'),
start: yup
.date()
.min(
currentDate.toDateString(),
`Maintenance start date field must be later than ${currentDate.toDateString()}`,
)
.required('Start date is required'),
until: yup
.date()
.required('Until date is required')
.min(
yup.ref('start'),
'Until date must be greater than or equal to start date',
),
}), }),
}),
),
shopMaintenance: yup
.object()
.when('isShopUnderMaintenance', {
is: (data: boolean) => data,
then: () =>
yup.object().shape({
title: yup.string().required('Title is required'),
description: yup.string().required('Description is required'),
start: yup
.date()
.min(
currentDate.toDateString(),
`Maintenance start date field must be later than ${currentDate.toDateString()}`,
)
.required('Start date is required'),
until: yup
.date()
.required('Until date is required')
.min(
yup.ref('start'),
'Until date must be greater than or equal to start date',
),
}),
})
.notRequired(),
}),
})
.notRequired(),
}); });
export const approveShopSchema = yup.object().shape({ export const approveShopSchema = yup.object().shape({

3
src/components/ui/button.tsx

@ -39,8 +39,10 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
children, children,
loading = false, loading = false,
disabled = false, disabled = false,
type = 'button', // Add type here with a default value
...rest ...rest
} = props; } = props;
const classesName = cn( const classesName = cn(
classes.root, classes.root,
{ {
@ -57,6 +59,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
return ( return (
<button <button
type={type} // Pass the type prop here
aria-pressed={active} aria-pressed={active}
data-variant={variant} data-variant={variant}
ref={ref} ref={ref}

194
src/components/ui/field-array.tsx

@ -0,0 +1,194 @@
import React, { useState, useEffect } from 'react';
import { socialIcon } from '@/settings/site.settings';
import { getIcon } from '@/utils/get-icon';
import * as socialIcons from '@/components/icons/social';
import Select from 'react-select';
import Input from './input';
import Button from '@/components/ui/button';
export const FieldArray = ({ data, setData }) => {
const [fields, setFields] = useState(data || [{ platform: null, url: '' }]); // Initialize with data from parent
const customOption = (props) => {
const { data, innerRef, innerProps } = props;
return (
<div
ref={innerRef}
{...innerProps}
className="flex items-center space-x-2 px-3 py-2 hover:bg-gray-100 cursor-pointer"
>
<span className="flex items-center justify-center w-6 h-6">
{getIcon({
iconList: socialIcons,
iconName: data.value,
className: 'w-5 h-5 text-gray-500',
})}
</span>
<span className="text-gray-800 text-sm">{data.label}</span>
</div>
);
};
const options = socialIcon.map((item) => ({
value: item.value,
label: item.label,
}));
// Filter out already selected platforms from options
const getAvailableOptions = (index) => {
const selectedValues = fields
.filter((_, i) => i !== index && fields[i].platform) // Get selected platforms except current field
.map((field) => field.platform.value); // Extract selected platform values
return options.filter((option) => !selectedValues.includes(option.value)); // Exclude selected platforms
};
const customStyles = {
control: (base) => ({
...base,
minHeight: '47px',
height: '47px',
padding: '0 10px',
display: 'flex',
alignItems: 'center',
}),
valueContainer: (base) => ({
...base,
display: 'flex',
alignItems: 'center',
height: '100%',
padding: '0 8px',
}),
input: (base) => ({
...base,
margin: '0',
padding: '0',
height: '100%',
}),
singleValue: (base) => ({
...base,
display: 'flex',
alignItems: 'center',
}),
dropdownIndicator: (base) => ({
...base,
padding: '8px',
}),
menu: (base) => ({
...base,
marginTop: 0,
borderRadius: '4px',
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
border: '1px solid #e5e7eb',
}),
option: (base, { isFocused }) => ({
...base,
display: 'flex',
alignItems: 'center',
padding: '10px 12px',
backgroundColor: isFocused ? '#f9fafb' : '#fff',
color: '#374151',
cursor: 'pointer',
}),
};
const handleAddField = () => {
const newFields = [...fields, { platform: null, url: '' }];
setFields(newFields);
setData(newFields); // Pass the updated data to parent
};
const handleRemoveField = (index) => {
const newFields = fields.filter((_, i) => i !== index);
setFields(newFields);
setData(newFields); // Pass the updated data to parent
};
const handleFieldChange = (index, key, value) => {
const updatedFields = fields.map((field, i) =>
i === index ? { ...field, [key]: value } : field,
);
setFields(updatedFields);
setData(updatedFields); // Pass the updated data to parent
};
// Update parent component when fields change
useEffect(() => {
setData(fields); // Pass the current fields data to parent
}, [fields, setData]);
return (
<div className="">
{fields.map((field, index) => (
<div key={index} className="flex gap-4 items-center pb-8 my-5 border-b border-gray-300 border-dashed sm:my-8">
<div>
<label
htmlFor={`social-platform-${index}`}
className="block text-sm font-medium text-gray-700"
>
Select social platform
</label>
<Select
styles={customStyles}
options={getAvailableOptions(index)} // Use filtered options
isSearchable={false}
value={field.platform}
onChange={(selectedOption) =>
handleFieldChange(index, 'platform', selectedOption)
}
getOptionLabel={(e) => (
<div className="flex items-center space-x-2">
<span className="flex items-center justify-center w-6 h-6">
{getIcon({
iconList: socialIcons,
iconName: e.value,
className: 'w-4 h-4 text-gray-500',
})}
</span>
<span className="text-gray-800 text-sm">{e.label}</span>
</div>
)}
components={{ Option: customOption }}
className="mt-4 w-96"
id={`social-platform-${index}`}
/>
</div>
<div className='w-96'>
<label
htmlFor={`url-${index}`}
className="block text-sm font-medium text-gray-700"
>
Url <span className="text-red-500">*</span>
</label>
<Input
id={`url-${index}`}
value={field.url}
variant="outline"
onChange={(e) => handleFieldChange(index, 'url', e.target.value)}
className="mt-1"
/>
</div>
<div className="ml-10">
<button
type="button"
onClick={() => handleRemoveField(index)}
className="text-sm text-red-500 transition-colors duration-200 hover:text-red-700 focus:outline-none sm:mt-8"
>
Remove
</button>
</div>
</div>
))}
<div>
<Button
type="button"
onClick={handleAddField}
className="w-full sm:w-auto text-sm md:text-base mt-3"
>
Add New Social Profile
</Button>
</div>
</div>
);
};

2
src/components/ui/input.tsx

@ -10,7 +10,7 @@ export interface Props extends InputHTMLAttributes<HTMLInputElement> {
label?: string; label?: string;
toolTipText?: string; toolTipText?: string;
note?: string; note?: string;
name: string;
name?: string;
error?: string; error?: string;
type?: string; type?: string;
shadow?: boolean; shadow?: boolean;

2
src/components/ui/lang-action/action.tsx

@ -58,7 +58,7 @@ export default function LanguageSwitcher({
) : ( ) : (
<ActionButtons <ActionButtons
id={record?.id} id={record?.id}
editUrl={routes.editWithoutLang(slug, shop)}
editUrl={routes.editWithoutLang(slug)}
previewUrl={preview} previewUrl={preview}
enablePreviewMode={enablePreviewMode} enablePreviewMode={enablePreviewMode}
deleteModalView={deleteModalView} deleteModalView={deleteModalView}

76
src/components/ui/switch-input.tsx

@ -1,17 +1,16 @@
import ValidationError from '@/components/ui/form-validation-error';
import { useState } from 'react';
import TooltipLabel from '@/components/ui/tooltip-label'; import TooltipLabel from '@/components/ui/tooltip-label';
import ValidationError from '@/components/ui/form-validation-error';
import { Switch } from '@headlessui/react'; import { Switch } from '@headlessui/react';
import classNames from 'classnames'; import classNames from 'classnames';
import { useTranslation } from 'next-i18next';
import { Control, Controller } from 'react-hook-form';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
interface Props { interface Props {
control: Control<any>;
error?: string;
name: string; name: string;
value?: boolean;
onChange?: (value: boolean) => void;
error?: string;
disabled?: boolean; disabled?: boolean;
[key: string]: unknown;
required?: boolean; required?: boolean;
label?: string; label?: string;
toolTipText?: string; toolTipText?: string;
@ -20,49 +19,48 @@ interface Props {
} }
const SwitchInput = ({ const SwitchInput = ({
control,
label,
name, name,
value = false,
onChange,
error, error,
disabled, disabled,
required, required,
label,
toolTipText, toolTipText,
className, className,
labelClassName, labelClassName,
...rest
}: Props) => { }: Props) => {
const { t } = useTranslation();
const [checked, setChecked] = useState(value);
const handleToggle = (newState: boolean) => {
setChecked(newState);
if (onChange) onChange(newState);
};
return ( return (
<> <>
<div <div
className={twMerge(classNames('flex items-center gap-x-4', className))} className={twMerge(classNames('flex items-center gap-x-4', className))}
> >
<Controller
name={name}
control={control}
{...rest}
render={({ field: { onChange, value } }) => (
<Switch
checked={value}
onChange={onChange}
disabled={disabled}
className={`${
value ? 'bg-accent' : 'bg-gray-300'
} relative inline-flex h-6 w-11 items-center rounded-full focus:outline-none ${
disabled ? 'cursor-not-allowed bg-[#EEF1F4]' : ''
}`}
dir="ltr"
id={name}
>
<span className="sr-only">Enable {label}</span>
<span
className={`${
value ? 'translate-x-6' : 'translate-x-1'
} inline-block h-4 w-4 transform rounded-full bg-light transition-transform`}
/>
</Switch>
)}
/>
<Switch
checked={checked}
onChange={handleToggle}
disabled={disabled}
className={`${
checked ? 'bg-accent' : 'bg-gray-300'
} relative inline-flex h-6 w-11 items-center rounded-full focus:outline-none ${
disabled ? 'cursor-not-allowed bg-[#EEF1F4]' : ''
}`}
dir="ltr"
id={name}
>
<span className="sr-only">Enable {label}</span>
<span
className={`${
checked ? 'translate-x-6' : 'translate-x-1'
} inline-block h-4 w-4 transform rounded-full bg-light transition-transform`}
/>
</Switch>
{label ? ( {label ? (
<TooltipLabel <TooltipLabel
htmlFor={name} htmlFor={name}
@ -71,11 +69,9 @@ const SwitchInput = ({
label={label} label={label}
required={required} required={required}
/> />
) : (
''
)}
) : null}
</div> </div>
{error ? <ValidationError message={error} /> : ''}
{error && <ValidationError message={error} />}
</> </>
); );
}; };

2
src/components/ui/text-area.tsx

@ -8,7 +8,7 @@ export interface Props extends TextareaHTMLAttributes<HTMLTextAreaElement> {
inputClassName?: string; inputClassName?: string;
toolTipText?: string; toolTipText?: string;
label?: string; label?: string;
name: string;
name?: string;
error?: string; error?: string;
shadow?: boolean; shadow?: boolean;
variant?: 'normal' | 'solid' | 'outline'; variant?: 'normal' | 'solid' | 'outline';

4
src/components/user/user-details.tsx

@ -26,8 +26,8 @@ const UserDetails: React.FC = () => {
const [miniSidebar, _] = useAtom(miniSidebarInitialValue); const [miniSidebar, _] = useAtom(miniSidebarInitialValue);
const { width } = useWindowSize(); const { width } = useWindowSize();
if (loading)
return <Loader text={t('text-loading')} className="!h-auto py-10" />;
// if (loading)
// return <Loader text={t('text-loading')} className="!h-auto py-10" />;
return ( return (
<div <div

65
src/components/validations/shop-validation-schema .tsx

@ -0,0 +1,65 @@
const validateForm = (formValues) => {
const errors = {};
// Validate shop name
if (!formValues.title) {
errors.title = 'Shop name is required';
} else if (formValues.title.length < 3) {
errors.title = 'Shop name must be at least 3 characters long';
}
// Validate slug
if (!formValues.slug) {
errors.slug = 'Slug is required';
} else if (!/^[a-z0-9-]+$/.test(formValues.slug)) {
errors.slug = 'Slug must contain only lowercase letters, numbers, and hyphens';
}
// Validate description
if (!formValues.description) {
errors.description = 'Description is required';
} else if (formValues.description.length < 10) {
errors.description = 'Description must be at least 10 characters long';
}
// Validate required file inputs (logo, cover_image)
if (!formValues.logo) {
errors.logo = 'Shop logo is required';
}
if (!formValues.cover_image) {
errors.cover_image = 'Cover image is required';
}
// Validate account holder name
if (!formValues.account_holder_name) {
errors.account_holder_name = 'Account holder name is required';
}
// Validate email format for account holder email
if (!formValues.account_holder_email) {
errors.account_holder_email = 'Account holder email is required';
} else if (!/\S+@\S+\.\S+/.test(formValues.account_holder_email)) {
errors.account_holder_email = 'Invalid email format';
}
// Validate bank name
if (!formValues.bank_name) {
errors.bank_name = 'Bank name is required';
}
// Validate account number
if (!formValues.account_number) {
errors.account_number = 'Account number is required';
} else if (!/^\d+$/.test(formValues.account_number)) {
errors.account_number = 'Account number must be digits only';
}
// Validate address
if (!formValues.country) {
errors.country = 'Country is required';
}
if (!formValues.city) {
errors.city = 'City is required';
}
return errors;
};

2
src/components/widgets/sticker-card.tsx

@ -19,6 +19,8 @@ const StickerCard = ({
iconClassName, iconClassName,
}: StickerCardProps) => { }: StickerCardProps) => {
const { t } = useTranslation('widgets'); const { t } = useTranslation('widgets');
console.log(price);
return ( return (
<div <div
className="flex h-full w-full flex-col rounded-lg border border-b-4 border-border-200 bg-light p-5 md:p-6" className="flex h-full w-full flex-col rounded-lg border border-b-4 border-border-200 bg-light p-5 md:p-6"

4
src/config/routes.ts

@ -71,7 +71,7 @@ export const Routes = {
...routesFactory('/products'), ...routesFactory('/products'),
}, },
shop: { shop: {
...routesFactory('/shops'),
...routesFactory('/shop'),
}, },
tax: { tax: {
...routesFactory('/taxes'), ...routesFactory('/taxes'),
@ -155,7 +155,7 @@ export const Routes = {
: `/${language}/products/${slug}/translate`; : `/${language}/products/${slug}/translate`;
}, },
}, },
visitStore: (slug: string) => `${process.env.NEXT_PUBLIC_SHOP_URL}/${slug}`,
visitStore: (slug: string) => `${process.env.NEXT_PUBLIC_SHOP_URL}/shops/${slug}`,
vendorRequestForFlashSale: { vendorRequestForFlashSale: {
...routesFactory('/flash-sale/vendor-request'), ...routesFactory('/flash-sale/vendor-request'),
}, },

5
src/contexts/settings.context.tsx

@ -8,7 +8,7 @@ const initialState = {
siteSubtitle: '', siteSubtitle: '',
currency: 'USD', currency: 'USD',
currencyOptions: { currencyOptions: {
formation: "en-US",
formation: 'en-US',
fractions: 2, fractions: 2,
}, },
logo: { logo: {
@ -18,6 +18,7 @@ const initialState = {
}, },
}; };
export const SettingsContext = React.createContext<State | any>(initialState); export const SettingsContext = React.createContext<State | any>(initialState);
SettingsContext.displayName = 'SettingsContext'; SettingsContext.displayName = 'SettingsContext';
@ -34,6 +35,8 @@ export const SettingsProvider: React.FC<{ initialValue: any }> = ({
}), }),
[state] [state]
); );
return <SettingsContext.Provider value={value} {...props} />; return <SettingsContext.Provider value={value} {...props} />;
}; };

5
src/data/category.ts

@ -90,6 +90,7 @@ export const useCategoryQuery = ({ slug, language }: GetParams) => {
export const useCategoriesQuery = (options: Partial<CategoryQueryOptions>) => { export const useCategoriesQuery = (options: Partial<CategoryQueryOptions>) => {
const { data, error, isLoading } = useQuery<CategoryPaginator, Error>( const { data, error, isLoading } = useQuery<CategoryPaginator, Error>(
[API_ENDPOINTS.CATEGORIES, options], [API_ENDPOINTS.CATEGORIES, options],
({ queryKey, pageParam }) => ({ queryKey, pageParam }) =>
categoryClient.paginated(Object.assign({}, queryKey[1], pageParam)), categoryClient.paginated(Object.assign({}, queryKey[1], pageParam)),
@ -97,11 +98,13 @@ export const useCategoriesQuery = (options: Partial<CategoryQueryOptions>) => {
keepPreviousData: true, keepPreviousData: true,
} }
); );
console.log(data);
return { return {
categories: data?.data ?? [],
categories: data ?? [],
paginatorInfo: mapPaginatorData(data), paginatorInfo: mapPaginatorData(data),
error, error,
loading: isLoading, loading: isLoading,
}; };
}; };

30
src/data/client/api-endpoints.ts

@ -1,13 +1,17 @@
export const API_ENDPOINTS = { export const API_ENDPOINTS = {
ATTACHMENTS: 'attachments',
ATTACHMENTS: 'upload-tmp-media/',
ANALYTICS: 'analytics', ANALYTICS: 'analytics',
ATTRIBUTES: 'attributes', ATTRIBUTES: 'attributes',
ATTRIBUTE_VALUES: 'attribute-values', ATTRIBUTE_VALUES: 'attribute-values',
ORDER_STATUS: 'order-status', ORDER_STATUS: 'order-status',
ORDERS: 'orders',
ORDERS: 'merchant-panel/orders',
USERS: 'users', USERS: 'users',
REGISTER: 'register',
PRODUCTS: 'products',
REGISTER: 'account/register/',
OTP: 'account/verify/',
PRODUCTS: 'merchant-panel/products/create/',
PRODUCTS_DELETE: 'merchant-panel/products/{id}/delete/',
PRODUCTS_GET: 'merchant-panel/products',
PRODUCTS_SLUG: "shop/products",
POPULAR_PRODUCTS: 'popular-products', POPULAR_PRODUCTS: 'popular-products',
COUPONS: 'coupons', COUPONS: 'coupons',
VERIFY_COUPONS: 'coupons/verify', VERIFY_COUPONS: 'coupons/verify',
@ -15,23 +19,25 @@ export const API_ENDPOINTS = {
TAXES: 'taxes', TAXES: 'taxes',
SHIPPINGS: 'shippings', SHIPPINGS: 'shippings',
SETTINGS: 'settings', SETTINGS: 'settings',
CATEGORIES: 'categories',
TAGS: 'tags',
CATEGORIES: 'shop/products/categories/',
TAGS: 'shop/tags/',
TYPES: 'types', TYPES: 'types',
PROFILE_UPDATE: 'profile-update', PROFILE_UPDATE: 'profile-update',
LOGOUT: 'logout', LOGOUT: 'logout',
ME: 'me',
TOKEN: 'token',
ME: 'account/merchant/profile/',
TOKEN: 'account/login/',
BLOCK_USER: 'users/block-user', BLOCK_USER: 'users/block-user',
UNBLOCK_USER: 'users/unblock-user', UNBLOCK_USER: 'users/unblock-user',
CHANGE_PASSWORD: 'change-password', CHANGE_PASSWORD: 'change-password',
FORGET_PASSWORD: 'forget-password',
VERIFY_FORGET_PASSWORD_TOKEN: 'verify-forget-password-token',
RESET_PASSWORD: 'reset-password',
FORGET_PASSWORD: 'account/recover/',
VERIFY_FORGET_PASSWORD_TOKEN: 'account/verify/',
RESET_PASSWORD: 'account/reset/',
DOWNLOAD_INVOICE: 'download/invoice', DOWNLOAD_INVOICE: 'download/invoice',
APPROVE_SHOP: 'approve-shop', APPROVE_SHOP: 'approve-shop',
DISAPPROVE_SHOP: 'disapprove-shop', DISAPPROVE_SHOP: 'disapprove-shop',
SHOPS: 'shops',
SHOPS: 'merchant-panel/create/',
EDIT_SHOPS: 'merchant-panel/update',
GET_SHOPS: 'merchant-panel/info',
MY_SHOPS: 'my-shops', MY_SHOPS: 'my-shops',
WITHDRAWS: 'withdraws', WITHDRAWS: 'withdraws',
APPROVE_WITHDRAW: 'approve-withdraw', APPROVE_WITHDRAW: 'approve-withdraw',

1
src/data/client/category.ts

@ -20,5 +20,6 @@ export const categoryClient = {
...params, ...params,
search: HttpClient.formatSearchParams({ type, name }), search: HttpClient.formatSearchParams({ type, name }),
}); });
}, },
}; };

18
src/data/client/curd-factory.ts

@ -1,5 +1,6 @@
import type { GetParams, PaginatorInfo } from '@/types'; import type { GetParams, PaginatorInfo } from '@/types';
import { HttpClient } from './http-client'; import { HttpClient } from './http-client';
import { API_ENDPOINTS } from './api-endpoints';
interface LanguageParam { interface LanguageParam {
language: string; language: string;
@ -19,10 +20,21 @@ export function crudFactory<Type, QueryParams extends LanguageParam, InputType>(
return HttpClient.get<Type>(`${endpoint}/${slug}`, { language }); return HttpClient.get<Type>(`${endpoint}/${slug}`, { language });
}, },
create(data: InputType) { create(data: InputType) {
return HttpClient.post<Type>(endpoint, data);
if (endpoint === API_ENDPOINTS.SHOPS) {
return HttpClient.post<Type>(API_ENDPOINTS.SHOPS, data);
}
if (endpoint === API_ENDPOINTS.PRODUCTS) {
return HttpClient.post<Type>(API_ENDPOINTS.PRODUCTS, data);
}
}, },
update({ id, ...input }: Partial<InputType> & { id: string }) {
return HttpClient.put<Type>(`${endpoint}/${id}`, input);
update({ ...input }: Partial<InputType> & { id: string }) {
if (endpoint === API_ENDPOINTS.PRODUCTS) {
return HttpClient.patch<Type>(`${API_ENDPOINTS.PRODUCTS_GET}/${input.slug}/update`, input);
}
return HttpClient.patch<Type>(`${API_ENDPOINTS.EDIT_SHOPS}/${input.slug}`, input);
}, },
delete({ id }: { id: string }) { delete({ id }: { id: string }) {
return HttpClient.delete<boolean>(`${endpoint}/${id}`); return HttpClient.delete<boolean>(`${endpoint}/${id}`);

25
src/data/client/http-client.ts

@ -10,9 +10,7 @@ invariant(
const Axios = axios.create({ const Axios = axios.create({
baseURL: process.env.NEXT_PUBLIC_REST_API_ENDPOINT, baseURL: process.env.NEXT_PUBLIC_REST_API_ENDPOINT,
timeout: 50000, timeout: 50000,
headers: {
'Content-Type': 'application/json',
},
}); });
// Change request data/error // Change request data/error
const AUTH_TOKEN_KEY = process.env.NEXT_PUBLIC_AUTH_TOKEN_KEY ?? 'authToken'; const AUTH_TOKEN_KEY = process.env.NEXT_PUBLIC_AUTH_TOKEN_KEY ?? 'authToken';
@ -20,12 +18,13 @@ Axios.interceptors.request.use((config) => {
const cookies = Cookies.get(AUTH_TOKEN_KEY); const cookies = Cookies.get(AUTH_TOKEN_KEY);
let token = ''; let token = '';
if (cookies) { if (cookies) {
token = JSON.parse(cookies)['token'];
token = JSON.parse(cookies);
} }
// @ts-ignore // @ts-ignore
config.headers = { config.headers = {
...config.headers, ...config.headers,
Authorization: `Bearer ${token}`,
Authorization:token ?`token ${token} ` : null,
}; };
return config; return config;
}); });
@ -40,8 +39,8 @@ Axios.interceptors.response.use(
(error.response && (error.response &&
error.response.data.message === 'CHAWKBAZAR_ERROR.NOT_AUTHORIZED') error.response.data.message === 'CHAWKBAZAR_ERROR.NOT_AUTHORIZED')
) { ) {
Cookies.remove(AUTH_TOKEN_KEY);
Router.reload();
// Cookies.remove(AUTH_TOKEN_KEY);
// Router.reload();
} }
return Promise.reject(error); return Promise.reject(error);
}, },
@ -78,11 +77,19 @@ interface SearchParamOptions {
export class HttpClient { export class HttpClient {
static async get<T>(url: string, params?: unknown) { static async get<T>(url: string, params?: unknown) {
const response = await Axios.get<T>(url, { params }); const response = await Axios.get<T>(url, { params });
console.log(response);
return response.data; return response.data;
} }
static async post<T>(url: string, data: unknown, options?: any) { static async post<T>(url: string, data: unknown, options?: any) {
const response = await Axios.post<T>(url, data, options); const response = await Axios.post<T>(url, data, options);
console.log(response);
return response.data; return response.data;
} }
@ -90,6 +97,10 @@ export class HttpClient {
const response = await Axios.put<T>(url, data); const response = await Axios.put<T>(url, data);
return response.data; return response.data;
} }
static async patch<T>(url: string, data: unknown) {
const response = await Axios.patch<T>(url, data);
return response.data;
}
static async delete<T>(url: string) { static async delete<T>(url: string) {
const response = await Axios.delete<T>(url); const response = await Axios.delete<T>(url);

11
src/data/client/product.ts

@ -13,10 +13,11 @@ import { HttpClient } from './http-client';
export const productClient = { export const productClient = {
...crudFactory<Product, QueryOptions, CreateProduct>(API_ENDPOINTS.PRODUCTS), ...crudFactory<Product, QueryOptions, CreateProduct>(API_ENDPOINTS.PRODUCTS),
get({ slug, language }: GetParams) {
return HttpClient.get<Product>(`${API_ENDPOINTS.PRODUCTS}/${slug}`, {
language,
with: 'type;shop;categories;tags;variations.attribute.values;variation_options;variation_options.digital_file;author;manufacturer;digital_file',
delete({id} : number) {
return HttpClient.delete<Product>(`merchant-panel/products/${id}/delete`)
},
get({ slug }: GetParams) {
return HttpClient.get<Product>(`${API_ENDPOINTS.PRODUCTS_SLUG}/${slug}`, {
}); });
}, },
paginated: ({ paginated: ({
@ -28,7 +29,7 @@ export const productClient = {
status, status,
...params ...params
}: Partial<ProductQueryOptions>) => { }: Partial<ProductQueryOptions>) => {
return HttpClient.get<ProductPaginator>(API_ENDPOINTS.PRODUCTS, {
return HttpClient.get<ProductPaginator>(API_ENDPOINTS.PRODUCTS_GET, {
searchJoin: 'and', searchJoin: 'and',
with: 'shop;type;categories', with: 'shop;type;categories',
shop_id, shop_id,

18
src/data/client/settings.ts

@ -4,13 +4,13 @@ import { crudFactory } from './curd-factory';
import { HttpClient } from '@/data/client/http-client'; import { HttpClient } from '@/data/client/http-client';
export const settingsClient = { export const settingsClient = {
...crudFactory<Settings, any, SettingsOptionsInput>(API_ENDPOINTS.SETTINGS),
all({ language }: { language: string }) {
return HttpClient.get<Settings>(API_ENDPOINTS.SETTINGS, {
language,
});
},
update: ({ ...data }: SettingsInput) => {
return HttpClient.post<Settings>(API_ENDPOINTS.SETTINGS, { ...data });
},
// ...crudFactory<Settings, any, SettingsOptionsInput>(API_ENDPOINTS.SETTINGS),
// all({ language }: { language: string }) {
// return HttpClient.get<Settings>(API_ENDPOINTS.SETTINGS, {
// language,
// });
// },
// update: ({ ...data }: SettingsInput) => {
// return HttpClient.post<Settings>(API_ENDPOINTS.SETTINGS, { ...data });
// },
}; };

4
src/data/client/shop.ts

@ -13,8 +13,8 @@ import { crudFactory } from './curd-factory';
export const shopClient = { export const shopClient = {
...crudFactory<Shop, QueryOptions, ShopInput>(API_ENDPOINTS.SHOPS), ...crudFactory<Shop, QueryOptions, ShopInput>(API_ENDPOINTS.SHOPS),
get({ slug }: { slug: String }) {
return HttpClient.get<Shop>(`${API_ENDPOINTS.SHOPS}/${slug}`);
get() {
return HttpClient.get<Shop>(`${API_ENDPOINTS.GET_SHOPS}`);
}, },
paginated: ({ name, ...params }: Partial<ShopQueryOptions>) => { paginated: ({ name, ...params }: Partial<ShopQueryOptions>) => {
return HttpClient.get<ShopPaginator>(API_ENDPOINTS.SHOPS, { return HttpClient.get<ShopPaginator>(API_ENDPOINTS.SHOPS, {

48
src/data/client/upload.ts

@ -1,22 +1,46 @@
// uploadClient.ts
import { HttpClient } from './http-client'; import { HttpClient } from './http-client';
import { API_ENDPOINTS } from './api-endpoints'; import { API_ENDPOINTS } from './api-endpoints';
import { Attachment } from '@/types'; import { Attachment } from '@/types';
export const uploadClient = { export const uploadClient = {
upload: async (variables: any) => { upload: async (variables: any) => {
let formData = new FormData();
// Create a new FormData instance to hold the files
const formData = new FormData();
// Loop over the variables (which are the files) and append them to the FormData object
variables.forEach((attachment: any) => { variables.forEach((attachment: any) => {
formData.append('attachment[]', attachment);
// Assuming 'attachment' is a file object
const modifiedFileName = attachment.name.replaceAll(" ", "");
const modifiedFile = new File([attachment], modifiedFileName, {
type: attachment.type,
lastModified: attachment.lastModified,
});
formData.append('file', modifiedFile); // Append file to the formData
}); });
const options = {
headers: {
'Content-Type': 'multipart/form-data',
},
};
return HttpClient.post<Attachment>(
API_ENDPOINTS.ATTACHMENTS,
formData,
options
);
// Get CSRF token from cookies or context if needed
// Set headers for the request (don't manually set 'Content-Type' as the browser handles it)
;
// Send the request using HttpClient and return the response data
try {
const response = await HttpClient.post<Attachment>(API_ENDPOINTS.ATTACHMENTS, formData);
// Check if the response is successful
// if (response.status === 200) {
return response; // This should return the uploaded file data or URL
// } else {
// throw new Error("Something went wrong during the file upload");
// }
} catch (error: any) {
console.error("Error uploading files:", error.response?.data || error.message);
throw error; // Rethrow the error so react-query can handle it
}
}, },
}; };

6
src/data/client/user.ts

@ -2,6 +2,7 @@ import {
AuthResponse, AuthResponse,
LoginInput, LoginInput,
RegisterInput, RegisterInput,
OTPInput,
User, User,
ChangePasswordInput, ChangePasswordInput,
ForgetPasswordInput, ForgetPasswordInput,
@ -26,7 +27,7 @@ export const userClient = {
me: () => { me: () => {
return HttpClient.get<User>(API_ENDPOINTS.ME); return HttpClient.get<User>(API_ENDPOINTS.ME);
}, },
login: (variables: LoginInput) => {
login: (variables: {password : string ,phone_number : string , user_type: 'merchant'}) => {
return HttpClient.post<AuthResponse>(API_ENDPOINTS.TOKEN, variables); return HttpClient.post<AuthResponse>(API_ENDPOINTS.TOKEN, variables);
}, },
logout: () => { logout: () => {
@ -35,6 +36,9 @@ export const userClient = {
register: (variables: RegisterInput) => { register: (variables: RegisterInput) => {
return HttpClient.post<AuthResponse>(API_ENDPOINTS.REGISTER, variables); return HttpClient.post<AuthResponse>(API_ENDPOINTS.REGISTER, variables);
}, },
OTP: (variables: OTPInput) => {
return HttpClient.post<AuthResponse>(API_ENDPOINTS.OTP, variables);
},
update: ({ id, input }: { id: string; input: UpdateUser }) => { update: ({ id, input }: { id: string; input: UpdateUser }) => {
return HttpClient.put<User>(`${API_ENDPOINTS.USERS}/${id}`, input); return HttpClient.put<User>(`${API_ENDPOINTS.USERS}/${id}`, input);
}, },

2
src/data/dashboard.ts

@ -5,7 +5,7 @@ import { dashboardClient } from '@/data/client/dashboard';
import { productClient } from '@/data/client/product'; import { productClient } from '@/data/client/product';
export function useAnalyticsQuery() { export function useAnalyticsQuery() {
return useQuery([API_ENDPOINTS.ANALYTICS], dashboardClient.analytics);
return useQuery([API_ENDPOINTS.GET_SHOPS], dashboardClient.analytics);
} }
export function usePopularProductsQuery(options: Partial<ProductQueryOptions>) { export function usePopularProductsQuery(options: Partial<ProductQueryOptions>) {

2
src/data/order.ts

@ -30,7 +30,7 @@ export const useOrdersQuery = (
}, },
); );
return { return {
orders: data?.data ?? [],
orders: data?.results ?? [],
paginatorInfo: mapPaginatorData(data), paginatorInfo: mapPaginatorData(data),
error, error,
loading: isLoading, loading: isLoading,

17
src/data/product.ts

@ -15,6 +15,7 @@ import { Routes } from '@/config/routes';
import { Config } from '@/config'; import { Config } from '@/config';
export const useCreateProductMutation = () => { export const useCreateProductMutation = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const router = useRouter(); const router = useRouter();
const { t } = useTranslation(); const { t } = useTranslation();
@ -30,7 +31,7 @@ export const useCreateProductMutation = () => {
}, },
// Always refetch after error or success: // Always refetch after error or success:
onSettled: () => { onSettled: () => {
queryClient.invalidateQueries(API_ENDPOINTS.PRODUCTS);
queryClient.invalidateQueries(API_ENDPOINTS.PRODUCTS_CREATE);
}, },
onError: (error: any) => { onError: (error: any) => {
const { data, status } = error?.response; const { data, status } = error?.response;
@ -78,10 +79,11 @@ export const useDeleteProductMutation = () => {
return useMutation(productClient.delete, { return useMutation(productClient.delete, {
onSuccess: () => { onSuccess: () => {
toast.success(t('common:successfully-deleted')); toast.success(t('common:successfully-deleted'));
Router.reload()
}, },
// Always refetch after error or success: // Always refetch after error or success:
onSettled: () => { onSettled: () => {
queryClient.invalidateQueries(API_ENDPOINTS.PRODUCTS);
queryClient.invalidateQueries(API_ENDPOINTS.PRODUCTS_DELETE);
}, },
onError: (error: any) => { onError: (error: any) => {
toast.error(t(`common:${error?.response?.data.message}`)); toast.error(t(`common:${error?.response?.data.message}`));
@ -89,10 +91,10 @@ export const useDeleteProductMutation = () => {
}); });
}; };
export const useProductQuery = ({ slug, language }: GetParams) => {
export const useProductQuery = ({ slug }: GetParams) => {
const { data, error, isLoading } = useQuery<Product, Error>( const { data, error, isLoading } = useQuery<Product, Error>(
[API_ENDPOINTS.PRODUCTS, { slug, language }],
() => productClient.get({ slug, language }),
[API_ENDPOINTS.PRODUCTS_SLUG, { slug }],
() => productClient.get({ slug }),
); );
return { return {
@ -107,7 +109,7 @@ export const useProductsQuery = (
options: any = {}, options: any = {},
) => { ) => {
const { data, error, isLoading } = useQuery<ProductPaginator, Error>( const { data, error, isLoading } = useQuery<ProductPaginator, Error>(
[API_ENDPOINTS.PRODUCTS, params],
[API_ENDPOINTS.PRODUCTS_GET, params],
({ queryKey, pageParam }) => ({ queryKey, pageParam }) =>
productClient.paginated(Object.assign({}, queryKey[1], pageParam)), productClient.paginated(Object.assign({}, queryKey[1], pageParam)),
{ {
@ -115,9 +117,10 @@ export const useProductsQuery = (
...options, ...options,
}, },
); );
console.log(data);
return { return {
products: data?.data ?? [],
products: data?.results ?? [],
paginatorInfo: mapPaginatorData(data), paginatorInfo: mapPaginatorData(data),
error, error,
loading: isLoading, loading: isLoading,

147
src/data/settings.ts

@ -9,6 +9,121 @@ import {
getMaintenanceDetails, getMaintenanceDetails,
setMaintenanceDetails, setMaintenanceDetails,
} from '@/utils/maintenance-utils'; } from '@/utils/maintenance-utils';
const settingsSampleData: Settings = {
id: "1",
language: "en",
options: {
siteTitle: "E-Commerce Platform",
siteSubtitle: "The Best Deals Online",
currency: "USD",
defaultAi: "ChatGPT",
useOtp: true,
useAi: true,
useGoogleMap: true,
isProductReview: true,
freeShipping: true,
freeShippingAmount: 50,
contactDetails: {
socials: [
{ icon: "facebook", url: "https://fastly.picsum.photos/id/14/2500/1667.jpg?hmac=ssQyTcZRRumHXVbQAVlXTx-MGBxm6NHWD3SryQ48G-o" },
{ icon: "twitter", url: "https://fastly.picsum.photos/id/14/2500/1667.jpg?hmac=ssQyTcZRRumHXVbQAVlXTx-MGBxm6NHWD3SryQ48G-o" },
],
contact: "+1-800-555-1234",
location: {
lat: 40.7128,
lng: -74.0060,
city: "New York",
state: "NY",
country: "USA",
zip: "10001",
formattedAddress: "123 Main St, New York, NY 10001, USA",
},
website: "https://yourbrand.com",
},
minimumOrderAmount: 10,
currencyToWalletRatio: 1.5,
signupPoints: 100,
maxShopDistance: 20,
maximumQuestionLimit: 5,
deliveryTime: [
{ title: "Standard Delivery", description: "Delivered in 3-5 business days" },
{ title: "Express Delivery", description: "Delivered in 1-2 business days" },
],
logo: {
thumbnail: "https://fastly.picsum.photos/id/14/2500/1667.jpg?hmac=ssQyTcZRRumHXVbQAVlXTx-MGBxm6NHWD3SryQ48G-o",
original: "https://fastly.picsum.photos/id/14/2500/1667.jpg?hmac=ssQyTcZRRumHXVbQAVlXTx-MGBxm6NHWD3SryQ48G-o",
},
collapseLogo: {
thumbnail: "https://fastly.picsum.photos/id/14/2500/1667.jpg?hmac=ssQyTcZRRumHXVbQAVlXTx-MGBxm6NHWD3SryQ48G-o",
original: "https://fastly.picsum.photos/id/14/2500/1667.jpg?hmac=ssQyTcZRRumHXVbQAVlXTx-MGBxm6NHWD3SryQ48G-o",
},
taxClass: "standard",
shippingClass: "express",
seo: {
metaTitle: "YourBrand - Best Deals Online",
metaDescription: "Find the best deals on our e-commerce platform.",
metaTags: "ecommerce, deals, shopping",
canonicalUrl: "https://locallhost.com",
ogTitle: "YourBrand - Best Deals Online",
ogDescription: "Shop the latest deals on our platform.",
ogImage: {
thumbnail: "https://fastly.picsum.photos/id/14/2500/1667.jpg?hmac=ssQyTcZRRumHXVbQAVlXTx-MGBxm6NHWD3SryQ48G-o",
original: "https://fastly.picsum.photos/id/14/2500/1667.jpg?hmac=ssQyTcZRRumHXVbQAVlXTx-MGBxm6NHWD3SryQ48G-o",
},
twitterHandle: "@YourBrand",
twitterCardType: "summary_large_image",
},
google: {
isEnable: true,
tagManagerId: "GTM-XXXXXX",
},
facebook: {
isEnable: true,
appId: "1234567890",
pageId: "yourbrandpage",
},
paymentGateway: {
stripe: true,
paypal: true,
},
defaultPaymentGateway: "stripe",
guestCheckout: true,
smsEvent: {
admin: { createOrder: true, deliverOrder: true, cancelOrder: true },
vendor: { createOrder: true, deliverOrder: false, cancelOrder: false },
customer: { createOrder: true, deliverOrder: true, cancelOrder: false },
},
emailEvent: {
admin: { createOrder: true, deliverOrder: true, cancelOrder: true },
vendor: { createOrder: true, deliverOrder: false, cancelOrder: false },
customer: { createOrder: true, deliverOrder: true, cancelOrder: true },
},
pushNotification: {
all: { storeNotice: true, order: true, message: true },
},
server_info: {
max_execution_time: "300s",
max_input_time: "60s",
memory_limit: "128M",
post_max_size: 50,
upload_max_filesize: 20,
},
enableEmailForDigitalProduct: true,
isPromoPopUp: true,
promoPopup: {
image: {
thumbnail: "https://fastly.picsum.photos/id/14/2500/1667.jpg?hmac=ssQyTcZRRumHXVbQAVlXTx-MGBxm6NHWD3SryQ48G-o",
original: "https://fastly.picsum.photos/id/14/2500/1667.jpg?hmac=ssQyTcZRRumHXVbQAVlXTx-MGBxm6NHWD3SryQ48G-o",
},
title: "Welcome to YourBrand!",
description: "Enjoy 10% off your first purchase!",
popUpDelay: 5,
popUpExpiredIn: 86400,
},
reviewSystem: "star",
isMultiCommissionRate: false,
},
};
export const useUpdateSettingsMutation = () => { export const useUpdateSettingsMutation = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -16,21 +131,23 @@ export const useUpdateSettingsMutation = () => {
const { updateSettings } = useSettings(); const { updateSettings } = useSettings();
return useMutation(settingsClient.update, { return useMutation(settingsClient.update, {
onError: (error) => {
console.log(error);
},
onSuccess: (data) => {
updateSettings(data?.options);
setMaintenanceDetails(
data?.options?.maintenance?.isUnderMaintenance,
data?.options?.maintenance,
);
toast.success(t('common:successfully-updated'));
},
// onError: (error) => {
// console.log(error);
// },
// onSuccess: (data) => {
// updateSettings(data?.options);
// setMaintenanceDetails(
// data?.options?.maintenance?.isUnderMaintenance,
// data?.options?.maintenance,
// );
// toast.success(t('common:successfully-updated'));
// console.log(data);
// },
// Always refetch after error or success: // Always refetch after error or success:
onSettled: () => {
queryClient.invalidateQueries(API_ENDPOINTS.SETTINGS);
},
// onSettled: () => {
// queryClient.invalidateQueries(API_ENDPOINTS.SETTINGS);
// },
}); });
}; };
@ -41,7 +158,7 @@ export const useSettingsQuery = ({ language }: { language: string }) => {
); );
return { return {
settings: data,
settings : settingsSampleData,
error, error,
loading: isLoading, loading: isLoading,
}; };

32
src/data/shop.ts

@ -43,11 +43,13 @@ export const useCreateShopMutation = () => {
const router = useRouter(); const router = useRouter();
return useMutation(shopClient.create, { return useMutation(shopClient.create, {
onSuccess: () => {
const { permissions } = getAuthCredentials();
if (hasAccess(adminOnly, permissions)) {
return router.push(Routes.adminMyShops);
}
onSuccess: (data) => {
// const { permissions } = getAuthCredentials();
// if (hasAccess(adminOnly, permissions)) {
// return router.push(Routes.adminMyShops);
// }
router.push(Routes.dashboard); router.push(Routes.dashboard);
}, },
// Always refetch after error or success: // Always refetch after error or success:
@ -57,13 +59,14 @@ export const useCreateShopMutation = () => {
}); });
}; };
export const useUpdateShopMutation = () => { export const useUpdateShopMutation = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const router = useRouter(); const router = useRouter();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation(shopClient.update, { return useMutation(shopClient.update, {
onSuccess: async (data) => { onSuccess: async (data) => {
await router.push(`/${data?.slug}/edit`, undefined, {
await router.push(`/shop/edit`, undefined, {
locale: Config.defaultLanguage, locale: Config.defaultLanguage,
}); });
toast.success(t('common:successfully-updated')); toast.success(t('common:successfully-updated'));
@ -89,17 +92,22 @@ export const useTransferShopOwnershipMutation = () => {
}); });
}; };
export const useShopQuery = ({ slug }: { slug: string }, options?: any) => {
export const useShopQuery = (options?: any) => {
return useQuery<Shop, Error>( return useQuery<Shop, Error>(
[API_ENDPOINTS.SHOPS, { slug }],
() => shopClient.get({ slug }),
options,
API_ENDPOINTS.GET_SHOPS,
() => shopClient.get().then((data) => {
return data;
}),
{
...options,
onError: (error) => console.error('Error fetching shop data:', error), // Debug log
}
); );
}; };
export const useShopsQuery = (options: Partial<ShopQueryOptions>) => {
export const useShopsQuery = () => {
const { data, error, isLoading } = useQuery<ShopPaginator, Error>( const { data, error, isLoading } = useQuery<ShopPaginator, Error>(
[API_ENDPOINTS.SHOPS, options],
[API_ENDPOINTS.SHOPS],
({ queryKey, pageParam }) => ({ queryKey, pageParam }) =>
shopClient.paginated(Object.assign({}, queryKey[1], pageParam)), shopClient.paginated(Object.assign({}, queryKey[1], pageParam)),
{ {

2
src/data/tag.ts

@ -93,7 +93,7 @@ export const useTagsQuery = (options: Partial<TagQueryOptions>) => {
); );
return { return {
tags: data?.data ?? [],
tags: data ?? [],
paginatorInfo: mapPaginatorData(data), paginatorInfo: mapPaginatorData(data),
error, error,
loading: isLoading, loading: isLoading,

18
src/data/upload.ts

@ -1,17 +1,25 @@
// useUploadMutation.ts
import { useMutation, useQueryClient } from 'react-query'; import { useMutation, useQueryClient } from 'react-query';
import { API_ENDPOINTS } from '@/data/client/api-endpoints';
import { uploadClient } from '@/data/client/upload'; import { uploadClient } from '@/data/client/upload';
import { API_ENDPOINTS } from '@/data/client/api-endpoints';
export const useUploadMutation = () => { export const useUploadMutation = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation( return useMutation(
(input: any) => {
return uploadClient.upload(input);
},
(files: any) => uploadClient.upload(files), // Pass the file(s) to the upload client
{ {
// Always refetch after error or success:
onSuccess: (data) => {
// Handle the success case, maybe update some cache or state
console.log('Upload successful', data);
},
onError: (error: any) => {
// Handle the error case, show error message, etc.
console.error('Upload failed:', error);
},
onSettled: () => { onSettled: () => {
// Optionally invalidate any related queries (for example, to refresh a list of uploaded files)
queryClient.invalidateQueries(API_ENDPOINTS.SETTINGS); queryClient.invalidateQueries(API_ENDPOINTS.SETTINGS);
}, },
} }

50
src/data/user.ts

@ -27,7 +27,8 @@ export const useMeQuery = () => {
return useQuery<User, Error>([API_ENDPOINTS.ME], userClient.me, { return useQuery<User, Error>([API_ENDPOINTS.ME], userClient.me, {
retry: false, retry: false,
onSuccess: () => {
onSuccess: (data) => {
if (router.pathname === Routes.verifyLicense) { if (router.pathname === Routes.verifyLicense) {
router.replace(Routes.dashboard); router.replace(Routes.dashboard);
} }
@ -43,6 +44,7 @@ export const useMeQuery = () => {
router.replace(Routes.verifyLicense); router.replace(Routes.verifyLicense);
return; return;
} }
console.log(err);
if (err.response?.status === 409) { if (err.response?.status === 409) {
setEmailVerified(false); setEmailVerified(false);
@ -50,7 +52,7 @@ export const useMeQuery = () => {
return; return;
} }
queryClient.clear(); queryClient.clear();
router.replace(Routes.login);
// router.replace(Routes.login);
} }
}, },
}); });
@ -64,15 +66,20 @@ export const useLogoutMutation = () => {
const router = useRouter(); const router = useRouter();
const { t } = useTranslation(); const { t } = useTranslation();
return useMutation(userClient.logout, {
onSuccess: () => {
Cookies.remove(AUTH_CRED);
router.replace(Routes.login);
toast.success(t('common:successfully-logout'), {
toastId: 'logoutSuccess',
});
},
});
const logout = () => {
// Remove the authentication cookie
Cookies.remove(AUTH_CRED);
// Redirect to the login page
router.replace(Routes.login);
// Display a success toast
toast.success(t('common:successfully-logout'), {
toastId: 'logoutSuccess',
});
};
return { logout };
}; };
export const useRegisterMutation = () => { export const useRegisterMutation = () => {
@ -80,14 +87,33 @@ export const useRegisterMutation = () => {
const { t } = useTranslation(); const { t } = useTranslation();
return useMutation(userClient.register, { return useMutation(userClient.register, {
onError : (err)=>{
console.log(err);
},
// Always refetch after error or success:
onSettled: () => {
queryClient.invalidateQueries(API_ENDPOINTS.REGISTER);
},
});
};
export const useOTPMutation = () => {
const queryClient = useQueryClient();
const { t } = useTranslation();
return useMutation(userClient.OTP, {
onSuccess: () => { onSuccess: () => {
toast.success(t('common:successfully-register'), { toast.success(t('common:successfully-register'), {
toastId: 'successRegister', toastId: 'successRegister',
}); });
}, },
onError : (err)=>{
console.log(err);
},
// Always refetch after error or success: // Always refetch after error or success:
onSettled: () => { onSettled: () => {
queryClient.invalidateQueries(API_ENDPOINTS.REGISTER);
queryClient.invalidateQueries(API_ENDPOINTS.OTP);
}, },
}); });
}; };

4
src/pages/_app.tsx

@ -27,8 +27,8 @@ const Noop: React.FC<{ children?: React.ReactNode }> = ({ children }) => (
const AppSettings: React.FC<{ children?: React.ReactNode }> = (props) => { const AppSettings: React.FC<{ children?: React.ReactNode }> = (props) => {
const { query, locale } = useRouter(); const { query, locale } = useRouter();
const { settings, loading, error } = useSettingsQuery({ language: locale! }); const { settings, loading, error } = useSettingsQuery({ language: locale! });
if (loading) return <PageLoader />;
if (error) return <ErrorMessage message={error.message} />;
// if (loading) return <PageLoader />;
// if (error) return <ErrorMessage message={error.message} />;
// TODO: fix it // TODO: fix it
// @ts-ignore // @ts-ignore
return <SettingsProvider initialValue={settings?.options} {...props} />; return <SettingsProvider initialValue={settings?.options} {...props} />;

6
src/pages/_document.tsx

@ -5,15 +5,15 @@ import Document, {
NextScript, NextScript,
DocumentContext, DocumentContext,
} from 'next/document'; } from 'next/document';
// import { Config } from '@/config';
import { Config } from '@/config';
export default class CustomDocument extends Document { export default class CustomDocument extends Document {
static async getInitialProps(ctx: DocumentContext) { static async getInitialProps(ctx: DocumentContext) {
return Document.getInitialProps(ctx); return Document.getInitialProps(ctx);
} }
render() { render() {
// const { locale } = this.props.__NEXT_DATA__;
// const dir = Config.getDirection(locale);
const { locale } = this.props.__NEXT_DATA__;
const dir = Config.getDirection(locale);
return ( return (
<Html> <Html>

28
src/pages/index.tsx

@ -16,7 +16,7 @@ const AdminDashboard = dynamic(() => import('@/components/dashboard/admin'));
const OwnerDashboard = dynamic(() => import('@/components/dashboard/owner')); const OwnerDashboard = dynamic(() => import('@/components/dashboard/owner'));
export default function Dashboard({ export default function Dashboard({
userPermissions,
userPermissions = [SUPER_ADMIN],
}: { }: {
userPermissions: string[]; userPermissions: string[];
}) { }) {
@ -36,17 +36,17 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
? `/${locale}${Routes.login}` ? `/${locale}${Routes.login}`
: Routes.login; : Routes.login;
const { token, permissions } = getAuthCredentials(ctx); const { token, permissions } = getAuthCredentials(ctx);
if (
!isAuthenticated({ token, permissions }) ||
!hasAccess(allowedRoles, permissions)
) {
return {
redirect: {
destination: generateRedirectUrl,
permanent: false,
},
};
}
// if (
// !isAuthenticated({ token, permissions }) ||
// !hasAccess(allowedRoles, permissions)
// ) {
// return {
// redirect: {
// destination: generateRedirectUrl,
// permanent: false,
// },
// };
// }
if (locale) { if (locale) {
return { return {
props: { props: {
@ -56,13 +56,13 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
'table', 'table',
'widgets', 'widgets',
])), ])),
userPermissions: permissions,
userPermissions: ['super_admin'],
}, },
}; };
} }
return { return {
props: { props: {
userPermissions: permissions,
userPermissions: ['super_admin'],
}, },
}; };
}; };

7
src/pages/logout.tsx

@ -6,17 +6,18 @@ import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
function SignOut() { function SignOut() {
const { t } = useTranslation(); const { t } = useTranslation();
const { mutate: logout } = useLogoutMutation();
const { logout } = useLogoutMutation(); // Call the client-side logout function
useEffect(() => { useEffect(() => {
logout();
}, []);
logout(); // Perform the logout when the component is mounted
}, [logout]);
return <Loader text={t('common:signing-out-text')} />; return <Loader text={t('common:signing-out-text')} />;
} }
export default SignOut; export default SignOut;
export const getStaticProps = async ({ locale }: any) => ({ export const getStaticProps = async ({ locale }: any) => ({
props: { props: {
...(await serverSideTranslations(locale, ['common'])), ...(await serverSideTranslations(locale, ['common'])),

22
src/pages/orders/index.tsx

@ -40,14 +40,7 @@ export default function Orders() {
setPage(current); setPage(current);
} }
const { data: shopData, isLoading: fetchingShop } = useShopQuery(
{
slug: shop as string,
},
{
enabled: !!shop,
}
);
const { data: shopData, isLoading: fetchingShop } = useShopQuery();
const shopId = shopData?.id!; const shopId = shopData?.id!;
const { orders, loading, paginatorInfo, error } = useOrdersQuery({ const { orders, loading, paginatorInfo, error } = useOrdersQuery({
language: locale, language: locale,
@ -63,11 +56,12 @@ export default function Orders() {
}, },
{ enabled: false } { enabled: false }
); );
console.log(orders , error);
if (loading) return <Loader text={t('common:text-loading')} />;
// if (loading) return <Loader text={t('common:text-loading')} />;
if (loading) return <Loader text={t('common:text-loading')} />;
if (error) return <ErrorMessage message={error.message} />;
// if (loading) return <Loader text={t('common:text-loading')} />;
// if (error) return <ErrorMessage message={error.message} />;
async function handleExportOrder() { async function handleExportOrder() {
const { data } = await refetch(); const { data } = await refetch();
@ -148,9 +142,9 @@ export default function Orders() {
); );
} }
Orders.authenticate = {
permissions: adminOnly,
};
// Orders.authenticate = {
// permissions: adminOnly,
// };
Orders.Layout = Layout; Orders.Layout = Layout;
export const getStaticProps = async ({ locale }: any) => ({ export const getStaticProps = async ({ locale }: any) => ({

3
src/pages/products/[productSlug]/[action].tsx

@ -23,6 +23,9 @@ export default function UpdateProductPage() {
query.action!.toString() === 'edit' ? locale! : Config.defaultLanguage, query.action!.toString() === 'edit' ? locale! : Config.defaultLanguage,
}); });
console.log(product);
if (loading) return <Loader text={t('common:text-loading')} />; if (loading) return <Loader text={t('common:text-loading')} />;
if (error) return <ErrorMessage message={error?.message as string} />; if (error) return <ErrorMessage message={error?.message as string} />;
return ( return (

45
src/pages/products/create.tsx

@ -0,0 +1,45 @@
import Layout from '@/components/layouts/admin';
import CreateOrUpdateProductForm from '@/components/product/product-form';
import ErrorMessage from '@/components/ui/error-message';
import Loader from '@/components/ui/loader/loader';
import { useProductQuery } from '@/data/product';
import { useRouter } from 'next/router';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { useTranslation } from 'next-i18next';
import { Config } from '@/config';
import Link from '@/components/ui/link';
export default function UpdateProductPage() {
const { query, locale } = useRouter();
const { t } = useTranslation();
const {
product,
isLoading: loading,
error,
} = useProductQuery({
slug: query.productSlug as string,
language: locale!,
});
// if (loading) return <Loader text={t('common:text-loading')} />;
// if (error) return <ErrorMessage message={error?.message as string} />;
return (
<>
<div className="flex items-center gap-5 border-b border-dashed border-border-base py-5 sm:py-8">
<h4 className="text-lg font-semibold text-heading">
{t('form:form-title-create-product')}
</h4>
</div>
<CreateOrUpdateProductForm initialValues={product} />
</>
);
}
UpdateProductPage.Layout = Layout;
export const getServerSideProps = async ({ locale }: any) => ({
props: {
...(await serverSideTranslations(locale, ['common', 'form'])),
},
});

9
src/pages/products/index.tsx

@ -113,8 +113,7 @@ export default function ProductsPage() {
setPage(1); setPage(1);
}} }}
enableCategory enableCategory
enableType
enableProductType
enableTag
/> />
</div> </div>
</div> </div>
@ -129,9 +128,9 @@ export default function ProductsPage() {
</> </>
); );
} }
ProductsPage.authenticate = {
permissions: adminOnly,
};
// ProductsPage.authenticate = {
// permissions: adminOnly,
// };
ProductsPage.Layout = Layout; ProductsPage.Layout = Layout;
export const getStaticProps = async ({ locale }: any) => ({ export const getStaticProps = async ({ locale }: any) => ({

6
src/pages/register.tsx

@ -16,9 +16,9 @@ export const getStaticProps: GetStaticProps = async ({ locale }) => ({
export default function RegisterPage() { export default function RegisterPage() {
const router = useRouter(); const router = useRouter();
const { token, permissions } = getAuthCredentials(); const { token, permissions } = getAuthCredentials();
if (isAuthenticated({ token, permissions })) {
router.replace(Routes.dashboard);
}
// if (isAuthenticated({ token, permissions })) {
// router.replace(Routes.dashboard);
// }
const { t } = useTranslation('common'); const { t } = useTranslation('common');
return ( return (
<AuthPageLayout> <AuthPageLayout>

0
src/pages/[shop]/attributes/[attributeId]/[action].tsx → src/pages/shop/attributes/[attributeId]/[action].tsx

0
src/pages/[shop]/attributes/create.tsx → src/pages/shop/attributes/create.tsx

0
src/pages/[shop]/attributes/index.tsx → src/pages/shop/attributes/index.tsx

0
src/pages/[shop]/authors/create.tsx → src/pages/shop/authors/create.tsx

0
src/pages/[shop]/authors/index.tsx → src/pages/shop/authors/index.tsx

0
src/pages/[shop]/coupons/[couponSlug]/[action].tsx → src/pages/shop/coupons/[couponSlug]/[action].tsx

0
src/pages/[shop]/coupons/create.tsx → src/pages/shop/coupons/create.tsx

0
src/pages/[shop]/coupons/index.tsx → src/pages/shop/coupons/index.tsx

6
src/pages/shops/create.tsx → src/pages/shop/create.tsx

@ -18,9 +18,9 @@ export default function CreateShopPage() {
</> </>
); );
} }
CreateShopPage.authenticate = {
permissions: adminAndOwnerOnly,
};
// CreateShopPage.authenticate = {
// permissions: adminAndOwnerOnly,
// };
CreateShopPage.Layout = OwnerLayout; CreateShopPage.Layout = OwnerLayout;
export const getStaticProps: GetStaticProps = async ({ locale }) => ({ export const getStaticProps: GetStaticProps = async ({ locale }) => ({

13
src/pages/[shop]/edit.tsx → src/pages/shop/edit.tsx

@ -26,9 +26,7 @@ export default function UpdateShopPage() {
data, data,
isLoading: loading, isLoading: loading,
error, error,
} = useShopQuery({
slug: shop as string,
});
} = useShopQuery();
if (loading) return <Loader text={t('common:text-loading')} />; if (loading) return <Loader text={t('common:text-loading')} />;
if (error) return <ErrorMessage message={error.message} />; if (error) return <ErrorMessage message={error.message} />;
if ( if (
@ -38,6 +36,7 @@ export default function UpdateShopPage() {
) { ) {
router.replace(Routes.dashboard); router.replace(Routes.dashboard);
} }
return ( return (
<> <>
<div className="flex py-5 border-b border-dashed border-border-base sm:py-8"> <div className="flex py-5 border-b border-dashed border-border-base sm:py-8">
@ -45,13 +44,13 @@ export default function UpdateShopPage() {
{t('form:form-title-edit-shop')} {t('form:form-title-edit-shop')}
</h1> </h1>
</div> </div>
<ShopForm initialValues={data} />
<ShopForm initialValues={data?.merchant_info} />
</> </>
); );
} }
UpdateShopPage.authenticate = {
permissions: adminAndOwnerOnly,
};
// UpdateShopPage.authenticate = {
// permissions: adminAndOwnerOnly,
// };
UpdateShopPage.Layout = ShopLayout; UpdateShopPage.Layout = ShopLayout;
export const getServerSideProps = async ({ locale }: any) => ({ export const getServerSideProps = async ({ locale }: any) => ({

0
src/pages/[shop]/faqs/[id]/[action].tsx → src/pages/shop/faqs/[id]/[action].tsx

0
src/pages/[shop]/faqs/create.tsx → src/pages/shop/faqs/create.tsx

0
src/pages/[shop]/faqs/index.tsx → src/pages/shop/faqs/index.tsx

0
src/pages/[shop]/flash-sale/[slug]/index.tsx → src/pages/shop/flash-sale/[slug]/index.tsx

0
src/pages/[shop]/flash-sale/index.tsx → src/pages/shop/flash-sale/index.tsx

0
src/pages/[shop]/flash-sale/my-products.tsx → src/pages/shop/flash-sale/my-products.tsx

0
src/pages/[shop]/flash-sale/vendor-request/[id]/[action].tsx → src/pages/shop/flash-sale/vendor-request/[id]/[action].tsx

0
src/pages/[shop]/flash-sale/vendor-request/[id]/index.tsx → src/pages/shop/flash-sale/vendor-request/[id]/index.tsx

0
src/pages/[shop]/flash-sale/vendor-request/create.tsx → src/pages/shop/flash-sale/vendor-request/create.tsx

0
src/pages/[shop]/flash-sale/vendor-request/index.tsx → src/pages/shop/flash-sale/vendor-request/index.tsx

19
src/pages/[shop]/index.tsx → src/pages/shop/index.tsx

@ -57,9 +57,8 @@ export default function ShopPage() {
data, data,
isLoading: loading, isLoading: loading,
error, error,
} = useShopQuery({
slug: shop!.toString(),
});
} = useShopQuery();
console.log(data);
const { price: totalEarnings } = usePrice( const { price: totalEarnings } = usePrice(
data && { data && {
@ -204,7 +203,7 @@ export default function ShopPage() {
<div className="self-end pt-4 xl:pt-0 space-x-4"> <div className="self-end pt-4 xl:pt-0 space-x-4">
<Link <Link
className="inline-flex items-center gap-1 rounded-full bg-accent px-[0.625rem] py-[0.5625rem] text-xs font-medium text-white hover:bg-accent-hover" className="inline-flex items-center gap-1 rounded-full bg-accent px-[0.625rem] py-[0.5625rem] text-xs font-medium text-white hover:bg-accent-hover"
href={`/${shop}/edit`}
href={`/shop/edit`}
> >
<EditFillIcon /> <EditFillIcon />
{t('common:text-edit-shop')} {t('common:text-edit-shop')}
@ -215,7 +214,7 @@ export default function ShopPage() {
) ? ( ) ? (
<Link <Link
className="inline-flex items-center gap-1 rounded-full bg-accent px-[0.625rem] py-[0.5625rem] text-xs font-medium text-white hover:bg-accent-hover" className="inline-flex items-center gap-1 rounded-full bg-accent px-[0.625rem] py-[0.5625rem] text-xs font-medium text-white hover:bg-accent-hover"
href={`/${shop}/transfer-ownership`}
href={`/shop/transfer-ownership`}
> >
<IosArrowDown /> <IosArrowDown />
{t('common:text-transfer-shop-ownership')} {t('common:text-transfer-shop-ownership')}
@ -300,15 +299,13 @@ export default function ShopPage() {
); );
} }
ShopPage.Layout = ShopLayout; ShopPage.Layout = ShopLayout;
ShopPage.authenticate = {
permissions: adminOwnerAndStaffOnly,
};
// ShopPage.authenticate = {
// permissions: adminOwnerAndStaffOnly,
// };
export const getStaticProps = async ({ locale }: any) => ({ export const getStaticProps = async ({ locale }: any) => ({
props: { props: {
...(await serverSideTranslations(locale, ['form', 'common', 'table'])), ...(await serverSideTranslations(locale, ['form', 'common', 'table'])),
}, },
}); });
export const getStaticPaths: GetStaticPaths = async () => {
return { paths: [], fallback: 'blocking' };
};

0
src/pages/[shop]/manufacturers/create.tsx → src/pages/shop/manufacturers/create.tsx

0
src/pages/[shop]/manufacturers/index.tsx → src/pages/shop/manufacturers/index.tsx

0
src/pages/[shop]/orders/[orderId]/index.tsx → src/pages/shop/orders/[orderId]/index.tsx

0
src/pages/[shop]/orders/index.tsx → src/pages/shop/orders/index.tsx

0
src/pages/[shop]/orders/transaction.tsx → src/pages/shop/orders/transaction.tsx

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

Loading…
Cancel
Save