Commit ca8f7144 by Arjun Jhukal

updated the chatbot

parent bb332c15
"use client";
import Chatbot from '@/components/atom/ChatbotIcon';
import DashboardLayout from '@/components/layouts/DashboardLayout';
import AgeVerificationModal from '@/components/organism/dialog';
import { useSearchParams } from 'next/navigation';
......@@ -20,6 +21,7 @@ function LayoutContent({ children }: { children: React.ReactNode }) {
<DashboardLayout>
{children}
<AgeVerificationModal />
<Chatbot />
</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 @@
import { InputLabel, OutlinedInput } from "@mui/material";
import { CloseCircle } from "@wandersonalwes/iconsax-react";
import React from "react";
import React, { useEffect, useMemo } from "react";
interface InputFileProps {
name: string;
......@@ -33,31 +33,89 @@ export default function InputFile({
serverFile,
onRemoveServerFile,
}: 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 files = e.target.files;
if (!files) return;
if (multiple) {
onChange(Array.from(files));
} else {
onChange(files[0]);
}
// reset input
e.target.value = "";
};
const handleRemoveFile = (fileToRemove: File) => {
if (!Array.isArray(value)) return;
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 (
<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>}
</InputLabel>
{/* Clickable Input */}
<div className="input_box relative">
<OutlinedInput
fullWidth
......@@ -65,24 +123,19 @@ export default function InputFile({
name={name}
type="text"
readOnly
// value={
// Array.isArray(value)
// ? value.map((f) => f.name).join(", ") || ""
// : value instanceof File
// ? value.name
// : ""
// }
// placeholder="Choose file"
onClick={() => document.getElementById(`${name}-file`)?.click()}
onClick={() =>
document.getElementById(`${name}-file`)?.click()
}
error={Boolean(touched && error)}
sx={{
cursor: "pointer",
}}
sx={{ 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>
{/* Hidden file input */}
{/* Hidden File Input */}
<input
type="file"
id={`${name}-file`}
......@@ -90,93 +143,120 @@ export default function InputFile({
accept={accept}
hidden
multiple={multiple}
onChange={(e) => {
handleFileChange(e);
e.target.value = "";
}}
onChange={handleFileChange}
onBlur={onBlur}
/>
{/* Preview thumbnails */}
<div className="flex gap-3 flex-wrap mt-2">
{value &&
(Array.isArray(value) ? (
value.map((f) => (
<div
key={f.name}
className="relative w-[80px] h-[80px] rounded-lg overflow-hidden border border-gray-200"
>
<img
src={URL.createObjectURL(f)}
alt={f.name}
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}
{/* Preview Section */}
<div className="flex gap-3 flex-wrap mt-3">
{/* Local File Preview */}
{previewFiles.map(({ file, url, isVideo }) => (
<div
key={file.name + url}
className="relative w-[90px] h-[90px] rounded-lg overflow-hidden border border-gray-200"
>
{isVideo ? (
<video
src={url}
className="w-full h-full object-cover"
autoPlay
loop
muted
playsInline
/>
<CloseCircle
size={16}
className="absolute top-1 right-1 cursor-pointer text-white hover:text-red-500"
onClick={() => onChange(null)}
) : (
<img
src={url}
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 &&
(Array.isArray(serverFile) ? (
serverFile.map((f) => (
(Array.isArray(serverFile)
? serverFile.map((url) => (
<div
key={f}
className="relative w-[80px] h-[80px] rounded-lg overflow-hidden border border-gray-200"
key={url}
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
size={16}
className="absolute top-1 right-1 cursor-pointer text-white hover:text-red-500"
size={18}
className="absolute top-1 right-1 cursor-pointer text-white hover:text-red-500 drop-shadow"
onClick={() =>
onRemoveServerFile && onRemoveServerFile(f)
onRemoveServerFile?.(url)
}
/>
</div>
))
) : (
<div
key={serverFile}
className="relative w-[80px] h-[80px] rounded-lg overflow-hidden border border-gray-200"
>
<img
src={serverFile}
alt={serverFile}
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={() =>
onRemoveServerFile && onRemoveServerFile(serverFile)
}
/>
</div>
))}
: (
<div
key={serverFile}
className="relative w-[90px] h-[90px] rounded-lg overflow-hidden border border-gray-200"
>
{isVideoUrl(serverFile) ? (
<video
src={serverFile}
className="w-full h-full object-cover"
autoPlay
loop
muted
playsInline
/>
) : (
<img
src={serverFile}
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>
{/* 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>
);
......
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";
import PageHeader from '@/components/molecules/PageHeader'
import React, { useState } from 'react'
import SiteSetting from './SiteSetting'
import AdminProfile from './AdminProfile'
import PageHeader from '@/components/molecules/PageHeader';
import { useState } from 'react';
import AdminProfile from './AdminProfile';
import BannerSlider from './BannerSlider';
import Chatbot from './Chatbot';
import SiteSetting from './SiteSetting';
export default function SettingPage() {
// Track the active tab index
......@@ -14,6 +15,7 @@ export default function SettingPage() {
{ title: "Site Settings", content: <SiteSetting /> },
{ title: "My Profile", content: <AdminProfile /> },
{ title: "Banner Slider", content: <BannerSlider /> },
{ title: "Chatbot", content: <Chatbot /> },
];
return (
......
import { GlobalResponse } from "@/types/config";
import { BannerResponseProps, ChatbotProps, SiteSettingResponseProps } from "@/types/setting";
import { createApi } from "@reduxjs/toolkit/query/react";
import { baseQuery } from "./baseQuery";
import { GlobalResponse } from "@/types/config";
import { BannerResponseProps, SiteSettingResponseProps } from "@/types/setting";
export const settingApi = createApi({
reducerPath: "settingApi",
baseQuery: baseQuery,
tagTypes: ['settings', 'banners'],
tagTypes: ['settings', 'banners', "Chatbot"],
endpoints: (builder) => ({
updateSetting: builder.mutation<GlobalResponse, FormData>({
query: (body) => ({
......@@ -39,7 +39,30 @@ export const settingApi = createApi({
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;
\ No newline at end of file
export const {
useUpdateSettingMutation,
useGetSettingsQuery,
useUpdateBannerMutation,
useGetAllBannerQuery,
useUpdateChatbotMutation,
useGetChatbotSettingQuery
} = settingApi;
\ No newline at end of file
......@@ -65,4 +65,12 @@ export interface BannerResponseProps {
message: string;
success: boolean;
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