diff --git a/package.json b/package.json
index a11777d..87f05eb 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
"cookie": "0.6.0",
"date-fns": "^4.1.0",
"dayjs": "1.11.10",
+ "emoji-picker-react": "^4.12.0",
"framer-motion": "10.16.4",
"i18next": "23.6.0",
"jotai": "2.5.1",
@@ -41,6 +42,7 @@
"rc-table": "7.22.2",
"react": "18.2.0",
"react-apexcharts": "1.4.1",
+ "react-audio-visualize": "^1.2.0",
"react-content-loader": "6.2.1",
"react-countdown": "2.3.5",
"react-datepicker": "4.21.0",
@@ -50,6 +52,7 @@
"react-i18next": "13.3.1",
"react-icons": "^5.4.0",
"react-laag": "2.0.5",
+ "react-media-recorder": "^1.7.1",
"react-phone-input-2": "2.15.1",
"react-query": "3.39.3",
"react-quill": "2.0.0",
diff --git a/public/image/Frame 1000005529.svg b/public/image/Frame 1000005529.svg
new file mode 100644
index 0000000..4afb24e
--- /dev/null
+++ b/public/image/Frame 1000005529.svg
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/image/State=Send.svg b/public/image/State=Send.svg
new file mode 100644
index 0000000..9754bf8
--- /dev/null
+++ b/public/image/State=Send.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/public/image/Vector.svg b/public/image/Vector.svg
new file mode 100644
index 0000000..c21a008
--- /dev/null
+++ b/public/image/Vector.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/public/image/VectorWhite.svg b/public/image/VectorWhite.svg
new file mode 100644
index 0000000..04d405d
--- /dev/null
+++ b/public/image/VectorWhite.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/public/image/bars.svg b/public/image/bars.svg
new file mode 100644
index 0000000..3d9f2eb
--- /dev/null
+++ b/public/image/bars.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/public/image/clock-fast-forward.png b/public/image/clock-fast-forward.png
new file mode 100644
index 0000000..b6e296c
Binary files /dev/null and b/public/image/clock-fast-forward.png differ
diff --git a/public/image/copy-06.svg b/public/image/copy-06.svg
new file mode 100644
index 0000000..5661b35
--- /dev/null
+++ b/public/image/copy-06.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/public/image/edit-04.svg b/public/image/edit-04.svg
new file mode 100644
index 0000000..e8acb5a
--- /dev/null
+++ b/public/image/edit-04.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/public/image/quill_checkmark-double.svg b/public/image/quill_checkmark-double.svg
new file mode 100644
index 0000000..9b0fac8
--- /dev/null
+++ b/public/image/quill_checkmark-double.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/public/image/search.svg b/public/image/search.svg
new file mode 100644
index 0000000..a9f61f6
--- /dev/null
+++ b/public/image/search.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/public/image/trash-03.svg b/public/image/trash-03.svg
new file mode 100644
index 0000000..2a879fb
--- /dev/null
+++ b/public/image/trash-03.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/components/chat/audio-message.tsx b/src/components/chat/audio-message.tsx
new file mode 100644
index 0000000..f8829dd
--- /dev/null
+++ b/src/components/chat/audio-message.tsx
@@ -0,0 +1,99 @@
+import { useRef, useState, useEffect } from "react";
+import { FaPlay, FaPause, FaCheckDouble } from "react-icons/fa";
+import { AudioVisualizer } from "react-audio-visualize";
+
+const AudioMessage = ({ audio, timestamp = "2:47 PM" }) => {
+ const audioRef = useRef(null);
+ const [isPlaying, setIsPlaying] = useState(false);
+ const [currentTime, setCurrentTime] = useState(0);
+ const [audioBlob, setAudioBlob] = useState(null);
+ const isUser = audio.by_user.account_type === "user"
+ useEffect(() => {
+ // If audio.content is a URL string, fetch and convert it to Blob
+ if (typeof audio.content === 'string') {
+ fetch(audio.content)
+ .then(res => res.blob())
+ .then(blob => {
+ setAudioBlob(blob);
+ })
+ .catch(err => console.error("Error fetching audio:", err));
+ } else if (audio.content instanceof Blob) {
+ // If audio.content is already a Blob, just set it
+ setAudioBlob(audio.content);
+ } else {
+ console.error("audio.content must be a Blob or a URL string.");
+ }
+ }, [audio.content]);
+
+ const togglePlay = () => {
+ if (!audioRef.current) return;
+ if (isPlaying) {
+ audioRef.current.pause();
+ } else {
+ audioRef.current.play();
+ }
+ };
+
+ const handlePlay = () => setIsPlaying(true);
+ const handlePause = () => setIsPlaying(false);
+ const handleEnded = () => {
+ setIsPlaying(false);
+ setCurrentTime(0);
+ };
+
+ const handleTimeUpdate = () => {
+ if (audioRef.current) {
+ setCurrentTime(audioRef.current.currentTime);
+ }
+ };
+
+ // Create a URL for the audio element if we have a blob
+ const audioUrl = audioBlob ? URL.createObjectURL(audioBlob) : null;
+
+ return (
+
+ {/* Play/Pause Button */}
+
+ {isPlaying ? : }
+
+
+ {/* Audio Visualization */}
+
+ {audioBlob && (
+
+ )}
+
+
+ {/* Timestamp and Status */}
+
+
+ {/* Hidden Audio Element */}
+ {audioUrl && (
+
+ )}
+
+ );
+};
+
+export default AudioMessage;
diff --git a/src/components/chat/chat-input.tsx b/src/components/chat/chat-input.tsx
new file mode 100644
index 0000000..5062d42
--- /dev/null
+++ b/src/components/chat/chat-input.tsx
@@ -0,0 +1,305 @@
+import { PiTrash } from "react-icons/pi";
+import Image from "next/image";
+import SendIcon from "public/assets/images/send-01.svg";
+import MicIcon from "public/assets/images/microphone-01.svg";
+import LinkIcon from "public/assets/images/link-simple.svg";
+import SmileIcon from "public/assets/images/face-smile.svg";
+import { MdClose } from "react-icons/md";
+import { useEffect, useState, useRef } from "react";
+import { useWebSocket } from "@/contexts/WebSocket.context";
+import FileInput from "../ui/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
+const ReactMediaRecorder = dynamic(
+ () => import("react-media-recorder").then((mod) => mod.ReactMediaRecorder),
+ { ssr: false }
+);
+
+// Helper function to format recording time
+const formatTime = (seconds) => {
+ const mins = Math.floor(seconds / 60)
+ .toString()
+ .padStart(2, "0");
+ const secs = (seconds % 60).toString().padStart(2, "0");
+ return `${mins}:${secs}`;
+};
+
+const Input = ({ product = {} }) => {
+ const {
+ sendMessage,
+ setLoadingMessage,
+ editingMessage,
+ setEditingMessage,
+ editMessage,
+ } = useWebSocket();
+
+ // State variables
+ const [showProduct, setShowProduct] = useState(!!product.id);
+ const [isRecording, setIsRecording] = useState(false);
+ const [recordingTime, setRecordingTime] = useState(0);
+ const [message, setMessage] = useState("");
+ const [showEmojiPicker, setShowEmojiPicker] = useState(false); // Emoji picker state
+
+ // Ref for the timer interval
+ const timerRef = useRef(null);
+ const messageInputRef = useRef(null);
+ const emojiPickerRef = useRef(null);
+ const emojiButtonRef = useRef(null); // Ref for the emoji toggle button
+
+ // Ref for sendRecording flag
+ const sendRecordingRef = useRef(false);
+
+ // Handle sending text messages
+ const handleSendMessage = () => {
+ if (message.trim()) {
+ if (Object.keys(editingMessage).length) {
+ const msg = {
+ content: message,
+ id: editingMessage.id,
+ };
+ editMessage(msg);
+ setEditingMessage({})
+ setMessage("");
+ } else {
+ sendMessage(message, product?.id);
+ setMessage("");
+ setShowProduct(false);
+ }
+ }
+ };
+
+ // Start the recording timer
+ const startTimer = () => {
+ setRecordingTime(0);
+ timerRef.current = setInterval(() => {
+ setRecordingTime((prevTime) => prevTime + 1);
+ }, 1000);
+ };
+
+ // Stop the recording timer
+ const stopTimer = () => {
+ if (timerRef.current) {
+ clearInterval(timerRef.current);
+ timerRef.current = null;
+ }
+ };
+
+ // Cleanup on component unmount
+ useEffect(() => {
+ return () => {
+ stopTimer();
+ };
+ }, []);
+
+ // Handle click outside to close the emoji picker
+ useEffect(() => {
+ const handleClickOutside = (event) => {
+ if (
+ emojiPickerRef.current &&
+ !emojiPickerRef.current.contains(event.target) &&
+ messageInputRef.current &&
+ !messageInputRef.current.contains(event.target) &&
+ emojiButtonRef.current &&
+ !emojiButtonRef.current.contains(event.target) // Ignore clicks on the emoji button
+ ) {
+ setShowEmojiPicker(false);
+ }
+ };
+
+ document.addEventListener("mousedown", handleClickOutside);
+ return () => {
+ document.removeEventListener("mousedown", handleClickOutside);
+ };
+ }, []);
+ useEffect(() => {
+ if (Object.keys(editingMessage).length) {
+ setMessage(editingMessage.content);
+ messageInputRef.current?.focus(); // Set focus on the input
+ }
+ }, [editingMessage]);
+
+ return (
+ {
+ setIsRecording(true);
+ sendRecordingRef.current = false; // Reset the flag on start
+ startTimer();
+ }}
+ onStop={(blobUrl, blob) => {
+ setIsRecording(false);
+ stopTimer();
+ if (sendRecordingRef.current) {
+ let selectedFile = blob;
+ selectedFile.status = "loading";
+ selectedFile.name = `audio ${new Date()}.wav`;
+ setLoadingMessage((prev) => [...prev, selectedFile]);
+ } else {
+ console.log("Recording discarded:", blobUrl);
+ }
+ sendRecordingRef.current = false; // Reset the flag after handling
+ }}
+ render={({ startRecording, stopRecording }) => (
+
+ {/* Product Preview */}
+ {product.id && showProduct && (
+
+
+
+
+ {product?.images && product.images.length > 0 && (
+
+ )}
+
+
+ {product.name || "Unnamed Product"}
+
+
{product.price}$
+
+
+
{setShowProduct(false)}}
+ size={20}
+ aria-label="Close product preview"
+ className="cursor-pointer"
+ />
+
+
+ )}
+
+ {!!Object.keys(editingMessage).length && (
+
+
+
+
+
+
+
+ {editingMessage.mime_type === "text"
+ ? editingMessage.content
+ : editingMessage.mime_type}
+
+
+
+
{setEditingMessage({}); setMessage("")}}
+ size={20}
+ aria-label="Close product preview"
+ className="cursor-pointer"
+ />
+
+
+ )}
+ {/* Message Input Area */}
+
+ {/* Emoji Button */}
+
{
+ 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"
+ >
+
+
+
+ {/* Emoji Picker */}
+ {showEmojiPicker && (
+
+ {
+ setMessage(
+ (prevMessage) => prevMessage + emojiObject.emoji
+ );
+ messageInputRef.current?.focus();
+ }}
+ emojiStyle="native"
+ skinTonesDisabled
+ previewConfig={{
+ showPreview: false,
+ }}
+ />
+
+ )}
+
+ {/* Textarea Input */}
+
+
+ )}
+ />
+ );
+};
+
+export default Input;
diff --git a/src/components/chat/contact-info.tsx b/src/components/chat/contact-info.tsx
new file mode 100644
index 0000000..966d4b6
--- /dev/null
+++ b/src/components/chat/contact-info.tsx
@@ -0,0 +1,32 @@
+import { useWebSocket } from '@/contexts/WebSocket.context';
+import Image from 'next/image';
+import React from 'react';
+
+const ContactInfo = () => {
+
+ const {roomInfo} = useWebSocket()
+
+ return (
+
+ {/* Left side: Contact image and name */}
+
+
+
+
{roomInfo?.merchant?.title}
+
{roomInfo?.merchant?.lastSeen ?? "Last Seen Recently"}
+
+
+
+ {/* Right side: Three dots menu */}
+
+ ...
+
+
+ );
+};
+
+export default ContactInfo;
diff --git a/src/components/chat/contex-menu.tsx b/src/components/chat/contex-menu.tsx
new file mode 100644
index 0000000..b3f9b9e
--- /dev/null
+++ b/src/components/chat/contex-menu.tsx
@@ -0,0 +1,79 @@
+// src/components/ContextMenu.tsx
+import { motion, AnimatePresence } from "framer-motion";
+import Image from "next/image";
+import Trash from "../../../public/image/trash-03.svg";
+import Copy from "../../../public/image/copy-06.svg";
+import Edit from "../../../public/image/edit-04.svg";
+
+interface ContextMenuProps {
+ isVisible: boolean;
+ onCopy: () => void;
+ onEdit: () => void;
+ onDelete: () => void;
+ menuRef: React.RefObject;
+ className?: string;
+ type: string;
+ isUser: boolean;
+}
+
+const ContextMenu = ({
+ isVisible,
+ onCopy,
+ onEdit,
+ onDelete,
+ menuRef,
+ className = "",
+ type,
+ isUser
+}: ContextMenuProps) => {
+ return (
+
+ {isVisible && (
+
+
+
+ Copy
+
+ {isUser && (
+ <>
+ {type === "text" && (
+
+
+ Edit
+
+ )}
+
+
+ Delete
+
+ >
+ )}
+
+ )}
+
+ );
+};
+
+export default ContextMenu;
diff --git a/src/components/chat/file-message.tsx b/src/components/chat/file-message.tsx
new file mode 100644
index 0000000..93fd75f
--- /dev/null
+++ b/src/components/chat/file-message.tsx
@@ -0,0 +1,173 @@
+import { useEffect, useState } from "react";
+import Image from "next/image";
+import { FaFileAlt, FaFileArchive, FaFile } from "react-icons/fa";
+import { FaFileCode } from "react-icons/fa6";
+import { useWebSocket } from "@/contexts/WebSocket.context";
+import { HttpClient as http } from "@/data/client/http-client";
+import Stop from "../../../public/image/Vector.svg";
+import StopWhite from "../../../public/image/VectorWhite.svg";
+
+// File type extensions
+const IMAGE_EXTENSIONS = ["jpg", "jpeg", "png", "gif", "bmp", "tiff", "webp", "svg"];
+const VIDEO_EXTENSIONS = ["mp4", "mkv", "webm", "avi", "mov", "flv", "wmv"];
+const AUDIO_EXTENSIONS = ["mp3", "wav", "ogg", "flac", "aac", "m4a"];
+
+const fileTypes = {
+ document: ["pdf", "doc", "docx", "ppt", "pptx", "xls", "xlsx", "txt", "rtf"],
+ compressed: ["zip", "rar", "tar", "gz", "7z"],
+ code: ["js", "css", "html", "json", "xml", "csv"],
+};
+
+const FileMessage = ({ file }) => {
+ const { sendFile } = useWebSocket();
+ const [percentage, setPercentage] = useState(1);
+ const isLoading = file.status === "loading";
+ const radius = 70; // Circle radius for loading animation
+ const circumference = 2 * Math.PI * radius;
+
+ // Extract the file name for display
+ const fileName = isLoading
+ ? file.name
+ : file.content.split("/").pop().split("-").slice(1).join("-");
+ const strokeDashoffset = circumference - (percentage / 100) * circumference;
+
+ const getFileType = (extension) => {
+ const ext = extension.toLowerCase();
+ if (IMAGE_EXTENSIONS.includes(ext)) return "image";
+ if (VIDEO_EXTENSIONS.includes(ext)) return "video";
+ if (AUDIO_EXTENSIONS.includes(ext)) return "audio";
+ return "file";
+ };
+
+ const getFileIcon = (ext) => {
+ if (fileTypes.document.includes(ext)) return ;
+ if (fileTypes.code.includes(ext)) return ;
+ if (fileTypes.compressed.includes(ext)) return ;
+ return ;
+ };
+
+ const formatBytes = (bytes, decimals = 2) => {
+ if (!bytes) return "0 Bytes";
+ const k = 1024;
+ const dm = decimals < 0 ? 0 : decimals;
+ const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
+ };
+
+ const handleDownload = () => {
+ if (file.content) {
+ const link = document.createElement("a");
+ link.href = file.content;
+ link.download = file.name || "download";
+ link.target = "_blank";
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ } else {
+ console.error("File URL is not available.");
+ }
+ };
+
+ const handleClose = () => {
+ console.log("Close button clicked");
+ };
+
+ const fileExtension = isLoading
+ ? file.name.split(".").pop()
+ : file.content.split("/").pop().split(".").pop();
+
+ useEffect(() => {
+ if (isLoading) {
+ const formData = new FormData();
+ formData.append("file", file, file.name);
+ http
+ .post("https://mesbahi.nwhco.ir/api/upload-tmp-media/", formData, {
+ headers: { "Content-Type": "multipart/form-data" },
+ onUploadProgress: (event) => {
+ if (event.total) {
+ const currentPercentage = Math.round(
+ (event.loaded * 100) / event.total
+ );
+ setPercentage(currentPercentage);
+ }
+ },
+ })
+ .then((res) => {
+ const fileType = getFileType(fileExtension);
+ sendFile({ ...res.data, type: fileType });
+ })
+ .catch((err) => console.error(err));
+ }
+ }, [file]);
+
+ if (isLoading) {
+ return (
+
+
+
+ {/* Background Circle */}
+
+ {/* Progress Circle */}
+
+
+
+
+
+
+
+
+ {fileName}
+
+
{formatBytes(file?.size)}
+
+
+ );
+ }
+
+ return (
+
+
+ {getFileIcon(fileExtension)}
+
+
+
+ {fileName}
+
+
{formatBytes(file?.size)}
+
+
+ );
+};
+
+export default FileMessage;
diff --git a/src/components/chat/image-message.tsx b/src/components/chat/image-message.tsx
new file mode 100644
index 0000000..6c61998
--- /dev/null
+++ b/src/components/chat/image-message.tsx
@@ -0,0 +1,50 @@
+import Image from "next/image";
+import { useRef, useState } from "react";
+import { FaPlay } from "react-icons/fa";
+
+const ImageMessage = ({ image }) => {
+ const [controls, setControls] = useState(false);
+ const video = useRef(null);
+
+ const handlePlayClick = () => {
+ setControls(true);
+ video.current.play(); // Play the video when the play button is clicked
+ };
+
+ const isUser = image?.by_user?.account_type === "user";
+
+ return (
+<>
+ {image.mime_type === "video" ? (
+
+ {/* Play Button Overlay */}
+ {!controls && (
+
+
+
+ )}
+
+
+ ) : (
+
+ )}
+ >
+ );
+};
+
+export default ImageMessage;
diff --git a/src/components/chat/message.tsx b/src/components/chat/message.tsx
new file mode 100644
index 0000000..7980141
--- /dev/null
+++ b/src/components/chat/message.tsx
@@ -0,0 +1,267 @@
+import { useState, useEffect, useCallback, useRef } from "react";
+import { useRouter } from "next/router";
+import ContextMenu from "./contex-menu"; // Import the ContextMenu component
+import Image from "next/image";
+import Seen from "public/image/quill_checkmark-double.svg";
+import UnSeen from "public/image/State=Send.svg";
+import Pending from "public/image/clock-fast-forward.png";
+import { useWebSocket } from "@/contexts/WebSocket.context";
+import Button from "../ui/button";
+import { Routes as ROUTES } from "@/config/routes";
+
+import { HttpClient as http } from "@/data/client/http-client";
+import FileMessage from "./file-message";
+import ImageMessage from "./image-message";
+import AudioMessage from "./audio-message";
+
+const Message = ({ msg }) => {
+ const isFileMessage = msg.status || msg?.mime_type === "file" ? true : false;
+ const isLoadingMessage = msg.status ? true : false;
+ const isAudioMessage = msg?.mime_type === "audio" ? true : false;
+ const isImageMessage =
+ msg?.mime_type === "image" || msg?.mime_type === "video" ? true : false;
+ const isTextMessage = msg.mime_type === "text" ? true : false;
+ if (isLoadingMessage) {
+ return ;
+ }
+
+ // if (isImageMessage) {
+
+ // return
+ // }
+ // if (isAudioMessage) {
+
+ // return
+ // }
+ const [showMenu, setShowMenu] = useState(false);
+ const [data, setData] = useState({});
+
+ const messageRef = useRef(null);
+ const menuRef = useRef(null);
+ const { roomInfo, setEditingMessage, deleteMessage } = useWebSocket();
+ const router = useRouter();
+
+ // Format time as "hh:mm AM/PM"
+ const formatTime = useCallback((timestamp = Date.now()) => {
+ const date = new Date(timestamp);
+ let hours = date.getHours();
+ const minutes = date.getMinutes();
+ const ampm = hours >= 12 ? "PM" : "AM";
+
+ hours %= 12;
+ hours = hours || 12; // Convert '0' to '12'
+ const formattedMinutes = minutes < 10 ? `0${minutes}` : minutes;
+
+ return `${hours}:${formattedMinutes} ${ampm}`;
+ }, []);
+
+
+ // Handlers for context menu actions
+ const handleCopy = useCallback(() => {
+ navigator.clipboard
+ .writeText(typeof msg === "string" ? msg : msg.content)
+ .then(() => {
+ console.log("Copied to clipboard");
+ })
+ .catch((err) => {
+ console.error("Failed to copy: ", err);
+ });
+ setShowMenu(false);
+ }, [msg]);
+
+ const handleEdit = useCallback(() => {
+ setEditingMessage(msg);
+ setShowMenu(false);
+ }, []);
+
+ const handleDelete = useCallback(() => {
+ deleteMessage(msg);
+ setShowMenu(false);
+ }, []);
+
+ // Toggle menu visibility on right-click
+ const handleContextMenu = useCallback(
+ (e) => {
+ e.preventDefault();
+ setShowMenu((prev) => !prev);
+ },
+ [setShowMenu]
+ );
+
+ // Close menu if clicked outside
+ const handleClickOutside = useCallback(
+ (e) => {
+ if (
+ menuRef.current &&
+ !menuRef.current.contains(e.target) &&
+ messageRef.current &&
+ !messageRef.current.contains(e.target)
+ ) {
+ setShowMenu(false);
+ }
+ },
+ [menuRef, messageRef]
+ );
+
+ // Handle escape key to close menu
+ const handleKeyDown = useCallback(
+ (e) => {
+ if (e.key === "Escape") {
+ setShowMenu(false);
+ }
+ },
+ [setShowMenu]
+ );
+
+ useEffect(() => {
+ if (showMenu) {
+ document.addEventListener("mousedown", handleClickOutside);
+ document.addEventListener("keydown", handleKeyDown);
+ } else {
+ document.removeEventListener("mousedown", handleClickOutside);
+ document.removeEventListener("keydown", handleKeyDown);
+ }
+
+ // Cleanup on unmount
+ return () => {
+ document.removeEventListener("mousedown", handleClickOutside);
+ document.removeEventListener("keydown", handleKeyDown);
+ };
+ }, [showMenu, handleClickOutside, handleKeyDown]);
+
+ const isStringMessage = typeof msg === "string";
+
+ const messageContent = isStringMessage ? msg : msg.content;
+ const messageKey = isStringMessage ? msg : msg.id;
+ const messageTime = isStringMessage
+ ? formatTime()
+ : formatTime(msg.created_at);
+ const isUser = !isStringMessage && msg?.by_user?.account_type === "user";
+ const isSeen = !isStringMessage && msg?.is_read;
+ const product =
+ !isStringMessage && Object.keys(msg?.product_reply).length
+ ? msg.product_reply
+ : false;
+
+ useEffect(() => {
+ if (product) {
+ http.get(`shop/products/${product.slug}/`).then((res) => {
+ setData(res.data);
+ });
+ }
+ }, [product]);
+
+ return (
+ {
+ e.stopPropagation();
+ }}
+ key={messageKey}
+ className={`rounded-lg border p-2 mb-2 max-w-[525px] flex flex-col whitespace-pre-line break-words relative ${
+ isUser
+ ? "self-end bg-[#323232] text-white"
+ : isStringMessage
+ ? "self-end bg-[#323232] text-white"
+ : "self-start bg-white text-gray-800"
+ }`}
+ tabIndex={0}
+ onKeyDown={(e) => {
+ if (e.key === "ContextMenu" || (e.shiftKey && e.key === "F10")) {
+ e.preventDefault();
+ setShowMenu(true);
+ }
+ }}
+ aria-haspopup="true"
+ aria-expanded={showMenu}
+ >
+
+
+ {/* Render file message */}
+
+ {product && (
+
+
+
+
+
+
+
+ {product?.name}
+
+
+ {roomInfo?.merchant.title}
+
+
+ {product?.price}$
+
+
+
+
+ {
+ router.push(`${ROUTES.PRODUCT}/${product.slug}`, undefined, {
+ locale: router.locale,
+ });
+ }}
+ className="w-full bg-[#C5C5C5]"
+ >
+ Product details
+
+
+ Add to cart
+
+
+
+ )}
+ {isImageMessage &&
}
+ {isFileMessage &&
}
+ {isAudioMessage &&
}
+ {isTextMessage &&
{messageContent}
}
+
+
+
+
{messageTime}
+ {isSeen && isUser && (
+
+ )}
+ {!isSeen && isUser && (
+
+ )}
+ {isStringMessage && (
+
+ )}
+
+
+
+ );
+};
+
+export default Message;
diff --git a/src/components/chat/messages-list.tsx b/src/components/chat/messages-list.tsx
new file mode 100644
index 0000000..3b556f6
--- /dev/null
+++ b/src/components/chat/messages-list.tsx
@@ -0,0 +1,71 @@
+import { useEffect, useRef, useMemo, useState } from "react";
+import Message from "./message"; // Ensure the path is correct and component is capitalized
+import { useWebSocket } from "@/contexts/WebSocket.context";
+import { get } from "lodash";
+// import noMessages from "public/assets/images/Frame 1000005529.svg";
+// import Image from "next/image";
+
+const MessageList = ({ messages }) => {
+ const chatBoxRef = useRef(null);
+ const { loadingMessage, getNextPage } = useWebSocket();
+
+ // if (messages) {
+ // return (
+ //
+ //
+ //
+ // );
+ // }
+
+ const allMessages = useMemo(() => {
+ const combined = [...(loadingMessage || []), ...(messages || [])];
+ return combined.reverse();
+ }, [messages, loadingMessage]);
+
+ useEffect(() => {
+ if (chatBoxRef.current && !(messages.length > 16)) {
+ // Scroll to the bottom whenever allMessages change
+ chatBoxRef.current.scrollTop = chatBoxRef.current.scrollHeight;
+ }
+ }, [allMessages]);
+
+ useEffect(() => {
+ const handleScroll = () => {
+ if (chatBoxRef.current && chatBoxRef.current.scrollTop === 0) {
+ getNextPage();
+ }
+ };
+
+ chatBoxRef.current?.addEventListener("scroll", handleScroll);
+ return () =>
+ chatBoxRef.current?.removeEventListener("scroll", handleScroll);
+ }, [messages]); // Include dependencies
+ return (
+ <>
+ {/* Background Image */}
+
+ {/* Chat Container */}
+
+
+ {allMessages.map((msg) => (
+
+ ))}
+
+
+ {/* Optional: Loading Indicator */}
+
+ >
+ );
+};
+
+export default MessageList;
diff --git a/src/components/layouts/admin/index.tsx b/src/components/layouts/admin/index.tsx
index 3bf46bd..21222f5 100644
--- a/src/components/layouts/admin/index.tsx
+++ b/src/components/layouts/admin/index.tsx
@@ -155,7 +155,6 @@ const AdminLayout: React.FC<{ children?: React.ReactNode }> = ({
)}
>
{children}
-
diff --git a/src/components/layouts/topbar/message-bar.tsx b/src/components/layouts/topbar/message-bar.tsx
index d01e0e7..b8a5152 100644
--- a/src/components/layouts/topbar/message-bar.tsx
+++ b/src/components/layouts/topbar/message-bar.tsx
@@ -96,142 +96,7 @@ const MessageBar = ({ user }: IProps) => {
title={t('text-messages')}
/> */}
-
-
-
- <>
-
- {t('text-messages')}
- {activeStatus?.unseen ? (
-
- {t('text-mark-all-read')}
-
- ) : (
- ''
- )}
-
-
- {conversations?.length ? (
- conversations?.map((item: any) => {
- const routes = permission
- ? Routes?.message?.details(item?.id)
- : Routes?.shopMessage?.details(item?.id);
-
- const seenMessage = (unseen: boolean) => {
- if (unseen) {
- createSeenMessage({
- id: item?.id,
- });
- }
- };
- return (
-
-
{
- router.push(`${routes}`);
- seenMessage(Boolean(item?.unseen));
- }}
- >
-
-
- {item?.unseen ? (
-
- ) : (
- ''
- )}
- {!isEmpty(item?.shop?.logo?.thumbnail) ? (
-
- ) : (
-
- )}
-
-
-
- {isEmpty(item?.latest_message?.body) ? (
-
- {item?.shop?.name}
-
- ) : (
-
- {item?.shop?.name}
-
- )}
- {item?.latest_message?.created_at ? (
-
- {dayjs().to(
- dayjs.utc(
- item?.latest_message?.created_at
- )
- )}
-
- ) : (
- ''
- )}
-
- {!isEmpty(item?.latest_message?.body) ? (
-
- {item?.latest_message?.body}
-
- ) : (
- ''
- )}
-
-
-
-
- );
- })
- ) : (
-
- {t('no-message-found')}
-
- )}
-
-
- {t('text-see-all-notification')}
-
- >
-
-
-
>
);
diff --git a/src/contexts/WebSocket.context.tsx b/src/contexts/WebSocket.context.tsx
new file mode 100644
index 0000000..fcd8474
--- /dev/null
+++ b/src/contexts/WebSocket.context.tsx
@@ -0,0 +1,341 @@
+import React, {
+ createContext,
+ ReactNode,
+ useContext,
+ useEffect,
+ useRef,
+ useState,
+} from "react";
+import { useRouter } from "next/router";
+import { useMeQuery } from "@/data/user";
+
+interface WebSocketData {
+ event: string;
+ token?: string;
+ account_type?: string;
+ page?: string;
+ per_page?: string;
+ message?: string;
+}
+
+interface ChatRoom {
+ id: string;
+ title: string;
+ avatar: string;
+ last_message: string;
+ created_at: string;
+}
+
+interface WebSocketContextType {
+ chatRooms: ChatRoom[];
+ sendMessage: (message: string, id?: number) => void;
+ sendFile: (file: File, id?: number) => void;
+ connected: boolean;
+ messages: [];
+ loadingMessage: string[];
+ roomInfo: {};
+}
+
+const WebSocketContext = createContext(null);
+
+export const useWebSocket = () => {
+ const context = useContext(WebSocketContext);
+ if (!context) {
+ throw new Error("useWebSocket must be used within a WebSocketProvider");
+ }
+ return context;
+};
+
+interface WebSocketProviderProps {
+ children: ReactNode;
+}
+
+export const WebSocketProvider: React.FC = ({
+ children,
+}) => {
+ const { user } = useMeQuery()
+ const [chatRooms, setChatRooms] = useState([]);
+ const [roomInfo, setRoomInfo] = useState([]);
+ const [messages, setMessages] = useState<[]>([]);
+ const socketRef = useRef(null);
+ const [connected, setConnected] = useState(false);
+ const [loadingMessage, setLoadingMessage] = useState([]);
+ const [editingMessage, setEditingMessage] = useState({});
+ const [roomID, setRommID] = useState();
+ const [nextPage, setNextPage] = useState(2);
+
+ const router = useRouter();
+ const userID = router.query.room;
+ const {
+ query: { slug },
+ } = router;
+
+ const getRooms = () => {
+ socketRef.current?.send(
+ JSON.stringify({
+ event: "get_rooms",
+ account_type: "user",
+ page: "1",
+ per_page: "16",
+ } as WebSocketData)
+ );
+ };
+ console.log(user);
+
+ useEffect(() => {
+ socketRef.current = new WebSocket("wss://mesbahi.nwhco.ir/chat/ws");
+ setMessages([]);
+
+ if (user?.token) {
+ socketRef.current.onopen = () => {
+ console.log("WebSocket Connected");
+
+ // Send connection request with user token
+ socketRef.current?.send(
+ JSON.stringify({
+ event: "connect",
+ token: user?.token, // Pass user token
+ })
+ );
+ if (socketRef.current?.readyState === WebSocket.OPEN) {
+ socketRef.current.send(
+ JSON.stringify({
+ event: "close_room",
+ page: "1",
+ per_page: "16",
+ })
+ );
+ getRooms();
+ if (userID && slug) {
+ socketRef.current?.send(
+ JSON.stringify({
+ event: "enter_room",
+ merchant_id: `${userID}`,
+ user_id: "",
+ page: "1",
+ per_page: "16",
+ })
+ );
+ } else if (userID) {
+ socketRef.current?.send(
+ JSON.stringify({
+ event: "enter_room",
+ room_id: `${userID}`,
+ user_id: "",
+ page: "1",
+ per_page: "16",
+ })
+ );
+ }
+ }
+ setConnected(true);
+ };
+ }
+
+ socketRef.current.onmessage = (event) => {
+
+ const data = JSON.parse(event.data);
+
+ if (data.event === "get_rooms") {
+ setChatRooms(data.rooms);
+ }
+ if (data.event === "new_message") {
+ // Update messages list when a new message is received
+ setChatRooms((prevMessages) => [data.rooms, ...prevMessages]);
+ }
+ if (data.event === "update_room") {
+ if (data.current_page > 1) {
+ // Append new messages to the existing ones
+ setMessages((prevMessages) => [ ...prevMessages , ...data.messages]);
+ setNextPage(data.current_page + 1);
+ console.log("next page received");
+ } else {
+ // Replace the messages if it's the first page
+ setMessages(data.messages);
+ }
+ setRommID(data.room_id);
+ setLoadingMessage([]);
+ getRooms();
+ setRoomInfo(data.current_room);
+ }
+
+ if (data.event === "update_room_list") {
+ // Add the new message to the messages state
+ getRooms();
+ }
+ };
+
+ socketRef.current.onerror = (error) => {
+ console.error("WebSocket error:", error);
+ };
+
+ socketRef.current.onclose = () => {
+ console.log("WebSocket closed");
+ setConnected(false);
+ };
+
+ return () => {
+ if (socketRef.current) {
+ socketRef.current.close();
+ }
+ };
+ }, [user, userID]);
+
+ const getNextPage = () => {
+ setNextPage((prevPage) => {
+ const newPage = prevPage;
+ console.log("Fetching page:", newPage);
+ socketRef.current?.send(
+ JSON.stringify({
+ event: "enter_room",
+ room_id: userID,
+ page: newPage.toString(),
+ per_page: "16",
+ })
+ );
+ return newPage;
+ });
+ };
+
+ const sendMessage = (message: string, id?: number) => {
+ setLoadingMessage((prev) => [...prev, message]);
+ if (socketRef.current && connected) {
+ if (message.trim() === "") return; // Avoid sending empty messages
+
+ let newMessage = {};
+ if (userID && slug && id) {
+ newMessage = {
+ account_type: "merchant",
+ event: "send_message_room",
+ room_id: roomID,
+ content: message,
+ content_type: "text", // Modify this based on content type (text, image, etc.)
+ product_reply: `${id}`,
+ // user_id: user?.id,
+ };
+ } else if (userID && slug) {
+ newMessage = {
+ account_type: "merchant",
+ event: "send_message_room",
+ room_id: roomID,
+ content: message,
+ content_type: "text", // Modify this based on content type (text, image, etc.)
+ // user_id: user?.id,
+ };
+ } else if (userID) {
+ newMessage = {
+ account_type: "merchant",
+ event: "send_message_room",
+ room_id: userID,
+ content: message,
+ content_type: "text", // Modify this based on content type (text, image, etc.)
+ // user_id: user?.id,
+ };
+ }
+
+ socketRef.current?.send(JSON.stringify(newMessage));
+ }
+ };
+ const sendFile = (
+ file: {
+ url: string;
+ type: string;
+ size: string;
+ },
+ id?: number
+ ) => {
+ if (socketRef.current && connected) {
+ if (!file) return; // Avoid sending empty messages
+
+ let newMessage = {};
+ if (userID && slug && id) {
+ newMessage = {
+ account_type: "merchant",
+ event: "send_message_room",
+ room_id: roomID,
+ content: file.url,
+ content_type: file.type, // Modify this based on content type (text, image, etc.)
+ content_size: file.size,
+ product_reply: `${id}`,
+ // user_id: user?.id,
+ };
+ } else if (userID && slug) {
+ newMessage = {
+ account_type: "merchant",
+ event: "send_message_room",
+ room_id: roomID,
+ content: file.url,
+ content_type: file.type, // Modify this based on content type (text, image, etc.)
+ content_size: file.size,
+ // user_id: user?.id,
+ };
+ } else if (userID) {
+ newMessage = {
+ account_type: "merchant",
+ event: "send_message_room",
+ room_id: userID,
+ content: file.url,
+ content_type: file.type, // Modify this based on content type (text, image, etc.)
+ content_size: file.size,
+ // user_id: user?.id,
+ };
+ }
+
+ socketRef.current?.send(JSON.stringify(newMessage));
+ }
+ };
+
+ const editMessage = (msg: {
+ id: string | number;
+ content: string;
+ type: string;
+ }) => {
+ if (socketRef.current && connected) {
+ socketRef.current?.send(
+ JSON.stringify({
+ event: "update_message_room",
+ content: msg.content,
+ content_type: msg.type,
+ message_id: msg.id,
+ room_id: roomID,
+ account_type: "merchant",
+ })
+ );
+ }
+ };
+
+ const deleteMessage = (msg: { id: string | number }) => {
+ if (socketRef.current && connected) {
+ socketRef.current?.send(
+ JSON.stringify({
+ event: "delete_message_room",
+ message_id: msg.id,
+ room_id: roomID,
+ account_type: "merchant",
+ })
+ );
+ }
+ };
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
index 09138ae..9a7b0ba 100644
--- a/src/pages/_app.tsx
+++ b/src/pages/_app.tsx
@@ -20,6 +20,7 @@ import type { NextPageWithLayout } from '@/types';
import { useRouter } from 'next/router';
import PrivateRoute from '@/utils/private-route';
import { Config } from '@/config';
+import { WebSocketProvider } from '@/contexts/WebSocket.context';
const Noop: React.FC<{ children?: React.ReactNode }> = ({ children }) => (
<>{children}>
);
@@ -51,24 +52,26 @@ const CustomApp = ({ Component, pageProps }: AppPropsWithLayout) => {
- <>
-
-
- {authProps ? (
-
+
+ <>
+
+
+ {authProps ? (
+
+
+
+
+
+ ) : (
-
- ) : (
-
-
-
- )}
-
-
-
- >
+ )}
+
+
+
+ >
+
diff --git a/src/pages/chat/chat-box.tsx b/src/pages/chat/chat-box.tsx
new file mode 100644
index 0000000..f8312dc
--- /dev/null
+++ b/src/pages/chat/chat-box.tsx
@@ -0,0 +1,46 @@
+import ContactInfo from "@/components/chat/contact-info";
+import MessageList from "@/components/chat/messages-list";
+import Input from "@/components/ui/input";
+import { useWebSocket } from "@/contexts/WebSocket.context";
+import noMessages from "public/image/Frame 1000005529.svg";
+import { useRouter } from "next/router";
+import Image from "next/image";
+export default function ChatBox({ product , searchValue }) {
+ const { messages } = useWebSocket();
+ const router = useRouter();
+ const userID = router.query.room;
+ // Combine and reverse messages using useMemo for optimization
+
+ // Initialize WebSocket connection when the component mounts
+
+ // Handle sending a message
+
+ // If there are no messages, show the no-message image
+ if (!userID) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ {/* Chat Content */}
+
+
+ {/* Input Bar */}
+
+ {/*
+ Send
+ */}
+
+
+ );
+}
diff --git a/src/pages/chat/contact-info.tsx b/src/pages/chat/contact-info.tsx
new file mode 100644
index 0000000..e74c9f3
--- /dev/null
+++ b/src/pages/chat/contact-info.tsx
@@ -0,0 +1,59 @@
+import { FC } from 'react';
+import { IoLocationSharp, IoMail, IoCallSharp } from 'react-icons/io5';
+import Link from '@/components/ui/link';
+import { useTranslation } from 'next-i18next';
+const mapImage = '/assets/images/map-image.jpg';
+const data = [
+ {
+ id: 1,
+ slug: '/',
+ icon: ,
+ name: 'text-address',
+ description: 'text-address-details',
+ },
+ {
+ id: 2,
+ slug: '/',
+ icon: ,
+ name: 'text-email',
+ description: 'text-email-details',
+ },
+ {
+ id: 3,
+ slug: '/',
+ icon: ,
+ name: 'text-phone',
+ description: 'text-phone-details',
+ },
+];
+interface Props {
+ image?: HTMLImageElement;
+}
+const ContactInfoBlock: FC = () => {
+ const { t } = useTranslation('common');
+ return (
+
+
+ {t('text-find-us-here')}
+
+ {data?.map((item: any) => (
+
+
+ {item.icon}
+
+
+
+ {t(`${item.name}`)}
+
+
+ {t(`${item.description}`)}
+
+
+
+ ))}
+
+
+ );
+};
+
+export default ContactInfoBlock;
diff --git a/src/pages/chat/converstions.tsx b/src/pages/chat/converstions.tsx
new file mode 100644
index 0000000..3caade9
--- /dev/null
+++ b/src/pages/chat/converstions.tsx
@@ -0,0 +1,126 @@
+import Image from "next/image";
+import Badge from "@/components/ui/badge/badge";
+import Search from "public/image/search.svg";
+import Bars from "public/image/bars.svg";
+import { useEffect, useState } from "react";
+import { useRouter } from "next/router";
+import { useWebSocket } from "@/contexts/WebSocket.context";
+import { useMeQuery } from "@/data/user";
+
+export default function Conversations() {
+ const router = useRouter();
+ const { chatRooms, connected } = useWebSocket();
+ const {data : user} = useMeQuery()
+ // States for chat rooms and search
+ const [filteredChatRooms, setFilteredChatRooms] = useState(chatRooms || []);
+ const [searchValue, setSearchValue] = useState("");
+
+ // Date formatting function
+ const formatDate = (dateString) => {
+ const date = new Date(dateString);
+ const options = { day: "numeric", month: "long" };
+ return date.toLocaleDateString("en-GB", options); // Formats date to '27 November'
+ };
+
+ // Sync WebSocket chatRooms with local state
+ useEffect(() => {
+ if (user?.token && connected) {
+ setFilteredChatRooms(chatRooms || []);
+ }
+ }, [user, connected, chatRooms]);
+
+ // Handle search
+ useEffect(() => {
+ if (searchValue.trim()) {
+ const filteredData = chatRooms.filter((item) =>
+ item.merchant.title.toLowerCase().includes(searchValue.toLowerCase())
+ );
+ setFilteredChatRooms(filteredData);
+ } else {
+ setFilteredChatRooms(chatRooms); // Reset to full data when search is cleared
+ }
+ }, [searchValue, chatRooms]);
+
+ // Handle room click
+ const handleRoomClick = (roomId, unreadCount) => {
+ const updatedRooms = filteredChatRooms.map((room) =>
+ room.id === roomId ? { ...room, unread_count: 0 } : room
+ );
+ setFilteredChatRooms(updatedRooms);
+ router.push({
+ pathname: router.pathname,
+ query: { room: roomId },
+ });
+ };
+
+ return (
+
+
+ {/* Header Section */}
+
+
+
+
+
+ setSearchValue(e.target.value)}
+ className="w-full focus:outline-none"
+ aria-label="Search conversations"
+ />
+
+
+
+
+
+
+ {/* Messages List */}
+
+ {filteredChatRooms?.length > 0 ? (
+ filteredChatRooms.map((msg) => {
+ const unreadCounts = msg?.unread_count ?? 0;
+
+ return (
+
handleRoomClick(msg?.id, unreadCounts)}
+ >
+ {/* Profile Image */}
+
+ {/* Message Info */}
+
+
+
+ {msg?.merchant?.title || "Unknown Merchant"}
+
+
+
{formatDate(msg?.created_at)}
+ {unreadCounts > 0 && (
+
+ {unreadCounts}
+
+ )}
+
+
+
+
+ {msg?.last_message?.content || "No message content"}
+
+
+
+
+ );
+ })
+ ) : (
+
No conversations found
+ )}
+
+
+
+ );
+}
diff --git a/src/pages/chat/index.tsx b/src/pages/chat/index.tsx
new file mode 100644
index 0000000..958c695
--- /dev/null
+++ b/src/pages/chat/index.tsx
@@ -0,0 +1,32 @@
+import { serverSideTranslations } from "next-i18next/serverSideTranslations";
+import Layout from '@/components/layouts/admin';
+import { GetStaticProps } from "next";
+import Conversations from "./converstions";
+import ChatBox from "./chat-box";
+
+export default function Chat() {
+ return (
+
+ );
+}
+
+Chat.Layout = Layout;
+
+export const getStaticProps: GetStaticProps = async ({ locale }) => {
+ return {
+ props: {
+ ...(await serverSideTranslations(locale!, [
+ "common",
+ "forms",
+ "menu",
+ "faq",
+ "footer",
+ ])),
+ },
+ };
+};
+// py-16 lg:py-20 px-0 max-w-5xl mx-auto space-y-4
\ No newline at end of file
diff --git a/src/pages/subscriptions/active-section.tsx b/src/pages/subscriptions/active-section.tsx
index 7f57fc7..e81da65 100644
--- a/src/pages/subscriptions/active-section.tsx
+++ b/src/pages/subscriptions/active-section.tsx
@@ -4,6 +4,7 @@ import { FaStar } from 'react-icons/fa';
import Image from 'next/image';
import background from '../../../public/image/Frame 1000005757.webp';
import { RiErrorWarningFill } from 'react-icons/ri';
+import Loader from '@/components/ui/loader/loader';
const ActiveSubscriptionSection: React.FC = () => {
const { data: activeData, isLoading: isActiveLoading } =
@@ -23,7 +24,7 @@ const ActiveSubscriptionSection: React.FC = () => {
};
if (isActiveLoading) {
- return Loading...
;
+ return ;
}
if (activeData?.status === 'no_subscription') {
diff --git a/src/pages/subscriptions/history-section.tsx b/src/pages/subscriptions/history-section.tsx
index 7e8f9e1..8a6a79d 100644
--- a/src/pages/subscriptions/history-section.tsx
+++ b/src/pages/subscriptions/history-section.tsx
@@ -4,6 +4,7 @@ import {
useGetAllSubscriptions,
useGetSubscriptionsHistory,
} from '@/data/subscription';
+import Loader from '@/components/ui/loader/loader';
const HistorySection: React.FC = () => {
const { data: historyData, isLoading: isHistoryLoading } =
@@ -31,7 +32,8 @@ const HistorySection: React.FC = () => {
return priceNumber.toFixed(0); // Removes decimals
};
if (isHistoryLoading) {
- return Loading...
;
+ return ;
+
}
console.log(historyData);
diff --git a/src/pages/subscriptions/plans-section.tsx b/src/pages/subscriptions/plans-section.tsx
index 6dfd718..e1e1413 100644
--- a/src/pages/subscriptions/plans-section.tsx
+++ b/src/pages/subscriptions/plans-section.tsx
@@ -10,6 +10,7 @@ import payPal from '../../../public/image/payments/🦆 icon _PayPal_.svg';
import Stripe from '../../../public/image/payments/🦆 icon _Stripe_.svg';
import Image from 'next/image';
import { toast } from 'react-toastify'; // Import toast
+import Loader from '@/components/ui/loader/loader';
interface Subscription {
id: number;
@@ -101,7 +102,8 @@ const PlansSection: React.FC = () => {
};
if (isAllLoading) {
- return Loading...
;
+ return ;
+
}
return (
@@ -155,7 +157,7 @@ const PlansSection: React.FC = () => {
-
+
Customizable Storefront: Merchants can personalize their
storefront with custom branding, logos, and banners to create a
@@ -163,7 +165,7 @@ const PlansSection: React.FC = () => {
-
+
Advanced Analytics Dashboard: Access detailed insights into sales,
traffic, and customer behavior to optimize listings and marketing
@@ -171,14 +173,14 @@ const PlansSection: React.FC = () => {
-
+
Bulk Listing Management: Easily upload and manage multiple
gemstone listings at once with bulk editing features.
-
+
Real-Time Inventory Tracking: Merchants can track stock levels in
real-time, ensuring they never over-sell or run out of inventory.