Browse Source

feat: update site name and logo, modify profile validation schema, and adjust API endpoints for user updates

master
sina_sajjadi 1 week ago
parent
commit
003020c952
  1. 25
      src/components/auth/change-password-from.tsx
  2. 5
      src/components/auth/email-update-form.tsx
  3. 77
      src/components/auth/profile-update-form.tsx
  4. 6
      src/components/auth/profile-validation-schema.tsx
  5. 286
      src/components/chat/chat-input.tsx
  6. 6
      src/components/chat/message.tsx
  7. 6
      src/components/chat/messages-list.tsx
  8. 2
      src/components/dashboard/admin.tsx
  9. 2
      src/components/layouts/topbar/message-bar.tsx
  10. 20
      src/components/product/product-form.tsx
  11. 6
      src/data/client/api-endpoints.ts
  12. 15
      src/data/client/http-client.ts
  13. 6
      src/data/client/user.ts
  14. 66
      src/data/user.ts
  15. 6
      src/pages/settings/index.tsx
  16. 4
      src/settings/site.settings.ts
  17. 82
      src/utils/auth-utils.ts

25
src/components/auth/change-password-from.tsx

@ -16,11 +16,10 @@ interface FormValues {
} }
const changePasswordSchema = yup.object().shape({ const changePasswordSchema = yup.object().shape({
oldPassword: yup.string().required('form:error-old-password-required'),
newPassword: yup.string().required('form:error-password-required'),
passwordConfirmation: yup
password: yup.string().required('form:error-password-required'),
password_confirmation: yup
.string() .string()
.oneOf([yup.ref('newPassword')], 'form:error-match-passwords')
.oneOf([yup.ref('password')], 'form:error-match-passwords')
.required('form:error-confirm-password'), .required('form:error-confirm-password'),
}); });
@ -42,8 +41,8 @@ const ChangePasswordForm = () => {
async function onSubmit(values: FormValues) { async function onSubmit(values: FormValues) {
changePassword( changePassword(
{ {
oldPassword: values.oldPassword,
newPassword: values.newPassword,
password: values.password,
password_confirmation: values.password_confirmation,
}, },
{ {
onError: (error: any) => { onError: (error: any) => {
@ -79,30 +78,30 @@ const ChangePasswordForm = () => {
/> />
<Card className="mb-5 w-full sm:w-8/12 md:w-2/3"> <Card className="mb-5 w-full sm:w-8/12 md:w-2/3">
<PasswordInput
{/* <PasswordInput
label={t('form:input-label-old-password')} label={t('form:input-label-old-password')}
{...register('oldPassword')} {...register('oldPassword')}
variant="outline" variant="outline"
error={t(errors.oldPassword?.message!)} error={t(errors.oldPassword?.message!)}
className="mb-5" className="mb-5"
/>
/> */}
<PasswordInput <PasswordInput
label={t('form:input-label-new-password')} label={t('form:input-label-new-password')}
{...register('newPassword')}
{...register('password')}
variant="outline" variant="outline"
error={t(errors.newPassword?.message!)}
error={t(errors.password?.message!)}
className="mb-5" className="mb-5"
/> />
<PasswordInput <PasswordInput
label={t('form:input-label-confirm-password')} label={t('form:input-label-confirm-password')}
{...register('passwordConfirmation')}
{...register('password_confirmation')}
variant="outline" variant="outline"
error={t(errors.passwordConfirmation?.message!)}
error={t(errors.password_confirmation?.message!)}
/> />
</Card> </Card>
<div className="text-end w-full"> <div className="text-end w-full">
<Button loading={loading} disabled={loading}>
<Button type='submit' loading={loading} disabled={loading}>
{t('form:button-label-change-password')} {t('form:button-label-change-password')}
</Button> </Button>
</div> </div>

5
src/components/auth/email-update-form.tsx

@ -25,11 +25,14 @@ export default function EmailUpdateForm({ me }: any) {
}); });
async function onSubmit(values: FormValues) { async function onSubmit(values: FormValues) {
console.log("hi");
const {email } = values; const {email } = values;
updateEmail({ updateEmail({
email: email, email: email,
}); });
} }
console.log(errors);
return ( return (
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
@ -52,7 +55,7 @@ export default function EmailUpdateForm({ me }: any) {
</Card> </Card>
<div className="text-end w-full"> <div className="text-end w-full">
<Button loading={loading} disabled={loading}>
<Button type='submit' loading={loading} disabled={loading}>
{t('form:button-label-save')} {t('form:button-label-save')}
</Button> </Button>
</div> </div>

77
src/components/auth/profile-update-form.tsx

@ -37,14 +37,14 @@ export default function ProfileUpdate({ me }: any) {
const { t } = useTranslation(); const { t } = useTranslation();
const { mutate: updateUser, isLoading: loading } = useUpdateUserMutation(); const { mutate: updateUser, isLoading: loading } = useUpdateUserMutation();
const { permissions } = getAuthCredentials(); const { permissions } = getAuthCredentials();
let permission = hasAccess(adminOnly, permissions);
const permission = hasAccess(adminOnly, permissions);
const { const {
register, register,
handleSubmit, handleSubmit,
control, control,
formState: { errors },
formState: { errors, dirtyFields },
} = useForm<FormValues>({ } = useForm<FormValues>({
//@ts-ignore
resolver: yupResolver(profileValidationSchema), resolver: yupResolver(profileValidationSchema),
defaultValues: { defaultValues: {
...(me && ...(me &&
@ -60,28 +60,31 @@ export default function ProfileUpdate({ me }: any) {
}); });
async function onSubmit(values: FormValues) { async function onSubmit(values: FormValues) {
const { name, profile } = values;
const { notifications } = profile;
const input = {
console.log(values);
// If nothing is dirty, no need to call the update
if (Object.keys(dirtyFields).length === 0) {
return;
}
// Construct your payload conditionally
// Always include the user id for the update
const inputPayload: any = {
id: me?.id, id: me?.id,
input: {
name: name,
profile: {
id: me?.profile?.id,
bio: profile?.bio,
contact: profile?.contact,
avatar: {
thumbnail: profile?.avatar?.thumbnail,
original: profile?.avatar?.original,
id: profile?.avatar?.id,
},
notifications: {
...notifications,
},
},
},
input: {},
}; };
updateUser({ ...input });
// Check top-level fields first
if (dirtyFields.fullname) {
inputPayload.input.fullname = values.fullname;
}
if (dirtyFields.avatar) {
inputPayload.input.avatar = values.avatar;
}
// Finally call the mutation with the built payload
updateUser(inputPayload);
} }
return ( return (
@ -94,17 +97,17 @@ export default function ProfileUpdate({ me }: any) {
/> />
<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="profile.avatar" control={control} multiple={false} />
<FileInput name="avatar" control={control} multiple={false} />
</Card> </Card>
</div> </div>
{permission ? (
{/* {permission && (
<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')}
details={t('form:form-notification-description')} details={t('form:form-notification-description')}
className="w-full px-0 pb-5 sm:w-4/12 sm:py-8 sm:pe-4 md:w-1/3 md:pe-5" className="w-full px-0 pb-5 sm:w-4/12 sm:py-8 sm:pe-4 md:w-1/3 md:pe-5"
/> />
<Card className="w-full mb-5 sm:w-8/12 md:w-2/3"> <Card className="w-full mb-5 sm:w-8/12 md:w-2/3">
<Input <Input
label={t('form:input-notification-email')} label={t('form:input-notification-email')}
@ -125,40 +128,38 @@ export default function ProfileUpdate({ me }: any) {
</div> </div>
</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:form-title-information')} title={t('form:form-title-information')}
details={t('form:profile-info-help-text')} details={t('form:profile-info-help-text')}
className="w-full px-0 pb-5 sm:w-4/12 sm:py-8 sm:pe-4 md:w-1/3 md:pe-5" className="w-full px-0 pb-5 sm:w-4/12 sm:py-8 sm:pe-4 md:w-1/3 md:pe-5"
/> />
<Card className="w-full mb-5 sm:w-8/12 md:w-2/3"> <Card className="w-full mb-5 sm:w-8/12 md:w-2/3">
<Input <Input
label={t('form:input-label-name')} label={t('form:input-label-name')}
{...register('name')}
error={t(errors.name?.message!)}
{...register('fullname')}
error={t(errors.fullname?.message!)}
variant="outline" variant="outline"
className="mb-5" className="mb-5"
/> />
<TextArea
{/* <TextArea
label={t('form:input-label-bio')} label={t('form:input-label-bio')}
{...register('profile.bio')} {...register('profile.bio')}
error={t(errors.profile?.bio?.message!)} error={t(errors.profile?.bio?.message!)}
variant="outline" variant="outline"
className="mb-6" className="mb-6"
/>
<PhoneNumberInput
/> */}
{/* <PhoneNumberInput
label={t('form:input-label-contact')} label={t('form:input-label-contact')}
{...register('profile.contact')}
{...register('contact')}
control={control} control={control}
error={t(errors.profile?.contact?.message!)} error={t(errors.profile?.contact?.message!)}
/>
/> */}
</Card> </Card>
<div className="w-full text-end"> <div className="w-full text-end">
<Button loading={loading} disabled={loading}>
<Button type="submit" loading={loading} disabled={loading}>
{t('form:button-label-save')} {t('form:button-label-save')}
</Button> </Button>
</div> </div>

6
src/components/auth/profile-validation-schema.tsx

@ -1,8 +1,6 @@
import * as yup from 'yup'; import * as yup from 'yup';
export const profileValidationSchema = yup.object().shape({ export const profileValidationSchema = yup.object().shape({
name: yup.string().required('form:error-name-required'),
profile: yup.object().shape({
contact: yup.string().max(19, 'maximum 19 digit').optional(),
}),
fullname: yup.string(),
}); });

286
src/components/chat/chat-input.tsx

@ -1,28 +1,28 @@
import { PiTrash } from "react-icons/pi";
import Image from "next/image";
import SendIcon from "public/image/send-01.svg";
import MicIcon from "public/image/microphone-01.svg";
import LinkIcon from "public/image/link-simple.svg";
import SmileIcon from "public/image/face-smile.svg";
import { MdClose } from "react-icons/md";
import { useEffect, useState, useRef } from "react";
import { useWebSocket } from "@/contexts/WebSocket.context";
import FileInput from "./file-input";
import dynamic from "next/dynamic";
import EmojiPicker from "emoji-picker-react";
import { BsFillReplyFill } from "react-icons/bs";
import { PiTrash } from 'react-icons/pi';
import Image from 'next/image';
import SendIcon from 'public/image/send-01.svg';
import MicIcon from 'public/image/microphone-01.svg';
import LinkIcon from 'public/image/link-simple.svg';
import SmileIcon from 'public/image/face-smile.svg';
import { MdClose } from 'react-icons/md';
import { useEffect, useState, useRef } from 'react';
import { useWebSocket } from '@/contexts/WebSocket.context';
import FileInput from './file-input';
import dynamic from 'next/dynamic';
import EmojiPicker from 'emoji-picker-react';
import { BsFillReplyFill } from 'react-icons/bs';
// Dynamically import ReactMediaRecorder to prevent SSR issues // Dynamically import ReactMediaRecorder to prevent SSR issues
const ReactMediaRecorder = dynamic( const ReactMediaRecorder = dynamic(
() => import("react-media-recorder").then((mod) => mod.ReactMediaRecorder),
{ ssr: false }
() => import('react-media-recorder').then((mod) => mod.ReactMediaRecorder),
{ ssr: false },
); );
// Helper function to format recording time // Helper function to format recording time
const formatTime = (seconds) => { const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60) const mins = Math.floor(seconds / 60)
.toString() .toString()
.padStart(2, "0");
const secs = (seconds % 60).toString().padStart(2, "0");
.padStart(2, '0');
const secs = (seconds % 60).toString().padStart(2, '0');
return `${mins}:${secs}`; return `${mins}:${secs}`;
}; };
@ -39,7 +39,7 @@ const Input = ({ product = {} }) => {
const [showProduct, setShowProduct] = useState(!!product.id); const [showProduct, setShowProduct] = useState(!!product.id);
const [isRecording, setIsRecording] = useState(false); const [isRecording, setIsRecording] = useState(false);
const [recordingTime, setRecordingTime] = useState(0); const [recordingTime, setRecordingTime] = useState(0);
const [message, setMessage] = useState("");
const [message, setMessage] = useState('');
const [showEmojiPicker, setShowEmojiPicker] = useState(false); // Emoji picker state const [showEmojiPicker, setShowEmojiPicker] = useState(false); // Emoji picker state
// Ref for the timer interval // Ref for the timer interval
@ -60,11 +60,11 @@ const Input = ({ product = {} }) => {
id: editingMessage.id, id: editingMessage.id,
}; };
editMessage(msg); editMessage(msg);
setEditingMessage({})
setMessage("");
setEditingMessage({});
setMessage('');
} else { } else {
sendMessage(message, product?.id); sendMessage(message, product?.id);
setMessage("");
setMessage('');
setShowProduct(false); setShowProduct(false);
} }
} }
@ -108,9 +108,9 @@ const Input = ({ product = {} }) => {
} }
}; };
document.addEventListener("mousedown", handleClickOutside);
document.addEventListener('mousedown', handleClickOutside);
return () => { return () => {
document.removeEventListener("mousedown", handleClickOutside);
document.removeEventListener('mousedown', handleClickOutside);
}; };
}, []); }, []);
useEffect(() => { useEffect(() => {
@ -119,7 +119,7 @@ const Input = ({ product = {} }) => {
messageInputRef.current?.focus(); // Set focus on the input messageInputRef.current?.focus(); // Set focus on the input
} }
}, [editingMessage]); }, [editingMessage]);
return ( return (
<ReactMediaRecorder <ReactMediaRecorder
audio audio
@ -133,166 +133,126 @@ const Input = ({ product = {} }) => {
stopTimer(); stopTimer();
if (sendRecordingRef.current) { if (sendRecordingRef.current) {
let selectedFile = blob; let selectedFile = blob;
selectedFile.status = "loading";
selectedFile.status = 'loading';
selectedFile.name = `audio ${new Date()}.wav`; selectedFile.name = `audio ${new Date()}.wav`;
setLoadingMessage((prev) => [...prev, selectedFile]); setLoadingMessage((prev) => [...prev, selectedFile]);
} else { } else {
console.log("Recording discarded:", blobUrl);
console.log('Recording discarded:', blobUrl);
} }
sendRecordingRef.current = false; // Reset the flag after handling sendRecordingRef.current = false; // Reset the flag after handling
}} }}
render={({ startRecording, stopRecording }) => ( render={({ startRecording, stopRecording }) => (
<div <div
className={`${ className={`${
product.id && showProduct ? "mb-28" : ""
product.id && showProduct ? 'mb-28' : ''
} z-10 m-7 border border-[#D9D9D9] self-end w-full`} } z-10 m-7 border border-[#D9D9D9] self-end w-full`}
> >
{/* Product Preview */}
{product.id && showProduct && (
<div className="w-full flex gap-2 bg-white border p-4">
<Image src={LinkIcon} alt="Link Icon" width={24} height={24} />
<div className="flex justify-between w-full items-center">
<div className="flex gap-2">
{product?.images && product.images.length > 0 && (
<Image
width={35}
height={35}
src={product.images[0].image_url.sm}
alt={product.name || "Product Image"}
className="object-cover rounded"
/>
)}
<div className="flex flex-col justify-between">
<p className="text-xs leading-none mb-0">
{product.name || "Unnamed Product"}
</p>
<p className="text-[10px]">{product.price}$</p>
</div>
</div>
<MdClose
onClick={() => {setShowProduct(false)}}
size={20}
aria-label="Close product preview"
className="cursor-pointer"
/>
</div>
</div>
)}
{!!Object.keys(editingMessage).length && (
<div className=" w-full flex bg-white border gap-2 p-4">
<BsFillReplyFill size={24} />
<div className="flex justify-between w-full items-center">
<div className="flex gap-2">
<div className="flex flex-col justify-between">
<p className="text-xs leading-none mb-0">
{editingMessage.mime_type === "text"
? editingMessage.content
: editingMessage.mime_type}
</p>
</div>
</div>
<MdClose
onClick={() => {setEditingMessage({}); setMessage("")}}
size={20}
aria-label="Close product preview"
className="cursor-pointer"
/>
</div>
</div>
)}
{/* Message Input Area */} {/* Message Input Area */}
<div className="relative h-14 max-h-40 z-10 border-2 bg-white flex items-center p-1"> <div className="relative h-14 max-h-40 z-10 border-2 bg-white flex items-center p-1">
{/* Emoji Button */}
<button
ref={emojiButtonRef} // Attach ref to the emoji toggle button
onClick={() => {
setShowEmojiPicker((prev) => !prev); // Toggle emoji picker state
}}
className="text-white px-3 py-2 rounded-md transition flex items-center hover:bg-slate-100"
aria-label="Insert emoji"
>
<Image width={25} height={25} src={SmileIcon} alt="Smile" />
</button>
{/* Emoji Picker */}
{showEmojiPicker && (
<div
ref={emojiPickerRef}
className="absolute bottom-14 left-0 z-20 bg-white border rounded shadow-md"
>
<EmojiPicker
onEmojiClick={(emojiObject) => {
setMessage(
(prevMessage) => prevMessage + emojiObject.emoji
);
messageInputRef.current?.focus();
}}
emojiStyle="native"
skinTonesDisabled
previewConfig={{
showPreview: false,
}}
/>
</div>
)}
{isRecording ? (
<div className="flex w-full justify-between items-center px-4">
{/* Trash Button */}
<div className='flex gap-4 items-center'>
<button
onClick={() => {
sendRecordingRef.current = false; // Discard recording
stopRecording();
}}
aria-label="Discard recording"
className="text-red-600 flex items-center gap-2 hover:text-red-800"
>
<PiTrash size={25} />
</button>
{/* Textarea Input */}
<textarea
className="flex-1 h-full rounded-md p-2 text-base focus:outline-none resize-none overflow-hidden"
ref={messageInputRef}
placeholder="Type your message..."
value={message}
onChange={(e) => setMessage(e.target.value)}
rows={1}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
handleSendMessage();
e.preventDefault();
}
}}
aria-label="Message input"
/>
{/* Timer with Red Dot */}
<div className="flex items-center gap-2 py-1 px-2 rounded-md bg-red-100 text-red-600 font-medium">
<div className="w-3 h-3 bg-red-500 rounded-full animate-pulse"></div>
<span className='text-black'>{formatTime(recordingTime)}</span>
</div>
{/* Send Button or File/Mic Buttons */}
{message.trim() || isRecording ? (
<button
onClick={() => {
if (isRecording) {
{/* Stop/Send Button */}
</div>
<button
onClick={() => {
sendRecordingRef.current = true; // Indicate sending sendRecordingRef.current = true; // Indicate sending
stopRecording(); stopRecording();
} else {
handleSendMessage();
}
}}
className="text-white px-3 py-2 rounded-md transition flex items-center hover:bg-slate-100"
aria-label={isRecording ? "Send recording" : "Send message"}
>
<Image src={SendIcon} alt="Send" width={24} height={24} />
</button>
}}
className="text-red-600 flex items-center gap-2 hover:text-red-800"
aria-label="Stop recording"
>
<Image src={SendIcon} alt="Send" width={24} height={24} />
</button>
</div>
) : ( ) : (
<> <>
{!isRecording && (
<>
<FileInput />
<button
onClick={() => {
startRecording();
{/* Emoji Button */}
<button
ref={emojiButtonRef}
onClick={() => {
setShowEmojiPicker((prev) => !prev);
}}
className="text-white px-3 py-2 rounded-md transition flex items-center hover:bg-slate-100"
aria-label="Insert emoji"
>
<Image width={25} height={25} src={SmileIcon} alt="Smile" />
</button>
{/* Emoji Picker */}
{showEmojiPicker && (
<div
ref={emojiPickerRef}
className="absolute bottom-14 left-0 z-20 bg-white border rounded shadow-md"
>
<EmojiPicker
onEmojiClick={(emojiObject) => {
setMessage(
(prevMessage) => prevMessage + emojiObject.emoji,
);
messageInputRef.current?.focus();
}} }}
className="text-white px-3 py-2 rounded-md transition flex items-center hover:bg-slate-100"
aria-label="Start recording"
>
<Image
src={MicIcon}
width={23}
height={23}
alt="Microphone"
className="object-contain"
/>
</button>
</>
emojiStyle="native"
skinTonesDisabled
previewConfig={{
showPreview: false,
}}
/>
</div>
)} )}
{/* Textarea Input */}
<textarea
className="flex-1 h-full rounded-md p-2 text-base focus:outline-none resize-none overflow-hidden"
ref={messageInputRef}
placeholder="Type your message..."
value={message}
onChange={(e) => setMessage(e.target.value)}
rows={1}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
handleSendMessage();
e.preventDefault();
}
}}
aria-label="Message input"
/>
{/* File Input and Mic Button */}
<FileInput />
<button
onClick={() => {
startRecording();
}}
className="text-white px-3 py-2 rounded-md transition flex items-center hover:bg-slate-100"
aria-label="Start recording"
>
<Image
src={MicIcon}
width={23}
height={23}
alt="Microphone"
className="object-contain"
/>
</button>
</> </>
)} )}
</div> </div>

6
src/components/chat/message.tsx

@ -228,17 +228,17 @@ const Message = ({ msg }) => {
locale: router.locale, locale: router.locale,
}); });
}} }}
className="w-[calc(50%-12px)] bg-[#C5C5C5]"
className="w-[calc(100%-12px)] bg-[#C5C5C5]"
> >
Product details Product details
</Button> </Button>
<Button
{/* <Button
className={`w-[calc(50%-12px)] ${ className={`w-[calc(50%-12px)] ${
isUser && 'bg-white' isUser && 'bg-white'
} !text-black hover:!text-white`} } !text-black hover:!text-white`}
> >
Add to cart Add to cart
</Button>
</Button> */}
</div> </div>
</div> </div>
)} )}

