Commit ca8f7144 by Arjun Jhukal

updated the chatbot

parent bb332c15
"use client"; "use client";
import Chatbot from '@/components/atom/ChatbotIcon';
import DashboardLayout from '@/components/layouts/DashboardLayout'; import DashboardLayout from '@/components/layouts/DashboardLayout';
import AgeVerificationModal from '@/components/organism/dialog'; import AgeVerificationModal from '@/components/organism/dialog';
import { useSearchParams } from 'next/navigation'; import { useSearchParams } from 'next/navigation';
...@@ -20,6 +21,7 @@ function LayoutContent({ children }: { children: React.ReactNode }) { ...@@ -20,6 +21,7 @@ function LayoutContent({ children }: { children: React.ReactNode }) {
<DashboardLayout> <DashboardLayout>
{children} {children}
<AgeVerificationModal /> <AgeVerificationModal />
<Chatbot />
</DashboardLayout> </DashboardLayout>
) )
} }
......
import { useGetChatbotSettingQuery } from '@/services/settingApi';
import { Button, Typography } from '@mui/material';
import Image from 'next/image';
export default function Chatbot() {
const { data } = useGetChatbotSettingQuery();
const fileUrl = data?.data?.chatbot_image_url;
const label = data?.data?.chatbot_label;
const isVideo = fileUrl?.toLowerCase().endsWith(".mp4");
return (
<Button
className="fixed! bottom-2 right-2 lg:bottom-4 lg:right-4 max-w-fit px-8!"
variant="contained"
color="primary"
fullWidth
LinkComponent={"a"}
href={data?.data?.chatbot_link || ""}
target='_black'
sx={{
justifyContent: "start"
}}
>
<div className=" w-full flex! justify-start! items-center! gap-4">
{fileUrl && (
isVideo ? (
<video
autoPlay
loop
muted
playsInline
className="w-11 h-11 rounded-full object-cover"
>
<source src={fileUrl} type="video/mp4" />
</video>
) : (
<Image
src={fileUrl}
alt="chatbot"
width={44}
height={44}
className="rounded-full object-cover"
/>
)
)}
<Typography variant="subtitle2">
{label}
</Typography>
</div>
</Button>
);
}
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import { InputLabel, OutlinedInput } from "@mui/material"; import { InputLabel, OutlinedInput } from "@mui/material";
import { CloseCircle } from "@wandersonalwes/iconsax-react"; import { CloseCircle } from "@wandersonalwes/iconsax-react";
import React from "react"; import React, { useEffect, useMemo } from "react";
interface InputFileProps { interface InputFileProps {
name: string; name: string;
...@@ -33,31 +33,89 @@ export default function InputFile({ ...@@ -33,31 +33,89 @@ export default function InputFile({
serverFile, serverFile,
onRemoveServerFile, onRemoveServerFile,
}: InputFileProps) { }: InputFileProps) {
/* =========================
Helpers
========================== */
const isVideoFile = (file: File) => file.type.startsWith("video/");
const isVideoUrl = (url: string) =>
/\.(mp4|webm|ogg|mov)$/i.test(url);
/* =========================
File Change Handler
========================== */
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files; const files = e.target.files;
if (!files) return; if (!files) return;
if (multiple) { if (multiple) {
onChange(Array.from(files)); onChange(Array.from(files));
} else { } else {
onChange(files[0]); onChange(files[0]);
} }
// reset input
e.target.value = "";
}; };
const handleRemoveFile = (fileToRemove: File) => { const handleRemoveFile = (fileToRemove: File) => {
if (!Array.isArray(value)) return; if (!Array.isArray(value)) return;
const updatedFiles = value.filter((f) => f !== fileToRemove); const updatedFiles = value.filter((f) => f !== fileToRemove);
onChange(updatedFiles.length > 0 ? updatedFiles : null); onChange(updatedFiles.length ? updatedFiles : null);
}; };
// const fileChosen = /* =========================
// (Array.isArray(value) && value.length > 0) || value || serverFile; Generate Preview URLs
========================== */
const previewFiles = useMemo(() => {
if (!value) return [];
if (Array.isArray(value)) {
return value.map((file) => ({
file,
url: URL.createObjectURL(file),
isVideo: isVideoFile(file),
}));
}
return [
{
file: value,
url: URL.createObjectURL(value),
isVideo: isVideoFile(value),
},
];
}, [value]);
/* =========================
Cleanup Object URLs
========================== */
useEffect(() => {
return () => {
previewFiles.forEach((item) => {
URL.revokeObjectURL(item.url);
});
};
}, [previewFiles]);
/* =========================
Render
========================== */
return ( return (
<div className="input__field"> <div className="input__field">
<InputLabel htmlFor={name} className="block text-sm font-semibold mb-2"> <InputLabel
htmlFor={name}
className="block text-sm font-semibold mb-2"
>
{label} {required && <span className="text-red-500">*</span>} {label} {required && <span className="text-red-500">*</span>}
</InputLabel> </InputLabel>
{/* Clickable Input */}
<div className="input_box relative"> <div className="input_box relative">
<OutlinedInput <OutlinedInput
fullWidth fullWidth
...@@ -65,24 +123,19 @@ export default function InputFile({ ...@@ -65,24 +123,19 @@ export default function InputFile({
name={name} name={name}
type="text" type="text"
readOnly readOnly
// value={ onClick={() =>
// Array.isArray(value) document.getElementById(`${name}-file`)?.click()
// ? value.map((f) => f.name).join(", ") || "" }
// : value instanceof File
// ? value.name
// : ""
// }
// placeholder="Choose file"
onClick={() => document.getElementById(`${name}-file`)?.click()}
error={Boolean(touched && error)} error={Boolean(touched && error)}
sx={{ sx={{ cursor: "pointer" }}
cursor: "pointer",
}}
/> />
<span className=" absolute left-2 top-1/2 translate-y-[-50%] text-[11px] text-title bg-[#D8D8DD] inline-block py-1 px-2 z-[-1] rounded-sm">Choose File</span>
<span className="absolute left-2 top-1/2 -translate-y-1/2 text-[11px] text-title bg-[#D8D8DD] inline-block py-1 px-2 z-[-1] rounded-sm">
Choose File
</span>
</div> </div>
{/* Hidden file input */} {/* Hidden File Input */}
<input <input
type="file" type="file"
id={`${name}-file`} id={`${name}-file`}
...@@ -90,93 +143,120 @@ export default function InputFile({ ...@@ -90,93 +143,120 @@ export default function InputFile({
accept={accept} accept={accept}
hidden hidden
multiple={multiple} multiple={multiple}
onChange={(e) => { onChange={handleFileChange}
handleFileChange(e);
e.target.value = "";
}}
onBlur={onBlur} onBlur={onBlur}
/> />
{/* Preview thumbnails */} {/* Preview Section */}
<div className="flex gap-3 flex-wrap mt-2"> <div className="flex gap-3 flex-wrap mt-3">
{value &&
(Array.isArray(value) ? ( {/* Local File Preview */}
value.map((f) => ( {previewFiles.map(({ file, url, isVideo }) => (
<div <div
key={f.name} key={file.name + url}
className="relative w-[80px] h-[80px] rounded-lg overflow-hidden border border-gray-200" className="relative w-[90px] h-[90px] rounded-lg overflow-hidden border border-gray-200"
> >
<img {isVideo ? (
src={URL.createObjectURL(f)} <video
alt={f.name} src={url}
className="w-full h-full object-cover"
/>
<CloseCircle
size={16}
className="absolute top-1 right-1 cursor-pointer text-white hover:text-red-500"
onClick={() => handleRemoveFile(f)}
/>
</div>
))
) : (
<div
key={value.name}
className="relative w-[80px] h-[80px] rounded-lg overflow-hidden border border-gray-200"
>
<img
src={URL.createObjectURL(value)}
alt={value.name}
className="w-full h-full object-cover" className="w-full h-full object-cover"
autoPlay
loop
muted
playsInline
/> />
<CloseCircle ) : (
size={16} <img
className="absolute top-1 right-1 cursor-pointer text-white hover:text-red-500" src={url}
onClick={() => onChange(null)} alt={file.name}
className="w-full h-full object-cover"
/> />
</div> )}
))}
{/* Server File preview */} <CloseCircle
size={18}
className="absolute top-1 right-1 cursor-pointer text-white hover:text-red-500 drop-shadow"
onClick={() =>
Array.isArray(value)
? handleRemoveFile(file)
: onChange(null)
}
/>
</div>
))}
{/* Server File Preview */}
{serverFile && {serverFile &&
(Array.isArray(serverFile) ? ( (Array.isArray(serverFile)
serverFile.map((f) => ( ? serverFile.map((url) => (
<div <div
key={f} key={url}
className="relative w-[80px] h-[80px] rounded-lg overflow-hidden border border-gray-200" className="relative w-[90px] h-[90px] rounded-lg overflow-hidden border border-gray-200"
> >
<img src={f} alt={f} className="w-full h-full object-cover" /> {isVideoUrl(url) ? (
<video
src={url}
className="w-full h-full object-cover"
autoPlay
loop
muted
playsInline
/>
) : (
<img
src={url}
alt={url}
className="w-full h-full object-cover"
/>
)}
<CloseCircle <CloseCircle
size={16} size={18}
className="absolute top-1 right-1 cursor-pointer text-white hover:text-red-500" className="absolute top-1 right-1 cursor-pointer text-white hover:text-red-500 drop-shadow"
onClick={() => onClick={() =>
onRemoveServerFile && onRemoveServerFile(f) onRemoveServerFile?.(url)
} }
/> />
</div> </div>
)) ))
) : ( : (
<div <div
key={serverFile} key={serverFile}
className="relative w-[80px] h-[80px] rounded-lg overflow-hidden border border-gray-200" className="relative w-[90px] h-[90px] rounded-lg overflow-hidden border border-gray-200"
> >
<img {isVideoUrl(serverFile) ? (
src={serverFile} <video
alt={serverFile} src={serverFile}
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> autoPlay
<CloseCircle loop
size={16} muted
className="absolute top-1 right-1 cursor-pointer text-white hover:text-red-500" playsInline
onClick={() => />
onRemoveServerFile && onRemoveServerFile(serverFile) ) : (
} <img
/> src={serverFile}
</div> alt={serverFile}
))} className="w-full h-full object-cover"
/>
)}
<CloseCircle
size={18}
className="absolute top-1 right-1 cursor-pointer text-white hover:text-red-500 drop-shadow"
onClick={() =>
onRemoveServerFile?.(serverFile)
}
/>
</div>
))}
</div> </div>
{/* Error */}
{touched && error && ( {touched && error && (
<span className="text-red-500 text-xs mt-1">{error}</span> <span className="text-red-500 text-xs mt-1 block">
{error}
</span>
)} )}
</div> </div>
); );
......
import InputFile from '@/components/atom/InputFile';
import { useAppDispatch } from '@/hooks/hook';
import { useGetChatbotSettingQuery, useUpdateChatbotMutation } from '@/services/settingApi';
import { showToast, ToastVariant } from '@/slice/toastSlice';
import { Button, InputLabel, OutlinedInput } from '@mui/material';
import { useFormik } from 'formik';
import * as Yup from "yup";
const validationSchema = Yup.object({
chatbot_label: Yup.string().required("Label is required"),
chatbot_link: Yup.string().required("Link is required"),
});
export default function Chatbot() {
const dispatch = useAppDispatch();
const { data } = useGetChatbotSettingQuery();
const [updateChatbotSetting, { isLoading }] = useUpdateChatbotMutation();
const formik = useFormik({
enableReinitialize: true,
initialValues: {
chatbot_label: data?.data?.chatbot_label || "",
chatbot_link: data?.data?.chatbot_link || "",
chatbot_image: null as File | null,
chatbot_image_url: data?.data?.chatbot_image_url || "",
},
validationSchema,
onSubmit: async (values) => {
try {
const formData = new FormData();
formData.append("chatbot_label", values.chatbot_label);
formData.append("chatbot_link", values.chatbot_link);
if (values.chatbot_image) {
formData.append("chatbot_image", values.chatbot_image);
}
const response = await updateChatbotSetting(formData).unwrap();
dispatch(
showToast({
variant: ToastVariant.SUCCESS,
message: response?.message || "Chatbot settings updated successfully",
})
);
} catch (e: any) {
dispatch(
showToast({
variant: ToastVariant.ERROR,
message: e?.data?.message || "Something went wrong",
})
);
}
},
});
return (
<form
onSubmit={formik.handleSubmit}
className="border border-gray rounded-[16px] mb-6"
>
<div className="py-6 px-10 border-b border-gray">
<h2 className="text-[20px] font-bold">Chatbot Settings</h2>
</div>
<div className="p-6 lg:p-10 space-y-6">
{/* Label */}
<div>
<InputLabel>
Label<span className="text-red-500">*</span>
</InputLabel>
<OutlinedInput
fullWidth
name="chatbot_label"
placeholder="Enter Label"
value={formik.values.chatbot_label}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
/>
<span className="text-red-500 text-sm">
{formik.touched.chatbot_label && formik.errors.chatbot_label}
</span>
</div>
{/* Link */}
<div>
<InputLabel>
Link<span className="text-red-500">*</span>
</InputLabel>
<OutlinedInput
fullWidth
name="chatbot_link"
placeholder="Enter Link"
value={formik.values.chatbot_link}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
/>
<span className="text-red-500 text-sm">
{formik.touched.chatbot_link && formik.errors.chatbot_link}
</span>
</div>
{/* Single Image Upload */}
<div>
<InputFile
name="chatbot_image"
label="Chatbot Icon"
value={formik.values.chatbot_image}
onChange={(file: File | File[] | null) =>
formik.setFieldValue("chatbot_image", file)
}
serverFile={formik.values.chatbot_image_url}
onRemoveServerFile={() =>
formik.setFieldValue("chatbot_image_url", "")
}
/>
</div>
<Button
type="submit"
variant="contained"
disabled={isLoading}
>
{isLoading ? "Updating..." : "Update"}
</Button>
</div>
</form>
);
}
"use client"; "use client";
import PageHeader from '@/components/molecules/PageHeader' import PageHeader from '@/components/molecules/PageHeader';
import React, { useState } from 'react' import { useState } from 'react';
import SiteSetting from './SiteSetting' import AdminProfile from './AdminProfile';
import AdminProfile from './AdminProfile'
import BannerSlider from './BannerSlider'; import BannerSlider from './BannerSlider';
import Chatbot from './Chatbot';
import SiteSetting from './SiteSetting';
export default function SettingPage() { export default function SettingPage() {
// Track the active tab index // Track the active tab index
...@@ -14,6 +15,7 @@ export default function SettingPage() { ...@@ -14,6 +15,7 @@ export default function SettingPage() {
{ title: "Site Settings", content: <SiteSetting /> }, { title: "Site Settings", content: <SiteSetting /> },
{ title: "My Profile", content: <AdminProfile /> }, { title: "My Profile", content: <AdminProfile /> },
{ title: "Banner Slider", content: <BannerSlider /> }, { title: "Banner Slider", content: <BannerSlider /> },
{ title: "Chatbot", content: <Chatbot /> },
]; ];
return ( return (
......
import { GlobalResponse } from "@/types/config";
import { BannerResponseProps, ChatbotProps, SiteSettingResponseProps } from "@/types/setting";
import { createApi } from "@reduxjs/toolkit/query/react"; import { createApi } from "@reduxjs/toolkit/query/react";
import { baseQuery } from "./baseQuery"; import { baseQuery } from "./baseQuery";
import { GlobalResponse } from "@/types/config";
import { BannerResponseProps, SiteSettingResponseProps } from "@/types/setting";
export const settingApi = createApi({ export const settingApi = createApi({
reducerPath: "settingApi", reducerPath: "settingApi",
baseQuery: baseQuery, baseQuery: baseQuery,
tagTypes: ['settings', 'banners'], tagTypes: ['settings', 'banners', "Chatbot"],
endpoints: (builder) => ({ endpoints: (builder) => ({
updateSetting: builder.mutation<GlobalResponse, FormData>({ updateSetting: builder.mutation<GlobalResponse, FormData>({
query: (body) => ({ query: (body) => ({
...@@ -39,7 +39,30 @@ export const settingApi = createApi({ ...@@ -39,7 +39,30 @@ export const settingApi = createApi({
providesTags: ['banners'] providesTags: ['banners']
}), }),
updateChatbot: builder.mutation<GlobalResponse, FormData>({
query: (body) => ({
url: "/api/admin/setting/chatbot",
method: "POST",
body: body,
}),
invalidatesTags: ['Chatbot']
}),
getChatbotSetting: builder.query<{ data: ChatbotProps }, void>({
query: () => ({
url: "/api/setting/chatbot",
method: "GET",
}),
providesTags: ['Chatbot']
}),
}) })
}) })
export const { useUpdateSettingMutation, useGetSettingsQuery, useUpdateBannerMutation, useGetAllBannerQuery } = settingApi; export const {
\ No newline at end of file useUpdateSettingMutation,
useGetSettingsQuery,
useUpdateBannerMutation,
useGetAllBannerQuery,
useUpdateChatbotMutation,
useGetChatbotSettingQuery
} = settingApi;
\ No newline at end of file
...@@ -65,4 +65,12 @@ export interface BannerResponseProps { ...@@ -65,4 +65,12 @@ export interface BannerResponseProps {
message: string; message: string;
success: boolean; success: boolean;
data: BannerProps[]; data: BannerProps[];
}
export interface ChatbotProps {
chatbot_link: string;
chatbot_image: File | null;
chatbot_image_url?: string;
chatbot_label: string;
} }
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment