diff --git a/.env.template b/.env.template deleted file mode 100644 index aa74b7d..0000000 --- a/.env.template +++ /dev/null @@ -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" diff --git a/README.md b/README.md deleted file mode 100644 index e69de29..0000000 diff --git a/next.config.js b/next.config.js index b3556c2..089f0b0 100644 --- a/next.config.js +++ b/next.config.js @@ -27,6 +27,8 @@ const nextConfig = { 'lh3.googleusercontent.com', 'chawkbazarlaravel.s3.ap-southeast-1.amazonaws.com', '127.0.0.1:8000', + "fastly.picsum.photos", + "mesbahi.nwhco.ir" ], }, ...(process.env.APPLICATION_MODE === 'production' && { diff --git a/package.json b/package.json index b67dbc6..f63cd24 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "@marvel/admin-rest", + "name": "@chawkbazar/admin-rest", "version": "6.6.0", "private": true, "scripts": { diff --git a/public/locales/en/common.json b/public/locales/en/common.json index e005333..57813e0 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -62,7 +62,7 @@ "sidebar-nav-item-settings": "Settings", "sidebar-nav-item-store-notice": "Store Notice", "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-dashboard": "Shops Dashboard", "sidebar-nav-item-tags": "Tags", @@ -229,6 +229,7 @@ "text-shop-approve-description": "Are you sure?", "text-approve-shop": "Approve Shop", "filter-by-group": "Filter By Brand", + "filter-by-tag": "Filter By Tag", "filter-by-group-placeholder": "Filter by Brand", "text-disapprove-shop": "Disapprove Shop ?", "filter-by-category": "Filter By Category", diff --git a/public/locales/en/form.json b/public/locales/en/form.json index e9ee901..8e9736a 100644 --- a/public/locales/en/form.json +++ b/public/locales/en/form.json @@ -63,8 +63,9 @@ "input-label-disable-variant": "Disable This Variant", "input-label-logo": "Logo", "input-label-collapse-logo": "Collapse Logo", - "input-label-email": "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-bio": "Bio", "input-label-contact": "Contact Number", @@ -79,11 +80,13 @@ "input-label-orders": "Orders", "input-label-order-id": "Order ID", "input-label-sku": "SKU", + "input-label-stock": "Stock", "input-note-multilang-sku": "Make sure SKU is identical for all languages.", "input-label-name": "Name", "input-label-description": "Description", "input-label-price": "Price", "input-label-sale-price": "Sale Price", + "input-label-discount": "Discount", "input-label-quantity": "Quantity", "input-label-unit": "Unit", "input-label-width": "Width", @@ -91,7 +94,7 @@ "input-label-length": "Length", "input-label-status": "Status", "input-label-under-review": "Under Review", - "input-label-published": "Published", + "input-label-published": "Publish", "input-label-unpublish": "Unpublish", "input-label-draft": "Draft", "input-label-approved": "Approved", @@ -226,6 +229,7 @@ "edit-attribute": "Edit Attribute", "error-message-required": "Message is required", "error-name-required": "Name is required", + "error-slug-required": "Slug is required", "error-value-required": "Value is required", "error-meta-required": "Meta is required", "error-author-name-required": "Author name is required", @@ -267,6 +271,7 @@ "input-label-account-holder-email": "Account Holder Email", "input-label-bank-name": "Bank Name", "input-label-account-number": "Account Number", + "input-label-iban-number": "International Bank Account Number", "shop-address": "Shop Address", "shop-address-helper-text": "Add your physical shop address from here", "footer-address": "Address", @@ -336,12 +341,20 @@ "error-color-required": "Color is required", "error-product-type-required": "Product type 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-account-must-number": "Account must be a number", "error-price-must-positive": "Price must be positive", "error-price-required": "Price is required", "error-sale-price-less-number": "Sale Price should be less than", "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-quantity-must-number": "Quantity must be a number", "error-quantity-must-positive": "Quantity must be positive", diff --git a/src/components/auth/forget-password/enter-email-view.tsx b/src/components/auth/forget-password/enter-email-view.tsx index 169e7ec..523cce9 100644 --- a/src/components/auth/forget-password/enter-email-view.tsx +++ b/src/components/auth/forget-password/enter-email-view.tsx @@ -6,38 +6,49 @@ import * as yup from 'yup'; import { useTranslation } from 'next-i18next'; interface Props { - onSubmit: (values: { email: string }) => void; + onSubmit: (values: { phone_number: string , range_phone : string }) => void; loading: boolean; } 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 { register, handleSubmit, formState: { errors }, - } = useForm<{ email: string }>({ resolver: yupResolver(schema) }); + } = useForm<{ phone_number: string , range_phone : string }>({ resolver: yupResolver(schema) }); return (
- -
diff --git a/src/components/auth/forget-password/enter-new-password-view.tsx b/src/components/auth/forget-password/enter-new-password-view.tsx index 1d2c0d6..7e8dd25 100644 --- a/src/components/auth/forget-password/enter-new-password-view.tsx +++ b/src/components/auth/forget-password/enter-new-password-view.tsx @@ -6,12 +6,16 @@ import * as yup from 'yup'; import { useTranslation } from 'next-i18next'; interface Props { - onSubmit: (values: { password: string }) => void; + onSubmit: (values: { password: string , password_confirmation : string }) => void; loading: boolean; } const schema = yup.object().shape({ 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) => { @@ -20,9 +24,10 @@ const EnterNewPasswordView = ({ onSubmit, loading }: Props) => { register, handleSubmit, formState: { errors }, - } = useForm<{ password: string }>({ resolver: yupResolver(schema) }); + } = useForm<{ password: string , password_confirmation : string }>({ resolver: yupResolver(schema) }); return ( +
{ variant="outline" className="mb-5" /> - + diff --git a/src/components/auth/forget-password/forget-password.tsx b/src/components/auth/forget-password/forget-password.tsx index 7b0fb3f..b30f483 100644 --- a/src/components/auth/forget-password/forget-password.tsx +++ b/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 Button from '@/components/ui/button'; import { useForgetPasswordMutation, useVerifyForgetPasswordTokenMutation, @@ -8,9 +9,11 @@ import { import dynamic from 'next/dynamic'; import Router from 'next/router'; 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 EnterTokenView = dynamic(() => import('./enter-token-view')); const EnterNewPasswordView = dynamic(() => import('./enter-new-password-view')); const ForgotPassword = () => { @@ -20,64 +23,196 @@ const ForgotPassword = () => { useVerifyForgetPasswordTokenMutation(); const { mutate: resetPassword, isLoading: resetting } = useResetPasswordMutation(); - const [errorMsg, setErrorMsg] = useState(''); + + 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(null); const [verifiedEmail, setVerifiedEmail] = useState(''); const [verifiedToken, setVerifiedToken] = useState(''); + const [errorMsg, setErrorMsg] = useState(''); + 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( { - email, + phone_number: '+' + range_phone + phone_number, + range_phone, + user_type: 'user', }, { 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( { - email: verifiedEmail, - token, + method: 'reset', + phone_number: verifiedEmail, + code: otp.join(''), + user_type: 'merchant', }, { 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( { - email: verifiedEmail, + phone_number: verifiedEmail, + password_confirmation, token: verifiedToken, password, }, { onSuccess: (data) => { if (data?.success) { - Router.push('/'); + Router.push(Routes.logout); } else { 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, + index: number, + ) => { + if (e.key === 'Backspace' && !otp[index] && index > 0) { + otpRefs.current[index - 1]?.focus(); + } + }; + + const renderOTP = () => ( +
+
+

+ Verification Code +

+

+ Enter the 5-digit code we sent to your phone. +

+
+
+ {otp.map((value, index) => ( + { + 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" + /> + ))} +
+

+ Didn’t receive the code?{' '} + + {time > 0 && ( + ({time}s) + )} +

+ +
+
+
+ ); + return ( <> {errorMsg && ( @@ -89,13 +224,11 @@ const ForgotPassword = () => { onClose={() => setErrorMsg('')} /> )} - {!verifiedEmail && ( + {stage === 'email' && ( )} - {verifiedEmail && !verifiedToken && ( - - )} - {verifiedEmail && verifiedToken && ( + {stage === 'otp' && renderOTP()} + {stage === 'password' && ( { const [errorMessage, setErrorMessage] = useState(null); const { mutate: login, isLoading, error } = useLogin(); - function onSubmit({ email, password }: LoginInput) { + function onSubmit({ phone_number, range_phone, password }: LoginInput) { + login( { - email, + phone_number: range_phone + phone_number, password, + user_type: 'merchant', }, { 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 = () => { validationSchema={loginFormSchema} onSubmit={onSubmit}> {({ register, formState: { errors } }) => ( <> - + { className="mb-4" forgotPageLink={Routes.forgotPassword} /> - diff --git a/src/components/auth/registration-form.tsx b/src/components/auth/registration-form.tsx index 3f2e244..83a2fe2 100644 --- a/src/components/auth/registration-form.tsx +++ b/src/components/auth/registration-form.tsx @@ -16,26 +16,39 @@ import { setAuthCredentials, } from '@/utils/auth-utils'; 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 = { - name: string; - email: string; + fullname: string; + phone_number: string; + range_phone: string; password: string; - permission: Permission; + password_confirmation: string; + user_type: 'merchant'; }; 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() - .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']), }); + const RegistrationForm = () => { const [errorMessage, setErrorMessage] = useState(null); 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(null); const { register, @@ -44,72 +57,161 @@ const RegistrationForm = () => { setError, } = useForm({ resolver: yupResolver(registrationFormSchema), - defaultValues: { - permission: Permission.StoreOwner, - }, }); const router = useRouter(); 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 ( - <> - + 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, + 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 ( + <> - + { variant="outline" className="mb-4" /> - @@ -130,23 +244,73 @@ const RegistrationForm = () => { onClose={() => setErrorMessage(null)} /> ) : null} - -
-
- - {t('common:text-or')} - -
-
- {t('form:text-already-account')}{' '} - - {t('form:button-label-login')} - + + ); + }; + + const renderOTP = () => ( +
+
+

+ Verification Code +

+

+ Enter the 5-digit code we sent to your phone. +

+
+
+ {otp.map((value, index) => ( + { + 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" + /> + ))} +
+

+ Didn’t receive the code?{' '} + + {time > 0 && ( + ({time}s) + )} +

+ +
- +
+ ); + + + return ( + //@ts-ignore +
+ {stage === 'signUp' && renderSignIn()} + {stage === 'OTP' && renderOTP()} +
); }; diff --git a/src/components/common/get-file-url.tsx b/src/components/common/get-file-url.tsx new file mode 100644 index 0000000..ff3a2fb --- /dev/null +++ b/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; diff --git a/src/components/common/uploader.tsx b/src/components/common/uploader.tsx index 0ac0d9b..394933e 100644 --- a/src/components/common/uploader.tsx +++ b/src/components/common/uploader.tsx @@ -44,33 +44,39 @@ export default function Uploader({ : { ...ACCEPTED_FILE_TYPES }), multiple, onDrop: async (acceptedFiles) => { + if (acceptedFiles.length) { upload( acceptedFiles, // it will be an array of uploaded attachments { onSuccess: (data: any) => { // 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; + 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) { onChange(mergedData); } + }, }, ); @@ -92,7 +98,7 @@ export default function Uploader({ }); const handleDelete = (image: string) => { - const images = files.filter((file) => file.thumbnail !== image); + const images = files.filter((file) => file.display_order !== image); setFiles(images); if (onChange) { onChange(images); @@ -112,15 +118,15 @@ export default function Uploader({ 'raw', ]; // let filename, fileType, isImage; - if (file && file.id) { + if (file) { // const processedFile = processFileWithName(file); - const splitArray = file?.file_name - ? file?.file_name.split('.') + const splitArray = file?.image + ? file?.image.split('.') : file?.thumbnail?.split('.'); 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 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 ******* // const splitArray = file?.original?.split('/'); @@ -150,7 +156,7 @@ export default function Uploader({ //
{filename} handleDelete(file.thumbnail)} + onClick={() => handleDelete(file.display_order)} > diff --git a/src/components/dashboard/admin.tsx b/src/components/dashboard/admin.tsx index e8d74b1..088dfbd 100644 --- a/src/components/dashboard/admin.tsx +++ b/src/components/dashboard/admin.tsx @@ -64,12 +64,16 @@ export default function Dashboard() { const [orderDataRange, setOrderDataRange] = useState( data?.todayTotalOrderByStatus, ); - + console.log(data); + const { price: total_revenue } = usePrice( data && { - amount: data?.totalRevenue!, + amount: data?.merchant_info?.summary.total_revenue!, }, ); + + console.log(total_revenue); + const { price: todays_revenue } = usePrice( data && { amount: data?.todaysRevenue!, @@ -165,26 +169,26 @@ export default function Dashboard() { } }); - if ( - loading || - orderLoading || - popularProductLoading || - withdrawLoading || - topRatedProductsLoading - ) { - return ; - } - if (orderError || popularProductError || topRatedProductsError) { - return ( - - ); - } + // if ( + // loading || + // orderLoading || + // popularProductLoading || + // withdrawLoading || + // topRatedProductsLoading + // ) { + // return ; + // } + // if (orderError || popularProductError || topRatedProductsError) { + // return ( + // + // ); + // } return (
@@ -201,14 +205,14 @@ export default function Dashboard() { subtitleTransKey="sticker-card-subtitle-rev" icon={} color="#1EAE98" - price={total_revenue} + price={data?.merchant_info?.summary.total_revenue} /> } color="#865DFF" - price={data?.totalOrders} + price={data?.merchant_info?.summary.total_revenue} /> = ({ tempContent.push(items); } - return ( - -
- {tempContent && tempContent.length > 0 - ? tempContent.map((content) => { - return ( -
- -
- ); - }) - : ''} -
-
- ); + // return ( + // + //
+ // {tempContent && tempContent.length > 0 + // ? tempContent.map((content) => { + // return ( + //
+ // + //
+ // ); + // }) + // : ''} + //
+ //
+ // ); }; export default WidgetOrderByStatus; diff --git a/src/components/filters/category-type-filter.tsx b/src/components/filters/category-type-filter.tsx index 25a9c45..440ae56 100644 --- a/src/components/filters/category-type-filter.tsx +++ b/src/components/filters/category-type-filter.tsx @@ -3,6 +3,7 @@ import Select from '@/components/ui/select/select'; import { useAuthorsQuery } from '@/data/author'; import { useCategoriesQuery } from '@/data/category'; import { useManufacturersQuery } from '@/data/manufacturer'; +import { useTagsQuery } from '@/data/tag'; import { useTypesQuery } from '@/data/type'; import { ProductType } from '@/types'; import cn from 'classnames'; @@ -24,7 +25,7 @@ type Props = { ) => void; className?: string; type?: string; - enableType?: boolean; + enableTag?: boolean; enableCategory?: boolean; enableAuthor?: boolean; enableProductType?: boolean; @@ -38,7 +39,7 @@ export default function CategoryTypeFilter({ onProductTypeFilter, className, type, - enableType, + enableTag, enableCategory, enableAuthor, enableProductType, @@ -48,7 +49,7 @@ export default function CategoryTypeFilter({ const { locale } = useRouter(); const { t } = useTranslation(); - const { types, loading } = useTypesQuery({ language: locale }); + const { tags, loading } = useTagsQuery({ language: locale }); const { categories, loading: categoryLoading } = useCategoriesQuery({ limit: 999, language: locale, @@ -77,11 +78,11 @@ export default function CategoryTypeFilter({ className, )} > - {enableType ? ( + {enableTag ? (
- + option.name} getOptionValue={(option: any) => option.slug} placeholder={t('common:filter-by-category-placeholder')} diff --git a/src/components/icons/social/facebook.tsx b/src/components/icons/social/facebook.tsx index 0bca505..bf0af41 100644 --- a/src/components/icons/social/facebook.tsx +++ b/src/components/icons/social/facebook.tsx @@ -1,9 +1,5 @@ -export const FacebookIcon: React.FC> = (props) => ( - - - -); + +export const TelegramIcon: React.FC> = (props) => ( + telegram_line + +); \ No newline at end of file diff --git a/src/components/icons/social/index.tsx b/src/components/icons/social/index.tsx index 5799805..23603de 100644 --- a/src/components/icons/social/index.tsx +++ b/src/components/icons/social/index.tsx @@ -1,4 +1,3 @@ -export { FacebookIcon } from './facebook'; +export { TelegramIcon } from './facebook'; export { InstagramIcon } from './instagram'; -export { TwitterIcon } from './twitter'; -export { YouTubeIcon } from './youtube'; +export { WhatsappIcon } from './twitter'; diff --git a/src/components/icons/social/twitter.tsx b/src/components/icons/social/twitter.tsx index b19ab53..ae5327b 100644 --- a/src/components/icons/social/twitter.tsx +++ b/src/components/icons/social/twitter.tsx @@ -1,9 +1,3 @@ -export const TwitterIcon: React.FC> = (props) => ( - - - +export const WhatsappIcon: React.FC> = (props) => ( + ); diff --git a/src/components/layouts/app.tsx b/src/components/layouts/app.tsx index 1cdcfe7..894ddce 100644 --- a/src/components/layouts/app.tsx +++ b/src/components/layouts/app.tsx @@ -5,7 +5,7 @@ const AdminLayout = dynamic(() => import('@/components/layouts/admin')); const OwnerLayout = dynamic(() => import('@/components/layouts/owner')); export default function AppLayout({ - userPermissions, + userPermissions = [SUPER_ADMIN], ...props }: { userPermissions: string[]; diff --git a/src/components/layouts/navigation/authorized-menu.tsx b/src/components/layouts/navigation/authorized-menu.tsx index eb46275..1f7d0a2 100644 --- a/src/components/layouts/navigation/authorized-menu.tsx +++ b/src/components/layouts/navigation/authorized-menu.tsx @@ -16,9 +16,10 @@ export default function AuthorizedMenu() { const { t } = useTranslation('common'); const { pathname, query } = useRouter(); const slug = (pathname === '/[shop]' && query?.shop) || ''; - const { role, permissions } = getAuthCredentials(); + // const { role, permissions } = getAuthCredentials(); - // Again, we're using framer-motion for the transition effect + // Again, we're using framer-motion for the transition effe + return (
- {data?.name} + {data?.fullname} - + {/* {role ? role.split('_').join(' ') : data?.email} - + */}
@@ -62,7 +63,7 @@ export default function AuthorizedMenu() {
- {data?.name} + {data?.fullname} - {data?.email} + {data?.phone_number}
@@ -82,7 +83,8 @@ export default function AuthorizedMenu() {
{siteSettings?.authorizedLinks?.map( ({ href, labelTransKey, icon, permission }, index) => { - const hasPermission = permission?.includes(role!); + const hasPermission = true + // permission?.includes(role!); return ( {hasPermission && ( diff --git a/src/components/layouts/navigation/sidebar-item.tsx b/src/components/layouts/navigation/sidebar-item.tsx index 8dda093..b8d8846 100644 --- a/src/components/layouts/navigation/sidebar-item.tsx +++ b/src/components/layouts/navigation/sidebar-item.tsx @@ -40,8 +40,8 @@ function SidebarShortItem({ content={() => ( <> {childMenu?.map((item: any, index: number) => { - if (shop && !hasAccess(item?.permissions, currentUserPermissions)) - return null; + // if (shop && !hasAccess(item?.permissions, currentUserPermissions)) + // return null; return (
{ ]); if (loading || shopLoading) { - return ; + // return ; } const { options } = settings!; @@ -160,7 +161,6 @@ const Navbar = () => { openModal('SEARCH_VIEW'); setSearchModal(true); } - return (
{width >= RESPONSIVE_WIDTH && isMaintenanceMode ? ( @@ -252,8 +252,8 @@ const Navbar = () => {
- {hasAccess(adminAndOwnerOnly, permissions) && ( <> + {(!useHasShop()) ? (
{ {t('common:text-create-shop')}
- + ) : (
+ )} + + {options?.pushNotification?.all?.order || options?.pushNotification?.all?.message || options?.pushNotification?.all?.storeNotice ? ( @@ -294,7 +297,6 @@ const Navbar = () => {
) : null} - )}
{enableMultiLang ? : null} diff --git a/src/components/layouts/shop/index.tsx b/src/components/layouts/shop/index.tsx index a79d84a..a92eb12 100644 --- a/src/components/layouts/shop/index.tsx +++ b/src/components/layouts/shop/index.tsx @@ -107,7 +107,8 @@ const SidebarItemMap = ({ menuItems }: any) => { const SideBarGroup = () => { const [miniSidebar, _] = useAtom(miniSidebarInitialValue); - const { role } = getAuthCredentials(); + const { role } = 'staff' + // getAuthCredentials(); const menuItems: MenuItemsProps = role === 'staff' ? siteSettings?.sidebarLinks?.staff diff --git a/src/components/layouts/topbar/visit-store.tsx b/src/components/layouts/topbar/visit-store.tsx index 8eef3ad..23f2cf8 100644 --- a/src/components/layouts/topbar/visit-store.tsx +++ b/src/components/layouts/topbar/visit-store.tsx @@ -3,24 +3,36 @@ import { Routes } from '@/config/routes'; import Link from 'next/link'; import { useRouter } from 'next/router'; import { useTranslation } from 'react-i18next'; +import { useState, useEffect } from 'react'; +import { useHasShop } from '@/utils/auth-utils'; const VisitStore = () => { const { t } = useTranslation(); 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 ( - <> - - - {slug === '/' ? t('text-visit-site') : t('text-visit-store')} - - + + + {slug === '/' ? t('text-visit-site') : t('text-visit-store')} + ); }; diff --git a/src/components/order/order-list.tsx b/src/components/order/order-list.tsx index 47a114e..ce4fcb3 100644 --- a/src/components/order/order-list.tsx +++ b/src/components/order/order-list.tsx @@ -77,7 +77,7 @@ const OrderList = ({ const columns = [ { title: t('table:table-item-tracking-number'), - dataIndex: 'tracking_number', + dataIndex: 'transaction_id', key: 'tracking_number', align: alignLeft, width: 200, @@ -92,7 +92,7 @@ const OrderList = ({ isActive={sortingObj.column === 'name'} /> ), - dataIndex: 'customer', + dataIndex: 'user_info', key: 'name', align: alignLeft, width: 250, @@ -109,9 +109,9 @@ const OrderList = ({ render: (customer: any) => (
{/* */} - +
- {customer?.name ? customer?.name : t('common:text-guest')} + {customer?.user ? customer?.user : t('common:text-guest')} {customer?.email} @@ -121,10 +121,10 @@ const OrderList = ({ }, { title: t('table:table-item-products'), - dataIndex: 'products', + dataIndex: 'items', key: 'products', align: 'center', - render: (products: Product) => {products.length}, + render: (products: Product) => {products?.length}, }, { // title: t('table:table-item-order-date'), @@ -139,10 +139,10 @@ const OrderList = ({ className="cursor-pointer" /> ), - dataIndex: 'created_at', - key: 'created_at', + dataIndex: 'created', + key: 'created', align: 'center', - onHeaderCell: () => onHeaderClick('created_at'), + onHeaderCell: () => onHeaderClick('created'), render: (date: string) => { dayjs.extend(relativeTime); dayjs.extend(utc); @@ -179,11 +179,11 @@ const OrderList = ({ className="cursor-pointer" /> ), - dataIndex: 'total', - key: 'total', + dataIndex: 'total_price', + key: 'total_price', align: 'center', width: 120, - onHeaderCell: () => onHeaderClick('total'), + onHeaderCell: () => onHeaderClick('total_price'), render: function Render(value: any) { const { price } = usePrice({ amount: value, @@ -193,7 +193,7 @@ const OrderList = ({ }, { title: t('table:table-item-status'), - dataIndex: 'order_status', + dataIndex: 'status', key: 'order_status', align: 'center', render: (order_status: string) => ( diff --git a/src/components/product/form-utils.ts b/src/components/product/form-utils.ts index b463acf..947095b 100644 --- a/src/components/product/form-utils.ts +++ b/src/components/product/form-utils.ts @@ -78,22 +78,24 @@ export function processOptions(options: any) { export function calculateMinMaxPrice(variationOptions: any) { 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 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) { @@ -118,23 +120,29 @@ export function getProductDefaultValues( image: [], gallery: [], video: [], - // isVariation: false, variations: [], variation_options: [], }; } + const { variations, variation_options, product_type, is_digital, digital_file, + images, } = product; + return cloneDeep({ ...product, product_type: productTypeOptions.find( (option) => product_type === option.value, ), + images: images.map((item, idx) => ({ + image: item.image_url.lg, + display_order: idx + 1, + })), ...(product_type === ProductType.Simple && { ...(is_digital && { digital_file_input: { @@ -148,22 +156,18 @@ export function getProductDefaultValues( ...(product_type === ProductType.Variable && { 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 && { type: null, categories: [], @@ -183,6 +187,7 @@ export function getProductDefaultValues( }); } + export function filterAttributes(attributes: any, variations: any) { let res = []; res = attributes?.filter((el: any) => { @@ -233,7 +238,7 @@ export function getProductInputValues( quantity, author, manufacturer, - image, + images, is_digital, categories, tags, @@ -241,25 +246,23 @@ export function getProductInputValues( variation_options, variations, in_flash_sale, + is_active, + discount, ...simpleValues } = values; // const { locale } = useRouter(); // const router = useRouter(); const processedFile = processFileWithName(digital_file_input); + console.log(values); + return { ...simpleValues, - is_digital, - in_flash_sale, // 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), tags: tags.map((tag) => tag?.id), - image: omitTypename(image), - gallery: values.gallery?.map((gi: any) => omitTypename(gi)), - + images: omitTypename(images), + is_active : values.is_active === "publish" ? true : false , + discount : `${values.discount}` , ...(product_type?.value === ProductType?.Simple && { quantity, ...(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 && { quantity: calculateQuantity(variation_options), variations: variations?.flatMap( diff --git a/src/components/product/product-category-input.tsx b/src/components/product/product-category-input.tsx index 36fc7d9..d33191b 100644 --- a/src/components/product/product-category-input.tsx +++ b/src/components/product/product-category-input.tsx @@ -27,12 +27,13 @@ const ProductCategoryInput = ({ control, setValue }: Props) => { } }, [type?.slug]); - const { categories, loading } = useCategoriesQuery({ + const { categories, loading , error} = useCategoriesQuery({ limit: 999, type: type?.slug, language: locale, - }); + }); + return (
@@ -43,7 +44,7 @@ const ProductCategoryInput = ({ control, setValue }: Props) => { getOptionLabel={(option: any) => option.name} getOptionValue={(option: any) => option.id} // @ts-ignore - options={categories} + options={categories.results} isLoading={loading} />
diff --git a/src/components/product/product-form.tsx b/src/components/product/product-form.tsx index 541905d..daae458 100644 --- a/src/components/product/product-form.tsx +++ b/src/components/product/product-form.tsx @@ -103,15 +103,16 @@ export default function CreateOrUpdateProductForm({ const isNewTranslation = router?.query?.action === 'translate'; const showPreviewButton = router?.query?.action === 'edit' && Boolean(initialValues?.slug); - const isSlugEditable = - router?.query?.action === 'edit' && - router?.locale === Config.defaultLanguage; + const isSlugEditable = true const methods = useForm({ //@ts-ignore resolver: yupResolver(productValidationSchema), shouldUnregister: true, // @ts-ignore - defaultValues: getProductDefaultValues(initialValues!, isNewTranslation), + defaultValues: { + ...getProductDefaultValues(initialValues!, isNewTranslation), + is_active: initialValues?.is_active || ProductStatus.Draft, // Set default value for status + }, }); const { register, @@ -130,24 +131,30 @@ export default function CreateOrUpdateProductForm({ const { mutate: updateProduct, isLoading: updating } = useUpdateProductMutation(); + + console.log(watch()); + const onSubmit = async (values: ProductFormValues) => { const inputValues = { - language: router.locale, ...getProductInputValues(values, initialValues), }; + console.log("submit"); + try { - if ( - !initialValues || - !initialValues.translated_languages.includes(router.locale!) - ) { + if (!initialValues) { + console.log("createProduct"); + //@ts-ignore createProduct({ + ...inputValues, - ...(initialValues?.slug && { slug: initialValues.slug }), - shop_id: shopId || initialValues?.shop_id, + // ...(initialValues?.slug && { slug: initialValues.slug }), + // shop_id: shopId || initialValues?.shop_id, }); } else { + console.log("updateProduct"); + //@ts-ignore updateProduct({ ...inputValues, @@ -169,10 +176,10 @@ export default function CreateOrUpdateProductForm({ const product_type = watch('product_type'); const is_digital = watch('is_digital'); 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 productDescriptionSuggestionLists = useMemo(() => { @@ -200,19 +207,9 @@ export default function CreateOrUpdateProductForm({ 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 { @@ -308,7 +305,7 @@ export default function CreateOrUpdateProductForm({ /> - + {/* {errors.image?.message && (

{t(errors?.image?.message!)} @@ -317,7 +314,7 @@ export default function CreateOrUpdateProductForm({

-
+ {/*
- + -
+
*/} -
+ {/*
- {/* Video url picker */}
{fields?.map((item: any, index: number) => (
-
+
*/}
- + /> */} {/* it's not needed in chawkbazar */} {/* */} @@ -452,25 +448,20 @@ export default function CreateOrUpdateProductForm({ disabled /> )} - + /> */}
- {options?.useAi && ( - - )} -
@@ -480,7 +471,7 @@ export default function CreateOrUpdateProductForm({ ? statusList?.map((status: any, index: number) => ( )} */} -
+ {/*
-
+
*/} {/* Simple Type */} - {product_type?.value === ProductType.Simple && ( - - )} + {/* Variation Type */} {product_type?.value === ProductType.Variable && ( @@ -596,6 +582,7 @@ export default function CreateOrUpdateProductForm({ )}