6
src/components/chat/messages-list.tsx

@ -23,7 +23,9 @@ const MessageList = ({ messages }) => {
}, [messages, loadingMessage]); }, [messages, loadingMessage]);
useEffect(() => { useEffect(() => {
if (chatBoxRef.current && !(messages.length > 16)) {
if (chatBoxRef.current ) {
console.log("scrolling to bottom" , messages , messages.length , (messages.length > 16));
// Scroll to the bottom whenever allMessages change // Scroll to the bottom whenever allMessages change
chatBoxRef.current.scrollTop = chatBoxRef.current.scrollHeight; chatBoxRef.current.scrollTop = chatBoxRef.current.scrollHeight;
} }
@ -31,7 +33,7 @@ const MessageList = ({ messages }) => {
useEffect(() => { useEffect(() => {
const handleScroll = () => { const handleScroll = () => {
if (chatBoxRef.current && chatBoxRef.current.scrollTop === 0) {
if (chatBoxRef.current && (chatBoxRef.current.scrollTop === 0) && (messages.length >= 16)) {
getNextPage(); getNextPage();
} }
}; };

2
src/components/dashboard/admin.tsx

@ -66,7 +66,7 @@ export default function Dashboard() {
const [orderDataRange, setOrderDataRange] = useState( const [orderDataRange, setOrderDataRange] = useState(
data?.todayTotalOrderByStatus, data?.todayTotalOrderByStatus,
); );
const token = getAuthCredentials();
// const token = getAuthCredentials();
const { price: total_revenue } = usePrice( const { price: total_revenue } = usePrice(

2
src/components/layouts/topbar/message-bar.tsx

@ -75,7 +75,7 @@ const MessageBar = ({ user }: IProps) => {
// here messages will be passed as a props in eventData. to keep the useEffect track of having a new message // here messages will be passed as a props in eventData. to keep the useEffect track of having a new message
return ( return (
<> <>
<Menu as="div" className="inline-block text-left sm:relative">
<Menu as="a" href='/dashboard/chat' className="inline-block text-left sm:relative">
<Menu.Button <Menu.Button
className={cn( className={cn(
'relative flex h-9 w-9 items-center justify-center gap-2 rounded-full border border-gray-200 bg-gray-50 text-gray-600 before:absolute before:top-0 before:right-0 before:h-2 before:w-2 before:rounded-full focus:outline-none data-[headlessui-state=open]:bg-white data-[headlessui-state=open]:text-accent', 'relative flex h-9 w-9 items-center justify-center gap-2 rounded-full border border-gray-200 bg-gray-50 text-gray-600 before:absolute before:top-0 before:right-0 before:h-2 before:w-2 before:rounded-full focus:outline-none data-[headlessui-state=open]:bg-white data-[headlessui-state=open]:text-accent',

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

@ -103,7 +103,7 @@ 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 = true
const isSlugEditable = true;
const methods = useForm<ProductFormValues>({ const methods = useForm<ProductFormValues>({
//@ts-ignore //@ts-ignore
resolver: yupResolver(productValidationSchema), resolver: yupResolver(productValidationSchema),
@ -111,7 +111,7 @@ export default function CreateOrUpdateProductForm({
// @ts-ignore // @ts-ignore
defaultValues: { defaultValues: {
...getProductDefaultValues(initialValues!, isNewTranslation), ...getProductDefaultValues(initialValues!, isNewTranslation),
is_active: initialValues?.is_active || ProductStatus.Draft, // Set default value for status
is_active: initialValues?.is_active || ProductStatus.Draft, // Set default value for status
}, },
}); });
const { const {
@ -131,30 +131,27 @@ export default function CreateOrUpdateProductForm({
const { mutate: updateProduct, isLoading: updating } = const { mutate: updateProduct, isLoading: updating } =
useUpdateProductMutation(); useUpdateProductMutation();
console.log(watch());
console.log(watch());
const onSubmit = async (values: ProductFormValues) => { const onSubmit = async (values: ProductFormValues) => {
const inputValues = { const inputValues = {
...getProductInputValues(values, initialValues), ...getProductInputValues(values, initialValues),
}; };
console.log("submit");
console.log('submit');
try { try {
if (!initialValues) { if (!initialValues) {
console.log("createProduct");
console.log('createProduct');
//@ts-ignore //@ts-ignore
createProduct({ createProduct({
...inputValues, ...inputValues,
// ...(initialValues?.slug && { slug: initialValues.slug }), // ...(initialValues?.slug && { slug: initialValues.slug }),
// shop_id: shopId || initialValues?.shop_id, // shop_id: shopId || initialValues?.shop_id,
}); });
} else { } else {
console.log("updateProduct");
console.log('updateProduct');
//@ts-ignore //@ts-ignore
updateProduct({ updateProduct({
...inputValues, ...inputValues,
@ -283,6 +280,7 @@ export default function CreateOrUpdateProductForm({
control, control,
name: 'variations', name: 'variations',
}); });
console.log(isSlugEditable, isSlugDisable, slugAutoSuggest);
return ( return (
<> <>
@ -424,6 +422,7 @@ export default function CreateOrUpdateProductForm({
<div className="relative mb-5"> <div className="relative mb-5">
<Input <Input
label={t('form:input-label-slug')} label={t('form:input-label-slug')}
value={slugAutoSuggest}
{...register('slug')} {...register('slug')}
error={t(errors.slug?.message!)} error={t(errors.slug?.message!)}
variant="outline" variant="outline"
@ -442,7 +441,6 @@ export default function CreateOrUpdateProductForm({
<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

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

@ -5,7 +5,7 @@ export const API_ENDPOINTS = {
ATTRIBUTE_VALUES: 'attribute-values', ATTRIBUTE_VALUES: 'attribute-values',
ORDER_STATUS: 'order-status', ORDER_STATUS: 'order-status',
ORDERS: 'merchant-panel/orders', ORDERS: 'merchant-panel/orders',
USERS: 'users',
USERS: 'account/merchant/profile/update/',
REGISTER: 'account/register/', REGISTER: 'account/register/',
OTP: 'account/verify/', OTP: 'account/verify/',
PRODUCTS: 'merchant-panel/products/create/', PRODUCTS: 'merchant-panel/products/create/',
@ -28,7 +28,7 @@ export const API_ENDPOINTS = {
TOKEN: 'account/login/', 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: 'account/reset/',
FORGET_PASSWORD: 'account/recover/', FORGET_PASSWORD: 'account/recover/',
VERIFY_FORGET_PASSWORD_TOKEN: 'account/verify/', VERIFY_FORGET_PASSWORD_TOKEN: 'account/verify/',
RESET_PASSWORD: 'account/reset/', RESET_PASSWORD: 'account/reset/',
@ -64,7 +64,7 @@ export const API_ENDPOINTS = {
ORDER_CREATE: 'order/create', ORDER_CREATE: 'order/create',
ORDER_INVOICE_DOWNLOAD: 'download-invoice-url', ORDER_INVOICE_DOWNLOAD: 'download-invoice-url',
SEND_VERIFICATION_EMAIL: '/email/verification-notification', SEND_VERIFICATION_EMAIL: '/email/verification-notification',
UPDATE_EMAIL: '/update-email',
UPDATE_EMAIL: '/account/merchant/profile/update/',
CONVERSIONS: '/conversations', CONVERSIONS: '/conversations',
MESSAGE: '/messages/conversations', MESSAGE: '/messages/conversations',
MESSAGE_SEEN: '/messages/seen', MESSAGE_SEEN: '/messages/seen',

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

@ -1,5 +1,4 @@
import axios from 'axios'; import axios from 'axios';
import Cookies from 'js-cookie';
import Router from 'next/router'; import Router from 'next/router';
import invariant from 'tiny-invariant'; import invariant from 'tiny-invariant';
@ -15,16 +14,18 @@ const Axios = axios.create({
// 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';
Axios.interceptors.request.use((config) => { Axios.interceptors.request.use((config) => {
const cookies = Cookies.get(AUTH_TOKEN_KEY);
let token = ''; let token = '';
if (cookies) {
token = JSON.parse(cookies);
if (typeof window !== 'undefined') {
const sessionToken = sessionStorage.getItem(AUTH_TOKEN_KEY);
if (sessionToken) {
token = JSON.parse(sessionToken);
}
} }
// @ts-ignore // @ts-ignore
config.headers = { config.headers = {
...config.headers, ...config.headers,
Authorization:token ?`token ${token} ` : null,
Authorization: token ? `token ${token} ` : null,
}; };
return config; return config;
}); });
@ -39,7 +40,7 @@ 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);
// sessionStorage.removeItem(AUTH_TOKEN_KEY);
// Router.reload(); // Router.reload();
} }
return Promise.reject(error); return Promise.reject(error);

6
src/data/client/user.ts

@ -42,7 +42,7 @@ export const userClient = {
return HttpClient.post<AuthResponse>(API_ENDPOINTS.OTP, variables); 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.patch<User>(`${API_ENDPOINTS.USERS}`, input);
}, },
changePassword: (variables: ChangePasswordInput) => { changePassword: (variables: ChangePasswordInput) => {
return HttpClient.post<any>(API_ENDPOINTS.CHANGE_PASSWORD, variables); return HttpClient.post<any>(API_ENDPOINTS.CHANGE_PASSWORD, variables);
@ -97,7 +97,9 @@ export const userClient = {
return HttpClient.post<any>(API_ENDPOINTS.SEND_VERIFICATION_EMAIL, {}); return HttpClient.post<any>(API_ENDPOINTS.SEND_VERIFICATION_EMAIL, {});
}, },
updateEmail: ({ email }: { email: string }) => { updateEmail: ({ email }: { email: string }) => {
return HttpClient.post<any>(API_ENDPOINTS.UPDATE_EMAIL, { email });
console.log("UPDATING EMAIL");
return HttpClient.patch<any>(API_ENDPOINTS.UPDATE_EMAIL, { email });
}, },
fetchVendors: ({ is_active, ...params }: Partial<UserQueryOptions>) => { fetchVendors: ({ is_active, ...params }: Partial<UserQueryOptions>) => {
return HttpClient.get<UserPaginator>(API_ENDPOINTS.VENDORS_LIST, { return HttpClient.get<UserPaginator>(API_ENDPOINTS.VENDORS_LIST, {

66
src/data/user.ts

@ -1,24 +1,14 @@
import { AUTH_CRED } from '@/utils/constants';
import { AUTH_CRED, EMAIL_VERIFIED, PERMISSIONS, TOKEN } from '@/utils/constants';
import { Routes } from '@/config/routes'; import { Routes } from '@/config/routes';
import Cookies from 'js-cookie';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useMutation, useQuery, useQueryClient } from 'react-query'; import { useMutation, useQuery, useQueryClient } from 'react-query';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { API_ENDPOINTS } from './client/api-endpoints'; import { API_ENDPOINTS } from './client/api-endpoints';
import { userClient } from './client/user'; import { userClient } from './client/user';
import {
User,
QueryOptionsType,
UserPaginator,
UserQueryOptions,
LicensedDomainPaginator,
LicenseAdditionalData,
} from '@/types';
import { mapPaginatorData } from '@/utils/data-mappers';
import axios from 'axios';
import { setEmailVerified } from '@/utils/auth-utils'; import { setEmailVerified } from '@/utils/auth-utils';
import { type } from 'os';
import { User, QueryOptionsType, UserPaginator, UserQueryOptions } from '@/types';
import { mapPaginatorData } from '@/utils/data-mappers';
export const useMeQuery = () => { export const useMeQuery = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@ -38,22 +28,22 @@ export const useMeQuery = () => {
}, },
onError: (err) => { onError: (err) => {
if (axios.isAxiosError(err)) {
if (err.response?.status === 417) {
router.replace(Routes.verifyLicense);
return;
}
console.log(err);
if (err.response?.status === 409) {
setEmailVerified(false);
router.replace(Routes.verifyEmail);
return;
}
Cookies.remove(AUTH_CRED);
queryClient.clear();
router.replace(Routes.login);
console.log("me error");
if (err.response?.status === 417) {
router.replace(Routes.verifyLicense);
return;
} }
console.log(err);
if (err.response?.status === 409) {
setEmailVerified(false);
router.replace(Routes.verifyEmail);
return;
}
sessionStorage.removeItem(AUTH_CRED);
queryClient.clear();
router.replace(Routes.login);
}, },
}); });
}; };
@ -67,8 +57,8 @@ export const useLogoutMutation = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const logout = () => { const logout = () => {
// Remove the authentication cookie
Cookies.remove(AUTH_CRED);
// Remove the authentication session storage
sessionStorage.removeItem(AUTH_CRED);
// Redirect to the login page // Redirect to the login page
router.replace(Routes.login); router.replace(Routes.login);
@ -138,11 +128,17 @@ export const useUpdateUserEmailMutation = () => {
toast.success(t('common:successfully-updated')); toast.success(t('common:successfully-updated'));
}, },
onError: (error) => { onError: (error) => {
const {
response: { data },
}: any = error ?? {};
toast.error(data?.message);
if (error.response?.data?.errors?.length) {
error.response?.data?.errors?.map((err) => {
if (err.field) {
toast.error(`${err.field} : ${err.message}`);
} else {
toast.error(err.message);
}
});
} else {
toast.error(error.message || t("errorOccurred"));
}
}, },
// Always refetch after error or success: // Always refetch after error or success:
onSettled: () => { onSettled: () => {

6
src/pages/settings/index.tsx

@ -25,9 +25,9 @@ export default function Settings() {
language: locale!, language: locale!,
}); });
if (loading || shippingLoading || taxLoading)
return <Loader text={t('common:text-loading')} />;
if (error) return <ErrorMessage message={error.message} />;
// if (loading || shippingLoading || taxLoading)
// return <Loader text={t('common:text-loading')} />;
// if (error) return <ErrorMessage message={error.message} />;
return ( return (
<> <>

4
src/settings/site.settings.ts

@ -8,11 +8,11 @@ import {
import { Routes } from '@/config/routes'; import { Routes } from '@/config/routes';
export const siteSettings = { export const siteSettings = {
name: 'ChawkBazar',
name: 'Mesbahi',
description: '', description: '',
logo: { logo: {
url: '/logo.svg', url: '/logo.svg',
alt: 'ChawkBazar',
alt: 'Mesbahi',
href: '/', href: '/',
width: 138, width: 138,
height: 34, height: 34,

82
src/utils/auth-utils.ts

@ -1,5 +1,3 @@
import Cookie from 'js-cookie';
import SSRCookie from 'cookie';
import { import {
AUTH_CRED, AUTH_CRED,
EMAIL_VERIFIED, EMAIL_VERIFIED,
@ -19,76 +17,92 @@ export const ownerOnly = [STORE_OWNER];
export const ownerAndStaffOnly = [STORE_OWNER, STAFF]; export const ownerAndStaffOnly = [STORE_OWNER, STAFF];
export function setAuthCredentials(token: string) { export function setAuthCredentials(token: string) {
console.log("Dfafasd");
Cookie.set(AUTH_CRED, JSON.stringify(token));
if (typeof window !== "undefined") {
sessionStorage.setItem(AUTH_CRED, JSON.stringify(token));
}
} }
export function setEmailVerified(emailVerified: boolean) { export function setEmailVerified(emailVerified: boolean) {
Cookie.set(EMAIL_VERIFIED, JSON.stringify({ emailVerified }));
if (typeof window !== "undefined") {
sessionStorage.setItem(EMAIL_VERIFIED, JSON.stringify({ emailVerified }));
}
} }
export function getEmailVerified(): { export function getEmailVerified(): {
emailVerified: boolean; emailVerified: boolean;
} { } {
const emailVerified = Cookie.get(EMAIL_VERIFIED);
return emailVerified ? JSON.parse(emailVerified) : false;
if (typeof window !== "undefined") {
const emailVerified = sessionStorage.getItem(EMAIL_VERIFIED);
return emailVerified ? JSON.parse(emailVerified) : { emailVerified: false };
}
return { emailVerified: false };
} }
export function getAuthCredentials(context?: any): { export function getAuthCredentials(context?: any): {
token: string | null; token: string | null;
} { } {
let authCred; let authCred;
if (context) {
authCred = parseSSRCookie(context)[AUTH_CRED];
} else {
authCred = Cookie.get(AUTH_CRED);
if (typeof window !== "undefined") {
// Check if running on the client-side
authCred = sessionStorage.getItem(AUTH_CRED);
} else if (context) {
// SSR context
authCred = parseSSRSession(context)[AUTH_CRED];
console.log("out");
} }
if (authCred) { if (authCred) {
return JSON.parse(authCred); return JSON.parse(authCred);
} }
return { token: null }; return { token: null };
} }
export function parseSSRCookie(context: any) {
return SSRCookie.parse(context.req.headers.cookie ?? '');
export function parseSSRSession(context: any) {
const cookies = context.req.headers.cookie || '';
const parsedCookies = Object.fromEntries(
cookies.split('; ').map((cookie) => cookie.split('='))
);
return parsedCookies;
} }
export function hasAccess( export function hasAccess(
_allowedRoles: string[], _allowedRoles: string[],
_userPermissions: string[] | undefined | null,
_userPermissions: string[] | undefined | null
) { ) {
// if (_userPermissions) {
// return Boolean(
// _allowedRoles?.find((aRole) => _userPermissions.includes(aRole)),
// );
// }
return true;
return true; // Adjust logic here if permissions need to be verified
} }
export function useHasShop(slug) { export function useHasShop(slug) {
const { const {
data, data,
isLoading: loading, isLoading: loading,
error, error,
} = useShopQuery(); } = useShopQuery();
if (slug) { if (slug) {
return data?.merchant_info.slug
return data?.merchant_info?.slug;
} }
if (data?.id) { if (data?.id) {
return true
return true;
} }
// if (_userPermissions) {
// return Boolean(
// _allowedRoles?.find((aRole) => _userPermissions.includes(aRole)),
// );
// }
return false; return false;
} }
export function isAuthenticated(_cookies: any) {
export function isAuthenticated(): boolean {
if (typeof window === "undefined") {
return false; // If on the server, no session storage
}
const token = sessionStorage.getItem(TOKEN);
const permissions = sessionStorage.getItem(PERMISSIONS);
return ( return (
!!_cookies[TOKEN] &&
Array.isArray(_cookies[PERMISSIONS]) &&
!!_cookies[PERMISSIONS].length
!!token &&
permissions &&
Array.isArray(JSON.parse(permissions)) &&
!!JSON.parse(permissions).length
); );
} }
Loading…
Cancel
Save