first commit

This commit is contained in:
2024-06-20 12:09:48 +05:30
parent e4cff65fc3
commit 24aa441388
162 changed files with 27927 additions and 0 deletions

21
.eslintrc.cjs Normal file
View File

@@ -0,0 +1,21 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
settings: { react: { version: '18.2' } },
plugins: ['react-refresh'],
rules: {
'react/jsx-no-target-blank': 'off',
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}

28
.gitignore vendored Normal file
View File

@@ -0,0 +1,28 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Other files to ignore
yarn.lock
.env

24
index.html Normal file
View File

@@ -0,0 +1,24 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/assets/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tanami Admin</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
<script>
const craftedMsg = "Crafted with ❤️ by WDI Team for a better web.";
const websiteMsg = "Website: www.wdipl.com";
const craftedStyles = 'font-size: 16px; font-family: monospace; background: #000; color: #E5195E; padding: 12px 19px; border: 1.8px dashed; border-right: 0px #000 solid';
const websiteStyles = 'font-size: 16px; font-family: monospace; background: #E5195E; color: #000; padding: 12px 19px; border: 1.8px dashed #000; border-left: 0px #000 solid';
console.log('%c' + craftedMsg + ' %c' + websiteMsg, craftedStyles, websiteStyles);
</script>
</body>
</html>

6191
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

46
package.json Normal file
View File

@@ -0,0 +1,46 @@
{
"name": "tanami-admin-dashboard",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@chakra-ui/icons": "^2.1.1",
"@chakra-ui/react": "^2.8.2",
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@hookform/resolvers": "^3.3.4",
"@reduxjs/toolkit": "^2.2.3",
"bootstrap": "5.3.3",
"dotenv": "^16.4.5",
"framer-motion": "^11.1.5",
"js-cookie": "^3.0.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.51.3",
"react-icons": "^5.1.0",
"react-quill": "^2.0.0",
"react-redux": "^9.1.1",
"react-router-dom": "^6.22.3",
"redux-persist": "^6.0.0",
"redux-persist-transform-encrypt": "^5.1.1",
"uuid": "^9.0.1",
"xlsx": "^0.18.5",
"yup": "^1.4.0"
},
"devDependencies": {
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@vitejs/plugin-react-swc": "^3.5.0",
"eslint": "^8.57.0",
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6",
"vite": "^5.2.0"
}
}

1
public/vite.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

301
src/App.css Normal file
View File

@@ -0,0 +1,301 @@
@import url("https://fonts.googleapis.com/css2?family=League+Spartan:wght@100..900&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap");
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
* {
box-sizing: border-box;
margin: 0;
padding: 0;
/* font-family: "League Spartan", sans-serif !important; */
font-family: "Poppins", sans-serif !important;
}
.pointer {
cursor: pointer !important;
}
.activee {
text-decoration: none; /* Remove underline */
font-weight: bold; /* Optionally change font weight for active link */
background: linear-gradient(
to right,
#7a45fb,
#de41b5
); /* Gradient background */
-webkit-background-clip: text; /* Clip text to the background area */
-webkit-text-fill-color: transparent; /* Fill text with the background color */
transition: all 0.3s ease-in-out;
}
.active {
background-color: #ced8e6a2;
/* background-color: #e2e8f01c; */
}
.link {
text-decoration: none;
transition: all 0.2s ease-in-out;
font-weight: 400;
}
.link:hover {
background-color: #ced8e6a2 !important;
/* background-color: #e2e8f01c !important; */
}
.active:hover {
background-color: #ced8e6a2 !important;
/* background-color: #e2e8f01c !important; */
}
.web-text-small {
font-size: 12px !important;
}
.web-text-xxsmall {
font-size: 9px !important;
}
.web-text-xsmall {
font-size: 11px !important;
}
.web-text-medium {
font-size: 13px !important;
}
.web-text-large {
font-size: 15px !important;
}
.rubix-text-dark {
color: #000000;
}
.arrow-button {
transition: all 0.5s;
}
.arrow-button:active {
background: #fff;
}
.arrow-button:focus {
background: #fff;
}
.arrow-button:hover {
background: linear-gradient(90deg, #de41b5 0%, #7a45fb 100%);
}
.greeting {
text-decoration: none; /* Remove underline */
font-weight: bold; /* Optionally change font weight for active link */
background: linear-gradient(
to right,
#7a45fb,
#de41b5
); /* Gradient background */
-webkit-background-clip: text; /* Clip text to the background area */
-webkit-text-fill-color: transparent; /* Fill text with the background color */
transition: all 0.3s ease-in-out;
}
.primary-btn {
background-color: #004118 !important;
}
.team-slider .swiper-button-next:after {
position: absolute;
top: 185px;
width: 38px;
height: 38px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50px;
font-size: 18px;
color: #000;
font-weight: 700;
right: 110px;
background-image: radial-gradient(
circle,
#ffffff,
#eee2f2,
#e7c3dc,
#e5a3ba,
#de858e
);
}
.team-slider .swiper-button-prev:after {
position: absolute;
top: 185px;
width: 38px;
height: 38px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50px;
font-size: 18px;
color: #000;
font-weight: 700;
left: 110px;
background-image: radial-gradient(
circle,
#ffffff,
#eee2f2,
#e7c3dc,
#e5a3ba,
#de858e
);
}
.text-animate {
animation-name: text;
animation-duration: 5s;
animation-iteration-count: 1;
}
/* Total scrollbar width */
::-webkit-scrollbar {
width: 4px;
height: 12px;
}
/* The track (background) of the scrollbar */
::-webkit-scrollbar-track {
background: transparent;
border-radius: 10px;
}
/* The draggable scrollbar handle */
::-webkit-scrollbar-thumb {
background: #004118;
border-radius: 10px;
cursor: grabbing;
}
/* On hover */
::-webkit-scrollbar-thumb:hover {
background: #e97d88;
}
@keyframes text {
0% {
color: #DE858E;
/* margin-bottom: -40px; */
}
30% {
letter-spacing: 10px;
/* margin-bottom: -40px; */
}
85% {
letter-spacing: 8px;
/* margin-bottom: -40px; */
}
100% {
/* margin-bottom: 20px; */
}
}
.rotate {
animation: animName 10s linear infinite;
}
@keyframes animName {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.lds-ellipsis {
display: inline-block;
position: relative;
width: 64px;
height: 10px;
}
.lds-ellipsis div {
position: absolute;
width: 10px;
height: 10px;
border-radius: 50%;
background: #fff;
animation-timing-function: cubic-bezier(0, 1, 1, 0);
}
.lds-ellipsis div:nth-child(1) {
left: 6px;
animation: lds-ellipsis1 0.6s infinite;
}
.lds-ellipsis div:nth-child(2) {
left: 6px;
animation: lds-ellipsis2 0.6s infinite;
}
.lds-ellipsis div:nth-child(3) {
left: 26px;
animation: lds-ellipsis2 0.6s infinite;
}
.lds-ellipsis div:nth-child(4) {
left: 45px;
animation: lds-ellipsis3 0.6s infinite;
}
@keyframes lds-ellipsis1 {
0% {
transform: scale(0);
}
100% {
transform: scale(1);
}
}
@keyframes lds-ellipsis3 {
0% {
transform: scale(1);
}
100% {
transform: scale(0);
}
}
@keyframes lds-ellipsis2 {
0% {
transform: translate(0, 0);
}
100% {
transform: translate(19px, 0);
}
}
/* Extra small devices (phones, 600px and down) */
@media only screen and (max-width: 600px) {
body {
font-size: 14px !important;
}
}
/* Small devices (portrait tablets and large phones, 600px and up) */
@media only screen and (min-width: 600px) {
body {
font-size: 16px !important;
}
}
/* Medium devices (landscape tablets, 768px and up) */
@media only screen and (min-width: 768px) {
body {
font-size: 18px !important;
}
}
/* Large devices (laptops/desktops, 992px and up) */
@media only screen and (min-width: 992px) {
body {
font-size: 20px !important;
}
}
/* Extra large devices (large laptops and desktops, 1200px and up) */
@media only screen and (min-width: 1200px) {
body {
font-size: 22px !important;
}
}

72
src/App.jsx Normal file
View File

@@ -0,0 +1,72 @@
import React, { useContext, useEffect, useState } from "react";
import "bootstrap/dist/css/bootstrap.min.css";
import "bootstrap/dist/js/bootstrap.bundle.min.js";
import {
BrowserRouter as Router,
Routes,
Route,
Navigate,
} from "react-router-dom";
import "./App.css"; // Import CSS file
import DefaultLayout from "./Layout/DefaultLayout";
import NotFound from "./Pages/NotFound";
import Login from "./Pages/Login";
import GlobalStateContext from "./Contexts/GlobalStateContext";
import Cookies from "js-cookie";
import NoInternetScreen from "./Pages/NoInternetScreen";
const App = () => {
// const { isAuthenticate } = useSelector((state) => state?.auth);
const { isAuthenticate } = useContext(GlobalStateContext);
const isAuthenticatedInCookie = Cookies.get("isAuthenticated");
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
const handleOnlineStatusChange = () => {
setIsOnline(navigator.onLine);
};
window.addEventListener("online", handleOnlineStatusChange);
window.addEventListener("offline", handleOnlineStatusChange);
return () => {
window.removeEventListener("online", handleOnlineStatusChange);
window.removeEventListener("offline", handleOnlineStatusChange);
};
}, []);
const PrivateRoute = ({ children }) => {
if (!isAuthenticate && isAuthenticatedInCookie !== "true") {
return <Navigate to="/login" replace />;
}
return children;
};
return (
<Router>
<Routes>
<Route path="/login" element={<Login />} />
<Route
path="/*"
element={
isOnline ? (
isAuthenticate || isAuthenticatedInCookie === "true" ? (
<DefaultLayout />
) : (
<Login />
)
) : (
<NoInternetScreen />
)
}
/>
<Route path="*" element={<NotFound />} />
</Routes>
</Router>
);
};
export default App;

View File

@@ -0,0 +1,424 @@
import {
Box,
FormControl,
FormHelperText,
FormLabel,
Input,
Stack,
Textarea,
Heading,
Button,
useToast,
Divider,
Image,
} from "@chakra-ui/react";
import React, { useState } from "react";
import fallbackImage from "../../assets/ultp-fallback-img.webp";
import { TiWarning } from "react-icons/ti";
import { motion } from "framer-motion";
import { OPACITY_ON_LOAD } from "../../Layout/animations";
import { yupResolver } from "@hookform/resolvers/yup";
import { useForm } from "react-hook-form";
import {
addCommunityBannerSchema,
addCommunitySchema,
} from "../../Validations/Validations";
import { useNavigate } from "react-router-dom";
import Loader01 from "../../Components/Loaders/Loader01";
import Header from "../Header";
import ToastBox from "../ToastBox";
import BannerMainCard from "./BannerMainCard";
const AddBanner = ({ createApi, navigateLink, title, center }) => {
const toast = useToast();
const navigate = useNavigate();
const [isLoading, setIsLoading] = useState(false);
const [selectedImage, setSelectedImage] = useState(fallbackImage);
const [largeImageData, setLargeImageData] = useState(null);
const {
register,
handleSubmit,
reset,
watch,
formState: { errors },
} = useForm({
resolver: yupResolver(addCommunityBannerSchema),
});
const formData = watch();
const onSubmit = async (data) => {
try {
setIsLoading(true);
const formData = new FormData();
formData.append("heading", data.heading);
formData.append("sub_heading", data.sub_heading);
formData.append("CTO_button_link", data.CTO_button_link);
formData.append("CTO_button_title", data.CTO_button_title);
if (selectedImage[0]) {
formData.append("banner_image", data.banner_image[0]);
}
// Trigger the mutation
createApi(formData)
.then((response) => {
// Handle the response here
// // console.log("Mutation response:", response?.data?.statusCode);
// // console.log("Mutation response:", response?.data?.message);
if (response?.data?.statusCode === 200) {
setIsLoading(false);
toast({
render: () => (
<ToastBox
status={"success"}
message={response?.data?.message}
/>
),
});
reset();
navigate(navigateLink);
} else if (response?.data?.statusCode === 500) {
setIsLoading(false);
toast({
render: () => (
<ToastBox
status={"success"}
message={response?.data?.message}
/>
),
});
}
})
.catch((error) => {
// Handle errors
// // console.error("Error creating community:", error);
setIsLoading(false);
// Handle error notification if needed
});
} catch (error) {
// Handle errors
// // console.error("Error creating community:", error);
setIsLoading(false);
}
};
const handleImageChange = (e) => {
const file = e.target.files[0];
setLargeImageData(file);
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
setSelectedImage(reader.result);
};
reader.readAsDataURL(file);
}
};
return (
<Box
{...OPACITY_ON_LOAD}
w={"100%"}
h={"100vh"}
className="overflow-auto "
display={"flex"}
flexDirection={"column"}
>
<Header title={title} />
<Box display={"flex"}>
<Box className="col-5 d-flex flex-column gap-2 pt-4">
<span className="web-text-large fw-bold rubix-text-dark">
Banner info
</span>
<span className="web-text-medium text-secondary">
Select the platform for which you need to create this campaign.
</span>
<Divider />
<span className="web-text-large fw-bold rubix-text-dark">
Banner image
</span>
<span className="web-text-medium text-secondary mb-4">
Below is the profile that will be displayed on the community page.
</span>
<Box
boxSize="sm"
className="d-flex w-100 justify-content-center flex-column align-items-center gap-3"
>
<>
{/* <Image
shadow={"md"}
rounded={8}
w={500}
h={240}
src={selectedImage}
alt="Selected Image"
/> */}
<BannerMainCard
imgLink={selectedImage}
heading={formData?.heading}
subHeading={formData?.sub_heading}
buttonTitle={formData?.CTO_button_title}
center={center}
/>
{selectedImage === fallbackImage || largeImageData === null ? (
""
) : (
<Box display={"flex"} flexDirection={"column"} w={"100%"}>
<span className="web-text-small">{largeImageData?.name}</span>
<span className="web-text-small text-secondary fst-italic">
{(largeImageData?.size / (1024 * 1024)).toFixed(2)} mb
</span>
</Box>
)}
</>
<Button
onClick={() => setSelectedImage(fallbackImage)}
backgroundColor="red.400"
color={"whitesmoke"}
transition={"0.5s"}
_hover={{
backgroundColor: "red.500",
}}
size="xs"
>
Remove
</Button>
</Box>
</Box>
<form
onSubmit={handleSubmit(onSubmit)}
className="col-7 pt-4 overflow-auto p-4"
>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Heading
</FormLabel>
<Input
{...register("heading")}
placeholder="Heading"
className="web-text-medium"
size="sm"
errorBorderColor="crimson"
isInvalid={formData?.heading?.length > 50}
// maxLength={51}
/>
<FormHelperText
color={formData?.heading?.length > 50 ? "red" : "gray.500"}
className="web-text-small"
>
If heading crosses 50 characters it will cause problem in
alignment on website.you have entered {formData?.heading?.length}{" "}
characters
</FormHelperText>
{errors.name && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " /> {errors.heading.message}
</span>
)}
</FormControl>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Sub heading
</FormLabel>
<Textarea
{...register("sub_heading")}
placeholder="Sub heading"
className="web-text-medium"
size="sm"
errorBorderColor="crimson"
isInvalid={formData?.sub_heading?.length > 230}
/>
<FormHelperText
color={formData?.sub_heading?.length > 230 ? "red" : "gray.500"}
className="web-text-small"
>
If sub heading crosses 230 characters it will cause problem in
alignment on website.you have entered{" "}
{formData?.sub_heading?.length} characters
</FormHelperText>
{errors.sub_heading && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.sub_heading.message}
</span>
)}
</FormControl>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Button title
</FormLabel>
<Input
{...register("CTO_button_title")}
placeholder="Button title"
className="web-text-medium"
size="sm"
errorBorderColor="crimson"
isInvalid={formData?.CTO_button_title?.length > 30}
/>
<FormHelperText
color={
formData?.CTO_button_title?.length > 30 ? "red" : "gray.500"
}
className="web-text-small"
>
If Button title crosses 50 characters it will cause problem in
alignment on website.you have entered{" "}
{formData?.CTO_button_title?.length} characters
</FormHelperText>
{errors.CTO_button_title && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.CTO_button_title.message}
</span>
)}
</FormControl>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Button link
</FormLabel>
<Input
{...register("CTO_button_link")}
placeholder="Button link"
className="web-text-medium"
size="sm"
/>
<FormHelperText className="web-text-small">
Please share proper linked in link here.
</FormHelperText>
{errors.CTO_button_link && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.CTO_button_link.message}
</span>
)}
</FormControl>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Banner image
</FormLabel>
{/* <ImageDropBox /> */}
<Box
borderColor="gray.300"
borderStyle="dashed"
borderWidth="2px"
rounded="md"
shadow="sm"
role="group"
transition="all 150ms ease-in-out"
_hover={{
shadow: "md",
}}
as={motion.div}
initial="rest"
animate="rest"
whileHover="hover"
height={"105px"}
className="pointer"
>
<Box position="relative" height="100%" width="100%">
<Box
position="absolute"
top="0"
left="0"
height="100%"
width="100%"
display="flex"
flexDirection="column"
>
<Stack
height="100%"
width="100%"
display="flex"
alignItems="center"
justify="center"
>
<span
className="d-flex flex-column align-items-center pointer"
spacing="1"
>
<Heading
fontSize="lg"
color="gray.700"
fontWeight="bold"
cursor={"pointer"}
>
Drop images here
</Heading>
<span
fontWeight="light"
className="web-text-large text-secondary text-center pointer"
>
or click to upload
</span>
</span>
</Stack>
</Box>
<Input
{...register("banner_image")}
type="file"
height="100%"
width="100%"
position="absolute"
top="0"
left="0"
opacity="0"
aria-hidden="true"
accept="image/*"
onChange={handleImageChange}
onDrop={handleImageChange}
// onDragEnter={startAnimation}
// onDragLeave={stopAnimation}
/>
</Box>
</Box>
{errors.banner_image && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.banner_image.message}
</span>
)}
<FormHelperText className="web-text-small">
Maximum limit of image is 10MB.
</FormHelperText>
</FormControl>
<Box className=" d-flex justify-content-end mb-0">
<Button
isLoading={isLoading}
spinner={<Loader01 />}
color={"whitesmoke"}
backgroundColor={"purple.900"}
_hover={{
backgroundColor: "purple.800",
}}
type="submit"
rounded={"sm"}
size="sm"
>
Create
</Button>
</Box>
</form>
</Box>
</Box>
);
};
export default AddBanner;

View File

@@ -0,0 +1,496 @@
import React, { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import {
useGetCommunityBannerByIdQuery,
useGetCommunityByIdQuery,
useUpdateCommunityBannerMutation,
useUpdateCommunityMutation,
} from "../../Services/api.service";
import { editCommunityBannerSchema, schemaEdit } from "../../Validations/Validations";
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import {
Box,
Divider,
FormControl,
FormHelperText,
FormLabel,
Heading,
Image,
Input,
Stack,
Textarea,
Button,
Skeleton,
useToast,
Switch,
Tag,
Text,
} from "@chakra-ui/react";
import { TiWarning } from "react-icons/ti";
import { OPACITY_ON_LOAD } from "../../Layout/animations";
import { motion } from "framer-motion";
import Loader01 from "../../Components/Loaders/Loader01";
import FullscreenLoaders from "../../Components/Loaders/FullscreenLoaders";
import fallbackImage from "../../assets/ultp-fallback-img.webp";
import Header from "../../Components/Header";
import CommunityBannerCard from "../../Pages/Community/CommunityBannerCard";
import BannerMainCard from "./BannerMainCard";
import ToastBox from "../ToastBox";
import { IMAGE_URI } from "../../Constants/Paginations";
const BannerEdit = ({isLoading, data, updateBanner, navigateTo, refetch, center}) => {
const { id } = useParams();
const toast = useToast();
const navigate = useNavigate();
const [isLoadingEdit, setIsLoadingEdit] = useState(false);
const [selectedImage, setSelectedImage] = useState();
const [largeImageData, setLargeImageData] = useState(null);
const [title, setTitle] = useState("");
const handleTitleChange = (e) => {
setTitle(e.target.value);
};
const {
register,
handleSubmit,
reset,
formState: { errors },
setValue,
watch
} = useForm({
resolver: yupResolver(editCommunityBannerSchema),
defaultValues: {
heading: data?.data?.Heading,
sub_heading: data?.data?.sub_heading,
CTO_button_title: data?.data?.CTO_button_title,
CTO_button_link: data?.data?.CTO_button_link,
},
});
// Watch form values to update preview
const formData = watch();
useEffect(() => {
if (data?.data) {
setSelectedImage(
`${IMAGE_URI}/${data?.data?.banner_image}`
);
setValue("heading", data?.data?.Heading);
setValue("sub_heading", data?.data?.sub_heading);
setValue("CTO_button_title", data?.data?.CTO_button_title);
setValue("CTO_button_link", data?.data?.CTO_button_link);
setValue("banner_image", data?.data?.banner_image);
watch()
}
}, [data, setValue]);
// useEffect(() => {
// const subscription = watch((value) => {setFormData(value)});
// return () => subscription.unsubscribe();
// }, [watch]);
const onSubmit = async (formData) => {
setIsLoadingEdit(true);
const form = new FormData();
form.append("heading", formData.heading);
form.append("sub_heading", formData.sub_heading);
form.append("CTO_button_title", formData.CTO_button_title);
form.append("CTO_button_link", formData.CTO_button_link);
if (formData.banner_image[0]) {
form.append("banner_image", formData.banner_image[0]);
}
if (formData?.banner_image === data?.data?.banner_image) {
form.delete("banner_image");
}
// Log the FormData entries
// for (const [key, value] of form.entries()) {
// // console.log(`${key}: ${value}`);
// }
const mutationResult = await updateBanner({ id: id, data: form })
.then((response) => {
// Handle the response here
// // console.log("Mutation response:", response?.data?.statusCode);
// // console.log("Mutation response:", response?.data?.message);
if (response?.data?.statusCode === 200) {
setIsLoadingEdit(false);
toast({
render: () => (
<ToastBox status={"success"} message={response?.data?.message} />
),
});
refetch()
navigate(navigateTo);
// setDeleteAlert(false);
}
})
.catch((error) => {
// // console.error("Error creating community:", error);
setIsLoadingEdit(false);
// setDeleteIsLoading(false);
// setDeleteAlert(false);
});
reset();
};
const handleImageChange = (e) => {
const file = e.target.files[0];
setLargeImageData(file);
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
setSelectedImage(reader.result);
};
reader.readAsDataURL(file);
}
};
return isLoading ? (
<FullscreenLoaders />
) : (
<Box
{...OPACITY_ON_LOAD}
overflowY={"scroll"}
w={"100%"}
h={"100vh"}
className="overflow-auto "
display={"flex"}
flexDirection={'column'}
>
<Header
title={"Banner's"}
/>
<Box display={'flex'}>
<Box className="col-5 d-flex flex-column gap-2 pt-4">
<span className="web-text-large fw-bold rubix-text-dark">
Display Info
</span>
<span className="web-text-medium text-secondary">
Select the platform for which you need to create this campaign.
</span>
<Divider />
<span className="web-text-large fw-bold rubix-text-dark">
Display banner
</span>
<span className="web-text-medium text-secondary mb-4">
Below is the profile that will be displayed on the community page.
</span>
<Box boxSize="sm"
className="d-flex w-100 justify-content-center flex-column align-items-center gap-3">
{/* <Image
shadow={"md"}
rounded={8}
w={500}
h={240}
src={selectedImage}
alt="Selected Image"
/> */}
<BannerMainCard
imgLink={selectedImage}
heading={formData?.heading}
subHeading={formData?.sub_heading}
buttonTitle={formData?.CTO_button_title}
center={center}
/>
{selectedImage === fallbackImage || largeImageData === null ? (
""
) : (
<Box display={"flex"} flexDirection={"column"} w={"100%"}>
<span className="web-text-small">{largeImageData?.name}</span>
<span className="web-text-small text-secondary fst-italic">
{(largeImageData?.size / (1024 * 1024)).toFixed(2)} mb
</span>
</Box>
)}
<Button
onClick={() => setSelectedImage(fallbackImage)}
backgroundColor="red.400"
color={"whitesmoke"}
transition={"0.5s"}
_hover={{
backgroundColor: "red.500",
}}
size="xs"
>
Remove
</Button>
</Box>
</Box>
<form
className="col-7 pt-4 overflow-auto p-4"
onSubmit={handleSubmit(onSubmit)}
>
{/* <Switch
size={"sm"}
colorScheme="teal"
onChange={() => handleUpdateStatus(item.id)}
isChecked={data?.data?.status}
/> */}
<Box className="web-text-large fw-bold mb-2 rubix-text-dark">
Status
</Box>
{data?.data?.status ? <Tag position={'sticky'} right={10} size={"sm"} variant="solid" colorScheme="teal">
Active
</Tag> : <Tag position={'sticky'} right={10} size={"sm"} variant="solid" colorScheme="red">
Inactive
</Tag>}
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Heading
</FormLabel>
<Input
{...register("heading")}
placeholder="Heading"
className="web-text-medium"
size="sm"
name="heading"
type="text"
id="heading"
errorBorderColor="crimson"
isInvalid={formData?.heading?.length > 50}
// maxLength={51}
/>
<FormHelperText
color={
formData?.heading?.length > 50
? "red"
: "gray.500"
}
className="web-text-small"
>
If heading crosses 50 characters it will cause problem in alignment on website.you have entered {formData?.heading?.length} characters
</FormHelperText>
{errors.name && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.heading.message}
</span>
)}
</FormControl>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Sub heading
</FormLabel>
<Textarea
{...register("sub_heading")}
placeholder="Sub heading"
className="web-text-medium"
size="sm"
id="sub_heading"
name="sub_heading"
errorBorderColor="crimson"
isInvalid={formData?.sub_heading?.length > 230}
/>
<FormHelperText
color={
formData?.sub_heading?.length > 230
? "red"
: "gray.500"
}
className="web-text-small"
>
If sub heading crosses 230 characters it will cause problem in alignment on website.you have entered {formData?.sub_heading?.length} characters
</FormHelperText>
{errors.sub_heading && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.sub_heading.message}
</span>
)}
</FormControl>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
CTO Button title
</FormLabel>
<Input
{...register("CTO_button_title")}
placeholder="Button title"
className="web-text-medium"
size="sm"
maxLength={90}
id="CTO_button_title"
name="CTO_button_title"
errorBorderColor="crimson"
isInvalid={formData?.CTO_button_title?.length > 30}
/>
<FormHelperText
color={
formData?.CTO_button_title?.length > 30
? "red"
: "gray.500"
}
className="web-text-small"
>
If sub heading crosses 30 characters it will cause problem in alignment on website.you have entered {formData?.CTO_button_title?.length} characters
</FormHelperText>
{errors.CTO_button_title && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.CTO_button_title.message}
</span>
)}
</FormControl>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
CTO Button link
</FormLabel>
<Input
{...register("CTO_button_link")}
placeholder="CTO_button_link link"
className="web-text-medium"
size="sm"
id="CTO_button_link"
name="CTO_button_link"
/>
{errors.CTO_button_link && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " /> {errors.CTO_button_link.message}
</span>
)}
<FormHelperText className="web-text-small">
Please share proper linked in link here.
</FormHelperText>
</FormControl>
<FormControl className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Display banner
</FormLabel>
{/* <ImageDropBox /> */}
<Box
borderColor="gray.300"
borderStyle="dashed"
borderWidth="2px"
rounded="md"
shadow="sm"
role="group"
transition="all 150ms ease-in-out"
_hover={{
shadow: "md",
}}
as={motion.div}
initial="rest"
animate="rest"
whileHover="hover"
height={"105px"}
className="pointer"
>
<Box position="relative" height="100%" width="100%">
<Box
position="absolute"
top="0"
left="0"
height="100%"
width="100%"
display="flex"
flexDirection="column"
>
<Stack
height="100%"
width="100%"
display="flex"
alignItems="center"
justify="center"
>
<span
className="d-flex flex-column align-items-center pointer"
spacing="1"
>
<Heading
fontSize="lg"
color="gray.700"
fontWeight="bold"
cursor={"pointer"}
>
Drop images here
</Heading>
<span
fontWeight="light"
className="web-text-large text-secondary text-center pointer"
>
or click to upload
</span>
</span>
</Stack>
</Box>
<Input
{...register("banner_image")}
type="file"
height="100%"
width="100%"
position="absolute"
top="0"
left="0"
opacity="0"
aria-hidden="true"
accept="image/*"
onChange={handleImageChange}
onDrop={handleImageChange}
// onDragEnter={startAnimation}
// onDragLeave={stopAnimation}
/>
</Box>
</Box>
{errors.banner_image && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.banner_image.message}
</span>
)}
<FormHelperText className="web-text-small">
Maximum limit of image is 10MB.
</FormHelperText>
</FormControl>
<Box className=" d-flex justify-content-end">
<Button
isLoading={isLoadingEdit}
spinner={<Loader01 />}
color={'whitesmoke'}
backgroundColor={'purple.900'}
_hover={{
backgroundColor: "purple.800",
}}
type="submit"
size="sm"
rounded={'sm'}
>
Update banner
</Button>
</Box>
</form>
</Box>
</Box>
);
};
export default BannerEdit;

View File

@@ -0,0 +1,63 @@
import { Box, Button, Text } from '@chakra-ui/react'
import React from 'react'
const BannerMainCard = ({heading, subHeading, buttonTitle, imgLink, center}) => {
const words = heading?.split(' ');
const firstThreeWords = words.slice(0, 3).join(' ');
const remainingWords = words.slice(3).join(' ');
return (
<Box
shadow={"md"}
rounded={8}
w={469}
h={250}
borderRadius={'sm'}
bgImage={imgLink}
bgSize="cover"
bgPosition="center"
ps={8}
pt={14}
>
<Box w={center ? "92%" : "100%"} display={'flex'} flexDirection={'column'} alignItems={center ? 'center' : "start"}>
<Text textAlign={center ? "center":'start'} w={center ? "80%" : "74%"}fontSize={"19px"} className="fw-bolder" color={"#FFFFFF"} display={'block'} as={"span"}>
<span style={{ color: '#DE858E' }}>{firstThreeWords}</span>
{' '}
<span>{remainingWords}</span>
</Text>
<Text textAlign={center ? "center":'start'} w={center ? "92%" : "74%"} fontSize={"10px"} mt={1} className=" fw-normal" color={"#ffffff"} display={'block'} as={"span"}>
{subHeading}
</Text>
<Button
fontWeight={"normal"}
fontSize={"9px"}
ps={4}
pe={4}
pt={1}
pb={1}
mt={2}
color={"#ffffff"}
_hover={{
bgGradient:"linear(to-r, #1E114B, purple)"
}}
// bg={'#1E114B'}
// bgGradient="linear(to-r, #1E114B, purple)"
variant={"outline"}
// colorScheme="purple"
rounded={4}
size={"xs"}
// border={'1px soild #fff'}
>
{buttonTitle}
</Button></Box>
</Box>
)
}
export default BannerMainCard

View File

@@ -0,0 +1,369 @@
import React, { useRef, useState } from "react";
import {
Box,
Text,
Tooltip,
HStack,
Input,
Select,
Image,
Menu,
MenuButton,
MenuList,
MenuItem,
Switch,
Portal,
useToast,
} from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { HiDotsVertical } from "react-icons/hi";
import { formatDate } from "../../Components/Functions/UTCConvertor";
import CustomAlertDialog from "../../Components/CustomAlertDialog";
import DataTable from "../DataTable/DataTable";
import Header from "../Header";
import { OPACITY_ON_LOAD } from "../../Layout/animations";
import { IMAGE_URI, TABLE_PAGINATION } from "../../Constants/Paginations";
import {
CheckCircleIcon,
ChevronLeftIcon,
ChevronRightIcon,
WarningIcon,
} from "@chakra-ui/icons";
import ToastBox from "../ToastBox";
const API_URL = import.meta.env.VITE_API_BASE_URL;
const BannerCommunity = ({
dataArray,
deleteApi,
statusUpdateApi,
title,
addLink,
viewLink,
editLink,
}) => {
// ====================================================[Hooks]===================================================================
const toast = useToast();
const [deleteAlert, setDeleteAlert] = useState(false);
const [actionId, setActionId] = useState(null);
const [actionStatus, setActionStatus] = useState(null);
const [deleteIsLoading, setDeleteIsLoading] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [statusFilter, setStatusFilter] = useState("all");
// const [pageSize, setPageSize] = useState(TABLE_PAGINATION?.size);
// const [currentPage, setCurrentPage] = useState(1);
// const [displayRange, setDisplayRange] = useState({
// start: TABLE_PAGINATION?.page,
// end: pageSize,
// });
// ====================================================[Functions]===================================================================
const handleDelete = async (bannerId, status) => {
if (status) {
return toast({
render: () => (
<ToastBox status={"warn"} message="You cant delete active banner" />
),
});
}
try {
// Trigger the mutation
setDeleteIsLoading(true);
await deleteApi(bannerId)
.then((response) => {
// Handle the response here
if (response?.data?.statusCode === 200) {
setDeleteIsLoading(false);
setDeleteAlert(false);
toast({
render: () => (
<ToastBox
status={"success"}
message={response?.data?.message}
/>
),
});
}
})
.catch((error) => {
// // console.error("Error creating community:", error);
setDeleteIsLoading(false);
setDeleteAlert(false);
});
} catch (error) {
// Handle errors
// // console.error("Error deleting community:", error);
}
};
const handleUpdateStatus = async (id, status) => {
if (status) {
return toast({
render: () => (
<ToastBox status={"warn"} message={"Please toggle another banner."} />
),
});
} else {
try {
// Trigger the mutation
await statusUpdateApi({ id })
.then((response) => {
if (response?.data?.statusCode === 201) {
toast({
render: () => <ToastBox message={response?.data?.message} />,
});
}
})
.catch((error) => {
// // console.log(error);
});
} catch (error) {
// Handle errors
// // console.error("Error updating community status:", error);
}
}
};
// ====================================================[Table Filter]================================================================
const filteredData = dataArray?.data?.data?.rows?.filter((item) => {
// Filter by name (case insensitive)
const name = item.Heading;
const searchLower = searchTerm.toLowerCase();
const nameMatches = name.toLowerCase().includes(searchLower);
const status = item.status;
const statusMatches =
statusFilter === "all" ||
(statusFilter === "active" && status === true) ||
(statusFilter === "inactive" && status === false);
return nameMatches && statusMatches;
});
// ====================================================[Table Setup]================================================================
const tableHeadRow = [
"Banner image",
"Heading",
"Sub heading",
"Button title",
"Active",
"Created At",
];
const extractedArray = filteredData?.map((item, index) => {
return {
"Banner image": (
<Image
w={150}
h={14}
rounded={4}
objectFit="cover"
src={`${IMAGE_URI}/${item.banner_image}`}
alt="Dan Abramov"
/>
),
Heading: (
<Tooltip
className="rounded-2 web-text-xsmall"
width={"fit-content"}
placement="top"
hasArrow
label={item?.Heading}
bg="blue.200"
>
<Box display={"flex"} alignItems={"center"} w={180}>
<Text as={"span"} isTruncated={true}>
{item?.Heading}
</Text>
</Box>
</Tooltip>
),
"Sub heading": (
<Tooltip
className="rounded-2 web-text-xsmall"
width={"fit-content"}
placement="top"
hasArrow
label={item?.sub_heading}
bg="blue.200"
>
<Box display={"flex"} alignItems={"center"} w={180}>
<Text as={"span"} isTruncated={true}>
{item?.sub_heading}
</Text>
</Box>
</Tooltip>
),
"Button title":
<Tooltip
className="rounded-2 web-text-xsmall"
width={"fit-content"}
placement="top"
hasArrow
label={item?.sub_heading}
bg="blue.200"
>
<Box display={"flex"} alignItems={"center"} w={180} ><Text as={'span'} isTruncated={true}>{item?.CTO_button_title}</Text></Box></Tooltip>,
Active: (
<Switch
size={"sm"}
colorScheme="purple"
onChange={() => handleUpdateStatus(item.id, item?.status)}
isChecked={item.status}
// disabled={item.status}
/>
),
"Created At": (
<span className="d-flex justify-content-between align-items-center">
<Text as={"span"} color={"gray.600"} className=" fw-bold">
{formatDate(item?.createdAt)}
</Text>
<Menu>
<MenuButton className="link p-1 rounded-1">
<HiDotsVertical className="rubix-text-dark fs-6" />
</MenuButton>
<Portal>
<MenuList minWidth="80px">
<RouterLink to={`${editLink}${item.id}`}>
<MenuItem className="web-text-medium">Edit</MenuItem>
</RouterLink>
<RouterLink to={`${viewLink}${item.id}`}>
<MenuItem className="web-text-medium">View</MenuItem>
</RouterLink>
<MenuItem
onClick={() => {
setActionId(item.id);
setDeleteAlert(true);
setActionStatus(item.status);
}}
className="web-text-medium"
>
Delete
</MenuItem>
</MenuList>
</Portal>
</Menu>
</span>
),
};
});
// ====================================================[Pagination Setup]================================================================
const paginationPrev = () => {
if (currentPage > 1) {
setCurrentPage(currentPage - 1);
updateDisplayRange(currentPage - 1);
}
};
const paginationNext = () => {
const totalPages = Math.ceil(community?.data?.data?.totalItems / pageSize);
if (currentPage < totalPages) {
setCurrentPage(currentPage + 1);
updateDisplayRange(currentPage + 1);
}
};
const updateDisplayRange = (page) => {
const start = (page - 1) * pageSize + 1;
const end = Math.min(
start + pageSize - 1,
community?.data?.data?.totalItems
);
setDisplayRange({ start, end });
};
return (
<Box
{...OPACITY_ON_LOAD}
overflowY={"scroll"}
paddingBottom={50}
height={"100vh"}
>
<Header title={title} btnTitle={`Create banner `} link={addLink} />
{/* ====================================================[ Top bar ]================================================================ */}
<Box pt={4}>
<HStack
display={"flex"}
justifyContent={"space-between"}
ps={1}
pe={1}
pb={4}
spacing="24px"
>
<Input
type="search"
width={300}
placeholder="Search..."
size="sm"
rounded="sm"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<HStack>
<Select
className="pointer web-text-small"
width={"90px"}
rounded="sm"
size="sm"
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
>
<option value="all">All</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</Select>
{/* <Select
className="pointer web-text-small"
width={"90px"}
rounded="sm"
size="sm"
value={pageSize}
onChange={(e) => setPageSize(e.target.value)}
>
<option value={pageSize}>{pageSize}</option>
<option value={20}>20 rows</option>
<option value={30}>30 rows</option>
</Select> */}
{/* <HStack>
<ChevronLeftIcon
onClick={paginationPrev}
className=" link rounded-3 pointer"
/>
<Text className="web-text-medium" as={"span"}>
{displayRange.start} - {displayRange.end} of{" "}
{dataArray?.data?.data?.totalItems}
</Text>
<ChevronRightIcon
onClick={paginationNext}
className=" link rounded-3 pointer"
/>
</HStack> */}
</HStack>
</HStack>
</Box>
{/* ====================================================[ Table ]================================================================ */}
<DataTable
emptyMessage={"We don't have any banner of this heading"}
tableHeadRow={tableHeadRow}
data={extractedArray}
isLoading={dataArray?.isLoading}
/>
{/* ====================================================[ Alert ]================================================================ */}
<CustomAlertDialog
onClose={() => setDeleteAlert(false)}
isOpen={deleteAlert}
alertHandler={() => handleDelete(actionId, actionStatus)}
message={"Are you sure you want to delete member?"}
isLoading={deleteIsLoading}
/>
</Box>
);
};
export default BannerCommunity;

View File

@@ -0,0 +1,165 @@
import React from "react";
import { useParams } from "react-router-dom";
import { useGetBuildBannerByIdQuery } from "../../Services/api.service";
import { OPACITY_ON_LOAD } from "../../Layout/animations";
import FullscreenLoaders from "../../Components/Loaders/FullscreenLoaders";
import { formatDate } from "../../Components/Functions/UTCConvertor";
import Header from "../../Components/Header";
import {
Box,
Button,
Divider,
Image,
StackDivider,
Tag,
Text,
VStack,
} from "@chakra-ui/react";
import BannerMainCard from "./BannerMainCard";
const API_URL = import.meta.env.VITE_API_BASE_URL;
const BannerView = ({data, isLoading, editLink, center}) => {
const banner = data?.data;
return isLoading ? (
<FullscreenLoaders />
) : (
<Box
{...OPACITY_ON_LOAD}
overflowY={"scroll"}
w={"100%"}
h={"100vh"}
className="overflow-auto "
display={"flex"}
flexDirection={"column"}
>
<Header
title={"Banner's"}
btnTitle={"Edit banner"}
link={`${editLink}/${banner.id}`}
/>
<Box display={"flex"}>
<Box className="col-5 d-flex flex-column gap-2 pt-4">
<span className="web-text-large fw-bold rubix-text-dark">
Banners Info
</span>
<span className="web-text-medium text-secondary">
Select the platform for which you need to create this campaign.
</span>
<Divider />
<span className="web-text-large fw-bold rubix-text-dark">
Display banner
</span>
<span className="web-text-medium text-secondary mb-4">
Below is the profile that will be displayed on the community page.
</span>
<Box
boxSize="sm"
className="d-flex w-100 justify-content-center flex-column align-items-center gap-3"
>
{/* <Image
shadow={"md"}
rounded={8}
w={500}
h={240}
src={`${API_URL}/${banner?.banner_image}`}
alt="Selected Image"
/> */}
<BannerMainCard
imgLink={`${API_URL}/${banner?.banner_image}`}
heading={banner?.Heading}
subHeading={banner?.sub_heading}
buttonTitle={banner?.CTO_button_title}
center={center}
/>
</Box>
</Box>
<Box className="col-7 pt-4 overflow-auto p-4">
<Box className="mb-3">
<Box className="web-text-large fw-bold rubix-text-dark mb-1">
Status
</Box>
{data?.data?.status ? (
<Tag size={"sm"} variant="solid" colorScheme="teal">
Active
</Tag>
) : (
<Tag size={"sm"} variant="solid" colorScheme="red">
Inactive
</Tag>
)}
</Box>
<Box className="mb-3">
<Box className="web-text-large fw-bold rubix-text-dark">
Heading
</Box>
<Box className="web-text-medium text-secondary">
{banner?.Heading}
</Box>
</Box>
<Box className="mb-3">
<Box className="web-text-large fw-bold rubix-text-dark">
Sub heading
</Box>
<Box className="web-text-medium text-secondary">
{banner?.sub_heading}
</Box>
</Box>
<Box className="mb-3">
<Box className="web-text-large fw-bold rubix-text-dark">
Button title
</Box>
<Box className="web-text-medium text-secondary">
{banner?.CTO_button_title}
</Box>
</Box>
<Box className="mb-3">
<Box className="web-text-large fw-bold rubix-text-dark">
Button link
</Box>
<Box className="web-text-medium text-secondary">
{banner?.CTO_button_link}
</Box>
</Box>
<Box className="mb-3">
<Box className="web-text-large fw-bold rubix-text-dark">
Created At
</Box>
<Box className="web-text-medium text-secondary">
{formatDate(banner?.createdAt)}
</Box>
</Box>
<Box className="mb-3">
<Box className="web-text-large fw-bold rubix-text-dark">
Updated At
</Box>
<Box className="web-text-medium text-secondary">
{formatDate(banner?.updatedAt)}
</Box>
</Box>
<Divider />
</Box>
</Box>
</Box>
);
};
export default BannerView;

View File

@@ -0,0 +1,84 @@
import { ArrowForwardIcon } from "@chakra-ui/icons";
import { Box, Button, HStack, Skeleton, Text } from "@chakra-ui/react";
import React from "react";
import { Link } from "react-router-dom";
import CommunityBannerCard from "../Pages/Community/CommunityBannerCard";
const BannerStack = ({
stackTitle,
viewAllLink,
bannerIsLoading,
bannerArray,
viewBannerLink,
}) => {
return (
<Box >
<HStack
display={"flex"}
justifyContent={"space-between"}
alignItems={"center"}
pe={1}
>
<Text as={"span"} color={"purple.900"} className="web-text-large fw-bold">
{stackTitle}
</Text>
<Link to={viewAllLink}>
<Button
variant="ghost"
rightIcon={<ArrowForwardIcon />}
colorScheme={"purple"}
size="sm"
>
View all
</Button>
</Link>
</HStack>
<Box
display={"flex"}
// bg={"red.500"}
alignItems={"start"}
flexWrap={"wrap"}
justifyContent={"start"}
w={"100%"}
// h={"auto"}
h={200}
ps={1}
>
{bannerIsLoading
? Array.from({ length: 3 }).map((_, index) => (
<Box key={index} className="col-4 p-2 ps-0">
<Skeleton w={"100%"} rounded={"md"} height={180} />
</Box>
))
: bannerArray?.map(
({
id,
CTO_button_title,
banner_image,
Heading,
createdAt,
sub_heading,
status
}) => (
<Link className="col-4 h-100 p-3 ps-0" key={id} to={`${viewBannerLink}/${id}`}>
<CommunityBannerCard
status={status}
bgImage={banner_image}
subHeading={sub_heading}
heading={Heading}
createdAt={createdAt}
ctoBtnTitle={CTO_button_title}
/>
</Link>
)
)}
</Box>
</Box>
);
};
export default BannerStack;

View File

@@ -0,0 +1,25 @@
import Loader01 from "../Loaders/Loader01";
const Button01 = ({ title, onClick, type, backgroundColor, hover, isLoading }) => {
return (
<button
style={{
backgroundColor: backgroundColor,
outline: "none",
border: "none",
height: 50,
transition: "opacity 0.3s", // Add transition for smooth hover effect
}}
type={type}
onClick={onClick ? onClick : null}
className=" rounded-3 p-2 w-100 text-white p-3 fs-6 fw-bold border-none"
onMouseEnter={(e) => (e.target.style.backgroundColor = hover)}
onMouseLeave={(e) => (e.target.style.backgroundColor = backgroundColor)}
>
{isLoading ? <Loader01/> : title}
</button>
);
};
export default Button01;

View File

@@ -0,0 +1,47 @@
import { RiLogoutCircleLine } from "react-icons/ri";
const Button02 = ({ title, onClick, type, gradientColors, height, width, open }) => {
const [startColor, endColor] = gradientColors;
return (
<button
style={{
background: `linear-gradient(to right, ${startColor}, ${endColor})`, // Apply linear gradient background
outline: "none",
border: "none",
height: height,
width: width,
opacity:0.9,
boxShadow:"rgba(50, 50, 93, 0.25) 0px 50px 100px -20px, rgba(0, 0, 0, 0.3) 0px 30px 60px -30px, rgba(10, 37, 64, 0.35) 0px -2px 6px 0px inset"
}}
type={type}
onClick={onClick ? onClick : null}
className=" rounded-4 text-white fs-6 fw-bold border-none text-center overflow-hidden d-flex align-items-center gap-2 justify-content-center"
// Add hover style
onMouseEnter={(e) =>
(e.target.style.opacity = 1)
}
onMouseLeave={(e) =>
(e.target.style.opacity = 0.9)
}
>
<RiLogoutCircleLine style={{
opacity: open ? 0 : 1,
transform: "translateX(14px)",
display: open ? 'none' : '',
transition: 'All 1s ease-in-out'
}} className="fs-4" />
<span
style={{
transform: open ? 'translateX(0px)' : 'translateX(-100px)',
fontSize: open ? '' : '6px',
// display: open ? '' : 'none',
transition: 'All 0.5s ease-in-out'
}}
>{title}</span>
</button>
);
};
export default Button02;

View File

@@ -0,0 +1,60 @@
label {
display: inline-block;
margin-bottom: 0.5rem;
font-weight: bold;
}
.input-container {
display: flex;
flex-wrap: wrap;
row-gap: 1rem;
align-items: center;
border: 1px solid rgb(212, 206, 206);
border-radius: 0.2rem;
}
.chips {
list-style: none;
display: flex;
}
.chip {
background-color: #cfe1ff;
display: flex;
align-items: center;
padding: 0.2rem;
border-radius: 0.5rem;
margin-right: 1rem;
}
.chip span {
color: #013380;
}
.chip svg {
font-size: 2.5rem;
fill: #3270d1;
cursor: pointer;
transition: fill 0.2s;
}
.chip svg:hover {
fill: #2857a3;
transition: all 0.2s;
}
input {
font-size: 1.6rem;
background: transparent;
border: none;
outline: none;
}
.error-message {
margin-top: 1rem;
color: red;
}

View File

@@ -0,0 +1,68 @@
import React, { useState } from "react";
import { Box, FormHelperText, Input, Tag, TagCloseButton, TagLabel } from "@chakra-ui/react";
import { TiWarning } from "react-icons/ti";
const ChipSelector = ({chips, setChips, type}) => {
const [text, setText] = useState("");
const [validationError, setValidationError] = useState("");
function removeChip(chipToRemove) {
const updatedChips = chips.filter((chip, index) => index !== chipToRemove);
setChips(updatedChips);
}
function handlePressEnter(e) {
if (e.key === "Enter") e.preventDefault();
if (e.key !== "Enter" || !text) return;
if (chips.includes(text)) {
return setValidationError("Cannot add the same input more than once");
}
setChips((prevState) => [...prevState, text]);
setText("");
setValidationError("");
}
return (
<div>
<Box
display={'flex'}
flexDirection={'column'}
gap={2}
className="">
<Input
type={type? type : "text"}
id="tags"
size="sm"
className="web-text-medium "
placeholder="Press Enter to add tag"
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={handlePressEnter}
/>
{validationError && <span className="error-message web-text-small text-danger d-flex align-items-center gap-1"><TiWarning className="fw-bold fs-5 " />{validationError}</span>}
<FormHelperText className="web-text-small mt-0">
{type ? "Please select and press enter to add date." : "Please type and press enter to add tags."}
</FormHelperText>
<Box
display={'flex'}
justifyContent={'start'}
flexWrap={'wrap'}
gap={2}
>
{chips?.map((chip, i) => (
<Tag key={i} size="sm" ps={3} pe={3} pt={0.5} pb={0.5} rounded="full" variant='solid' backgroundColor={'#565263'}>
<TagLabel className="text-uppercase">{chip}</TagLabel>
<TagCloseButton onClick={() => removeChip(i)} />
</Tag>
))}
</Box>
</Box>
</div>
);
};
export default ChipSelector;

View File

@@ -0,0 +1,153 @@
import { Box, Button, Container, Text } from "@chakra-ui/react";
import banner from "../assets/communityBanner.webp";
const BannerContent = [
{
heading1: `Welcome To The`,
heading2: `Rubix Community`,
},
{
subheading: `This is a space for enterprises, dApp developers, stakeholders
and blockchain advocates to aggregate resources and ideas to make a difference
through our green blockchain technology.`,
},
{
btn: `Explore our community`,
},
];
const CommunityBanner = () => {
return (
<Box
height={"75%"}
backgroundImage={`url(${banner})`}
backgroundRepeat={"no-repeat"}
backgroundSize={"cover"}
display={"grid"}
placeContent={"center"}
>
<Container
alignItems={"center"}
display={"flex"}
height={"100%"}
alignContent={"center"}
maxW="container.xl"
textAlign={"left"}
marginTop={"2rem"}
paddingLeft={"3.5rem"}
>
<Box
width={"75%"}
>
<Text
fontWeight={700}
fontSize={"30px"}
// textTransform={"upperCase"}
color={"#DE858E"}
lineHeight={"42px"}
letterSpacing={"1px"}
sx={{
"@media (max-width: 996px)": {
fontSize: "46px",
},
"@media (max-width: 600px)": {
fontSize: "40px",
marginBottom: "0rem",
lineHeight: "54px",
},
}}
>
<span
style={{
color: "#fff",
}}
>
{BannerContent[0].heading1}
</span>{" "}
<br />
{BannerContent[0].heading2}
</Text>
<Box
marginTop={"0.5rem"}
width={"80%"}
sx={{
"@media (max-width: 500px)": {
width: "100%",
},
}}
>
<Text
color={"#fff"}
fontSize={"15px"}
fontWeight={"400"}
lineHeight={"27.5px"}
fontFamily={"Poppins"}
textTransform={"capitalize"}
>
{BannerContent[1].subheading}
</Text>
</Box>
<Button
position={"relative"}
backgroundColor={"transparent"}
cursor={"pointer"}
transition="0.3s ease-in-out"
color={"#fff"}
width={"180px"}
height={"44px"}
fontFamily={"Poppins"}
fontWeight={"400"}
border={"1px solid white"}
borderRadius={"10px"}
fontSize={"12px"}
zIndex={"1"}
overflow={"hidden"}
marginTop={"0.5rem"}
sx={{
"::before": {
content: '""',
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: "65px",
height: "65px",
borderRadius: "50%",
transition: "0.35s linear",
zIndex: -1,
bgGradient:
"radial-gradient(circle, #ffffff, #eee2f2, #e7c3dc, #e5a3ba, #de858e)",
opacity: "0",
},
"&:hover::before": {
width: "100%",
height: "120%",
borderRadius: "0px",
opacity: "1",
},
"@media (max-width: 500px)": {
fontSize: "14px",
width: "230px",
height: "44px",
marginTop: "2rem",
bgGradient:
"radial-gradient(circle, #ffffff, #eee2f2, #e7c3dc, #e5a3ba, #de858e)",
color: "#000",
fontWeight: "600",
},
}}
_hover={{
color: "#000",
border: "1px solid white",
zIndex: 1,
}}
>
{BannerContent[2].btn}
</Button>
</Box>
</Container>
</Box>
);
};
export default CommunityBanner;

View File

@@ -0,0 +1,46 @@
import { AlertDialog, AlertDialogBody, AlertDialogCloseButton, AlertDialogContent, AlertDialogFooter, AlertDialogOverlay, Button, useDisclosure } from "@chakra-ui/react";
import React, { useRef } from "react";
const CustomAlertDialog = ({ isOpen, onOpen, onClose, alertHandler, isLoading, message }) => {
// const cancelRef = useRef();
return (
<AlertDialog
motionPreset="slideInBottom"
// leastDestructiveRef={cancelRef}
onClose={onClose}
isOpen={isOpen}
isCentered
>
<AlertDialogOverlay />
<AlertDialogContent w={400}>
<AlertDialogCloseButton className="web-text-xsmall link" />
<AlertDialogBody className="text-center web-text-large fw-bold" pt={8}>
{message}
</AlertDialogBody>
<AlertDialogFooter display={"flex"} justifyContent={"center"}>
<Button
size={"sm"}
// ref={cancelRef}
onClick={onClose}
>
No
</Button>
<Button
backgroundColor={"#ff6b6b"}
isLoading={isLoading}
onClick={alertHandler}
size={"sm"}
colorScheme="red"
ml={3}
>
Yes
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
export default CustomAlertDialog;

View File

@@ -0,0 +1,51 @@
import React from "react";
import { Table, TableContainer, Tbody, Td, Th, Thead, Tr, Skeleton, TableCaption, Tfoot } from "@chakra-ui/react";
import EmptySearchList from "../EmptySearchList";
const DataTable = ({ data, isLoading, tableHeadRow, emptyMessage }) => {
const columnWidth = data && data[0] ? `${(100 / Object.keys(data[0]).length).toFixed(2)}%` : "auto";
return (
<TableContainer overflowX={"hidden"} className="h-auto mb-3 w-100">
{data?.length === 0 ? (
<EmptySearchList message={emptyMessage} />
) : (
<Table size="sm">
<TableCaption>Rubix v1.0.0</TableCaption>
<Thead backgroundColor="purple.50">
<Tr>
{tableHeadRow.map((heading, index) => (
<Th key={index} p={3} w={columnWidth}>
{isLoading ? <Skeleton height="20px" /> : heading}
{/* {heading} */}
</Th>
))}
</Tr>
</Thead>
<Tbody className="web-text-small">
{isLoading
? Array.from({ length: 12 }).map((_, index) => (
<Tr key={index}>
{tableHeadRow.map((_, i) => (
<Td key={i} style={{ whiteSpace: "nowrap", textOverflow: "ellipsis" }} className="web-text-small" w={columnWidth}>
<Skeleton height="20px" mb={1} mt={1} />
</Td>
))}
</Tr>
))
: data?.map((item, index) => (
<Tr key={index}>
{tableHeadRow.map((heading, i) => (
<Td color={"gray.600"} key={i} style={{ whiteSpace: "nowrap", textOverflow: "ellipsis" }} className="web-text-small" >
{item[heading]}
</Td>
))}
</Tr>
))}
</Tbody>
</Table>
)}
</TableContainer>
);
};
export default DataTable;

View File

@@ -0,0 +1,21 @@
import { Box, Image, Text } from "@chakra-ui/react"
import EmptySearchListImage from "../assets/EmptySearchList.svg"
const EmptySearchList = ({message}) => {
return (
<Box
display={'flex'}
justifyContent={'center'}
alignItems={'center'}
flexDirection={'column'}
w={"100%"} h={"80vh"}
>
<Image w={200} mb={8} h={200} src={EmptySearchListImage} alt='Dan Abramov' />
<Text className=" fw-bold fs-5" >{message}</Text>
<Text as={'p'} className="web-text-medium">Posts of rubix will appear here.</Text>
</Box>
)
}
export default EmptySearchList

View File

@@ -0,0 +1,13 @@
import React from "react";
const DebounceFunction = (func, delay) => {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
};
export default DebounceFunction;

View File

@@ -0,0 +1,8 @@
const extractFilename = (filePath) => {
// Use the split method to break the path into parts based on '/'
const parts = filePath.split('/');
// Return the last part, which is the filename
return parts[parts.length - 1];
};
export default extractFilename ;

View File

@@ -0,0 +1,35 @@
import { useState, useEffect } from "react";
const TimeCalculator = ({ JoiningDate }) => {
const [elapsedTime, setElapsedTime] = useState(0);
useEffect(() => {
const calculateTimeElapsed = () => {
const startDate = new Date(JoiningDate);
const currentDate = new Date();
const difference = currentDate - startDate;
setElapsedTime(Math.floor(difference / 1000)); // Convert milliseconds to seconds
};
// Calculate time elapsed initially
calculateTimeElapsed();
// Update time elapsed every second
const intervalId = setInterval(calculateTimeElapsed, 1000);
// Clear interval on component unmount
return () => clearInterval(intervalId);
}, []);
// Convert seconds to years, months, days, hours, minutes, and remaining seconds
const years = Math.floor(elapsedTime / (365 * 24 * 60 * 60));
const months = Math.floor((elapsedTime % (365 * 24 * 60 * 60)) / (30 * 24 * 60 * 60)); // Approximating a month to 30 days
const days = Math.floor((elapsedTime % (30 * 24 * 60 * 60)) / (24 * 60 * 60));
const hours = Math.floor((elapsedTime % (24 * 60 * 60)) / (60 * 60));
const minutes = Math.floor((elapsedTime % (60 * 60)) / 60);
const seconds = elapsedTime % 60;
return `${years}Y ${months}M ${hours}H ${minutes}M ${seconds}S`;
};
export default TimeCalculator;

View File

@@ -0,0 +1,11 @@
import { useToast } from "@chakra-ui/react";
export const toaster = () => {
const toast = useToast();
return toast({
title: "Loged In",
status: "success",
isClosable: true,
});
}

View File

@@ -0,0 +1,16 @@
export const formatDate = (dateString) => {
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
// Create a new Date object from the provided string
const date = new Date(dateString);
// Extract day, month, and year components
const day = date.getDate();
const month = months[date.getMonth()];
const year = date.getFullYear();
// Format the date in "DD-Mon-YYYY" format
const formattedDate = `${day}-${month}-${year}`;
return formattedDate;
}

83
src/Components/Header.jsx Normal file
View File

@@ -0,0 +1,83 @@
import { AddIcon } from "@chakra-ui/icons";
import { Box, Button, Text } from "@chakra-ui/react";
import React from "react";
import { Link } from "react-router-dom";
import { IoMdDownload } from "react-icons/io";
import * as XLSX from "xlsx";
import { useGetNewsLetterEmailQuery } from "../Services/api.service";
const Header = ({ link, btnTitle, title }) => {
const { data, error, isLoading } = useGetNewsLetterEmailQuery();
const handleDownload = () => {
if (Array.isArray(data?.data?.rows)) {
const worksheet = XLSX.utils.json_to_sheet(data?.data?.rows);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, "Sheet1");
XLSX.writeFile(workbook, "newsletter_emails.xlsx");
} else {
// // console.error(
// "Expected data to be an array but received:",
// data?.data?.rows
// );
}
};
return (
<Box
backgroundColor={"#fff"}
// bg="white.900"
// backdropFilter="blur(10px) hue-rotate(90deg)"
position={"sticky"}
top={0}
me={0.5}
// zIndex={999}
className={`${
link && btnTitle ? "" : " pt-3 pb-3 "
} p-2 pe-2 ps-0 fw-400 border-bottom d-flex justify-content-between align-items-center`}
>
{/* <span className="fs-5">Community</span> */}
<Text
as={"span"}
fontWeight={"bold"}
color={"forestGreen.500"}
className="fs-6 "
>
{title}
</Text>
{/* <Text fontWeight='bold' bgGradient='linear(to-l, #A5626D, #331E8C)' bgClip='text' as={"span"} className="fs-6 ">
{title}
</Text> */}
{btnTitle ? btnTitle != "Export email" ? (
<Link to={link}>
<Button
leftIcon={<AddIcon />}
colorScheme={"forestGreen"}
size="sm"
rounded={"lg"}
>
{btnTitle}
</Button>
</Link>
) : (
<Button
leftIcon={<IoMdDownload />}
backgroundColor={"green.900"}
_hover={{
backgroundColor: "green.800",
}}
color={"whitesmoke"}
size="sm"
rounded={"sm"}
onClick={handleDownload}
>
{btnTitle}
</Button>
):""}
</Box>
);
};
export default Header;

View File

@@ -0,0 +1,110 @@
import { AddIcon } from "@chakra-ui/icons";
import {
Avatar,
Box,
Button,
Popover,
PopoverArrow,
PopoverBody,
PopoverContent,
PopoverFooter,
PopoverTrigger,
Portal,
Text,
} from "@chakra-ui/react";
import React from "react";
import { Link } from "react-router-dom";
import { IoMdDownload } from "react-icons/io";
import * as XLSX from "xlsx";
import { useGetNewsLetterEmailQuery } from "../Services/api.service";
const HeaderMain = ({ link, btnTitle, title, icon, logOutHandler }) => {
const { data, error, isLoading } = useGetNewsLetterEmailQuery();
const handleDownload = () => {
if (Array.isArray(data?.data?.rows)) {
const worksheet = XLSX.utils.json_to_sheet(data?.data?.rows);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, "Sheet1");
XLSX.writeFile(workbook, "newsletter_emails.xlsx");
} else {
// // console.error(
// "Expected data to be an array but received:",
// data?.data?.rows
// );
}
};
return (
<Box
backgroundColor={"#fff"}
// bg="white.900"
// backdropFilter="blur(10px) hue-rotate(90deg)"
// h={12}
className={` pt-2 pb-2 fw-400 border-bottom d-flex justify-content-between align-items-center`}
>
{/* <span className="fs-5">Community</span> */}
<Text
as={"span"}
fontWeight={"bold"}
color={"forestGreen.500"}
className="fs-6 "
>
{/* <icon /> */}
{title}
</Text>
<Box me={4} className="d-flex justify-content-center ">
<Popover placement="bottom">
<Portal>
<PopoverContent maxW="200px" className="">
<PopoverArrow />
<PopoverBody className="web-text-medium pointer link">
Profile
</PopoverBody>
<Link to={"/help-and-support"}>
<PopoverBody className="web-text-medium pointer ">
Help & Support
</PopoverBody>
</Link>
<PopoverFooter
onClick={logOutHandler}
className="web-text-medium pointer link"
>
Log Out
</PopoverFooter>
</PopoverContent>
</Portal>
<PopoverTrigger>
<Box
// onClick={logOutHandler}
className="d-flex pointer align-items-center"
>
<Avatar
size="sm"
name="Dan Abrahmov"
src="https://bit.ly/dan-abramov"
/>
<Box
color={"gray.800"}
style={{
display: "flex",
}}
className=" overflow-hidden ms-3 flex-column "
>
<Text as={"span"} className="web-text-small">
Hello, developer admin
</Text>
<Text as={"span"} className="web-text-xsmall">
siddhesh@rubix.com
</Text>
</Box>
</Box>
</PopoverTrigger>
</Popover>
</Box>
</Box>
);
};
export default HeaderMain;

View File

@@ -0,0 +1,77 @@
import {
AspectRatio,
Box,
Container,
Heading,
Input,
Stack,
Text,
} from "@chakra-ui/react";
import React from "react";
import { motion, useAnimation } from "framer-motion";
const ImageDropBox = () => {
return (
<Box
borderColor="gray.300"
borderStyle="dashed"
borderWidth="2px"
rounded="md"
shadow="sm"
role="group"
transition="all 150ms ease-in-out"
_hover={{
shadow: "md",
}}
as={motion.div}
initial="rest"
animate="rest"
whileHover="hover"
height={"105px"}
className="pointer"
>
<Box position="relative" height="100%" width="100%">
<Box
position="absolute"
top="0"
left="0"
height="100%"
width="100%"
display="flex"
flexDirection="column"
>
<Stack
height="100%"
width="100%"
display="flex"
alignItems="center"
justify="center"
>
<span className="d-flex flex-column align-items-center" spacing="1">
<Heading fontSize="lg" color="gray.700" fontWeight="bold">
Drop images here
</Heading>
<span fontWeight="light" className="web-text-large text-secondary text-center">or click to upload</span>
</span>
</Stack>
</Box>
<Input
type="file"
height="100%"
width="100%"
position="absolute"
top="0"
left="0"
opacity="0"
aria-hidden="true"
accept="image/*"
// onDragEnter={startAnimation}
// onDragLeave={stopAnimation}
/>
</Box>
</Box>
);
};
export default ImageDropBox;

View File

@@ -0,0 +1,44 @@
import React, { useState } from "react";
const input01Style = {
width: "100%",
border: "1px solid #ADB3BD",
outline: "none",
height: 50,
fontSize: "16px",
};
const focusedInputStyle = {
border: "1px solid #38187C", // Change border color for focused state
};
const Input01 = ({ type, placeholder, onChange, name, register }) => {
const [isFocused, setIsFocused] = useState(false);
const handleFocus = () => {
setIsFocused(true);
};
const handleBlur = () => {
setIsFocused(false);
};
return (
<input
style={
isFocused ? { ...input01Style, ...focusedInputStyle } : input01Style
}
type={type}
name={name}
className="rounded-3 ps-3 d-flex align-items-center"
onChange={onChange}
onFocus={handleFocus}
onBlur={handleBlur}
placeholder={placeholder}
{...register}
/>
);
};
export default Input01;

View File

@@ -0,0 +1,18 @@
import { Box, Spinner } from "@chakra-ui/react";
import React from "react";
const FullscreenLoaders = () => {
return (
<Box
display={"flex"}
justifyContent={"center"}
alignItems={"center"}
w={"100%"}
h={"90%"}
>
<Spinner color='teal.700' />
</Box>
);
};
export default FullscreenLoaders;

View File

@@ -0,0 +1,14 @@
import React from "react";
const Loader01 = () => {
return (
<div className="lds-ellipsis ">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
);
};
export default Loader01;

View File

@@ -0,0 +1,136 @@
import { Box, HStack, Input, Select, Text } from "@chakra-ui/react";
import { OPACITY_ON_LOAD } from "../../Layout/animations";
import { TABLE_PAGINATION } from "../../Constants/Paginations";
import { useGetVideosQuery } from "../../Services/api.service";
import { useState } from "react";
import Header from "../../Components/Header";
import { ChevronLeftIcon, ChevronRightIcon } from "@chakra-ui/icons";
import DataTable from "../../Components/DataTable/DataTable";
const TabularView = ({
apiData,
tableHeadRow,
title,
btnTitle,
link,
extractedArray,
searchTerm,
setSearchTerm,
statusFilter,
setStatusFilter,
currentPage,
setCurrentPage,
pageSize,
setPageSize,
totalPages,
noDataTitle,
totalItems,
}) => {
const [displayRange, setDisplayRange] = useState({
start: TABLE_PAGINATION?.page,
end: pageSize,
});
// ====================================================[Pagination Setup]================================================================
const paginationPrev = () => {
if (currentPage > 1) {
setCurrentPage(currentPage - 1);
updateDisplayRange(currentPage - 1);
}
};
const paginationNext = () => {
if (currentPage < totalPages) {
setCurrentPage(currentPage + 1);
updateDisplayRange(currentPage + 1);
}
};
const updateDisplayRange = (page) => {
const start = (page - 1) * pageSize + 1;
const end = Math.min(start + pageSize - 1, totalItems);
setDisplayRange({ start, end });
};
return (
<Box
overflowY={"scroll"}
paddingBottom={50}
height={"100vh"}
{...OPACITY_ON_LOAD}
>
<Header title={title} btnTitle={btnTitle} link={link} />
<Box bg="white.500">
<HStack
display={"flex"}
justifyContent={"space-between"}
ps={1}
pe={1}
pb={4}
pt={4}
spacing="24px"
>
<Input
type="search"
width={300}
placeholder="Search..."
size="sm"
rounded="sm"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<HStack>
<Select
className="pointer web-text-small"
width={"90px"}
rounded="sm"
size="sm"
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
>
<option value="all">All</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</Select>
<Select
className="pointer web-text-small"
width={"90px"}
rounded="sm"
size="sm"
value={pageSize}
onChange={(e) => setPageSize(e.target.value)}
>
<option value={5}>{5}</option>
<option value={10}>{10}</option>
<option value={15}>{15}</option>
</Select>
<HStack>
<ChevronLeftIcon
onClick={paginationPrev}
className=" link rounded-3 pointer"
/>
<Text className="web-text-medium" as={"span"}>
{displayRange.start} - {displayRange.end} of {totalItems}
</Text>
<ChevronRightIcon
onClick={paginationNext}
className=" link rounded-3 pointer"
/>
</HStack>
</HStack>
</HStack>
</Box>
{/* ====================================================[ Table ]================================================================ */}
<DataTable
emptyMessage={`We don't have any ${noDataTitle} `}
tableHeadRow={tableHeadRow}
data={extractedArray}
isLoading={apiData?.isLoading}
/>
</Box>
);
};
export default TabularView;

View File

@@ -0,0 +1,21 @@
import { CheckCircleIcon, WarningIcon } from "@chakra-ui/icons";
import { Box, Text } from "@chakra-ui/react";
import React from "react";
const ToastBox = ({ message, status }) => {
return (
<Box
color="white"
rounded={"md"}
className="web-text-large d-flex gap-2 align-items-center"
p={3}
bg={status === "error" ? "red.500" : status === "warn" ? "blue.500" : "green.500"}
>
{status === "error" || status === "warn" ? <WarningIcon/> : <CheckCircleIcon /> }
<Text as={"span"}>{message}</Text>
</Box>
);
};
export default ToastBox;

View File

@@ -0,0 +1,68 @@
import { Button } from '@chakra-ui/react'
import React from 'react'
const WebButton = ({title}) => {
return ( <Button
position={"relative"}
backgroundColor={"transparent"}
cursor={"pointer"}
transition="0.3s ease-in-out"
color={"#fff"}
width={"auto"}
height={"auto"}
p={6}
pt={2}
pb={2}
fontFamily={"Poppins"}
fontWeight={"600"}
border={"1px solid white"}
borderRadius={"4px"}
fontSize={"14px"}
zIndex={"1"}
overflow={"hidden"}
sx={{
"::before": {
content: '""',
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: "65px",
height: "65px",
borderRadius: "50%",
transition: "0.35s linear",
zIndex: -1,
bgGradient:
"radial-gradient(circle, #ffffff, #eee2f2, #e7c3dc, #e5a3ba, #de858e)",
opacity: "0",
},
"&:hover::before": {
width: "100%",
height: "120%",
borderRadius: "0px",
opacity: "1",
},
"@media (max-width: 500px)": {
fontSize: "14px",
width: "230px",
height: "44px",
marginTop: "2rem",
bgGradient:
"radial-gradient(circle, #ffffff, #eee2f2, #e7c3dc, #e5a3ba, #de858e)",
color: "#000",
fontWeight: "600",
},
}}
_hover={{
color: "#000",
border: "1px solid white",
zIndex: 1,
}}
>
{title}
</Button>
)
}
export default WebButton

View File

@@ -0,0 +1,2 @@
export const TABLE_PAGINATION = { page: 1, size: 10 }
export const IMAGE_URI = import.meta.env.VITE_API_IMAGE_URL

View File

@@ -0,0 +1,6 @@
// GlobalStateContext.js
import { createContext } from 'react';
const GlobalStateContext = createContext();
export default GlobalStateContext;

View File

@@ -0,0 +1,30 @@
// GlobalStateContext.js
import React, { useState } from "react";
import GlobalStateContext from "./GlobalStateContext";
function generateUID() {
// Generates a random 8-character alphanumeric string
return Math.random().toString(36).substring(2, 10);
}
const GlobalStateProvider = ({ children }) => {
const [isAuthenticate, setIsAuthenticate] = useState(false);
const [memberIfo, setMemberInfo] = useState();
const [communityMembers, setCommityMembers] = useState();
return (
<GlobalStateContext.Provider
value={{
isAuthenticate,
setIsAuthenticate,
memberIfo,
setMemberInfo,
communityMembers,
setCommityMembers,
}}
>
{children}
</GlobalStateContext.Provider>
);
};
export default GlobalStateProvider;

BIN
src/Images/dark-bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

BIN
src/Images/light-bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

BIN
src/Images/logo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
src/Images/logoDark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
src/Images/logoDarkMini.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
src/Images/logoLight.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
src/Images/miniLogo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

BIN
src/Images/reactLogo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 MiB

View File

@@ -0,0 +1,342 @@
import React, { useContext, useState } from "react";
import logo from "../assets/logo2.png";
import logoMini from "../assets/logo-min.png";
import { useDispatch } from "react-redux";
import { loginUser } from "../Redux/Slice/auth";
import Button02 from "../Components/Buttons/Button02";
import { TbArrowBadgeLeftFilled } from "react-icons/tb";
import { TbArrowBadgeRightFilled } from "react-icons/tb";
import { ArrowBackIcon, ArrowLeftIcon, ArrowRightIcon } from "@chakra-ui/icons";
import {
Link,
NavLink,
Route,
Routes,
useLocation,
useNavigate,
} from "react-router-dom";
import { RouteLink } from "../Routes/Routes";
import NotFound from "../Pages/NotFound";
import { nav } from "../Routes/Nav";
import {
Avatar,
Box,
Button,
PopoverArrow,
PopoverBody,
PopoverCloseButton,
PopoverContent,
PopoverFooter,
PopoverHeader,
PopoverTrigger,
Portal,
Text,
WrapItem,
Popover,
Tag,
} from "@chakra-ui/react";
import GlobalStateContext from "../Contexts/GlobalStateContext";
import Cookies from "js-cookie"; // Import the Cookies library
import Header from "../Components/Header";
import HeaderMain from "../Components/HeaderMain";
const DashboardLayout = () => {
const navigate = useNavigate();
const dispach = useDispatch();
const location = useLocation();
const path = location.pathname;
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const [openDrawerClick, setOpenDrawerClick] = useState(true);
const { setIsAuthenticate } = useContext(GlobalStateContext);
const openDrawerOnClick = () => {
setOpenDrawerClick(!openDrawerClick);
};
const logOutHandler = () => {
// dispach(loginUser(false));
setIsAuthenticate(false);
Cookies.remove("isAuthenticated");
navigate("/login");
};
// // Function to get the title based on the route
const getTitle = () => {
switch (path) {
case "/":
return "👋🏻 Hi, Admin";
case "/investment":
return "Investment";
case "/blogs-articles":
return "Blogs and Articles";
case "/videos":
return "Videos";
case "/news":
return "News";
case "/events":
return "Events";
case "/whitepaper":
return "Whitepaper";
case "/community/":
return "Community";
case "/community":
return "Community";
case "/community/view/":
return "Community";
case "/community/add-comunity":
return (
<Text color={"teal.800"} className="d-flex align-items-center">
<Link to={"/community/"}>
<ArrowBackIcon className="me-2 fs-3 link p-1 rounded-1" />
</Link>
Community
</Text>
);
default:
if (path.startsWith("/community/view/")) {
return (
<span className="d-flex align-items-center">
<Link to={"/community/"}>
<ArrowBackIcon className="me-2 fs-3 link p-1 rounded-1" />
</Link>
Community
</span>
);
} else if (path.startsWith("/community/edit/")) {
return (
<span className="d-flex align-items-center">
<Link to={"/community/"}>
<ArrowBackIcon className="me-2 fs-3 link p-1 rounded-1" />
</Link>
Community
</span>
);
}
return "Tanami";
}
};
return (
<Box
style={{
height: "100vh",
width: "100vw",
position: "relative",
overflow: "hidden",
// backgroundColor:"#101015"
// backgroundColor:"#000000"
}}
className="d-flex"
pe={0.5}
>
<aside
className="h-100 position-relative sideBar pe-1"
// onMouseOver={() => setIsDrawerOpen(true)}
// onMouseLeave={() => setIsDrawerOpen(false)}
style={{
width: isDrawerOpen || openDrawerClick ? 225 : 70,
transition: "width 0.3s ease-in-out", // Smooth transition for width change
overflow: "hidden", // Hide overflow to prevent content overflow during transition
backgroundColor:"#0041180A",
// backgroundColor: "#002F0F",
}}
>
<div
className={`d-flex ${isDrawerOpen || openDrawerClick ? "justify-content-start" : "justify-content-center"} p-3 pt-3 pb-4 position-relative `}
height={"10%"}
>
{isDrawerOpen || openDrawerClick ? (
<img
style={{
width: 120,
}}
src={logo}
alt="Logo"
/>
) : (
<img
style={{
width: 30,
}}
src={logoMini}
alt="Logo"
/>
)}
</div>
<div
className="ps-2 scroll-bar "
style={{ height: "80%", overflowY: "scroll", overflowX:"hidden" }}
>
{nav.map(({ title, path, Icon }, index) => (
<Box
// color={"whitesmoke"}
color={"gray.600"}
key={index}
className=" mb-1 w-100 d-flex "
>
{path ? (
<NavLink
style={{
height: "auto",
}}
className={`${
isDrawerOpen || openDrawerClick
? "p-1 web-text-medium ps-3"
: "p-2 ps-1 web-text-xlarge justify-content-center"
} rounded-1 link d-flex align-items-center gap-2 w-100 `}
to={path}
>
<span
style={{
display:
isDrawerOpen || openDrawerClick ? "flex" : "flex",
alignItems: "center",
paddingBottom: 0,
}}
>
<Icon className="web-text-large" />
</span>
<span
style={{
display:
isDrawerOpen || openDrawerClick ? "flex" : "none",
alignItems: "center",
padding: 3,
overflow: "hidden",
}}
>
{title}
</span>
</NavLink>
) : (
<span className="web-text-xxsmall fw-600 mt-1 text-secondary fw-bold">
{title}
</span>
)}
</Box>
))}
</div>
{/* <section
className="d-flex justify-content-center border-top p-2 "
style={{
position: "absolute",
left: 0,
bottom: 0,
width: "100%",
}}
>
<Popover placement="top">
<Portal>
<PopoverContent maxW="220px" className="ms-2">
<PopoverArrow />
<PopoverBody className="web-text-medium pointer link">
Profile
</PopoverBody>
<Link to={"/help-and-support"}>
<PopoverBody className="web-text-medium pointer ">
Help & Support
</PopoverBody>
</Link>
<PopoverFooter
onClick={logOutHandler}
className="web-text-medium pointer link"
>
Log Out
</PopoverFooter>
</PopoverContent>
</Portal>
<PopoverTrigger>
<Box
// onClick={logOutHandler}
className="d-flex pointer align-items-center"
>
<Avatar
size="xs"
name="Dan Abrahmov"
src="https://bit.ly/dan-abramov"
/>
<Box
color={"whitesmoke"}
style={{
opacity: isDrawerOpen || openDrawerClick ? 1 : 0,
display: isDrawerOpen || openDrawerClick ? "flex" : "none",
transition: "opacity 0.3s ease-in-out",
}}
className=" overflow-hidden ms-3 flex-column "
>
<Text as={"span"} className="web-text-small">
Hello, developer admin
</Text>
<Text as={"span"} className="web-text-xsmall">
siddhesh@rubix.com
</Text>
</Box>
</Box>
</PopoverTrigger>
</Popover>
</section> */}
</aside>
<main
className={`h-100 ${path === "/" ? "ps-0" : "ps-3" } `}
style={{
width: `calc(100% - ${isDrawerOpen || openDrawerClick ? 225 : 70}px)`,
transition: "width 0.3s ease-in-out",
position: "relative",
// boxShadow:
// "rgba(50, 50, 93, 0.25) 0px 50px 100px -20px, rgba(0, 0, 0, 0.3) 0px 30px 60px -30px, rgba(10, 37, 64, 0.35) 0px -2px 6px 0px inset",
}}
>
<Button
colorScheme={"forestGreen"}
rounded={"lg"}
// onMouseOver={() => setIsDrawerOpen(true)}
// onMouseLeave={() => setIsDrawerOpen(false)}
onClick={openDrawerOnClick}
style={{
width: 18,
height: 26,
position: "absolute",
left: -28,
bottom: 80,
zIndex: 6,
}}
>
{isDrawerOpen || openDrawerClick ? (
<ArrowLeftIcon className="web-text-small" />
) : (
<ArrowRightIcon className="web-text-small " />
)}
</Button>
{/* <header className="p-2 ps-0 pt-3 fw-400 border-bottom">
<span className="fs-5">{getTitle()}</span>
</header> */}
<HeaderMain logOutHandler={logOutHandler} icon title={getTitle()}/>
<AppContent />
</main>
</Box>
);
};
export default DashboardLayout;
const AppContent = () => {
return (
<Routes>
{RouteLink.map(({ path, Component }, index) => (
<Route key={index} path={path} element={<Component />} />
))}
<Route path="*" element={<NotFound />} />
</Routes>
);
};

23
src/Layout/animations.jsx Normal file
View File

@@ -0,0 +1,23 @@
import { motion } from "framer-motion";
export const OPACITY_ON_LOAD = {
as: motion.div,
initial: { opacity: 0 },
animate: { opacity: 1 }
}
export const SLIDE_IN_BOTTOM = {
as: motion.div,
initial: { opacity: 0, y: 50 },
animate: { opacity: 1, y: 0 },
transition: { duration: 0.5, ease: "easeInOut" }
};
export const FADE_IN_SCALE_UP = {
as: motion.div,
initial: { opacity: 0, scale: 0.9 },
animate: { opacity: 1, scale: 1 },
transition: { duration: 0.5, ease: "easeInOut" }
};

View File

@@ -0,0 +1,81 @@
import { Box, Divider } from "@chakra-ui/react";
import React from "react";
import { OPACITY_ON_LOAD } from "../../Layout/animations";
import {
useGetBuildBannerQuery,
useGetCommunityBannerQuery,
useGetEcoBannerQuery,
useGetHomeBannerQuery,
useGetLearnBannerQuery,
useGetNewsBannerQuery,
} from "../../Services/api.service";
import Header from "../../Components/Header";
import BannerStack from "../../Components/BannerStack";
const Banner = () => {
const communityBanner = useGetCommunityBannerQuery();
const learnBanner = useGetLearnBannerQuery();
const buildBanner = useGetBuildBannerQuery();
const newsBanner = useGetNewsBannerQuery();
const homeBanner = useGetHomeBannerQuery();
const ecoBanner = useGetEcoBannerQuery();
return (
<Box {...OPACITY_ON_LOAD} overflowY={"scroll"} height={"100vh"}>
<Header title={"Banners"} />
<BannerStack
stackTitle={"Home banner"}
viewAllLink={"/banner/home"}
bannerIsLoading={homeBanner?.isLoading}
bannerArray={homeBanner?.data?.data?.rows?.slice(0, 3)}
viewBannerLink={"/banner/home/view"}
/>
<Divider/>
<BannerStack
stackTitle={"Community banner"}
viewAllLink={"/banner/banner-community"}
bannerIsLoading={communityBanner?.isLoading}
bannerArray={communityBanner?.data?.data?.rows?.slice(0, 3)}
// bannerArray={communityBanner?.data?.data?.rows?.filter(item => item?.status === true)}
viewBannerLink={"/banner/banner-community/view"}
/>
<Divider />
<BannerStack
stackTitle={"Learn banner"}
viewAllLink={"/banner/learn"}
bannerIsLoading={learnBanner?.isLoading}
bannerArray={learnBanner?.data?.data?.rows?.slice(0, 3)}
viewBannerLink={"/banner/learn/view"}
/>
<Divider />
<BannerStack
stackTitle={"Build banner"}
viewAllLink={"/banner/build"}
bannerIsLoading={buildBanner?.isLoading}
bannerArray={buildBanner?.data?.data?.rows?.slice(0, 3)}
viewBannerLink={"/banner/build/view"}
/>
{/* <Divider />
<BannerStack
stackTitle={"News banner"}
viewAllLink={"/banner/news"}
bannerIsLoading={newsBanner?.isLoading}
bannerArray={newsBanner?.data?.data?.rows?.slice(0, 3)}
viewBannerLink={"/banner/news/view"}
/> */}
<Divider />
<BannerStack
stackTitle={"Ecosystem banner"}
viewAllLink={"/banner/eco"}
bannerIsLoading={ecoBanner?.isLoading}
bannerArray={ecoBanner?.data?.data?.rows?.slice(0, 3)}
viewBannerLink={"/banner/eco/view"}
/>
</Box>
);
};
export default Banner;

View File

@@ -0,0 +1,27 @@
import React from "react";
import {
useDeleteBuildBannerMutation,
useGetBuildBannerQuery,
useUpdateBuildBannerStatusMutation,
} from "../../../Services/api.service";
import BannerTable from "../../../Components/Banner/BannerTable";
const BannerBuild = () => {
const buildBanner = useGetBuildBannerQuery();
const [deleteBuildBanner] = useDeleteBuildBannerMutation();
const [updateLearnBuildStatus] = useUpdateBuildBannerStatusMutation();
return (
<BannerTable
addLink={"/banner/build/add-banner"}
title={"Build Banner"}
viewLink={"/banner/build/view/"}
editLink={"/banner/build/edit/"}
deleteApi={deleteBuildBanner}
dataArray={buildBanner}
statusUpdateApi={updateLearnBuildStatus}
/>
);
};
export default BannerBuild;

View File

@@ -0,0 +1,18 @@
import AddBanner from "../../../Components/Banner/AddBanner";
import {
useCreateBuildBannerMutation,
useCreateLearnBannerMutation,
} from "../../../Services/api.service";
const BannerBuildAdd = () => {
const [createBuildBannerData] = useCreateBuildBannerMutation();
return (
<AddBanner
title={"Build banner"}
navigateLink={"/banner/build"}
createApi={createBuildBannerData}
/>
);
};
export default BannerBuildAdd;

View File

@@ -0,0 +1,15 @@
import React from 'react'
import BannerEdit from '../../../Components/Banner/BannerEdit'
import { useParams } from 'react-router-dom';
import { useGetBuildBannerByIdQuery, useUpdateBuildBannerMutation } from '../../../Services/api.service';
const BannerBuildEdit = () => {
const { id } = useParams();
const { data, error, isLoading, refetch } = useGetBuildBannerByIdQuery(id);
const [updateBuildBanner] = useUpdateBuildBannerMutation();
return (<BannerEdit refetch={refetch} navigateTo={'/banner/build'} isLoading={isLoading} data={data} updateBanner={updateBuildBanner} />
)
}
export default BannerBuildEdit

View File

@@ -0,0 +1,15 @@
import React from "react";
import { useParams } from "react-router-dom";
import { useGetBuildBannerByIdQuery } from "../../../Services/api.service";
import BannerView from "../../../Components/Banner/BannerView";
const BannerBuildView = () => {
const { id } = useParams();
const { data, error, isLoading } = useGetBuildBannerByIdQuery(id);
return (
<BannerView editLink={'/banner/build/edit'} isLoading={isLoading} data={data} />
);
};
export default BannerBuildView;

View File

@@ -0,0 +1,30 @@
import React from "react";
import {
useDeleteCommunityBannerMutation,
useGetCommunityBannerQuery,
useUpdateCommunityBannerStatusMutation,
} from "../../../Services/api.service";
import BannerTable from "../../../Components/Banner/BannerTable";
const BannerCommunity = () => {
const communityBanner = useGetCommunityBannerQuery();
const [deleteCommunityBanner] = useDeleteCommunityBannerMutation();
const [updateCommunityBannerStatus] =
useUpdateCommunityBannerStatusMutation();
return (
<BannerTable
title={'Community banner'}
addLink={"/banner/banner-community/add-banner"}
viewLink={"/banner/banner-community/view/"}
editLink={"/banner/banner-community/edit/"}
deleteApi={deleteCommunityBanner}
dataArray={communityBanner}
statusUpdateApi={updateCommunityBannerStatus}
/>
);
};
export default BannerCommunity;

View File

@@ -0,0 +1,16 @@
import { useCreateCommunityBannerMutation } from "../../../Services/api.service";
import AddBanner from "../../../Components/Banner/AddBanner";
const BannerCommunityAdd = () => {
const [createCommunityBannerData] = useCreateCommunityBannerMutation();
return (
<AddBanner
title={"Learn banner"}
navigateLink={"/banner/banner-community"}
createApi={createCommunityBannerData}
/>
);
};
export default BannerCommunityAdd;

View File

@@ -0,0 +1,17 @@
import { useParams } from "react-router-dom";
import {
useGetCommunityBannerByIdQuery,
useUpdateCommunityBannerMutation,
} from "../../../Services/api.service";
import BannerEdit from "../../../Components/Banner/BannerEdit";
const BannerComunityEditPage = () => {
const { id } = useParams();
const { data, error, isLoading, refetch } = useGetCommunityBannerByIdQuery(id);
const [updateCommunityBanner] = useUpdateCommunityBannerMutation();
return <BannerEdit refetch={refetch} navigateTo={'/banner/banner-community'} isLoading={isLoading} data={data} updateBanner={updateCommunityBanner} />
};
export default BannerComunityEditPage;

View File

@@ -0,0 +1,27 @@
import React, { useEffect } from "react";
import { useParams } from "react-router-dom";
import { useGetCommunityBannerByIdQuery } from "../../../Services/api.service";
import {
Box,
Button,
Divider,
Image,
StackDivider,
Tag,
VStack,
} from "@chakra-ui/react";
import { OPACITY_ON_LOAD } from "../../../Layout/animations";
import FullscreenLoaders from "../../../Components/Loaders/FullscreenLoaders";
import { formatDate } from "../../../Components/Functions/UTCConvertor";
import Header from "../../../Components/Header";
import BannerView from "../../../Components/Banner/BannerView";
const BannerComunityViewPage = () => {
const { id } = useParams();
const { data, error, isLoading } = useGetCommunityBannerByIdQuery(id);
return <BannerView editLink={'/banner/banner-community/edit'} isLoading={isLoading} data={data} />;
};
export default BannerComunityViewPage;

View File

@@ -0,0 +1,27 @@
import React from "react";
import {
useDeleteLearnBannerMutation,
useGetLearnBannerQuery,
useUpdateLearnBannerStatusMutation,
} from "../../../Services/api.service";
import BannerTable from "../../../Components/Banner/BannerTable";
const BannerLearn = () => {
const learnBanner = useGetLearnBannerQuery();
const [deleteLearnBanner] = useDeleteLearnBannerMutation();
const [updateLearnBannerStatus] = useUpdateLearnBannerStatusMutation();
return (
<BannerTable
addLink={"/banner/learn/add-banner"}
title={"Learn Banner"}
viewLink={"/banner/learn/view/"}
editLink={"/banner/learn/edit/"}
deleteApi={deleteLearnBanner}
dataArray={learnBanner}
statusUpdateApi={updateLearnBannerStatus}
/>
);
};
export default BannerLearn;

View File

@@ -0,0 +1,15 @@
import AddBanner from "../../../Components/Banner/AddBanner";
import { useCreateLearnBannerMutation } from "../../../Services/api.service";
const BannerLearnAdd = () => {
const [createLearnBannerData] = useCreateLearnBannerMutation();
return (
<AddBanner
title={"Learn banner"}
navigateLink={"/banner/learn"}
createApi={createLearnBannerData}
/>
);
};
export default BannerLearnAdd;

View File

@@ -0,0 +1,15 @@
import React from 'react'
import BannerEdit from '../../../Components/Banner/BannerEdit'
import { useGetCommunityBannerByIdQuery, useGetLearnBannerByIdQuery, useUpdateCommunityBannerMutation, useUpdateLearnBannerMutation } from '../../../Services/api.service';
import { useParams } from 'react-router-dom';
const BannerLearnEdit = () => {
const { id } = useParams();
const { data, error, isLoading, refetch} = useGetLearnBannerByIdQuery(id);
const [updateLearnBanner] = useUpdateLearnBannerMutation();
return (
<BannerEdit refetch={refetch} navigateTo="/banner/learn" isLoading={isLoading} data={data} updateBanner={updateLearnBanner} />
)
}
export default BannerLearnEdit

View File

@@ -0,0 +1,14 @@
import React from "react";
import { useParams } from "react-router-dom";
import { useGetLearnBannerByIdQuery } from "../../../Services/api.service";
import BannerView from "../../../Components/Banner/BannerView";
const BannerLearnView = () => {
const { id } = useParams();
const { data, error, isLoading } = useGetLearnBannerByIdQuery(id);
return <BannerView editLink={'/banner/learn/edit'} isLoading={isLoading} data={data} />;
};
export default BannerLearnView;

View File

@@ -0,0 +1,33 @@
import React from "react";
import {
useDeleteBuildBannerMutation,
useDeleteLearnBannerMutation,
useDeleteNewsBannerMutation,
useGetBuildBannerQuery,
useGetLearnBannerQuery,
useGetNewsBannerQuery,
useUpdateBuildBannerStatusMutation,
useUpdateLearnBannerStatusMutation,
useUpdateNewsBannerStatusMutation,
} from "../../../Services/api.service";
import BannerTable from "../../../Components/Banner/BannerTable";
const BannerNews = () => {
const newsBanner = useGetNewsBannerQuery();
const [deleteNewsBanner] = useDeleteNewsBannerMutation();
const [updateNewsBuildStatus] = useUpdateNewsBannerStatusMutation();
const { data, error, isLoading } = useUpdateNewsBannerStatusMutation();
return (
<BannerTable
addLink={"/banner/news/add-banner"}
title={"News Banner"}
viewLink={"/banner/news/view/"}
editLink={"/banner/news/edit/"}
deleteApi={deleteNewsBanner}
dataArray={newsBanner}
statusUpdateApi={updateNewsBuildStatus}
/>
);
};
export default BannerNews;

View File

@@ -0,0 +1,15 @@
import AddBanner from "../../../Components/Banner/AddBanner";
import { useCreateLearnBannerMutation, useCreateNewsBannerMutation } from "../../../Services/api.service";
const BannerNewsAdd = () => {
const [createNewsBannerData] = useCreateNewsBannerMutation();
return (
<AddBanner
title={"News banner"}
navigateLink={"/banner/news"}
createApi={createNewsBannerData}
/>
);
};
export default BannerNewsAdd;

View File

@@ -0,0 +1,15 @@
import React from 'react'
import BannerEdit from '../../../Components/Banner/BannerEdit'
import { useParams } from 'react-router-dom';
import { useGetNewsBannerByIdQuery, useUpdateNewsBannerMutation } from '../../../Services/api.service';
const BannerNewsEdit = () => {
const { id } = useParams();
const { data, error, isLoading } = useGetNewsBannerByIdQuery(id);
const [updateNewsBanner] = useUpdateNewsBannerMutation();
return (
<BannerEdit navigateTo="/banner/build" isLoading={isLoading} data={data} updateBanner={updateNewsBanner} />
)
}
export default BannerNewsEdit

View File

@@ -0,0 +1,14 @@
import React from "react";
import { useParams } from "react-router-dom";
import { useGetLearnBannerByIdQuery, useGetNewsBannerByIdQuery } from "../../../Services/api.service";
import BannerView from "../../../Components/Banner/BannerView";
const BannerNewsView = () => {
const { id } = useParams();
const { data, error, isLoading } = useGetNewsBannerByIdQuery(id);
return <BannerView editLink={'/banner/news/edit'} isLoading={isLoading} data={data} />;
};
export default BannerNewsView;

View File

@@ -0,0 +1,36 @@
import React from "react";
import {
useDeleteBuildBannerMutation,
useDeleteEcoBannerMutation,
useDeleteLearnBannerMutation,
useDeleteNewsBannerMutation,
useGetBuildBannerQuery,
useGetEcoBannerQuery,
useGetLearnBannerQuery,
useGetNewsBannerQuery,
useUpdateBuildBannerStatusMutation,
useUpdateEcoBannerStatusMutation,
useUpdateLearnBannerStatusMutation,
useUpdateNewsBannerStatusMutation,
} from "../../../Services/api.service";
import BannerTable from "../../../Components/Banner/BannerTable";
const EcoBanner = () => {
const ecoBanner = useGetEcoBannerQuery();
const [deleteEcoBanner] = useDeleteEcoBannerMutation();
const [updateEcoBuildStatus] = useUpdateEcoBannerStatusMutation();
const { data, error, isLoading } = useUpdateNewsBannerStatusMutation();
return (
<BannerTable
addLink={"/banner/eco/add-banner"}
title={"Ecosystem Banner"}
viewLink={"/banner/eco/view/"}
editLink={"/banner/eco/edit/"}
deleteApi={deleteEcoBanner}
dataArray={ecoBanner}
statusUpdateApi={updateEcoBuildStatus}
/>
);
};
export default EcoBanner;

View File

@@ -0,0 +1,15 @@
import AddBanner from "../../../Components/Banner/AddBanner";
import { useCreateEcoBannerMutation, useCreateLearnBannerMutation, useCreateNewsBannerMutation } from "../../../Services/api.service";
const EcoBannerAdd = () => {
const [createEcoBannerData] = useCreateEcoBannerMutation();
return (
<AddBanner
title={"Ecosystem banner"}
navigateLink={"/banner/eco"}
createApi={createEcoBannerData}
/>
);
};
export default EcoBannerAdd;

View File

@@ -0,0 +1,15 @@
import React from 'react'
import BannerEdit from '../../../Components/Banner/BannerEdit'
import { useParams } from 'react-router-dom';
import { useGetEcoBannerByIdQuery, useGetNewsBannerByIdQuery, useUpdateEcoBannerMutation, useUpdateNewsBannerMutation } from '../../../Services/api.service';
const EcoBannerEdit = () => {
const { id } = useParams();
const { data, error, isLoading, refetch } = useGetEcoBannerByIdQuery(id);
const [updateNewsBanner] = useUpdateEcoBannerMutation();
return (
<BannerEdit refetch={refetch} navigateTo="/banner/eco" isLoading={isLoading} data={data} updateBanner={updateNewsBanner} />
)
}
export default EcoBannerEdit

View File

@@ -0,0 +1,14 @@
import React from "react";
import { useParams } from "react-router-dom";
import { useGetEcoBannerByIdQuery, useGetLearnBannerByIdQuery, useGetNewsBannerByIdQuery } from "../../../Services/api.service";
import BannerView from "../../../Components/Banner/BannerView";
const EcoBannerView = () => {
const { id } = useParams();
const { data, error, isLoading } = useGetEcoBannerByIdQuery(id);
return <BannerView editLink={'/banner/eco/edit'} isLoading={isLoading} data={data} />;
};
export default EcoBannerView;

View File

@@ -0,0 +1,39 @@
import React from "react";
import {
useDeleteBuildBannerMutation,
useDeleteHomeBannerMutation,
useDeleteLearnBannerMutation,
useDeleteNewsBannerMutation,
useGetBuildBannerQuery,
useGetHomeBannerQuery,
useGetLearnBannerQuery,
useGetNewsBannerQuery,
useUpdateBuildBannerStatusMutation,
useUpdateHomeBannerStatusMutation,
useUpdateLearnBannerStatusMutation,
useUpdateNewsBannerStatusMutation,
} from "../../../Services/api.service";
import BannerTable from "../../../Components/Banner/BannerTable";
const HomeBanner = () => {
const homeBanner = useGetHomeBannerQuery();
const [deleteHomeBanner] = useDeleteHomeBannerMutation();
const [updateHomeBuildStatus] = useUpdateHomeBannerStatusMutation();
const { data, error, isLoading } = useUpdateNewsBannerStatusMutation();
return (
<BannerTable
addLink={"/banner/home/add-banner"}
title={"Home Banner"}
viewLink={"/banner/home/view/"}
editLink={"/banner/home/edit/"}
deleteApi={deleteHomeBanner}
dataArray={homeBanner}
statusUpdateApi={updateHomeBuildStatus}
/>
);
};
export default HomeBanner;

View File

@@ -0,0 +1,16 @@
import AddBanner from "../../../Components/Banner/AddBanner";
import { useCreateHomeBannerMutation } from "../../../Services/api.service";
const HomeBannerAdd = () => {
const [createLearnBannerData] = useCreateHomeBannerMutation();
return (
<AddBanner
center={true}
title={"Home banner"}
navigateLink={"/banner/home"}
createApi={createLearnBannerData}
/>
);
};
export default HomeBannerAdd;

View File

@@ -0,0 +1,18 @@
import React from 'react'
import BannerEdit from '../../../Components/Banner/BannerEdit'
import { useGetCommunityBannerByIdQuery, useGetHomeBannerByIdQuery, useGetLearnBannerByIdQuery, useUpdateCommunityBannerMutation, useUpdateHomeBannerMutation, useUpdateLearnBannerMutation } from '../../../Services/api.service';
import { useParams } from 'react-router-dom';
const BannerHomeEdit = () => {
const { id } = useParams();
const { data, error, isLoading, refetch } = useGetHomeBannerByIdQuery(id);
const [updateLearnBanner] = useUpdateHomeBannerMutation();
// useEffect(() => {
// refetch();
// }, [id, refetch]);
return (
<BannerEdit center={true} refetch={refetch} navigateTo="/banner/home" isLoading={isLoading} data={data} updateBanner={updateLearnBanner} />
)
}
export default BannerHomeEdit

View File

@@ -0,0 +1,14 @@
import React from "react";
import { useParams } from "react-router-dom";
import { useGetHomeBannerByIdQuery, useGetNewsBannerByIdQuery } from "../../../Services/api.service";
import BannerView from "../../../Components/Banner/BannerView";
const HomeBannerView = () => {
const { id } = useParams();
const { data, error, isLoading } = useGetHomeBannerByIdQuery(id);
return <BannerView center={true} editLink={'/banner/home/edit'} isLoading={isLoading} data={data} />;
};
export default HomeBannerView;

View File

@@ -0,0 +1,772 @@
import {
Box,
FormControl,
FormHelperText,
FormLabel,
Input,
Text,
Stack,
Textarea,
Heading,
Button,
useToast,
Divider,
Image,
} from "@chakra-ui/react";
import React, { useState } from "react";
import fallbackImage from "../../assets/fallBackImage.png";
import fallbackImageLarge from "../../assets/ultp-fallback-img.webp";
import { TiWarning } from "react-icons/ti";
import { motion } from "framer-motion";
import { OPACITY_ON_LOAD } from "../../Layout/animations";
import { yupResolver } from "@hookform/resolvers/yup";
import { useForm } from "react-hook-form";
import { addBlogSchema } from "../../Validations/Validations";
import { useCreateBlogMutation } from "../../Services/api.service";
import { useNavigate } from "react-router-dom";
import Loader01 from "../../Components/Loaders/Loader01";
import ReactQuill from "react-quill";
import "react-quill/dist/quill.snow.css";
import ChipSelector from "../../Components/ChipSelector/ChipSelector";
import Header from "../../Components/Header";
import ToastBox from "../../Components/ToastBox";
const AddBlogsAndArticles = () => {
const toast = useToast();
const navigate = useNavigate();
const [createBlog] = useCreateBlogMutation(); // Invoke the hook to get the mutation function
const [isLoading, setIsLoading] = useState(false);
const [selectedImage, setSelectedImage] = useState(fallbackImage);
const [selectedImageLarge, setSelectedImageLarge] =
useState(fallbackImageLarge);
const [largeImageData, setLargeImageData] = useState(null);
const [smallImageData, setSmallImageData] = useState(null);
const [chips, setChips] = useState([]);
const [value, setValue] = useState("");
const [metaDescription, setMetaDescription] = useState("");
const handleDescriptionChange = (e) => {
setMetaDescription(e.target.value);
};
const {
register,
handleSubmit,
reset,
setValue: setYupFormValue,
formState: { errors },
} = useForm({
resolver: yupResolver(addBlogSchema),
});
const onSubmit = async (data) => {
setYupFormValue('content', value)
const formData = new FormData();
formData.append("author_name", data.author_name);
formData.append("author_designation", data.author_designation);
formData.append("meta_description", data.meta_description);
formData.append("title", data.title);
formData.append("category", data.category);
formData.append("summary", data.summary);
formData.append("content", value); // Add the content to formData
if (data.profile_image[0]) {
formData.append("profile_image", data.profile_image[0]);
}
if (data.content_image_large[0]) {
formData.append("content_image_large", data.content_image_large[0]);
}
if (chips.length === 0) {
return toast({
title: "Please add tags",
status: "error",
isClosable: true,
});
} else {
// formData.append("tags", chips);
chips.forEach((tag, i) => {
formData.append(`tags[${i}]`, tag); // Append each tag as an array element
});
}
try {
setIsLoading(true);
createBlog(formData)
.then((response) => {
// Handle the response here
// // console.log("Mutation response:", response?.data?.statusCode);
// // console.log("Mutation response:", response?.data?.message);
if (response?.data?.statusCode === 201) {
setIsLoading(false);
toast({
render: () => (
<ToastBox status={"success"} message={response?.data?.message} />
),
});
reset();
navigate("/blogs-articles");
}else if(response?.error?.status === 500){
setIsLoading(false);
toast({
render: () => (
<ToastBox
status={"success"}
message={response?.error?.status?.error?.message}
/>
),
});
}
})
.catch((error) => {
// Handle errors
// // console.error("Error creating community:", error);
setIsLoading(false);
// Handle error notification if needed
});
} catch (error) {
// Handle errors
// // console.error("Error creating community:", error);
}
};
const handleImageChange = (e) => {
const file = e.target.files[0];
setSmallImageData(file);
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
setSelectedImage(reader.result);
};
reader.readAsDataURL(file);
}
};
const handleImageChangeLarge = (e) => {
const file = e.target.files[0];
setLargeImageData(file);
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
setSelectedImageLarge(reader.result);
};
reader.readAsDataURL(file);
}
};
return (
<Box
{...OPACITY_ON_LOAD}
overflowY={"scroll"}
height={"100vh"}
display={"flex"}
flexDirection={"column"}
>
<Header title={"Blog"} />
<form onSubmit={handleSubmit(onSubmit)}>
<Box display={"flex"}>
<Box className="col-5 d-flex flex-column justify-content-between gap-2 pt-4">
<Box flexDirection={"column"} display={"flex"}>
<span className="web-text-large fw-bold rubix-text-dark">
Blog Info
</span>
<span className="web-text-medium text-secondary">
Select the platform for which you need to create this campaign.
</span>
</Box>
<Box flexDirection={"column"} display={"flex"}>
<Divider />
<span className="web-text-large fw-bold rubix-text-dark">
Blog's Banner image
</span>
<span className="web-text-medium text-secondary mb-4">
Below is the profile that will be displayed on the community
page.
</span>
<Box
boxSize="sm"
className="d-flex w-100 p-2 justify-content-center flex-column align-items-center gap-3"
>
{false ? (
<FormControl isRequired className="mb-3">
{/* <FormLabel className="web-text-large fw-bold rubix-text-dark">
Display profile
</FormLabel> */}
{/* <ImageDropBox /> */}
<Box
borderColor="gray.300"
borderStyle="dashed"
borderWidth="2px"
rounded="md"
shadow="sm"
role="group"
transition="all 150ms ease-in-out"
_hover={{
shadow: "md",
}}
as={motion.div}
initial="rest"
animate="rest"
whileHover="hover"
h={220}
className="pointer"
>
<Box position="relative" height="100%" width="100%">
<Box
position="absolute"
top="0"
left="0"
height="100%"
width="100%"
display="flex"
flexDirection="column"
>
<Stack
height="100%"
width="100%"
display="flex"
alignItems="center"
justify="center"
>
<span
className="d-flex flex-column align-items-center pointer"
spacing="1"
>
<Heading
fontSize="lg"
color="gray.700"
fontWeight="bold"
cursor={"pointer"}
>
Drop images here
</Heading>
<span
fontWeight="light"
className="web-text-large text-secondary text-center pointer"
>
or click to upload
</span>
</span>
</Stack>
</Box>
<Input
{...register("content_image_large")}
type="file"
height="100%"
width="100%"
position="absolute"
top="0"
left="0"
opacity="0"
aria-hidden="true"
accept="image/*"
onChange={handleImageChangeLarge}
onDrop={handleImageChangeLarge}
// onDragEnter={startAnimation}
// onDragLeave={stopAnimation}
/>
</Box>
</Box>
{errors.content_image_large && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.content_image_large.message}
</span>
)}
<FormHelperText className="web-text-small">
Maximum limit of image is 10MB.
</FormHelperText>
</FormControl>
) : (
<>
<Image
shadow={"md"}
rounded={8}
w={500}
h={240}
src={selectedImageLarge}
alt="Selected Image"
/>
{selectedImageLarge === fallbackImageLarge ||
largeImageData === null ? (
""
) : (
<Box display={"flex"} flexDirection={"column"} w={"100%"}>
<span className="web-text-small">
{largeImageData?.name}
</span>
<span className="web-text-small text-secondary fst-italic">
{(largeImageData?.size / (1024 * 1024)).toFixed(2)} mb
</span>
</Box>
)}
</>
)}
<Button
onClick={() => setSelectedImageLarge(fallbackImageLarge)}
backgroundColor="red.400"
color={"whitesmoke"}
transition={"0.5s"}
_hover={{
backgroundColor: "red.500",
}}
size="xs"
>
Remove
</Button>
</Box>
</Box>
</Box>
<Box className="col-7 pt-4 p-4">
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Blog title
</FormLabel>
<Input
{...register("title")}
placeholder="Blog title"
className="web-text-medium"
size="sm"
maxLength={90}
/>
<FormHelperText className="web-text-small">
Maximum characters must be 100 characters.
</FormHelperText>
{errors.title && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " /> {errors.title.message}
</span>
)}
</FormControl>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Blog summary
</FormLabel>
<Textarea
rows={2}
{...register("summary")}
placeholder="Summary"
className="web-text-medium"
size="sm"
/>
{/* <FormHelperText className="web-text-small">
Please share proper linked in link here.
</FormHelperText> */}
{errors.summary && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.summary.message}
</span>
)}
</FormControl>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Blog description
</FormLabel>
<Textarea
rows={4}
{...register("meta_description")}
placeholder="Blog description"
className="web-text-medium"
errorBorderColor="crimson"
isInvalid={metaDescription.length > 160}
onChange={handleDescriptionChange}
size="sm"
/>
<FormHelperText
color={
metaDescription.length > 160
? "red"
: "green.400"
}
fontWeight={""
}
className="web-text-small"
>
If description crosses 160 characters it will cause problem in SEO
optimisation.you have entered {metaDescription.length} characters
</FormHelperText>
{errors.meta_description && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.meta_description.message}
</span>
)}
</FormControl>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Blog content
</FormLabel>
{/* <Textarea
rows={4}
{...register("content")}
placeholder="content link"
className="web-text-medium"
size="sm"
/> */}
<ReactQuill
className="rounded-3"
theme="snow"
value={value}
onChange={setValue}
/>
<FormHelperText className="web-text-small">
Please enter your content here. You can format your text using the toolbar above.
</FormHelperText>
</FormControl>
<FormControl className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Tags
</FormLabel>
<ChipSelector chips={chips} setChips={setChips} />
</FormControl>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Category
</FormLabel>
<Input
{...register("category")}
placeholder="Category"
className="web-text-medium"
size="sm"
maxLength={50}
/>
<FormHelperText className="web-text-small">
Maximum characters must be 50 characters.
</FormHelperText>
{errors.category && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.category.message}
</span>
)}
</FormControl>
<FormControl isRequired>
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Blog banner
</FormLabel>
{/* <ImageDropBox /> */}
<Box
borderColor="gray.300"
borderStyle="dashed"
borderWidth="2px"
rounded="md"
shadow="sm"
role="group"
transition="all 150ms ease-in-out"
_hover={{
shadow: "md",
}}
as={motion.div}
initial="rest"
animate="rest"
whileHover="hover"
h={105}
className="pointer"
>
<Box position="relative" height="100%" width="100%">
<Box
position="absolute"
top="0"
left="0"
height="100%"
width="100%"
display="flex"
flexDirection="column"
>
<Stack
height="100%"
width="100%"
display="flex"
alignItems="center"
justify="center"
>
<span
className="d-flex flex-column align-items-center pointer"
spacing="1"
>
<Heading
fontSize="lg"
color="gray.700"
fontWeight="bold"
cursor={"pointer"}
>
Drop images here
</Heading>
<span
fontWeight="light"
className="web-text-large text-secondary text-center pointer"
>
or click to upload
</span>
</span>
</Stack>
</Box>
<Input
{...register("content_image_large")}
type="file"
height="100%"
width="100%"
position="absolute"
top="0"
left="0"
opacity="0"
aria-hidden="true"
accept="image/*"
onChange={handleImageChangeLarge}
onDrop={handleImageChangeLarge}
// onDragEnter={startAnimation}
// onDragLeave={stopAnimation}
/>
</Box>
</Box>
{errors.content_image_large && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.content_image_large.message}
</span>
)}
<FormHelperText className="web-text-small">
Maximum limit of image is 10MB.
</FormHelperText>
</FormControl>
</Box>
</Box>
<Divider />
<Box display={"flex"}>
<Box className="col-5 d-flex flex-column gap-2 pt-4">
{/* <span className="web-text-large fw-bold rubix-text-dark">
Author's Info
</span>
<span className="web-text-medium text-secondary">
Select the platform for which you need to create this campaign.
</span>
<Divider /> */}
<span className="web-text-large fw-bold rubix-text-dark">
Author's display profile
</span>
<span className="web-text-medium text-secondary mb-0">
Below is the profile that will be displayed on the community page.
</span>
<Box
boxSize="sm"
className="d-flex flex-column align-items-center gap-3 justify-content-center"
>
<Image
shadow={"md"}
rounded={8}
w={214}
h={240}
src={selectedImage}
alt="Selected Image"
/>
{selectedImage === fallbackImage || smallImageData === null ? (
""
) : (
<Box display={"flex"} flexDirection={"column"} w={"100%"}>
<span className="web-text-small">{smallImageData?.name}</span>
<span className="web-text-small text-secondary fst-italic">
{(smallImageData?.size / (1024 * 1024)).toFixed(2)} mb
</span>
</Box>
)}
<Button
onClick={() => setSelectedImage(fallbackImage)}
backgroundColor="red.400"
color={"whitesmoke"}
transition={"0.5s"}
_hover={{
backgroundColor: "red.500",
}}
size="xs"
>
Remove
</Button>
</Box>
</Box>
<Box className="col-7 pt-4 p-4">
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Author name
</FormLabel>
<Input
{...register("author_name")}
placeholder="Name"
className="web-text-medium"
size="sm"
/>
{errors.name && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.author_name.message}
</span>
)}
</FormControl>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Author designation
</FormLabel>
<Input
{...register("author_designation")}
placeholder="Author designation"
className="web-text-medium"
size="sm"
/>
{errors.author_designation && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.author_designation.message}
</span>
)}
</FormControl>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Display profile
</FormLabel>
{/* <ImageDropBox /> */}
<Box
borderColor="gray.300"
borderStyle="dashed"
borderWidth="2px"
rounded="md"
shadow="sm"
role="group"
transition="all 150ms ease-in-out"
_hover={{
shadow: "md",
}}
as={motion.div}
initial="rest"
animate="rest"
whileHover="hover"
height={105}
className="pointer"
>
<Box position="relative" height="100%" width="100%">
<Box
position="absolute"
top="0"
left="0"
height="100%"
width="100%"
display="flex"
flexDirection="column"
>
<Stack
height="100%"
width="100%"
display="flex"
alignItems="center"
justify="center"
>
<span
className="d-flex flex-column align-items-center pointer"
spacing="1"
>
<Heading
fontSize="lg"
color="gray.700"
fontWeight="bold"
cursor={"pointer"}
>
Drop images here
</Heading>
<span
fontWeight="light"
className="web-text-large text-secondary text-center pointer"
>
or click to upload
</span>
</span>
</Stack>
</Box>
<Input
{...register("profile_image")}
type="file"
height="100%"
width="100%"
position="absolute"
top="0"
left="0"
opacity="0"
aria-hidden="true"
accept="image/*"
onChange={handleImageChange}
onDrop={handleImageChange}
// onDragEnter={startAnimation}
// onDragLeave={stopAnimation}
/>
</Box>
</Box>
{errors.profile_image && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.profile_image.message}
</span>
)}
<FormHelperText className="web-text-small">
Maximum limit of image is 10MB.
</FormHelperText>
</FormControl>
<Box className=" d-flex justify-content-end">
<Button
isLoading={isLoading}
spinner={<Loader01 />}
color={"whitesmoke"}
backgroundColor={"purple.900"}
_hover={{
backgroundColor: "purple.800",
}}
type="submit"
size="sm"
rounded={"sm"}
>
Create blog
</Button>
</Box>
</Box>
</Box>
</form>
</Box>
);
};
export default AddBlogsAndArticles;

View File

@@ -0,0 +1,299 @@
import React, { useRef, useState } from "react";
import {
Avatar,
Box,
Link,
Tag,
Text,
WrapItem,
Tooltip,
Divider,
Stack,
HStack,
Input,
Button,
Select,
Image,
Menu,
MenuButton,
MenuList,
MenuItem,
Switch,
Portal,
useDisclosure,
AlertDialog,
AlertDialogOverlay,
AlertDialogContent,
AlertDialogHeader,
AlertDialogCloseButton,
AlertDialogBody,
AlertDialogFooter,
useToast,
Skeleton,
VStack,
Badge,
} from "@chakra-ui/react";
import { AddIcon } from "@chakra-ui/icons";
import DataTable from "../../Components/DataTable/DataTable";
import { OPACITY_ON_LOAD } from "../../Layout/animations";
import { Link as RouterLink } from "react-router-dom";
import {
useDeleteBlogMutation,
useDeleteCommunityMutation,
useGetBlogQuery,
useGetCommunityBannerQuery,
useGetCommunityByIdQuery,
useGetCommunityQuery,
useUpdateBlogStatusMutation,
useUpdateCommunityStatusMutation,
} from "../../Services/api.service";
import { HiDotsVertical } from "react-icons/hi";
import { formatDate } from "../../Components/Functions/UTCConvertor";
import CustomAlertDialog from "../../Components/CustomAlertDialog";
import Header from "../../Components/Header";
import ToastBox from "../../Components/ToastBox";
import TabularView from "../../Components/TabularView/TabularView";
import { TABLE_PAGINATION } from "../../Constants/Paginations";
const API_URL = import.meta.env.VITE_API_BASE_URL;
const BlogsAndArticles = () => {
// ====================================================[Hooks]===================================================================
const toast = useToast();
const [pageSize, setPageSize] = useState(TABLE_PAGINATION?.size);
const [currentPage, setCurrentPage] = useState(1);
const [deleteAlert, setDeleteAlert] = useState(false);
const [actionId, setActionId] = useState(null);
const [deleteIsLoading, setDeleteIsLoading] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [statusFilter, setStatusFilter] = useState("all");
const blog = useGetBlogQuery({ page: currentPage, size: pageSize });
const [deleteBlog] = useDeleteBlogMutation();
const [updateBlogStatus] = useUpdateBlogStatusMutation();
// ====================================================[Functions]===================================================================
const handleDelete = async (communityId) => {
try {
// Trigger the mutation
setDeleteIsLoading(true);
await deleteBlog(communityId)
.then((response) => {
// Handle the response here
// // console.log("Mutation response:", response?.data?.statusCode);
// // console.log("Mutation response:", response?.data?.message);
if (response?.data?.statusCode === 201) {
setDeleteIsLoading(false);
setDeleteAlert(false);
}
})
.catch((error) => {
// // console.error("Error creating community:", error);
setDeleteIsLoading(false);
setDeleteAlert(false);
});
} catch (error) {
// Handle errors
// // console.error("Error deleting community:", error);
}
};
const handleUpdateStatus = async (id) => {
try {
// Trigger the mutation
await updateBlogStatus({ id })
.then((response) => {
if (response?.data?.statusCode === 201) {
toast({
render: () => (
<ToastBox
status={"success"}
message={response?.data?.message}
/>
),
});
}
})
.catch((error) => {
// // console.log(error);
});
} catch (error) {
// Handle errors
// // console.error("Error updating community status:", error);
}
};
// ====================================================[Table Filter]================================================================
const filteredData = blog?.data?.data?.rows?.filter((item) => {
// Filter by name (case insensitive)
const name = item.author_name;
const searchLower = searchTerm.toLowerCase();
const nameMatches = name.toLowerCase().includes(searchLower);
// Filter by status
const status = item.active_blog;
const statusMatches =
statusFilter === "all" ||
(statusFilter === "active" && status === true) ||
(statusFilter === "inactive" && status === false);
return nameMatches && statusMatches;
});
// ====================================================[Table Setup]================================================================
const tableHeadRow = [
"Auther Name",
// "Discription",
"Summary",
"Tags",
"Active",
"Created At",
];
const extractedArray = filteredData?.map((item, index) => {
return {
"Auther Name": (
<RouterLink
to={`view/${item.id}`}
className="d-flex align-items-center gap-2 pointer"
>
<Avatar
size="sm"
name="KC Reddy"
src={`${API_URL}/${item.profile_image}`}
/>
<span className="d-flex flex-column">
<Text
as={"span"}
color={"gray.600"}
className="d-flex fw-bold align-items-center web-text-small"
>
{item?.author_name}
</Text>
<span className="d-flex align-items-center web-text-xsmall text-secondary">
{item?.author_designation}
</span>
</span>
</RouterLink>
),
// Discription: (
// <Tooltip
// className="rounded-2 web-text-xsmall"
// width={"fit-content"}
// placement="top"
// hasArrow
// label={item?.meta_description}
// bg="blue.200"
// >
// <Box display={"flex"} alignItems={"center"} w={200}>
// <Text as={"span"} isTruncated={true}>
// {item?.meta_description}
// </Text>
// </Box>
// </Tooltip>
// ),
Summary: (
<Tooltip
className="rounded-2 web-text-xsmall"
width={"fit-content"}
placement="top"
hasArrow
label={item?.summary}
bg="blue.200"
>
<Box display={"flex"} alignItems={"center"} w={400}>
<Text as={"span"} isTruncated={true}>
{item?.summary}
</Text>
</Box>
</Tooltip>
),
Tags: (
<Box
display={"flex"}
flexWrap={"wrap"}
gap={1}
alignItems={"center"}
w={200}
>
{item?.tags?.map(({ id, tag }) => (
<Badge rounded={'full'} key={id} variant="solid" fontWeight={'normal'} size={"sm"} ps={3} pe={3} pt={0.5} pb={0.5} backgroundColor={'#565263'}>
{tag}
</Badge>
))}
</Box>
),
Active: (
<Switch
size={"sm"}
colorScheme="teal"
onChange={() => handleUpdateStatus(item.id)}
isChecked={item.active_blog}
/>
),
"Created At": (
<span className="d-flex justify-content-between align-items-center">
<Text as={"span"} color={"gray.600"} className=" fw-bold">
{formatDate(item?.createdAt)}
</Text>
<Menu>
<MenuButton className="link p-1 rounded-1">
<HiDotsVertical className="rubix-text-dark fs-6" />
</MenuButton>
<Portal>
<MenuList minWidth="80px">
<RouterLink to={`edit/${item.id}`}>
<MenuItem className="web-text-medium">Edit</MenuItem>
</RouterLink>
<RouterLink to={`view/${item.id}`}>
<MenuItem className="web-text-medium">View</MenuItem>
</RouterLink>
<MenuItem
onClick={() => {
setActionId(item.id);
setDeleteAlert(true);
}}
className="web-text-medium"
>
Delete
</MenuItem>
</MenuList>
</Portal>
</Menu>
</span>
),
};
});
return (
<>
<TabularView
title={"Blogs and articles"}
btnTitle={"Create Blog"}
link={"/blogs-articles/add-blog"}
apiData={blog}
tableHeadRow={tableHeadRow}
extractedArray={extractedArray}
searchTerm={searchTerm}
setSearchTerm={setSearchTerm}
statusFilter={statusFilter}
setStatusFilter={setStatusFilter}
pageSize={pageSize}
setPageSize={setPageSize}
currentPage={currentPage}
setCurrentPage={setCurrentPage}
totalItems={blog?.data?.data?.totalItems}
totalPages={blog?.data?.data?.totalPages}
noDataTitle={'blog'}
/>
<CustomAlertDialog
onClose={() => setDeleteAlert(false)}
isOpen={deleteAlert}
alertHandler={() => handleDelete(actionId)}
message={"Are you sure you want to delete blogs?"}
isLoading={deleteIsLoading}
/>
</>
);
};
export default BlogsAndArticles;

View File

@@ -0,0 +1,703 @@
import React, { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import {
Box,
Button,
Divider,
FormControl,
Textarea,
FormHelperText,
FormLabel,
Heading,
Image,
Input,
Stack,
useToast,
} from "@chakra-ui/react";
import {
useGetBlogByIdQuery,
useUpdateBlogMutation,
} from "../../Services/api.service";
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import FullscreenLoaders from "../../Components/Loaders/FullscreenLoaders";
import { addBlogSchema, editBlogSchema } from "../../Validations/Validations";
import fallbackImage from "../../assets/fallBackImage.png";
import fallbackImageLarge from "../../assets/ultp-fallback-img.webp";
import { OPACITY_ON_LOAD } from "../../Layout/animations";
import { TiWarning } from "react-icons/ti";
import ReactQuill from "react-quill";
import ChipSelector from "../../Components/ChipSelector/ChipSelector";
import { motion } from "framer-motion";
import Loader01 from "../../Components/Loaders/Loader01";
import Header from "../../Components/Header";
import ToastBox from "../../Components/ToastBox";
const API_URL = import.meta.env.VITE_API_BASE_URL;
const EditBlogsAndArticles = () => {
const { id } = useParams();
const toast = useToast();
const navigate = useNavigate();
const [updateBlog] = useUpdateBlogMutation();
const { data, error, isLoading, refetch } = useGetBlogByIdQuery(id);
const blog = data?.data;
const [isLoading01, setIsLoading01] = useState(false);
const [selectedImage, setSelectedImage] = useState(fallbackImage);
const [selectedImageLarge, setSelectedImageLarge] =
useState(fallbackImageLarge);
const [largeImageData, setLargeImageData] = useState(null);
const [smallImageData, setSmallImageData] = useState(null);
const [chips, setChips] = useState();
const [valueQuill, setValueQuill] = useState(blog?.content);
const {
register,
handleSubmit,
reset,
formState: { errors },
setValue,
} = useForm({
resolver: yupResolver(editBlogSchema),
defaultValues: {
author_name: "",
author_designation: "",
meta_description: "",
title: "",
category: "",
summary: "",
content: "",
profile_image: "",
content_image_large: "",
tags: "",
},
});
useEffect(() => {
if (data?.data) {
setSelectedImage(`${API_URL}/${blog?.profile_image}`);
setSelectedImageLarge(
`${API_URL}/${blog?.content_image_large}`
);
setValue("author_name", blog?.author_name);
setValue("author_designation", blog?.author_designation);
setValue("meta_description", blog?.meta_description);
setValue("title", blog?.title);
setValue("category", blog?.category?.blog_category);
setValue("summary", blog?.summary);
setValue("content", blog?.content);
setValue("tags", blog?.tags);
setValueQuill(blog?.content);
setChips(blog?.tags?.map((tagObject) => tagObject.tag));
}
}, [data, blog, setValue]);
const onSubmit = async (data) => {
const formData = new FormData();
formData.append("author_name", data.author_name);
formData.append("author_designation", data.author_designation);
formData.append("meta_description", data.meta_description);
formData.append("title", data.title);
formData.append("category", data.category);
formData.append("summary", data.summary);
formData.append("content", valueQuill); // Add the content to formData
if (data.profile_image[0]) {
formData.append("profile_image", data.profile_image[0]);
}
if (data.content_image_large[0]) {
formData.append("content_image_large", data.content_image_large[0]);
}
if (chips.length === 0) {
return toast({
title: "Please add tags",
status: "error",
isClosable: true,
});
} else {
// formData.append("tags", chips);
chips.forEach((tag, i) => {
formData.append(`tags[${i}]`, tag); // Append each tag as an array element
});
}
try {
setIsLoading01(true);
updateBlog({ id: id, data: formData })
.then((response) => {
// Handle the response here
// // console.log("Mutation response:", response?.data?.statusCode);
// // console.log("Mutation response:", response?.data?.message);
if (response?.data?.statusCode === 201) {
setIsLoading01(false);
toast({
render: () => (
<ToastBox
status={"success"}
message={response?.data?.message}
/>
),
});
reset();
refetch();
navigate("/blogs-articles");
} else if (response?.error?.status === 500) {
setIsLoading01(false);
toast({
render: () => (
<ToastBox
status={"success"}
message={response?.error?.status?.error?.message}
/>
),
});
} else {
setIsLoading01(false);
toast({
render: () => (
<ToastBox status={"error"} message={"File size too large"} />
),
});
}
})
.catch((error) => {
// Handle errors
// // console.error("Error creating community:", error);
setIsLoading01(false);
// Handle error notification if needed
});
} catch (error) {
// Handle errors
// // console.error("Error creating community:", error);
}
};
const handleImageChange = (e) => {
const file = e.target.files[0];
setSmallImageData(file);
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
setSelectedImage(reader.result);
};
reader.readAsDataURL(file);
}
};
const handleImageChangeLarge = (e) => {
const file = e.target.files[0];
setLargeImageData(file);
// console.log(largeImageData);
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
setSelectedImageLarge(reader.result);
};
reader.readAsDataURL(file);
}
};
if (isLoading) {
return <FullscreenLoaders />;
}
return (
<Box
{...OPACITY_ON_LOAD}
overflowY={"scroll"}
w={"100%"}
h={"100vh"}
className="overflow-auto "
display={"flex"}
flexDirection={"column"}
>
<Header title={"Blog"} />
<form className="w-100" onSubmit={handleSubmit(onSubmit)}>
<Box display={"flex"}>
<Box className="col-5 d-flex flex-column justify-content-between gap-2 pt-4">
<Box flexDirection={"column"} display={"flex"}>
<span className="web-text-large fw-bold rubix-text-dark">
Blog Info
</span>
<span className="web-text-medium text-secondary">
Select the platform for which you need to create this campaign.
</span>
</Box>
<Box flexDirection={"column"} display={"flex"}>
<Divider />
<span className="web-text-large fw-bold rubix-text-dark">
Blog's Banner image
</span>
<span className="web-text-medium text-secondary mb-4">
Below is the profile that will be displayed on the community
page.
</span>
<Box
boxSize="sm"
className="d-flex w-100 p-2 justify-content-center flex-column align-items-center gap-3"
>
<Image
shadow={"md"}
rounded={8}
w={500}
h={240}
src={selectedImageLarge}
alt="Selected Image"
/>
{selectedImageLarge === fallbackImageLarge ? (
""
) : (
largeImageData && <Box display={"flex"} flexDirection={"column"} w={"100%"}>
<span className="web-text-small">
{largeImageData?.name}
</span>
<span className="web-text-small text-secondary fst-italic">
{(largeImageData?.size / (1024 * 1024)).toFixed(2)} mb
</span>
</Box>
)}
<Button
onClick={() => setSelectedImageLarge(fallbackImageLarge)}
backgroundColor="red.400"
color={"whitesmoke"}
transition={"0.5s"}
_hover={{
backgroundColor: "red.500",
}}
size="xs"
>
Remove
</Button>
</Box>
</Box>
</Box>
<Box className="col-7 pt-4 p-4">
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Blog title
</FormLabel>
<Input
{...register("title")}
placeholder="Blog title"
className="web-text-medium"
size="sm"
maxLength={90}
/>
<FormHelperText className="web-text-small">
Maximum characters must be 100 characters.
</FormHelperText>
{errors.title && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " /> {errors.title.message}
</span>
)}
</FormControl>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Blog description
</FormLabel>
<Textarea
rows={4}
{...register("meta_description")}
placeholder="Blog description"
className="web-text-medium"
size="sm"
/>
<FormHelperText className="web-text-small">
Please share proper linked in link here.
</FormHelperText>
{errors.meta_description && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.meta_description.message}
</span>
)}
</FormControl>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Blog content
</FormLabel>
{/* <Textarea
rows={4}
{...register("content")}
placeholder="content link"
className="web-text-medium"
size="sm"
/> */}
<ReactQuill
className="rounded-3"
theme="snow"
value={valueQuill}
onChange={setValueQuill}
/>
<FormHelperText className="web-text-small">
Please share proper linked in link here.
</FormHelperText>
</FormControl>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Blog summary
</FormLabel>
<Textarea
rows={2}
{...register("summary")}
placeholder="Summary"
className="web-text-medium"
size="sm"
/>
<FormHelperText className="web-text-small">
Please share proper linked in link here.
</FormHelperText>
{errors.summary && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.summary.message}
</span>
)}
</FormControl>
<FormControl className="mb-3" isRequired >
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Tags
</FormLabel>
<ChipSelector chips={chips} setChips={setChips} />
</FormControl>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Category
</FormLabel>
<Input
{...register("category")}
placeholder="Category"
className="web-text-medium"
size="sm"
maxLength={90}
/>
<FormHelperText className="web-text-small">
Maximum characters must be 100 characters.
</FormHelperText>
{errors.category && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.category.message}
</span>
)}
</FormControl>
<FormControl>
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Blog banner
</FormLabel>
{/* <ImageDropBox /> */}
<Box
borderColor="gray.300"
borderStyle="dashed"
borderWidth="2px"
rounded="md"
shadow="sm"
role="group"
transition="all 150ms ease-in-out"
_hover={{
shadow: "md",
}}
as={motion.div}
initial="rest"
animate="rest"
whileHover="hover"
h={105}
className="pointer"
>
<Box position="relative" height="100%" width="100%">
<Box
position="absolute"
top="0"
left="0"
height="100%"
width="100%"
display="flex"
flexDirection="column"
>
<Stack
height="100%"
width="100%"
display="flex"
alignItems="center"
justify="center"
>
<span
className="d-flex flex-column align-items-center pointer"
spacing="1"
>
<Heading
fontSize="lg"
color="gray.700"
fontWeight="bold"
cursor={"pointer"}
>
Drop images here
</Heading>
<span
fontWeight="light"
className="web-text-large text-secondary text-center pointer"
>
or click to upload
</span>
</span>
</Stack>
</Box>
<Input
{...register("content_image_large")}
type="file"
height="100%"
width="100%"
position="absolute"
top="0"
left="0"
opacity="0"
aria-hidden="true"
accept="image/*"
onChange={handleImageChangeLarge}
onDrop={handleImageChangeLarge}
// onDragEnter={startAnimation}
// onDragLeave={stopAnimation}
/>
</Box>
</Box>
{errors.content_image_large && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.content_image_large.message}
</span>
)}
<FormHelperText className="web-text-small">
Maximum limit of image is 10MB.
</FormHelperText>
</FormControl>
</Box>
</Box>
<Divider />
<Box display={"flex"}>
<Box className="col-5 d-flex flex-column gap-2 pt-4">
{/* <span className="web-text-large fw-bold rubix-text-dark">
Author's Info
</span>
<span className="web-text-medium text-secondary">
Select the platform for which you need to create this campaign.
</span>
<Divider /> */}
<span className="web-text-large fw-bold rubix-text-dark">
Author's display profile
</span>
<span className="web-text-medium text-secondary mb-0">
Below is the profile that will be displayed on the community page.
</span>
<Box
boxSize="sm"
className="d-flex flex-column align-items-center gap-3 justify-content-center"
>
<Image
shadow={"md"}
rounded={8}
w={214}
h={240}
src={selectedImage}
alt="Selected Image"
/>
{selectedImage === fallbackImage ? (
""
) : (
smallImageData && <Box display={"flex"} flexDirection={"column"} w={"100%"}>
<span className="web-text-small">{smallImageData?.name}</span>
<span className="web-text-small text-secondary fst-italic">
{(smallImageData?.size / (1024 * 1024)).toFixed(2)} mb
</span>
</Box>
)}
<Button
onClick={() => setSelectedImage(fallbackImage)}
backgroundColor="red.400"
color={"whitesmoke"}
transition={"0.5s"}
_hover={{
backgroundColor: "red.500",
}}
size="xs"
>
Remove
</Button>
</Box>
</Box>
<Box className="col-7 pt-4 p-4">
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Author name
</FormLabel>
<Input
{...register("author_name")}
placeholder="Name"
className="web-text-medium"
size="sm"
/>
{errors.name && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.author_name.message}
</span>
)}
</FormControl>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Author designation
</FormLabel>
<Input
{...register("author_designation")}
placeholder="Author designation"
className="web-text-medium"
size="sm"
/>
{errors.author_designation && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.author_designation.message}
</span>
)}
</FormControl>
<FormControl className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Display profile
</FormLabel>
{/* <ImageDropBox /> */}
<Box
borderColor="gray.300"
borderStyle="dashed"
borderWidth="2px"
rounded="md"
shadow="sm"
role="group"
transition="all 150ms ease-in-out"
_hover={{
shadow: "md",
}}
as={motion.div}
initial="rest"
animate="rest"
whileHover="hover"
height={105}
className="pointer"
>
<Box position="relative" height="100%" width="100%">
<Box
position="absolute"
top="0"
left="0"
height="100%"
width="100%"
display="flex"
flexDirection="column"
>
<Stack
height="100%"
width="100%"
display="flex"
alignItems="center"
justify="center"
>
<span
className="d-flex flex-column align-items-center pointer"
spacing="1"
>
<Heading
fontSize="lg"
color="gray.700"
fontWeight="bold"
cursor={"pointer"}
>
Drop images here
</Heading>
<span
fontWeight="light"
className="web-text-large text-secondary text-center pointer"
>
or click to upload
</span>
</span>
</Stack>
</Box>
<Input
{...register("profile_image")}
type="file"
height="100%"
width="100%"
position="absolute"
top="0"
left="0"
opacity="0"
aria-hidden="true"
accept="image/*"
onChange={handleImageChange}
onDrop={handleImageChange}
// onDragEnter={startAnimation}
// onDragLeave={stopAnimation}
/>
</Box>
</Box>
{errors.profile_image && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.profile_image.message}
</span>
)}
<FormHelperText className="web-text-small">
Maximum limit of image is 10MB.
</FormHelperText>
</FormControl>
<Box className=" d-flex justify-content-end">
<Button
isLoading={isLoading01}
spinner={<Loader01 />}
color={"whitesmoke"}
backgroundColor={"purple.900"}
_hover={{
backgroundColor: "purple.800",
}}
type="submit"
size="sm"
rounded={"sm"}
>
Save edit
</Button>
</Box>
</Box>
</Box>
</form>
</Box>
);
};
export default EditBlogsAndArticles;

View File

@@ -0,0 +1,228 @@
import {
Badge,
Box,
Button,
Divider,
Image,
Tag,
Text,
useToast,
} from "@chakra-ui/react";
import React from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
import { useGetBlogByIdQuery } from "../../Services/api.service";
import FullscreenLoaders from "../../Components/Loaders/FullscreenLoaders";
import { OPACITY_ON_LOAD } from "../../Layout/animations";
import Header from "../../Components/Header";
const API_URL = import.meta.env.VITE_API_BASE_URL;
const ViewBlogsAndArticles = () => {
const { id } = useParams();
const toast = useToast();
const navigate = useNavigate();
const { data, error, isLoading } = useGetBlogByIdQuery(id);
const blog = data?.data;
if (isLoading) {
return <FullscreenLoaders />;
}
return (
<Box
{...OPACITY_ON_LOAD}
h={"100vh"}
overflowY={"scroll"}
display={"flex"}
flexDirection={"column"}
>
<Header
title={'Blog'}
btnTitle={'Edit blog'}
link={`/blogs-articles/edit/${id}`}
/>
<Box display={"flex"}>
<Box className="col-5 d-flex flex-column gap-2 pt-4">
<span className="web-text-large fw-bold rubix-text-dark">
Blog Info
</span>
<span className="web-text-medium text-secondary">
Select the platform for which you need to create this campaign.
</span>
<Divider />
<span className="web-text-large fw-bold rubix-text-dark">
Blog's Banner image
</span>
<span className="web-text-medium text-secondary mb-4">
Below is the profile that will be displayed on the community page.
</span>
<Box
boxSize="sm"
className="d-flex h-auto w-100 justify-content-start flex-column align-items-center gap-3"
>
<Image
shadow={"md"}
rounded={8}
w={500}
h={240}
src={`${API_URL}/${blog?.content_image_large}`}
alt="Selected Image"
/>
{/* <Button
onClick={() => setSelectedImage(fallbackImage)}
backgroundColor="red.400"
color={"whitesmoke"}
transition={"0.5s"}
_hover={{
backgroundColor: "red.500",
}}
size="xs"
>
Remove
</Button> */}
</Box>
</Box>
<Box className="col-7 pt-4 p-4">
<Box className="d-flex flex-column align-items-start gap-1 mb-3">
<Box className="web-text-large fw-bold rubix-text-dark">
Status
</Box>
{blog.active_blog ? (
<Badge size={"sm"} colorScheme="teal">
Active
</Badge>
) : (
<Badge
size={"sm"}
variant="solid"
colorScheme="red"
>
Inactive
</Badge>
)}
</Box>
<Box className="mb-3">
<Box className="web-text-large fw-bold rubix-text-dark">Title</Box>
<Box className="web-text-medium text-secondary">{blog?.title}</Box>
</Box>
<Box className="mb-3">
<Box className="web-text-large fw-bold rubix-text-dark">
Blog description
</Box>
<Box className="web-text-medium text-secondary">
{blog?.meta_description}
</Box>
</Box>
<Box className="mb-3">
<Box className="web-text-large fw-bold rubix-text-dark">
Content
</Box>
<Box className="web-text-medium">
<Text
pb={2}
className="text-dark"
dangerouslySetInnerHTML={{ __html: blog?.content }}
/>
</Box>
</Box>
<Box className="mb-3">
<Box className="web-text-large fw-bold rubix-text-dark">
Blog summary
</Box>
<Box className="web-text-medium text-secondary">
{blog?.summary}
</Box>
</Box>
<Box className="mb-3">
<Box className="web-text-large fw-bold rubix-text-dark mb-2">
Tags
</Box>
<Box
display={"flex"}
flexWrap={"wrap"}
gap={2}
alignItems={"center"}
w={"100%"}
>
{blog?.tags?.map(({ id, tag }) => (
<Badge rounded={'full'} key={id} variant="solid" size={"sm"} ps={3} pe={3} pt={0.5} pb={0.5} backgroundColor={'#565263'}>
{tag}
</Badge>
))}
</Box>
</Box>
<Box className="mb-3">
<Box className="web-text-large fw-bold rubix-text-dark">
Category
</Box>
<Box className="web-text-medium text-secondary">
{blog?.category?.blog_category}
</Box>
</Box>
</Box>
</Box>
<Divider/>
<Box display={"flex"} mb={6}>
<Box className="col-5 d-flex flex-column gap-2 pt-">
<span className="web-text-large fw-bold rubix-text-dark">
Author's display profile
</span>
<span className="web-text-medium text-secondary mb-4">
Below is the profile that will be displayed on the community page.
</span>
<Box
className="d-flex w-100 justify-content-center flex-column align-items-center "
>
<Image
shadow={"md"}
rounded={8}
w={214}
h={240}
src={`${API_URL}/${blog?.profile_image}`}
alt="Selected Image"
/>
</Box>
</Box>
<Box className="col-7 pt-0 p-4">
<Box className="mb-3">
<Box className="web-text-large fw-bold rubix-text-dark">
Author name
</Box>
<Box className="web-text-medium text-secondary">
{blog?.author_name}
</Box>
</Box>
<Box className="mb-3">
<Box className="web-text-large fw-bold rubix-text-dark">
Author designation
</Box>
<Box className="web-text-medium text-secondary">
{blog?.author_designation}
</Box>
</Box>
</Box>
</Box>
</Box>
);
};
export default ViewBlogsAndArticles;

View File

@@ -0,0 +1,384 @@
import {
Box,
FormControl,
FormHelperText,
FormLabel,
Input,
Text,
Stack,
Textarea,
Heading,
Button,
useToast,
Divider,
Image,
} from "@chakra-ui/react";
import React, { useState } from "react";
import fallbackImage from "../../assets/fallBackImage.png";
import { TiWarning } from "react-icons/ti";
import { motion } from "framer-motion";
import { OPACITY_ON_LOAD } from "../../Layout/animations";
import { yupResolver } from "@hookform/resolvers/yup";
import { useForm } from "react-hook-form";
import { addCommunitySchema } from "../../Validations/Validations";
import {
useCreateCommunityMutation,
useGetCommunityQuery,
} from "../../Services/api.service";
import { useNavigate } from "react-router-dom";
import Loader01 from "../../Components/Loaders/Loader01";
import Header from "../../Components/Header";
import { CloseIcon } from "@chakra-ui/icons";
import ToastBox from "../../Components/ToastBox";
const AddComunity = () => {
const toast = useToast();
const navigate = useNavigate();
const getCommunityQuery = useGetCommunityQuery();
const [createCommunityData] = useCreateCommunityMutation(); // Invoke the hook to get the mutation function
const [isLoading, setIsLoading] = useState(false);
const [selectedImage, setSelectedImage] = useState(fallbackImage);
const [imageData, setImageData] = useState(null);
const {
register,
handleSubmit,
reset,
watch,
formState: { errors },
} = useForm({
resolver: yupResolver(addCommunitySchema),
});
const onSubmit = async (data) => {
try {
setIsLoading(true);
const formData = new FormData();
formData.append("member_name", data.member_name);
formData.append("designation", data.designation);
formData.append("description", data.description);
formData.append("linkedin", data.linkedin);
if (data.profile_image[0]) {
formData.append("profile_image", data.profile_image[0]);
}
// Trigger the mutation
createCommunityData(formData)
.then((response) => {
// Handle the response here
// // console.log("Mutation response:", response?.data?.statusCode);
// // console.log("Mutation response:", response?.data?.message);
if (response?.data?.statusCode === 200) {
setIsLoading(false);
toast({
render: () => (
<ToastBox status={"success"} message={response?.data?.message} />
),
});
reset();
navigate("/community");
}
})
.catch((error) => {
// Handle errors
// // console.error("Error creating community:", error);
setIsLoading(false);
// Handle error notification if needed
});
} catch (error) {
// Handle errors
// // console.error("Error creating community:", error);
}
};
const handleImageChange = (e) => {
const file = e.target.files[0];
setImageData(file);
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
setSelectedImage(reader.result);
};
reader.readAsDataURL(file);
}
};
return (
<Box
{...OPACITY_ON_LOAD}
w={"100%"}
h={"100vh"}
overflowY={"scroll"}
display={"flex"}
flexDirection={"column"}
>
<Header title={"Community"} />
<Box display={"flex"}>
<Box className="col-5 d-flex flex-column gap-2 pt-4">
<span className="web-text-large fw-bold rubix-text-dark">
Members Info
</span>
<span className="web-text-medium text-secondary">
Select the platform for which you need to create this campaign.
</span>
<Divider />
<span className="web-text-large fw-bold rubix-text-dark">
Display profile
</span>
<span className="web-text-medium text-secondary mb-4">
Below is the profile that will be displayed on the community page.
</span>
<Box
boxSize="sm"
className="d-flex w-100 justify-content-center flex-column align-items-center gap-3"
>
<Image
shadow={"md"}
rounded={8}
w={214}
h={240}
src={selectedImage}
alt="Selected Image"
/>
{selectedImage === fallbackImage || imageData === null ? (
""
) : (
<Box w={"100%"} display={"flex"} justifyContent={'space-between'} >
<Box display={"flex"} flexDirection={"column"}>
<span className="web-text-small">{imageData?.name}</span>
<span className="web-text-small text-secondary fst-italic">
{(imageData?.size / (1024 * 1024)).toFixed(2)} mb
</span>
</Box>
<Box onClick={()=> setSelectedImage(fallbackImage)} className=" web-text-large link rounded-2 pointer p-1" as="span" >
<CloseIcon className="web-text-small" /></Box>
</Box>
)}
</Box>
</Box>
<form
onSubmit={handleSubmit(onSubmit)}
className="col-7 pt-4 overflow-auto p-4"
>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Name
</FormLabel>
<Input
{...register("member_name")}
placeholder="Name"
className="web-text-medium"
size="sm"
minLength={4}
errorBorderColor="crimson"
isInvalid={watch()?.member_name?.length > 50}
// maxLength={51}
/>
<FormHelperText
color={watch()?.member_name?.length > 50 ? "red" : "gray.500"}
className="web-text-small"
>
If name crosses 50 characters it will cause problem in
alignment on website.you have entered {watch()?.member_name?.length}{" "}
characters
</FormHelperText>
{errors.member_name && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.member_name.message}
</span>
)}
</FormControl>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Designation
</FormLabel>
<Input
{...register("designation")}
placeholder="Designation"
className="web-text-medium"
size="sm"
/>
{errors.designation && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.designation.message}
</span>
)}
</FormControl>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Description
</FormLabel>
<Textarea
{...register("description")}
placeholder="Description"
className="web-text-medium"
size="sm"
rows={2}
minLength={4}
errorBorderColor="crimson"
isInvalid={watch()?.description?.length > 230}
// maxLength={51}
/>
<FormHelperText
color={watch()?.description?.length > 230 ? "red" : "gray.500"}
className="web-text-small"
>
If title crosses 230 characters it will cause problem in
alignment on website.you have entered {watch()?.description?.length}{" "}
characters
</FormHelperText>
{errors.description && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.description.message}
</span>
)}
</FormControl>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Linked In
</FormLabel>
<Input
{...register("linkedin")}
placeholder="Linkedin link"
className="web-text-medium"
size="sm"
/>
<FormHelperText className="web-text-small">
Please share proper linked in link here.
</FormHelperText>
{errors.linkedin && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.linkedin.message}
</span>
)}
</FormControl>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Display profile
</FormLabel>
{/* <ImageDropBox /> */}
<Box
borderColor="gray.300"
borderStyle="dashed"
borderWidth="2px"
rounded="md"
shadow="sm"
role="group"
transition="all 150ms ease-in-out"
_hover={{
shadow: "md",
}}
as={motion.div}
initial="rest"
animate="rest"
whileHover="hover"
height={"105px"}
className="pointer"
>
<Box position="relative" height="100%" width="100%">
<Box
position="absolute"
top="0"
left="0"
height="100%"
width="100%"
display="flex"
flexDirection="column"
>
<Stack
height="100%"
width="100%"
display="flex"
alignItems="center"
justify="center"
>
<span
className="d-flex flex-column align-items-center pointer"
spacing="1"
>
<Heading
fontSize="lg"
color="gray.700"
fontWeight="bold"
cursor={"pointer"}
>
Drop images here
</Heading>
<span
fontWeight="light"
className="web-text-large text-secondary text-center pointer"
>
or click to upload
</span>
</span>
</Stack>
</Box>
<Input
{...register("profile_image")}
type="file"
height="100%"
width="100%"
position="absolute"
top="0"
left="0"
opacity="0"
aria-hidden="true"
accept="image/*"
onChange={handleImageChange}
onDrop={handleImageChange}
// onDragEnter={startAnimation}
// onDragLeave={stopAnimation}
/>
</Box>
</Box>
{errors.profile_image && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.profile_image.message}
</span>
)}
<FormHelperText className="web-text-small">
Maximum limit of image is 10MB.
</FormHelperText>
</FormControl>
<Box className=" d-flex justify-content-end ">
<Button
isLoading={isLoading}
spinner={<Loader01 />}
color={"whitesmoke"}
backgroundColor={"purple.900"}
_hover={{
backgroundColor: "purple.800",
}}
type="submit"
size="sm"
rounded={"sm"}
>
Create member
</Button>
</Box>
</form>
</Box>
</Box>
);
};
export default AddComunity;

View File

@@ -0,0 +1,86 @@
/* eslint-disable react/prop-types */
/* eslint-disable no-unused-vars */
import { Text, SimpleGrid, Card, Image, Box, Flex } from "@chakra-ui/react";
// import map from "../../assets/images/map-pin.png";
import linkedin from "../../assets/linkedin.png";
import { Link } from "react-router-dom";
const API_URL = import.meta.env.VITE_API_BASE_URL;
const CommCard = ({ id, imageUrl, name, jobTitle, description, linkdin }) => {
return (
<Box
// height={"100vh"}
background={"#101015"}
backgroundSize={"cover"}
backgroundRepeat={"no-repeat"}
>
<Text display={"flex"}>
<Text
position="relative"
overflow="hidden"
_hover={{
"&::before": {
content: '""',
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
background:
"linear-gradient(to bottom, #f8697a8c 0%, #8d54f86e 86%)",
borderRadius: "5px",
},
}}
>
<Image src={`${API_URL}/${imageUrl}`} />
</Text>
<Text
position={"relative"}
marginLeft={"10px"}
_before={{
content: "''",
position: "absolute",
bottom: 0,
left: "15px",
height: "84%",
width: "100%",
borderLeft: "1px solid #ffffff70",
}}
>
<Link to={linkdin}>
<img
src={linkedin}
style={{ minWidth: "34px", height: "34px", marginBottom: "10px" }}
/>
</Link>
{/* <img src={games} style={{ minWidth: "34px", height: "34px" }} /> */}
</Text>
</Text>
{/* <Text
color={"#fff"}
fontSize={"16px"}
marginTop={"12px"}
maxWidth={"460px"}
display={"flex"}
>
<img src={map} style={{ marginRight: "10px" }} /> {location}
</Text> */}
<Text
color={"#fff"}
fontSize={"16px"}
maxWidth={"460px"}
>
{name}
</Text>
<Text fontSize={"12px"} color={"#DEDEDE"} margin={"2px 0px"}>
{jobTitle}
</Text>
<Text fontSize={"11px"} color={"#DEDEDE"} margin={"3px 0px"}>
{description}
</Text>
</Box>
);
};
export default CommCard;

View File

@@ -0,0 +1,324 @@
import React, { useRef, useState } from "react";
import {
Avatar,
Box,
Link,
Tag,
Text,
WrapItem,
Tooltip,
Divider,
Stack,
HStack,
Input,
Button,
Select,
Image,
Menu,
MenuButton,
MenuList,
MenuItem,
Switch,
Portal,
useDisclosure,
AlertDialog,
AlertDialogOverlay,
AlertDialogContent,
AlertDialogHeader,
AlertDialogCloseButton,
AlertDialogBody,
AlertDialogFooter,
useToast,
Skeleton,
VStack,
} from "@chakra-ui/react";
import { GrAdd } from "react-icons/gr";
import {
AddIcon,
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
HamburgerIcon,
} from "@chakra-ui/icons";
import DataTable from "../../Components/DataTable/DataTable";
import CommunityBanner from "../../Components/CommunityBanner";
import { OPACITY_ON_LOAD } from "../../Layout/animations";
import { v4 as uuidv4 } from "uuid";
import { Link as RouterLink } from "react-router-dom";
import {
useDeleteCommunityMutation,
useGetCommunityBannerQuery,
useGetCommunityByIdQuery,
useGetCommunityQuery,
useUpdateCommunityStatusMutation,
} from "../../Services/api.service";
import { HiDotsVertical } from "react-icons/hi";
import TimeCalculator from "../../Components/Functions/TimeCalculator";
import { formatDate } from "../../Components/Functions/UTCConvertor";
import CustomAlertDialog from "../../Components/CustomAlertDialog";
import WebButton from "../../Components/WebButton";
import CommunityCardDisplay from "./CommunityCardDisplay";
import CommunityBannerCard from "./CommunityBannerCard";
import Header from "../../Components/Header";
import { TABLE_PAGINATION } from "../../Constants/Paginations";
import ToastBox from "../../Components/ToastBox";
import TabularView from "../../Components/TabularView/TabularView";
const API_URL = import.meta.env.VITE_API_BASE_URL;
const Community = () => {
// ====================================================[Hooks]===================================================================
const cancelRef = useRef();
const toast = useToast();
const { isOpen, onOpen, onClose } = useDisclosure();
const [deleteAlert, setDeleteAlert] = useState(false);
const [actionId, setActionId] = useState(null);
const [deleteIsLoading, setDeleteIsLoading] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [statusFilter, setStatusFilter] = useState("all");
const [pageSize, setPageSize] = useState(TABLE_PAGINATION?.size);
const [currentPage, setCurrentPage] = useState(1);
const [displayRange, setDisplayRange] = useState({
start: TABLE_PAGINATION?.page,
end: pageSize,
});
const community = useGetCommunityQuery({ page: currentPage, size: pageSize });
const [deleteCommunity] = useDeleteCommunityMutation();
const [updateCommunityStatus] = useUpdateCommunityStatusMutation();
// ====================================================[Functions]===================================================================
const handleDelete = async (communityId) => {
try {
// Trigger the mutation
setDeleteIsLoading(true);
await deleteCommunity(communityId)
.then((response) => {
// Handle the response here
// // console.log("Mutation response:", response?.data?.statusCode);
// // console.log("Mutation response:", response?.data?.message);
if (response?.data?.statusCode === 201) {
setDeleteIsLoading(false);
setDeleteAlert(false);
}
})
.catch((error) => {
// // console.error("Error creating community:", error);
setDeleteIsLoading(false);
setDeleteAlert(false);
});
} catch (error) {
// Handle errors
// // console.error("Error deleting community:", error);
}
};
const handleUpdateStatus = async (id) => {
try {
// Trigger the mutation
await updateCommunityStatus({ id })
.then((response) => {
if (response?.data?.statusCode === 201) {
toast({
render: () => (
<ToastBox
status={"success"}
message={response?.data?.message}
/>
),
});
}
})
.catch((error) => {
// // console.log(error);
});
} catch (error) {
// Handle errors
// // console.error("Error updating community status:", error);
}
};
// ====================================================[Table Filter]================================================================
const filteredData = community?.data?.data?.rows?.filter((item) => {
// Filter by name (case insensitive)
const name = item.member_name;
const searchLower = searchTerm.toLowerCase();
const nameMatches = name.toLowerCase().includes(searchLower);
// Filter by status
const status = item.status;
const statusLower = status ? "active" : "inactive";
const statusMatches =
statusFilter === "all" ||
(statusFilter === "active" && status === true) ||
(statusFilter === "inactive" && status === false);
return nameMatches && statusMatches;
});
// ====================================================[Table Setup]================================================================
const tableHeadRow = [
"Name",
"Description",
"Linked In",
"Active",
"Created At",
];
const extractedArray = filteredData?.map((item, index) => {
return {
Name: (
<RouterLink
to={`view/${item.id}`}
className="d-flex align-items-center gap-2 pointer"
>
<Avatar
size="sm"
name="KC Reddy"
src={`${API_URL}/${item.profile_image}`}
/>
<span className="d-flex flex-column">
<Text
as={"span"}
color={"teal.700"}
className="d-flex fw-bold align-items-center web-text-small"
>
{item?.member_name}
</Text>
<span className="d-flex align-items-center web-text-xsmall text-secondary">
{item?.designation}
</span>
</span>
</RouterLink>
),
Description: (
<Box w={350} isTruncated={true}>
<Text as={"span"} color={"teal.900"}>
{item?.description}
</Text>
</Box>
),
"Linked In": (
<Link href={item?.linkedin} isExternal>
<Tooltip
className="rounded-2 web-text-xsmall"
width={"fit-content"}
placement="top"
hasArrow
label={item?.linkedin}
bg="blue.200"
>
<Tag
variant="solid"
size={"sm"}
borderRadius={2}
colorScheme="linkedin"
>
Linked In
</Tag>
</Tooltip>
</Link>
),
Active: (
<Switch
size={"sm"}
colorScheme="teal"
onChange={() => handleUpdateStatus(item.id)}
isChecked={item.status}
/>
),
"Created At": (
<span className="d-flex justify-content-between align-items-center">
<Text as={"span"} color={"gray.600"} className=" fw-bold">
{formatDate(item?.createdAt)}
</Text>
<Menu>
<MenuButton className="link p-1 rounded-1">
<HiDotsVertical className="rubix-text-dark fs-6" />
</MenuButton>
<Portal>
<MenuList minWidth="80px">
<RouterLink to={`edit/${item.id}`}>
<MenuItem className="web-text-medium">Edit</MenuItem>
</RouterLink>
<RouterLink to={`view/${item.id}`}>
<MenuItem className="web-text-medium">View</MenuItem>
</RouterLink>
<MenuItem
onClick={() => {
setActionId(item.id);
setDeleteAlert(true);
}}
className="web-text-medium"
>
Delete
</MenuItem>
</MenuList>
</Portal>
</Menu>
</span>
),
};
});
// ====================================================[Pagination Setup]================================================================
const paginationPrev = () => {
if (currentPage > 1) {
setCurrentPage(currentPage - 1);
updateDisplayRange(currentPage - 1);
}
};
const paginationNext = () => {
const totalPages = Math.ceil(community?.data?.data?.totalItems / pageSize);
if (currentPage < totalPages) {
setCurrentPage(currentPage + 1);
updateDisplayRange(currentPage + 1);
}
};
const updateDisplayRange = (page) => {
const start = (page - 1) * pageSize + 1;
const end = Math.min(
start + pageSize - 1,
community?.data?.data?.totalItems
);
setDisplayRange({ start, end });
};
return (
<>
<TabularView
title={"Community"}
btnTitle={"Create member"}
link={"/community/add-community"}
apiData={community}
tableHeadRow={tableHeadRow}
extractedArray={extractedArray}
searchTerm={searchTerm}
setSearchTerm={setSearchTerm}
statusFilter={statusFilter}
setStatusFilter={setStatusFilter}
pageSize={pageSize}
setPageSize={setPageSize}
currentPage={currentPage}
setCurrentPage={setCurrentPage}
totalItems={community?.data?.data?.totalItems}
totalPages={community?.data?.data?.totalPages}
noDataTitle={"community's"}
/>
{/* ====================================================[ Alert ]================================================================ */}
<CustomAlertDialog
onClose={() => setDeleteAlert(false)}
isOpen={deleteAlert}
alertHandler={() => handleDelete(actionId)}
message={"Are you sure you want to delete community?"}
isLoading={deleteIsLoading}
/>
</>
);
};
export default Community;

View File

@@ -0,0 +1,188 @@
import {
Badge,
Box,
Button,
Card,
CardBody,
CardHeader,
Heading,
Image,
Text,
Tooltip,
} from "@chakra-ui/react";
import React from "react";
import WebButton from "../../Components/WebButton";
import { formatDate } from "../../Components/Functions/UTCConvertor";
const API_URL = import.meta.env.VITE_API_BASE_URL;
const CommunityBannerCard = ({
bgImage,
subHeading,
heading,
ctoBtnTitle,
createdAt,
status,
}) => {
return (
<Card
color={"teal.900"}
w={"100%"}
h={"100%"}
size={"md"}
// boxShadow="md"
// overflow={"hidden"}
position={"relative"}
// rounded={"lg"}
boxShadow={
" rgba(50, 50, 93, 0.25) 0px 2px 5px -1px, rgba(0, 0, 0, 0.3) 0px 1px 3px -1px"
}
borderRadius={8.5}
>
<CardBody
position={"absolute"}
h={"100%"}
w={"100%"}
bottom={0}
// border={"1px solid #9B4651"}
borderRadius={'sm'}
overflow={"hidden"}
bgImage={`${API_URL}/${bgImage}`}
bgSize="cover"
bgPosition="center"
// backgroundColor={status ? "#ffe5ea": '#ffffff'}
backdropFilter="blur(1px)"
>
{status ? (
<Badge
position={"absolute"}
top={2}
right={-2}
pe={3}
colorScheme="green"
variant='solid'
>
Active
</Badge>
) : (
<Badge
position={"absolute"}
top={2}
right={-2}
pe={3}
colorScheme="red"
variant='solid'
>
Inactive
</Badge>
)}
<Box
mb={0}
display={"flex"}
justifyContent={"space-between"}
h={"100%"}
w={"100%"}
>
<Box
w={40}
display={"flex"}
flexDirection={"column"}
alignItems={"start"}
>
<Tooltip
className="rounded-2 web-text-xsmall"
width={"fit-content"}
placement="top"
hasArrow
label={heading}
bg="blue.200"
>
<Box
display={"flex"}
className="web-text-large fw-bold"
alignItems={"center"}
w={"100%"}
>
<Text color={"#DE858E"} as={"span"} isTruncated={true}>
{heading}
</Text>
</Box>
</Tooltip>
<Box
display={"flex"}
className="web-text-large fw-bold"
alignItems={"center"}
w={180}
>
<Tooltip
className="rounded-2 web-text-xsmall"
width={"fit-content"}
placement="top"
hasArrow
label={subHeading}
bg="blue.200"
>
<Text
className="web-text-medium"
as={"span"}
isTruncated={true}
color={'whitesmoke'}
>
{subHeading}
</Text>
</Tooltip>
</Box>
<Button
fontWeight={"normal"}
className="web-text-xsmall"
ps={3}
pe={3}
pt={1}
pb={1}
mt={2}
color={"#ffffff"}
_hover={{
bgGradient:"linear(to-r, #1E114B, purple)"
}}
// bg={'#1E114B'}
// bgGradient="linear(to-r, #1E114B, purple)"
variant={"outline"}
// colorScheme="purple"
rounded={"sm"}
size={"xs"}
// border={'1px soild #fff'}
>
{ctoBtnTitle}
</Button>
</Box>
{/* <Box display={"flex"} alignItems={"center"}>
<Image
boxShadow={"inner"}
rounded={"md"}
w={130}
h={"75px"}
src={`${API_URL}/${bgImage}`}
/>
</Box> */}
</Box>
</CardBody>
<span
className="web-text-xsmall text-secondary fw-bold"
style={{
position: "absolute",
bottom: 4,
right: 10,
opacity: 1,
}}
>
{formatDate(createdAt)}
</span>
</Card>
);
};
export default CommunityBannerCard;

View File

@@ -0,0 +1,71 @@
import React from 'react'
import { useGetCommunityQuery } from '../../Services/api.service';
import CommCard from './CommCard';
import { Box, Container, SimpleGrid, Text } from '@chakra-ui/react';
const CommunityCardDisplay = () => {
const community = useGetCommunityQuery();
const communityData = community.data?.data?.rows
return (
<Box rounded={4} backgroundColor={"#101015"}>
<Container
cursor={'pointer'}
maxW={"1200px"}
padding={"0rem"}
paddingBottom={"2rem"}
display={'flex'}
alignItems={'center'}
flexDirection={'column'}
sx={{
"@media (max-width: 1024px)": {
padding: "3rem",
},
"@media (max-width: 435px)": {},
}}
>
<Text
as={"h2"}
paddingTop={"1rem"}
paddingBottom={"1rem"}
fontWeight={700}
fontSize={"30px"}
textAlign={"left"}
textTransform={"capitalize"}
color={"#fff"}
sx={{
"@media (max-width: 435px)": {
fontSize: "35px",
},
"@media (max-width: 375px)": {
fontSize: "28px",
textAlign: "center",
},
}}
>
Rubix Community
</Text>
<SimpleGrid
w={'100%'}
p={4}
spacing={"20px"}
templateColumns="repeat(auto-fill, minmax(200px, 1fr))"
>
{communityData?.map((item) => (
<CommCard
key={item.id}
location={item.member_name}
name={item.member_name}
jobTitle={item.designation}
description={item.description}
linkdInd={item.linkedin}
imageUrl={item.profile_image}
/>
))}
</SimpleGrid>
</Container>
</Box>
)
}
export default CommunityCardDisplay

View File

@@ -0,0 +1,9 @@
import React from 'react'
const CommunityCardsTableView = () => {
return (
<div>CommunityCardsTableView</div>
)
}
export default CommunityCardsTableView

View File

@@ -0,0 +1,455 @@
import React, { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import {
useGetCommunityByIdQuery,
useUpdateCommunityMutation,
} from "../../Services/api.service";
import { addCommunitySchema, schemaEdit } from "../../Validations/Validations";
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import {
Box,
Divider,
FormControl,
FormHelperText,
FormLabel,
Heading,
Image,
Input,
Stack,
Textarea,
Button,
Skeleton,
useToast,
Switch,
Tag,
} from "@chakra-ui/react";
import { TiWarning } from "react-icons/ti";
import { OPACITY_ON_LOAD } from "../../Layout/animations";
import { motion } from "framer-motion";
import Loader01 from "../../Components/Loaders/Loader01";
import FullscreenLoaders from "../../Components/Loaders/FullscreenLoaders";
import fallbackImage from "../../assets/fallBackImage.png";
import Header from "../../Components/Header";
import ToastBox from "../../Components/ToastBox";
const API_URL = import.meta.env.VITE_API_BASE_URL;
const ComunityEditPage = () => {
const { id } = useParams();
const toast = useToast();
const navigate = useNavigate();
const { data, error, isLoading } = useGetCommunityByIdQuery(id);
const [isLoadingEdit, setIsLoadingEdit] = useState(false);
const [selectedImage, setSelectedImage] = useState();
const [updateCommunity] = useUpdateCommunityMutation();
const {
register,
handleSubmit,
reset,
watch,
formState: { errors },
setValue,
} = useForm({
resolver: yupResolver(addCommunitySchema),
defaultValues: {
member_name: "",
designation: "",
description: "",
linkedin: "",
profile_image: null,
},
});
useEffect(() => {
if (data?.data) {
setSelectedImage(
`${API_URL}/${data?.data?.profile_image}`
);
setValue("member_name", data?.data?.member_name);
setValue("designation", data?.data?.designation);
setValue("description", data?.data?.description);
setValue("linkedin", data?.data?.linkedin);
setValue("profile_image", data?.data?.profile_image);
}
}, [data, setValue]);
const onSubmit = async (formData) => {
setIsLoadingEdit(true);
const form = new FormData();
form.append("member_name", formData.member_name);
form.append("designation", formData.designation);
form.append("description", formData.description);
form.append("linkedin", formData.linkedin);
if (formData.profile_image[0]) {
form.append("profile_image", formData.profile_image[0]);
}
if (formData?.profile_image === data?.data?.profile_image) {
form.delete("profile_image");
}
await updateCommunity({ id: id, data: form })
.then((response) => {
// Handle the response here
// // console.log("Mutation response:", response?.data?.statusCode);
// // console.log("Mutation response:", response?.data?.message);
if (response?.data?.statusCode === 200) {
setIsLoadingEdit(false);
toast({
render: () => (
<ToastBox status={"success"} message={response?.data?.message} />
),
});
navigate("/community");
// setDeleteAlert(false);
} else {
setIsLoading01(false);
toast({
render: () => (
<ToastBox status={"error"} message={"File size too large"} />
),
});
}
})
.catch((error) => {
// // console.error("Error creating community:", error);
setIsLoadingEdit(false);
// setDeleteIsLoading(false);
// setDeleteAlert(false);
});
// Log the FormData entries
// for (const [key, value] of form.entries()) {
// // console.log(`${key}: ${value}`);
// }
reset();
};
const handleImageChange = (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
setSelectedImage(reader.result);
};
reader.readAsDataURL(file);
}
};
return isLoading ? (
<FullscreenLoaders />
) : (
<Box
{...OPACITY_ON_LOAD}
w={"100%"}
h={"100vh"}
overflowY={"scroll"}
display={"flex"}
flexDirection={"column"}
>
<Header title={"Community"} />
<Box display={"flex"}>
<Box className="col-5 d-flex flex-column gap-2 pt-4">
<span className="web-text-large fw-bold rubix-text-dark">
Members Info
</span>
<span className="web-text-medium text-secondary">
Select the platform for which you need to create this campaign.
</span>
<Divider />
<span className="web-text-large fw-bold rubix-text-dark">
Display profile
</span>
<span className="web-text-medium text-secondary mb-4">
Below is the profile that will be displayed on the community page.
</span>
<Box
boxSize="sm"
className="d-flex w-100 justify-content-center flex-column align-items-center gap-3"
>
<Image
shadow={"md"}
rounded={8}
w={214}
h={240}
src={selectedImage}
alt="Selected Image"
/>
<Button
onClick={() => setSelectedImage(fallbackImage)}
backgroundColor="red.400"
color={"whitesmoke"}
transition={"0.5s"}
_hover={{
backgroundColor: "red.500",
}}
size="xs"
>
Remove
</Button>
</Box>
</Box>
<form
className="col-7 pt-4 overflow-auto p-4"
onSubmit={handleSubmit(onSubmit)}
>
{/* <Switch
size={"sm"}
colorScheme="teal"
onChange={() => handleUpdateStatus(item.id)}
isChecked={data?.data?.status}
/> */}
<Box className="web-text-large fw-bold mb-2 rubix-text-dark">
Status
</Box>
{data?.data?.status ? (
<Tag
position={"sticky"}
right={10}
size={"sm"}
variant="solid"
colorScheme="teal"
>
Active
</Tag>
) : (
<Tag
position={"sticky"}
right={10}
size={"sm"}
variant="solid"
colorScheme="red"
>
Inactive
</Tag>
)}
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Name
</FormLabel>
<Input
{...register("member_name")}
placeholder="Name"
className="web-text-medium"
size="sm"
name="member_name"
type="text"
id="member_name"
minLength={4}
errorBorderColor="crimson"
isInvalid={watch()?.member_name?.length > 50}
// maxLength={51}
/>
<FormHelperText
color={watch()?.member_name?.length > 50 ? "red" : "gray.500"}
className="web-text-small"
>
If name crosses 50 characters it will cause problem in alignment
on website.you have entered {watch()?.member_name?.length}{" "}
characters
</FormHelperText>
{errors.name && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.member_name.message}
</span>
)}
</FormControl>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Designation
</FormLabel>
<Input
{...register("designation")}
placeholder="Designation"
className="web-text-medium"
size="sm"
id="designation"
name="designation"
/>
{errors.designation && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.designation.message}
</span>
)}
</FormControl>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Description
</FormLabel>
<Textarea
{...register("description")}
placeholder="Description"
className="web-text-medium"
size="sm"
rows={2}
minLength={4}
errorBorderColor="crimson"
isInvalid={watch()?.description?.length > 230}
// maxLength={51}
/>
<FormHelperText
color={watch()?.description?.length > 230 ? "red" : "gray.500"}
className="web-text-small"
>
If Description crosses 230 characters it will cause problem in
alignment on website.you have entered {watch()?.description?.length}{" "}
characters
</FormHelperText>
{errors.description && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.description.message}
</span>
)}
</FormControl>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Linked In
</FormLabel>
<Input
{...register("linkedin")}
placeholder="Linkedin link"
className="web-text-medium"
size="sm"
id="linkedin"
name="linkedin"
/>
{errors.linkedin && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.linkedin.message}
</span>
)}
<FormHelperText className="web-text-small">
Please share proper linked in link here.
</FormHelperText>
</FormControl>
<FormControl className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Display profile
</FormLabel>
{/* <ImageDropBox /> */}
<Box
borderColor="gray.300"
borderStyle="dashed"
borderWidth="2px"
rounded="md"
shadow="sm"
role="group"
transition="all 150ms ease-in-out"
_hover={{
shadow: "md",
}}
as={motion.div}
initial="rest"
animate="rest"
whileHover="hover"
height={"105px"}
className="pointer"
>
<Box position="relative" height="100%" width="100%">
<Box
position="absolute"
top="0"
left="0"
height="100%"
width="100%"
display="flex"
flexDirection="column"
>
<Stack
height="100%"
width="100%"
display="flex"
alignItems="center"
justify="center"
>
<span
className="d-flex flex-column align-items-center pointer"
spacing="1"
>
<Heading
fontSize="lg"
color="gray.700"
fontWeight="bold"
cursor={"pointer"}
>
Drop images here
</Heading>
<span
fontWeight="light"
className="web-text-large text-secondary text-center pointer"
>
or click to upload
</span>
</span>
</Stack>
</Box>
<Input
{...register("profile_image")}
type="file"
height="100%"
width="100%"
position="absolute"
top="0"
left="0"
opacity="0"
aria-hidden="true"
accept="image/*"
onChange={handleImageChange}
onDrop={handleImageChange}
// onDragEnter={startAnimation}
// onDragLeave={stopAnimation}
/>
</Box>
</Box>
{errors.profile_image && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.profile_image.message}
</span>
)}
<FormHelperText className="web-text-small">
Maximum limit of image is 10MB.
</FormHelperText>
</FormControl>
<Box className=" d-flex justify-content-end ">
<Button
isLoading={isLoadingEdit}
spinner={<Loader01 />}
color={"whitesmoke"}
backgroundColor={"purple.900"}
_hover={{
backgroundColor: "purple.800",
}}
type="submit"
size="sm"
rounded={"sm"}
>
Save edit
</Button>
</Box>
</form>
</Box>
</Box>
);
};
export default ComunityEditPage;

View File

@@ -0,0 +1,158 @@
import React, { useEffect, useState } from "react";
import { Link, useParams } from "react-router-dom";
import {
useGetCommunityByIdQuery,
} from "../../Services/api.service";
import BannerView from "../../Components/Banner/BannerView";
import FullscreenLoaders from "../../Components/Loaders/FullscreenLoaders";
import { Box, Divider, Image, Tag } from "@chakra-ui/react";
import Header from "../../Components/Header";
import { OPACITY_ON_LOAD } from "../../Layout/animations";
import { formatDate } from "../../Components/Functions/UTCConvertor";
const API_URL = import.meta.env.VITE_API_BASE_URL;
const ComunityViewPage = () => {
const { id } = useParams();
const { data, error, isLoading } = useGetCommunityByIdQuery(id);
return isLoading ? (
<FullscreenLoaders />
) : (
<Box
{...OPACITY_ON_LOAD}
overflowY={"scroll"}
w={"100%"}
h={"100vh"}
className="overflow-auto "
display={"flex"}
flexDirection={"column"}
>
<Header
title={"Banner's"}
btnTitle={"Edit banner"}
link={`/community/edit/${id}`}
/>
<Box display={"flex"}>
<Box className="col-5 d-flex flex-column gap-2 pt-4">
<span className="web-text-large fw-bold rubix-text-dark">
Banners Info
</span>
<span className="web-text-medium text-secondary">
Select the platform for which you need to create this campaign.
</span>
<Divider />
<span className="web-text-large fw-bold rubix-text-dark">
Display banner
</span>
<span className="web-text-medium text-secondary mb-4">
Below is the profile that will be displayed on the community page.
</span>
<Box
boxSize="sm"
className="d-flex w-100 justify-content-center flex-column align-items-center gap-3"
>
<Image
shadow={"md"}
rounded={8}
w={214}
h={240}
src={`${API_URL}/${data?.data?.profile_image}`}
alt="Selected Image"
/>
{/* <Button
onClick={() => setSelectedImage(fallbackImage)}
backgroundColor="red.400"
color={"whitesmoke"}
transition={"0.5s"}
_hover={{
backgroundColor: "red.500",
}}
size="xs"
>
Remove
</Button> */}
</Box>
</Box>
<Box className="col-7 pt-4 overflow-auto p-4">
<Box className="mb-3">
<Box className="web-text-large fw-bold rubix-text-dark mb-1">
Status
</Box>
{data?.data?.status ? (
<Tag size={"sm"} variant="solid" colorScheme="teal">
Active
</Tag>
) : (
<Tag size={"sm"} variant="solid" colorScheme="red">
Inactive
</Tag>
)}
</Box>
<Box className="mb-3">
<Box className="web-text-large fw-bold rubix-text-dark">
Author name
</Box>
<Box className="web-text-medium text-secondary">
{data?.data?.member_name}
</Box>
</Box>
<Box className="mb-3">
<Box className="web-text-large fw-bold rubix-text-dark">
Designation
</Box>
<Box className="web-text-medium text-secondary">
{data?.data?.designation}
</Box>
</Box>
<Box className="mb-3">
<Box className="web-text-large fw-bold rubix-text-dark">
Description
</Box>
<Box className="web-text-medium text-secondary">
{data?.data?.description}
</Box>
</Box>
<Box className="mb-3">
<Box className="web-text-large fw-bold rubix-text-dark">
Linked In
</Box>
<Box className="web-text-medium text-secondary">
{data?.data?.linkedin}
</Box>
</Box>
<Box className="mb-3">
<Box className="web-text-large fw-bold rubix-text-dark">
Created At
</Box>
<Box className="web-text-medium text-secondary">
{formatDate(data?.data?.createdAt)}
</Box>
</Box>
<Box className="mb-3">
<Box className="web-text-large fw-bold rubix-text-dark">
Updated At
</Box>
<Box className="web-text-medium text-secondary">
{formatDate(data?.data?.updatedAt)}
</Box>
</Box>
<Divider />
</Box>
</Box>
</Box>
);
};
export default ComunityViewPage;

View File

@@ -0,0 +1,582 @@
import { OPACITY_ON_LOAD } from "../../Layout/animations";
import Header from "../../Components/Header";
import {
AspectRatio,
Box,
Button,
Divider,
FormControl,
FormHelperText,
FormLabel,
Heading,
Image,
Input,
Stack,
Textarea,
useToast,
} from "@chakra-ui/react";
import fallbackImage from "../../assets/ultp-fallback-img.webp";
import { useState } from "react";
import { yupResolver } from "@hookform/resolvers/yup";
import { useForm } from "react-hook-form";
import { addEvents } from "../../Validations/Validations";
import { TiWarning } from "react-icons/ti";
import Loader01 from "../../Components/Loaders/Loader01";
import { useNavigate } from "react-router-dom";
import { motion } from "framer-motion";
import { useCreateEventsMutation } from "../../Services/api.service";
import ToastBox from "../../Components/ToastBox";
const AddEvents = () => {
const toast = useToast();
const navigate = useNavigate();
const [createEvents] = useCreateEventsMutation(); // Invoke the hook to get the mutation function
const [selectedImage, setSelectedImage] = useState(fallbackImage);
const [largeImageData, setLargeImageData] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [eventDatesInput, setEventsDatesInputs] = useState([]);
// Function to handle adding new date inputs
const addDateInput = () => {
setEventsDatesInputs([...eventDatesInput, ""]);
};
// Function to handle the change in date inputs
const handleDateChange = (index, event) => {
const newDates = [...eventDatesInput];
newDates[index] = event.target.value;
setEventsDatesInputs(newDates);
};
const {
register,
handleSubmit,
reset,
watch,
formState: { errors },
} = useForm({
resolver: yupResolver(addEvents),
});
const handleImageChange = (e) => {
const file = e.target.files[0];
setLargeImageData(file);
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
setSelectedImage(reader.result);
};
reader.readAsDataURL(file);
}
};
const onSubmit = (data) => {
setIsLoading(true);
const formData = new FormData();
formData.append("title", data.title);
formData.append("content", data.content);
formData.append("location", data.location);
formData.append("organizer_name", data.organizer_name);
formData.append("organizer_mobile_number", data.organizer_mobile_number);
formData.append("organizer_email", data.organizer_email);
if (eventDatesInput.length === 0 || eventDatesInput[0]==="") {
setIsLoading(false);
return toast({
render: () => (
<ToastBox status={"warn"} message={"Please add events date"} />
),
});
} else {
eventDatesInput.forEach((date, index) => {
formData.append(`dates[${index}]`, date);
});
}
if (data.banner_image[0]) {
formData.append("banner_image", data.banner_image[0]);
}
// for (const [key, value] of formData.entries()) {
// // console.log(`${key}: ${value}`);
// }
// Trigger the mutationconst
const res = createEvents(formData)
.then((response) => {
// Handle the response here
// // console.log("Mutation response:", response?.data?.statusCode);
// // console.log("Mutation response:", response?.data?.message);
if (response?.data?.statusCode === 201) {
setIsLoading(false);
toast({
title: response?.data?.message,
status: "success",
isClosable: true,
});
reset();
navigate("/events");
} else if (response?.data?.statusCode === 500) {
setIsLoading(false);
toast({
title: response?.data?.message,
status: "error", // Change status to error
isClosable: true,
});
}
})
.catch((error) => {
// Handle errors
// // console.error("Error creating community:", error?.message);
setIsLoading(false);
// Handle error notification if needed
});
};
return (
<Box
{...OPACITY_ON_LOAD}
w={"100%"}
h={"100vh"}
className="overflow-auto "
display={"flex"}
flexDirection={"column"}
>
<Header title={"Events"} />
{/* <Box width="100%" maxWidth="1200px" mx="auto" my={4}>
<AspectRatio ratio={16 / 6}>
<iframe
title="naruto"
src="https://rubix.betadelivery.com/events"
width="100%"
height="100%"
style={{ border: 'none' }}
allowFullScreen
/>
</AspectRatio>
</Box> */}
<Box className="d-flex">
<form className="w-100" onSubmit={handleSubmit(onSubmit)}>
<Box display={"flex"}>
<Box className="col-5 d-flex flex-column gap-2 pt-4">
<Box h={340} gap={4} display={'flex'} flexDirection={'column'}>
<span className="web-text-large fw-bold rubix-text-dark">
Banner info
</span>
<span className="web-text-medium text-secondary">
Select the platform for which you need to create this campaign.
</span>
</Box>
<Divider />
<span className="web-text-large fw-bold rubix-text-dark">
Banner image
</span>
<span className="web-text-medium text-secondary mb-4">
Below is the profile that will be displayed on the community
page.
</span>
<Box
boxSize="sm"
className="d-flex w-100 justify-content-center flex-column align-items-center gap-3"
>
<Image
shadow={"md"}
rounded={8}
w={500}
h={240}
src={selectedImage}
alt="Selected Image"
/>
{selectedImage === fallbackImage || largeImageData === null ? (
""
) : (
<Box display={"flex"} flexDirection={"column"} w={"100%"}>
<span className="web-text-small">
{largeImageData?.name}
</span>
<span className="web-text-small text-secondary fst-italic">
{(largeImageData?.size / (1024 * 1024)).toFixed(2)} mb
</span>
</Box>
)}
<Button
onClick={() => setSelectedImage(fallbackImage)}
backgroundColor="red.400"
color={"whitesmoke"}
transition={"0.5s"}
_hover={{
backgroundColor: "red.500",
}}
size="xs"
>
Remove
</Button>
</Box>
</Box>
<Box className="col-7 pt-4 mb-3 overflow-auto p-4">
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Title
</FormLabel>
<Input
{...register("title")}
placeholder="Title"
className="web-text-medium"
size="sm"
errorBorderColor="crimson"
isInvalid={watch()?.title?.length > 50}
// maxLength={51}
/>
<FormHelperText
color={watch()?.title?.length > 50 ? "red" : "gray.500"}
className="web-text-small"
>
If title crosses 50 characters it will cause problem in
alignment on website.you have entered {watch()?.title?.length}{" "}
characters
</FormHelperText>
{errors.name && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.title.message}
</span>
)}
</FormControl>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Location
</FormLabel>
<Input
{...register("location")}
placeholder="Location"
className="web-text-medium"
size="sm"
/>
{errors.location && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.location.message}
</span>
)}
</FormControl>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Content
</FormLabel>
<Textarea
{...register("content")}
placeholder="Name"
className="web-text-medium"
size="sm"
name="content"
type="text"
id="content"
errorBorderColor="crimson"
isInvalid={watch()?.content?.length > 230}
// maxLength={51}
/>
<FormHelperText
color={watch()?.content?.length > 230 ? "red" : "gray.500"}
className="web-text-small"
>
If content crosses 230 characters it will cause problem in
alignment on website.you have entered{" "}
{watch()?.content?.length} characters
</FormHelperText>
{errors.content && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.content.message}
</span>
)}
</FormControl>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Organisation email
</FormLabel>
<Input
type="email"
{...register("organizer_email")}
placeholder="Organisation email"
className="web-text-medium"
size="sm"
/>
{errors.organizer_email && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.organizer_email.message}
</span>
)}
</FormControl>
<FormControl className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Event dates
</FormLabel>
{eventDatesInput.map((date, index) => (
<Input
key={index}
type="date"
value={date}
onChange={(event) => handleDateChange(index, event)}
className="web-text-medium"
size="sm"
/>
))}
<Box display={"flex"} justifyContent={"flex-end"} mt={2}>
<Button
size={"xs"}
rounded={"sm"}
type="button"
onClick={addDateInput}
>
Add Date
</Button>
</Box>
{/* <FormHelperText className="web-text-small">
{data?.data?.eventDates?.map(({ date }, index) => (
<span className="web-text-small me-2">
{formatDate(date)}
</span>
))}
</FormHelperText> */}
{errors.eventDates && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.eventDates.message}
</span>
)}
</FormControl>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Banner image
</FormLabel>
{/* <ImageDropBox /> */}
<Box
borderColor="gray.300"
borderStyle="dashed"
borderWidth="2px"
rounded="md"
shadow="sm"
role="group"
transition="all 150ms ease-in-out"
_hover={{
shadow: "md",
}}
as={motion.div}
initial="rest"
animate="rest"
whileHover="hover"
height={"105px"}
className="pointer"
>
<Box position="relative" height="100%" width="100%">
<Box
position="absolute"
top="0"
left="0"
height="100%"
width="100%"
display="flex"
flexDirection="column"
>
<Stack
height="100%"
width="100%"
display="flex"
alignItems="center"
justify="center"
>
<span
className="d-flex flex-column align-items-center pointer"
spacing="1"
>
<Heading
fontSize="lg"
color="gray.700"
fontWeight="bold"
cursor={"pointer"}
>
Drop images here
</Heading>
<span
fontWeight="light"
className="web-text-large text-secondary text-center pointer"
>
or click to upload
</span>
</span>
</Stack>
</Box>
<Input
{...register("banner_image")}
type="file"
height="100%"
width="100%"
position="absolute"
top="0"
left="0"
opacity="0"
aria-hidden="true"
accept="image/*"
onChange={handleImageChange}
onDrop={handleImageChange}
// onDragEnter={startAnimation}
// onDragLeave={stopAnimation}
/>
</Box>
</Box>
{errors.banner_image && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.banner_image.message}
</span>
)}
<FormHelperText className="web-text-small">
Maximum limit of image is 10MB.
</FormHelperText>
</FormControl>
</Box>
</Box>
<Divider />
<Box display={"flex"}>
<Box className="col-5 d-flex flex-column gap-2 pt-4">
<span className="web-text-large fw-bold rubix-text-dark">
Orgainsation info
</span>
<span className="web-text-medium text-secondary mb-0">
Lorem ipsum dolor sit amet consectetur adipisicing elit. Tenetur
dicta exercitationem laboriosam fugit vel ipsam hic, consectetur
eum nesciunt adipisci?
</span>
</Box>
<Box className="col-7 pt-4 p-4">
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Organisation name
</FormLabel>
<Input
{...register("organizer_name")}
placeholder="Name"
className="web-text-medium"
size="sm"
name="organizer_name"
type="text"
id="organizer_name"
errorBorderColor="crimson"
isInvalid={watch()?.organizer_name?.length > 50}
// maxLength={51}
/>
<FormHelperText
color={
watch()?.organizer_name?.length > 50 ? "red" : "gray.500"
}
className="web-text-small"
>
If Organisation name crosses 50 characters it will cause
problem in alignment on website.you have entered{" "}
{watch()?.organizer_name?.length} characters
</FormHelperText>
{errors.organizer_name && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.organizer_name.message}
</span>
)}
</FormControl>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Organisation number
</FormLabel>
<Input
{...register("organizer_mobile_number")}
placeholder="Name"
className="web-text-medium"
size="sm"
name="organizer_mobile_number"
type="text"
id="organizer_mobile_number"
/>
{errors.organizer_mobile_number && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.organizer_mobile_number.message}
</span>
)}
</FormControl>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Organisation email
</FormLabel>
<Input
{...register("organizer_email")}
placeholder="Name"
className="web-text-medium"
size="sm"
name="organizer_email"
type="text"
id="organizer_email"
/>
<FormHelperText className="web-text-small">
Please enter valid email
</FormHelperText>
{errors.organizer_email && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.organizer_email.message}
</span>
)}
</FormControl>
<Box className=" d-flex justify-content-end mb-5">
<Button
isLoading={isLoading}
spinner={<Loader01 />}
color={"whitesmoke"}
backgroundColor={"purple.700"}
_hover={{
backgroundColor: "purple.800",
}}
rounded={"sm"}
type="submit"
size="sm"
>
Create event
</Button>
</Box>
</Box>
</Box>
</form>
</Box>
</Box>
);
};
export default AddEvents;

View File

@@ -0,0 +1,601 @@
import React, { useEffect, useState } from "react";
import {
useGetEventsByIdQuery,
useUpdateEventsMutation,
} from "../../Services/api.service";
import fallbackImage from "../../assets/ultp-fallback-img.webp";
import { useNavigate, useParams } from "react-router-dom";
import {
Box,
Button,
Divider,
FormControl,
FormHelperText,
FormLabel,
Heading,
Image,
Input,
Stack,
Tag,
Textarea,
useToast,
} from "@chakra-ui/react";
import { addEvents } from "../../Validations/Validations";
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import FullscreenLoaders from "../../Components/Loaders/FullscreenLoaders";
import { OPACITY_ON_LOAD } from "../../Layout/animations";
import Header from "../../Components/Header";
import { TiWarning } from "react-icons/ti";
import { motion } from "framer-motion";
import Loader01 from "../../Components/Loaders/Loader01";
import ToastBox from "../../Components/ToastBox";
import { formatDate } from "../../Components/Functions/UTCConvertor";
import ChipSelector from "../../Components/ChipSelector/ChipSelector";
const convertToDateArray = (dateArray) => {
return dateArray?.map((dateString) => {
const date = new Date(dateString);
const year = date.getUTCFullYear();
const month = String(date.getUTCMonth() + 1).padStart(2, "0"); // getUTCMonth() is zero-based
const day = String(date.getUTCDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
});
};
const EditEvents = () => {
const { id } = useParams();
const toast = useToast();
const navigate = useNavigate();
const { data, error, isLoading } = useGetEventsByIdQuery(id);
const [isLoadingEdit, setIsLoadingEdit] = useState(false);
const [selectedImage, setSelectedImage] = useState(fallbackImage);
const [largeImageData, setLargeImageData] = useState(null);
const [updateEvents] = useUpdateEventsMutation();
const [eventsDate, setEventsDate] = useState(
convertToDateArray(data?.data?.eventDates.map((event) => event.date))
);
const {
register,
handleSubmit,
reset,
watch,
formState: { errors },
setValue,
} = useForm({
resolver: yupResolver(addEvents),
defaultValues: {
title: "",
content: "",
location: "",
organizer_name: "",
organizer_mobile_number: "",
// eventDates: eventsDate,
organizer_email: "",
banner_image: null,
},
});
useEffect(() => {
if (data?.data) {
setSelectedImage(
`${API_URL}/${data?.data?.banner_image}`
);
setValue("title", data?.data?.title);
setValue("content", data?.data?.content);
setValue("location", data?.data?.location);
setValue("organizer_name", data?.data?.organizer_name);
setValue("organizer_mobile_number", data?.data?.organizer_mobile_number);
setValue("organizer_email", data?.data?.organizer_email);
setValue("content", data?.data?.content);
// setValue("eventDates", data?.data?.eventDates);
setValue("banner_image", data?.data?.banner_image);
// setValue("eventDates", eventsDate);
}
}, [data, setValue]);
const handleImageChange = (e) => {
const file = e.target.files[0];
setLargeImageData(file);
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
setSelectedImage(reader.result);
};
reader.readAsDataURL(file);
}
};
const onSubmit = (data) => {
setIsLoadingEdit(true);
const formData = new FormData();
formData.append("title", data.title);
formData.append("content", data.content);
formData.append("location", data.location);
formData.append("organizer_name", data.organizer_name);
formData.append("organizer_mobile_number", data.organizer_mobile_number);
formData.append("organizer_email", data.organizer_email);
if (eventsDate.length === 0) {
setIsLoadingEdit(false);
return toast({
render: () => (
<ToastBox status={"warn"} message={"Please add events date"} />
),
})
}else{
eventsDate.forEach((date, index) => {
formData.append(`dates[${index}]`, date);
});
}
if (data.banner_image[0]) {
formData.append("banner_image", data.banner_image[0]);
}
// for (const [key, value] of formData.entries()) {
// // console.log(`${key}: ${value}`);
// }
// Trigger the mutationconst
const res = updateEvents({ id: id, data: formData })
.then((response) => {
// Handle the response here
// // console.log("Mutation response:", response?.data?.statusCode);
// // console.log("Mutation response:", response?.data?.message);
if (response?.data?.statusCode === 201) {
setIsLoadingEdit(false);
toast({
render: () => (
<ToastBox status={"success"} message={response?.data?.message} />
),
});
reset();
navigate("/events");
} else if (response?.data?.statusCode === 500) {
setIsLoadingEdit(false);
toast({
render: () => (
<ToastBox status={"error"} message={response?.data?.message} />
),
});
}
})
.catch((error) => {
// Handle errors
// // console.error("Error creating community:", error?.message);
setIsLoadingEdit(false);
// Handle error notification if needed
});
};
return isLoading ? (
<FullscreenLoaders />
) : (
<Box
{...OPACITY_ON_LOAD}
overflowY={"scroll"}
paddingBottom={50}
height={"100vh"}
>
<Header title={"Events"} />
<Box display={"flex"}>
<form className="w-100" onSubmit={handleSubmit(onSubmit)}>
<Box display={"flex"}>
<Box className="col-5 d-flex flex-column gap-2 pt-4">
<span className="web-text-large fw-bold rubix-text-dark">
Events Info
</span>
<span className="web-text-medium text-secondary">
Select the platform for which you need to create this campaign.
</span>
<Divider />
<span className="web-text-large fw-bold rubix-text-dark">
Display image
</span>
<span className="web-text-medium text-secondary mb-4">
Below is the profile that will be displayed on the events page.
</span>
<Box
boxSize="sm"
className="d-flex w-100 justify-content-center flex-column align-items-center gap-3"
>
<Image
shadow={"md"}
rounded={8}
w={"100%"}
h={240}
src={selectedImage}
alt="Selected Image"
/>
{selectedImage === fallbackImage || largeImageData === null ? (
""
) : (
<Box display={"flex"} flexDirection={"column"} w={"100%"}>
<span className="web-text-small">
{largeImageData && largeImageData?.name}
</span>
<span className="web-text-small text-secondary fst-italic">
{largeImageData &&
(largeImageData?.size / (1024 * 1024)).toFixed(2)}{" "}
mb
</span>
</Box>
)}
<Button
onClick={() => setSelectedImage(fallbackImage)}
backgroundColor="red.400"
color={"whitesmoke"}
transition={"0.5s"}
_hover={{
backgroundColor: "red.500",
}}
size="xs"
>
Remove
</Button>
</Box>
</Box>
<Box
className="col-7 pt-4 overflow-auto p-4"
// onSubmit={handleSubmit(onSubmit)}
>
<Box className="web-text-large fw-bold mb-2 rubix-text-dark">
Status
</Box>
{data?.data?.status ? (
<Tag
position={"sticky"}
right={10}
size={"sm"}
variant="solid"
colorScheme="teal"
>
Active
</Tag>
) : (
<Tag
position={"sticky"}
right={10}
size={"sm"}
variant="solid"
colorScheme="red"
>
Inactive
</Tag>
)}
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Title
</FormLabel>
<Input
{...register("title")}
placeholder="Name"
className="web-text-medium"
size="sm"
name="title"
type="text"
id="title"
errorBorderColor="crimson"
isInvalid={watch()?.title?.length > 50}
// maxLength={51}
/>
<FormHelperText
color={watch()?.title?.length > 50 ? "red" : "gray.500"}
className="web-text-small"
>
If title crosses 50 characters it will cause problem in
alignment on website.you have entered {watch()?.title?.length}{" "}
characters
</FormHelperText>
{errors.title && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.title.message}
</span>
)}
</FormControl>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Location
</FormLabel>
<Input
{...register("location")}
placeholder="Name"
className="web-text-medium"
size="sm"
name="location"
type="text"
id="location"
/>
{errors.location && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.location.message}
</span>
)}
</FormControl>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Content
</FormLabel>
<Textarea
{...register("content")}
placeholder="Name"
className="web-text-medium"
size="sm"
name="content"
type="text"
id="content"
errorBorderColor="crimson"
isInvalid={watch()?.content?.length > 230}
// maxLength={51}
/>
<FormHelperText
color={watch()?.content?.length > 230 ? "red" : "gray.500"}
className="web-text-small"
>
If content crosses 230 characters it will cause problem in
alignment on website.you have entered{" "}
{watch()?.content?.length} characters
</FormHelperText>
{errors.content && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.content.message}
</span>
)}
</FormControl>
<FormControl className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Event dates
</FormLabel>
<ChipSelector
type={"date"}
chips={eventsDate}
setChips={setEventsDate}
/>
{errors.eventDates && (
<span className="text-warning web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.eventDates.message}
</span>
)}
</FormControl>
<FormControl className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Banner image
</FormLabel>
{/* <ImageDropBox /> */}
<Box
borderColor="gray.300"
borderStyle="dashed"
borderWidth="2px"
rounded="md"
shadow="sm"
role="group"
transition="all 150ms ease-in-out"
_hover={{
shadow: "md",
}}
as={motion.div}
initial="rest"
animate="rest"
whileHover="hover"
height={"105px"}
className="pointer"
>
<Box position="relative" height="100%" width="100%">
<Box
position="absolute"
top="0"
left="0"
height="100%"
width="100%"
display="flex"
flexDirection="column"
>
<Stack
height="100%"
width="100%"
display="flex"
alignItems="center"
justify="center"
>
<span
className="d-flex flex-column align-items-center pointer"
spacing="1"
>
<Heading
fontSize="lg"
color="gray.700"
fontWeight="bold"
cursor={"pointer"}
>
Drop images here
</Heading>
<span
fontWeight="light"
className="web-text-large text-secondary text-center pointer"
>
or click to upload
</span>
</span>
</Stack>
</Box>
<Input
{...register("banner_image")}
type="file"
height="100%"
width="100%"
position="absolute"
top="0"
left="0"
opacity="0"
aria-hidden="true"
accept="image/*"
onChange={handleImageChange}
onDrop={handleImageChange}
// onDragEnter={startAnimation}
// onDragLeave={stopAnimation}
/>
</Box>
</Box>
{errors.banner_image && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.banner_image.message}
</span>
)}
<FormHelperText className="web-text-small">
Maximum limit of image should be 1mb to protect website from
slow loading.
</FormHelperText>
</FormControl>
</Box>
</Box>
<Divider />
<Box display={"flex"}>
<Box className="col-5 d-flex flex-column gap-2 pt-4">
<span className="web-text-large fw-bold rubix-text-dark">
Orgainsation info
</span>
<span className="web-text-medium text-secondary mb-0">
Lorem ipsum dolor sit amet consectetur adipisicing elit. Tenetur
dicta exercitationem laboriosam fugit vel ipsam hic, consectetur
eum nesciunt adipisci?
</span>
</Box>
<Box className="col-7 pt-4 p-4">
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Organisation name
</FormLabel>
<Input
{...register("organizer_name")}
placeholder="Name"
className="web-text-medium"
size="sm"
name="organizer_name"
type="text"
id="organizer_name"
errorBorderColor="crimson"
isInvalid={watch()?.organizer_name?.length > 50}
// maxLength={51}
/>
<FormHelperText
color={
watch()?.organizer_name?.length > 50 ? "red" : "gray.500"
}
className="web-text-small"
>
If Organisation name crosses 50 characters it will cause
problem in alignment on website.you have entered{" "}
{watch()?.organizer_name?.length} characters
</FormHelperText>
{errors.organizer_name && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.organizer_name.message}
</span>
)}
</FormControl>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Organisation number
</FormLabel>
<Input
{...register("organizer_mobile_number")}
placeholder="Name"
className="web-text-medium"
size="sm"
name="organizer_mobile_number"
type="text"
id="organizer_mobile_number"
/>
{errors.organizer_mobile_number && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.organizer_mobile_number.message}
</span>
)}
</FormControl>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Organisation email
</FormLabel>
<Input
{...register("organizer_email")}
placeholder="Name"
className="web-text-medium"
size="sm"
name="organizer_email"
type="text"
id="organizer_email"
/>
<FormHelperText className="web-text-small">
Please enter valid email
</FormHelperText>
{errors.organizer_email && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.organizer_email.message}
</span>
)}
</FormControl>
<Box className=" d-flex justify-content-end">
<Button
isLoading={isLoadingEdit}
spinner={<Loader01 />}
color={"whitesmoke"}
backgroundColor={"purple.900"}
_hover={{
backgroundColor: "purple.800",
}}
type="submit"
size="sm"
rounded={"sm"}
>
Save edit
</Button>
</Box>
</Box>
</Box>
</form>
</Box>
</Box>
);
};
export default EditEvents;

246
src/Pages/Events/Events.jsx Normal file
View File

@@ -0,0 +1,246 @@
import {
Box,
HStack,
Input,
Menu,
MenuButton,
MenuItem,
MenuList,
Portal,
Select,
Switch,
Text,
useToast,
} from "@chakra-ui/react";
import { OPACITY_ON_LOAD } from "../../Layout/animations";
import {
useDeleteEventsMutation,
useGetEventsQuery,
useUpdateEventsStatusMutation,
} from "../../Services/api.service";
import { useState } from "react";
import { TABLE_PAGINATION } from "../../Constants/Paginations";
import Header from "../../Components/Header";
import { ChevronLeftIcon, ChevronRightIcon } from "@chakra-ui/icons";
import DataTable from "../../Components/DataTable/DataTable";
import { HiDotsVertical } from "react-icons/hi";
import { Link as RouterLink } from "react-router-dom";
import { formatDate } from "../../Components/Functions/UTCConvertor";
import CustomAlertDialog from "../../Components/CustomAlertDialog";
import ToastBox from "../../Components/ToastBox";
import TabularView from "../../Components/TabularView/TabularView";
const Events = () => {
// ====================================================[Hooks]===================================================================
const toast = useToast();
const [deleteAlert, setDeleteAlert] = useState(false);
const [actionId, setActionId] = useState(null);
const [deleteIsLoading, setDeleteIsLoading] = useState(false);
// ====================================================[Constants]===================================================================
const [searchTerm, setSearchTerm] = useState("");
const [statusFilter, setStatusFilter] = useState("all");
const [pageSize, setPageSize] = useState(TABLE_PAGINATION?.size);
const [currentPage, setCurrentPage] = useState(TABLE_PAGINATION?.page);
const [displayRange, setDisplayRange] = useState({
start: TABLE_PAGINATION?.page,
end: pageSize,
});
// ====================================================[RTK Hooks]===================================================================
const events = useGetEventsQuery({ page: currentPage, size: pageSize });
const [updateEventsStatus] = useUpdateEventsStatusMutation();
const [deleteEvents] = useDeleteEventsMutation();
// ====================================================[Functions]===================================================================
const handleDelete = async (id) => {
try {
// Trigger the mutation
setDeleteIsLoading(true);
await deleteEvents(id)
.then((response) => {
if (response?.data?.statusCode === 201) {
setDeleteIsLoading(false);
setDeleteAlert(false);
toast({
render: () => (
<ToastBox
status={"success"}
message={response?.data?.message}
/>
),
});
}
})
.catch((error) => {
// console.error("Error creating community:", error);
setDeleteIsLoading(false);
setDeleteAlert(false);
});
} catch (error) {
// Handle errors
// console.error("Error deleting community:", error);
}
};
const handleUpdateStatus = async (id) => {
try {
// Trigger the mutation
await updateEventsStatus({ id })
.then((response) => {
// console.log(response?.data);
if (response?.data?.statusCode === 201) {
toast({
render: () => (
<ToastBox
status={"success"}
message={response?.data?.message}
/>
),
});
}
})
.catch((error) => {
// console.log(error);
});
} catch (error) {
// Handle errors
// console.error("Error updating community status:", error);
}
};
// ====================================================[Pagination Setup]================================================================
const paginationPrev = () => {
if (currentPage > 1) {
setCurrentPage(currentPage - 1);
updateDisplayRange(currentPage - 1);
}
};
const paginationNext = () => {
const totalPages = Math.ceil(events?.data?.data?.totalItems / pageSize);
if (currentPage < totalPages) {
setCurrentPage(currentPage + 1);
updateDisplayRange(currentPage + 1);
}
};
const updateDisplayRange = (page) => {
const start = (page - 1) * pageSize + 1;
const end = Math.min(start + pageSize - 1, events?.data?.data?.totalItems);
setDisplayRange({ start, end });
};
// ====================================================[Table Filter]================================================================
const filteredData = events?.data?.data?.rows?.filter((item) => {
// Filter by name (case insensitive)
const name = item.title;
const searchLower = searchTerm.toLowerCase();
const nameMatches = name.toLowerCase().includes(searchLower);
// Filter by status
const status = item.status;
const statusMatches =
statusFilter === "all" ||
(statusFilter === "active" && status === true) ||
(statusFilter === "inactive" && status === false);
return nameMatches && statusMatches;
});
// ====================================================[Table Setup]================================================================
const tableHeadRow = [
"Title",
"Organisation name",
"Contact no",
"Email",
"Location",
"Status",
"Created At",
];
const extractedArray = filteredData?.map((item, index) => {
return {
Title: <RouterLink to={`view/${item.id}`}>{item?.title}</RouterLink>,
"Organisation name": item?.organizer_name,
"Contact no": item?.organizer_mobile_number,
Email: item?.organizer_email,
Location: item?.location,
Status: (
<Switch
size={"sm"}
colorScheme="pink.500"
onChange={() => handleUpdateStatus(item?.id)}
isChecked={item?.status}
/>
),
"Created At": (
<span className="d-flex justify-content-between align-items-center">
<Text as={"span"} color={"gray.600"} className=" fw-bold">
{formatDate(item?.createdAt)}
</Text>
<Menu>
<MenuButton className="link p-1 rounded-1">
<HiDotsVertical className="rubix-text-dark fs-6" />
</MenuButton>
<Portal>
<MenuList minWidth="80px">
<RouterLink to={`edit/${item.id}`}>
<MenuItem className="web-text-medium">Edit</MenuItem>
</RouterLink>
<RouterLink to={`view/${item.id}`}>
<MenuItem className="web-text-medium">View</MenuItem>
</RouterLink>
<MenuItem
onClick={() => {
setActionId(item.id);
setDeleteAlert(true);
}}
className="web-text-medium"
>
Delete
</MenuItem>
</MenuList>
</Portal>
</Menu>
</span>
),
};
});
return (
<>
<TabularView
title={"Events"}
btnTitle={"Create event"}
link={"/events/add-events"}
apiData={events}
tableHeadRow={tableHeadRow}
extractedArray={extractedArray}
searchTerm={searchTerm}
setSearchTerm={setSearchTerm}
statusFilter={statusFilter}
setStatusFilter={setStatusFilter}
pageSize={pageSize}
setPageSize={setPageSize}
currentPage={currentPage}
setCurrentPage={setCurrentPage}
totalItems={events?.data?.data?.totalItems}
totalPages={events?.data?.data?.totalPages}
noDataTitle={"event's"}
/>
{/* ====================================================[ Alert ]================================================================ */}
<CustomAlertDialog
onClose={() => setDeleteAlert(false)}
isOpen={deleteAlert}
alertHandler={() => handleDelete(actionId)}
message={"Are you sure you want to delete event?"}
isLoading={deleteIsLoading}
/>
</>
);
};
export default Events;

View File

@@ -0,0 +1,193 @@
import React from 'react'
import { useGetEventsByIdQuery } from '../../Services/api.service';
import { useNavigate, useParams } from 'react-router-dom';
import { Box, Divider, Image, Tag, useToast } from '@chakra-ui/react';
import FullscreenLoaders from '../../Components/Loaders/FullscreenLoaders';
import { OPACITY_ON_LOAD } from '../../Layout/animations';
import Header from '../../Components/Header';
import { formatDate } from '../../Components/Functions/UTCConvertor';
const API_URL = import.meta.env.VITE_API_BASE_URL;
const ViewEvents = () => {
const { id } = useParams();
const toast = useToast();
const navigate = useNavigate();
const { data, error, isLoading } = useGetEventsByIdQuery(id);
const events = data?.data;
if (isLoading) {
return <FullscreenLoaders />;
}
return(
<Box
{...OPACITY_ON_LOAD}
w={"100%"}
h={"100vh"}
className="overflow-auto "
display={"flex"}
flexDirection={"column"}
>
<Header
title={"Events"}
btnTitle={'Edit events'}
link={`/events/edit/${id}`}
/>
<Box display={"flex"}>
<Box className="col-5 d-flex flex-column gap-2 pt-4">
<span className="web-text-large fw-bold rubix-text-dark">
Events Info
</span>
<span className="web-text-medium text-secondary">
Select the platform for which you need to create this campaign.
</span>
<Divider />
<span className="web-text-large fw-bold rubix-text-dark">
Events Banner image
</span>
<span className="web-text-medium text-secondary mb-4">
Below is the profile that will be displayed on the community page.
</span>
<Box
boxSize="sm"
className="d-flex h-auto w-100 justify-content-start flex-column align-items-center gap-3"
>
<Image
shadow={"md"}
rounded={8}
objectFit='cover'
w={500}
h={240}
src={`${API_URL}/${events?.banner_image}`}
alt="Selected Image"
/>
</Box>
</Box>
<Box className="col-7 pt-4 p-4">
<Box>
<Box className="web-text-large fw-bold mb-1 rubix-text-dark">
Status
</Box>
{events?.status ? (
<Tag size={"sm"} borderRadius="full" colorScheme="teal">
Active
</Tag>
) : (
<Tag size={"sm"} borderRadius="full" colorScheme="red">
Inactive
</Tag>
)}
</Box>
<Box className="mb-3">
<Box className="web-text-large fw-bold rubix-text-dark">Title</Box>
<Box className="web-text-medium text-secondary">{events?.title}</Box>
</Box>
<Box className="mb-3">
<Box className="web-text-large fw-bold rubix-text-dark">
Content
</Box>
<Box className="web-text-medium text-secondary">
{events?.content}
</Box>
</Box>
<Box className="mb-3">
<Box className="web-text-large fw-bold rubix-text-dark">
Location
</Box>
<Box className="web-text-medium text-secondary">
{events?.location}
</Box>
</Box>
<Box className="mb-3">
<Box className="web-text-large fw-bold rubix-text-dark">
Organisation name
</Box>
<Box className="web-text-medium text-secondary">
{events?.organizer_name}
</Box>
</Box>
<Box className="mb-3">
<Box className="web-text-large fw-bold rubix-text-dark">
Organisation number
</Box>
<Box className="web-text-medium text-secondary">
{events?.organizer_mobile_number}
</Box>
</Box>
<Box className="mb-3">
<Box className="web-text-large fw-bold rubix-text-dark">
Organisation email
</Box>
<Box className="web-text-medium text-secondary">
{events?.organizer_email}
</Box>
</Box>
<Box className="mb-3">
<Box className="web-text-large fw-bold rubix-text-dark">
Event Dates
</Box>
<Box className="web-text-medium text-secondary">
{events?.eventDates?.map(({ date }, index) => (
<span key={index} className="web-text-small me-2">
{formatDate(date)}
</span>
))}
</Box>
</Box>
<Box className="mb-3">
<Box className="web-text-large fw-bold rubix-text-dark">
Created at
</Box>
<Box className="web-text-medium text-secondary">
{formatDate(events?.createdAt)}
</Box>
</Box>
<Box className="mb-3">
<Box className="web-text-large fw-bold rubix-text-dark">
Updated at
</Box>
<Box className="web-text-medium text-secondary">
{formatDate(events?.updatedAt)}
</Box>
</Box>
</Box>
</Box>
</Box>)
}
export default ViewEvents

344
src/Pages/Faq/AddFaq.jsx Normal file
View File

@@ -0,0 +1,344 @@
import {
Box,
Button,
Divider,
FormControl,
FormHelperText,
FormLabel,
Heading,
Image,
Input,
Stack,
useToast,
} from "@chakra-ui/react";
import React, { useState } from "react";
import { OPACITY_ON_LOAD } from "../../Layout/animations";
import Header from "../../Components/Header";
import { useNavigate } from "react-router-dom";
import fallbackImage from "../../assets/ultp-fallback-img.webp";
import { addFaq } from "../../Validations/Validations";
import { TiWarning } from "react-icons/ti";
import { yupResolver } from "@hookform/resolvers/yup";
import { useForm } from "react-hook-form";
import { motion } from "framer-motion";
import Loader01 from "../../Components/Loaders/Loader01";
import { useCreateFaqMutation } from "../../Services/api.service";
import ToastBox from "../../Components/ToastBox";
import ReactQuill from "react-quill";
const AddFaq = () => {
const toast = useToast();
const navigate = useNavigate();
const [isLoading, setIsLoading] = useState(false);
const [selectedImage, setSelectedImage] = useState(fallbackImage);
const [imageData, setImageData] = useState(null);
const [value, setValue] = useState(null);
const [createFaq] = useCreateFaqMutation();
// const termContent = data?.data;
const {
register,
handleSubmit,
watch,
reset,
setValue: setYupFormValue,
formState: { errors },
} = useForm({
resolver: yupResolver(addFaq),
defaultValues: {
question: "",
answer: "",
},
});
// console.log(errors);
// const handleImageChange = (e) => {
// const file = e.target.files[0];
// setImageData(file);
// // setYupFormValue("banner_image", file);
// if (file) {
// const reader = new FileReader();
// reader.onloadend = () => {
// setSelectedImage(reader.result);
// };
// reader.readAsDataURL(file);
// }
// };
const onSubmit = async (data) => {
if (value === null) {
return toast({
render: () => (
<ToastBox status={"warn"} message={"Answer can not be empty."} />
),
});
}
setYupFormValue("answer", value);
try {
setIsLoading(true);
// Trigger the mutation
createFaq(data)
.then((response) => {
// Handle the response here
if (response?.data?.statusCode === 201) {
setIsLoading(false);
toast({
render: () => (
<ToastBox
status={"success"}
message={response?.data?.message}
/>
),
});
reset();
navigate("/faq");
} else if (response?.data?.statusCode === 500) {
setIsLoading(false);
toast({
render: () => (
<ToastBox
status={"success"}
message={response?.data?.message}
/>
),
});
} else {
setIsLoading(false);
toast({
render: () => (
<ToastBox
status={"Some thing went wrong"}
message={response?.data?.message}
/>
),
});
}
})
.catch((error) => {
// Handle errors
// console.error("Error creating community:", error);
setIsLoading(false);
// Handle error notification if needed
});
} catch (error) {
// Handle errors
// console.error("Error creating community:", error);
setIsLoading(false);
}
};
return (
<Box
{...OPACITY_ON_LOAD}
w={"100%"}
h={"100vh"}
className="overflow-auto "
display={"flex"}
flexDirection={"column"}
>
<Header title={"Add FAQ"} />
<Box className="d-flex">
{/* <Box className="col-5 d-flex flex-column gap-2 pt-4">
<Box
boxSize="sm"
className="d-flex w-100 justify-content-center flex-column align-items-center gap-3"
>
<Image
shadow={"md"}
rounded={8}
w={500}
h={240}
src={selectedImage}
alt="Selected Image"
/>
{selectedImage === fallbackImage || imageData === null ? (
""
) : (
<Box display={"flex"} flexDirection={"column"} w={"100%"}>
<span className="web-text-small">{imageData?.name}</span>
<span className="web-text-small text-secondary fst-italic">
{(imageData?.size / (1024 * 1024)).toFixed(2)} mb
</span>
</Box>
)}
<Button
onClick={() => setSelectedImage(fallbackImage)}
backgroundColor="red.400"
color={"whitesmoke"}
transition={"0.5s"}
_hover={{
backgroundColor: "red.500",
}}
size="xs"
>
Remove
</Button>
</Box>
</Box> */}
<form
onSubmit={handleSubmit(onSubmit)}
className="col-7 pt-4 mb-3 overflow-auto p-4"
>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Question
</FormLabel>
<Input
{...register("question")}
placeholder="Question"
className="web-text-medium"
size="sm"
minLength={4}
errorBorderColor="crimson"
isInvalid={watch()?.question?.length > 50}
// maxLength={51}
/>
<FormHelperText
color={watch()?.question?.length > 50 ? "red" : "gray.500"}
className="web-text-small"
>
If question crosses 50 characters it will cause problem in
alignment on website.you have entered {watch()?.question?.length}{" "}
characters
</FormHelperText>
{errors.question && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.question.message}
</span>
)}
</FormControl>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Answer
</FormLabel>
<ReactQuill
className="rounded-3"
theme="snow"
value={value}
onChange={setValue}
/>
{errors.answer && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " /> {errors.answer.message}
</span>
)}
</FormControl>
{/* <FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Banner image
</FormLabel>
<Box
borderColor="gray.300"
borderStyle="dashed"
borderWidth="2px"
rounded="md"
shadow="sm"
role="group"
transition="all 150ms ease-in-out"
_hover={{
shadow: "md",
}}
as={motion.div}
initial="rest"
animate="rest"
whileHover="hover"
height={"105px"}
className="pointer"
>
<Box position="relative" height="100%" width="100%">
<Box
position="absolute"
top="0"
left="0"
height="100%"
width="100%"
display="flex"
flexDirection="column"
>
<Stack
height="100%"
width="100%"
display="flex"
alignItems="center"
justify="center"
>
<span
className="d-flex flex-column align-items-center pointer"
spacing="1"
>
<Heading
fontSize="lg"
color="gray.700"
fontWeight="bold"
cursor={"pointer"}
>
Drop images here
</Heading>
<span
fontWeight="light"
className="web-text-large text-secondary text-center pointer"
>
or click to upload
</span>
</span>
</Stack>
</Box>
<Input
{...register("image")}
type="file"
height="100%"
width="100%"
position="absolute"
top="0"
left="0"
opacity="0"
aria-hidden="true"
accept="image/*"
onChange={handleImageChange}
onDrop={handleImageChange}
/>
</Box>
</Box>
{errors.image && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " /> {errors.image.message}
</span>
)}
<FormHelperText className="web-text-small">
Maximum limit of image is 10MB.
</FormHelperText>
</FormControl> */}
<Box className=" d-flex justify-content-end mb-5">
<Button
isLoading={isLoading}
spinner={<Loader01 />}
color={"whitesmoke"}
backgroundColor={"purple.900"}
_hover={{
backgroundColor: "purple.800",
}}
type="submit"
rounded={"sm"}
size="sm"
>
Create
</Button>
</Box>
</form>
</Box>
</Box>
);
};
export default AddFaq;

182
src/Pages/Faq/EditFaq.jsx Normal file
View File

@@ -0,0 +1,182 @@
import React, { useEffect, useState } from "react";
import Header from "../../Components/Header";
import {
Box,
Button,
Divider,
FormControl,
FormHelperText,
FormLabel,
Input,
useToast,
} from "@chakra-ui/react";
import { OPACITY_ON_LOAD } from "../../Layout/animations";
import { useNavigate, useParams } from "react-router-dom";
import {
useGetFaqByIdQuery,
useUpdateFaqMutation,
} from "../../Services/api.service";
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import { TiWarning } from "react-icons/ti";
import { addFaq } from "../../Validations/Validations";
import FullscreenLoaders from "../../Components/Loaders/FullscreenLoaders";
import ToastBox from "../../Components/ToastBox";
import ReactQuill from "react-quill";
const EditFaq = () => {
const { id } = useParams();
const toast = useToast();
const navigate = useNavigate();
const { data, error, isLoading } = useGetFaqByIdQuery(id);
const [isLoadingEdit, setIsLoadingEdit] = useState(false);
const [updateFaq] = useUpdateFaqMutation();
const [valueQuill, setValueQuill] = useState(data?.data?.answer);
// console.log(data);
const {
register,
handleSubmit,
watch,
reset,
formState: { errors },
setValue,
} = useForm({
resolver: yupResolver(addFaq),
defaultValues: {
question: "",
answer: "",
},
});
useEffect(() => {
if (data?.data) {
setValue("question", data?.data?.question);
setValueQuill(data?.data?.answer || ""); // Set initial value for Quill
}
}, [data, setValue]);
const onSubmit = async (formData) => {
if (!valueQuill || valueQuill.trim() === "") {
toast({
render: () => (
<ToastBox status={"error"} message={"Answer cannot be empty"} />
),
});
return;
}
setIsLoadingEdit(true);
const updatedData = {
...formData,
answer: valueQuill, // Include the value from ReactQuill
};
await updateFaq({ id, data: updatedData })
.then((response) => {
if (response?.data?.statusCode === 200) {
setIsLoadingEdit(false);
toast({
render: () => (
<ToastBox status={"success"} message={response?.data?.message} />
),
});
reset();
navigate("/faq");
} else {
setIsLoadingEdit(false);
toast({
render: () => (
<ToastBox status={"error"} message={"Something went wrong"} />
),
});
}
})
.catch((error) => {
// console.error("Error updating FAQ:", error);
setIsLoadingEdit(false);
});
};
if (isLoading) {
return <FullscreenLoaders />;
}
return (
<Box
{...OPACITY_ON_LOAD}
overflowY={"scroll"}
paddingBottom={50}
height={"100vh"}
>
<Header title={"FAQ"} />
<Box display={"flex"}>
<form
onSubmit={handleSubmit(onSubmit)}
className="col-7 pt-4 mb-3 overflow-auto p-4"
>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Title
</FormLabel>
<Input
{...register("question")}
placeholder="Question"
className="web-text-medium"
size="sm"
minLength={4}
errorBorderColor="crimson"
isInvalid={watch()?.question?.length > 50}
// maxLength={51}
/>
<FormHelperText
color={watch()?.question?.length > 50 ? "red" : "gray.500"}
className="web-text-small"
>
If question crosses 50 characters it will cause problem in
alignment on website.you have entered {watch()?.question?.length}{" "}
characters
</FormHelperText>
{errors.question && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.question.message}
</span>
)}
</FormControl>
<FormControl isRequired className="mb-3">
<ReactQuill
className="rounded-3"
theme="snow"
value={valueQuill}
onChange={setValueQuill}
/>
</FormControl>
<Box className="d-flex justify-content-end">
<Button
isLoading={isLoadingEdit}
color={"whitesmoke"}
backgroundColor={"purple.900"}
_hover={{
backgroundColor: "purple.800",
}}
type="submit"
size="sm"
rounded={"sm"}
>
Save edit
</Button>
</Box>
<Divider />
</form>
</Box>
</Box>
);
};
export default EditFaq;

254
src/Pages/Faq/Faq.jsx Normal file
View File

@@ -0,0 +1,254 @@
import {
Box,
Image,
Menu,
MenuButton,
MenuItem,
MenuList,
Portal,
Switch,
Text,
Tooltip,
useToast,
} from "@chakra-ui/react";
import { OPACITY_ON_LOAD } from "../../Layout/animations";
import { TABLE_PAGINATION } from "../../Constants/Paginations";
import {
useCreateFaqMutation,
useDeleteFaqMutation,
useGetFaqQuery,
useUpdateFaqStatusMutation,
} from "../../Services/api.service";
import { useState } from "react";
import TabularView from "../../Components/TabularView/TabularView";
import CustomAlertDialog from "../../Components/CustomAlertDialog";
import { HiDotsVertical } from "react-icons/hi";
import { Link } from "react-router-dom";
import { formatDate } from "../../Components/Functions/UTCConvertor";
import ToastBox from "../../Components/ToastBox";
import extractFilename from "../../Components/Functions/FileNameAlter";
const Faq = () => {
const toast = useToast();
const [pageSize, setPageSize] = useState(TABLE_PAGINATION?.size);
const [currentPage, setCurrentPage] = useState(1);
const [searchTerm, setSearchTerm] = useState("");
const [statusFilter, setStatusFilter] = useState("all");
const [deleteAlert, setDeleteAlert] = useState(false);
const [actionId, setActionId] = useState(null);
const [actionStatus, setActionStatus] = useState(null);
const [deleteIsLoading, setDeleteIsLoading] = useState(false);
const faqPage = useCreateFaqMutation();
const faq = useGetFaqQuery({ page: currentPage, size: pageSize });
const [deleteFaq] = useDeleteFaqMutation();
const [updateFaqStatus] = useUpdateFaqStatusMutation();
const filteredData = faq?.data?.data?.rows?.filter((item) => {
// Filter by name (case insensitive)
const name = item.question;
const searchLower = searchTerm.toLowerCase();
const nameMatches = name.toLowerCase().includes(searchLower);
// Filter by status
const status = item.status;
const statusMatches =
statusFilter === "all" ||
(statusFilter === "active" && status === true) ||
(statusFilter === "inactive" && status === false);
return nameMatches && statusMatches;
});
// ====================================================[Table Setup]================================================================
const tableHeadRow = [
"Title",
// "Content",
"Active",
"Created At",
];
const extractedArray = filteredData?.map((item, index) => {
return {
Title: (
<Link to={`/faq/view/${item.id}`}>
<Tooltip
className="rounded-2 web-text-xsmall"
width={"fit-content"}
placement="top"
hasArrow
label={item?.question}
bg="blue.200"
>
<Box display={"flex"} alignItems={"center"} w={200}>
<Text as={"span"} isTruncated={true}>
{item?.question}
</Text>
</Box>
</Tooltip>
</Link>
),
Active: (
<Switch
size={"sm"}
colorScheme="teal"
onChange={() => handleUpdateStatus(item.id, item?.status)}
isChecked={item.status}
// disabled={item.status}
/>
),
"Created At": (
<span className="d-flex justify-content-between align-items-center">
<Text as={"span"} color={"gray.600"} className=" fw-bold">
{formatDate(item?.createdAt)}
</Text>
<Menu>
<MenuButton className="link p-1 rounded-1">
<HiDotsVertical className="rubix-text-dark fs-6" />
</MenuButton>
<Portal>
<MenuList minWidth="80px">
<Link to={`/faq/edit/${item.id}`}>
<MenuItem className="web-text-medium">Edit</MenuItem>
</Link>
<Link to={`/faq/view/${item.id}`}>
<MenuItem className="web-text-medium">View</MenuItem>
</Link>
<MenuItem
onClick={() => {
setActionId(item.id);
setDeleteAlert(true);
setActionStatus(item.status);
}}
className="web-text-medium"
>
Delete
</MenuItem>
</MenuList>
</Portal>
</Menu>
</span>
),
};
});
// ====================================================[Functions]===================================================================
const handleDelete = async (communityId, status) => {
if (status) {
return toast({
render: () => (
<ToastBox
status={"warn"}
message={"Can't delete Partner. Status is true."}
/>
),
});
}
try {
// Trigger the mutation
setDeleteIsLoading(true);
const response = await deleteFaq(communityId);
// Handle the response here
// console.log("Mutation response:", response?.data?.statusCode);
// console.log("Mutation response:", response?.data?.message);
if (response?.data?.statusCode === 200) {
toast({
render: () => (
<ToastBox status={"success"} message={response?.data?.message} />
),
});
} else {
toast({
render: () => (
<ToastBox
status={"error"}
message={"Failed to delete partner. Please try again."}
/>
),
});
}
} catch (error) {
// console.error("Error deleting community:", error);
toast({
render: () => (
<ToastBox
status={"error"}
message={"Error deleting partner. Please try again."}
/>
),
});
} finally {
// Ensure the loading state is reset and alert is closed
setDeleteIsLoading(false);
setDeleteAlert(false);
}
};
const handleUpdateStatus = async (id) => {
try {
await updateFaqStatus({ id })
.then((response) => {
// console.log(response?.data);
if (response?.data?.statusCode === 201) {
toast({
render: () => (
<ToastBox
status={"success"}
message={"Please toggle another banner."}
/>
),
});
// whitePaper?.refetch()
}
})
.catch((error) => {
// console.log(error);
});
} catch (error) {
// Handle errors
// console.error("Error updating community status:", error);
}
};
return (
<>
<TabularView
title={"FAQ"}
btnTitle={"Create Faq"}
link={"/faq/add-faq"}
apiData={faq}
tableHeadRow={tableHeadRow}
extractedArray={extractedArray}
searchTerm={searchTerm}
setSearchTerm={setSearchTerm}
statusFilter={statusFilter}
setStatusFilter={setStatusFilter}
pageSize={pageSize}
setPageSize={setPageSize}
currentPage={currentPage}
setCurrentPage={setCurrentPage}
totalPages={faq?.data?.data?.totalPages}
totalItems={faq?.data?.data?.totalItems}
noDataTitle={"FAQ's"}
/>
<CustomAlertDialog
onClose={() => setDeleteAlert(false)}
isOpen={deleteAlert}
alertHandler={() => handleDelete(actionId)}
message={"Are you sure you want to delete Faq?"}
isLoading={deleteIsLoading}
/>
</>
);
};
export default Faq;

97
src/Pages/Faq/ViewFaq.jsx Normal file
View File

@@ -0,0 +1,97 @@
import React from "react";
import { OPACITY_ON_LOAD } from "../../Layout/animations";
import { Box, Divider, Image, Tag, Text, useToast } from "@chakra-ui/react";
import { useNavigate, useParams } from "react-router-dom";
import Header from "../../Components/Header";
import {
useGetFaqByIdQuery,
useGetPolicyByIdQuery,
} from "../../Services/api.service";
import { AttachmentIcon } from "@chakra-ui/icons";
import extractFilename from "../../Components/Functions/FileNameAlter";
import FullscreenLoaders from "../../Components/Loaders/FullscreenLoaders";
import pdf from "../../assets/pdfscreen.png";
const API_URL = import.meta.env.VITE_API_BASE_URL;
const ViewFaq = () => {
const { id } = useParams();
const toast = useToast();
const navigate = useNavigate();
const { data, error, isLoading } = useGetFaqByIdQuery(id);
const viewFaq = data?.data;
// // console.log(viewPolicy?.banner_image);
// // console.log(`https://rubix.betadelivery.com/${viewPolicy?.banner_image}`);
if (isLoading) {
return <FullscreenLoaders />;
}
return (
<Box
{...OPACITY_ON_LOAD}
w={"100%"}
h={"100vh"}
className="overflow-auto "
display={"flex"}
flexDirection={"column"}
>
<Header title={"FAQ"} btnTitle={"Edit FAQ"} link={`/faq/edit/${id}`} />
<Box display={"flex"}>
{/* <Box className="col-5 d-flex flex-column gap-2 pt-4">
<Box
boxSize="sm"
className="d-flex h-auto w-100 justify-content-start flex-column align-items-center gap-3"
>
<Image
shadow={"md"}
rounded={8}
objectFit="cover"
w={500}
h={240}
src={`${API_URL}/${viewPolicy?.banner_image}`}
alt="Selected Image"
/>
</Box>
</Box> */}
<Box className="col-7 pt-4 p-4">
<Box>
<Box className="web-text-large fw-bold mb-1 rubix-text-dark">
Status
</Box>
{viewFaq?.status ? (
<Tag size={"sm"} borderRadius="full" colorScheme="teal">
Active
</Tag>
) : (
<Tag size={"sm"} borderRadius="full" colorScheme="red">
Inactive
</Tag>
)}
</Box>
<Box className="mb-3">
<Box className="web-text-large fw-bold rubix-text-dark">Title</Box>
<Box className="web-text-medium text-secondary">
{viewFaq?.question}
</Box>
</Box>
<Box className="mb-3">
<Box className="web-text-large fw-bold rubix-text-dark">
Content
</Box>
<Box className="web-text-medium text-secondary">
{viewFaq?.answer}
</Box>
</Box>
<Divider />
</Box>
</Box>
</Box>
);
};
export default ViewFaq;

View File

@@ -0,0 +1,11 @@
import { Box } from '@chakra-ui/react'
import React from 'react'
import { OPACITY_ON_LOAD } from '../../Layout/animations'
const Investment = () => {
return (
<Box {...OPACITY_ON_LOAD} bg={"green.100"} overflowY={"scroll"} height={"100vh"}></Box>
)
}
export default Investment

273
src/Pages/Login.jsx Normal file
View File

@@ -0,0 +1,273 @@
import { useNavigate } from "react-router-dom";
import Input01 from "../Components/Inputs/Input01";
import logo from "../assets/logo2.png";
import { useDispatch, useSelector } from "react-redux";
import { loginUser } from "../Redux/Slice/auth";
import { useContext, useEffect, useState } from "react";
import Button01 from "../Components/Buttons/Button01";
import { yupResolver } from "@hookform/resolvers/yup";
import { useForm } from "react-hook-form";
import { TiWarning } from "react-icons/ti";
import Loader01 from "../Components/Loaders/Loader01";
import Asset1 from "../assets/asset1.png";
import Asset2 from "../assets/asset2.png";
import {
Button,
FormControl,
FormLabel,
Input,
InputGroup,
InputRightElement,
useToast,
} from "@chakra-ui/react";
import GlobalStateContext from "../Contexts/GlobalStateContext";
import Cookies from "js-cookie";
import { validationSchema } from "../Validations/Validations";
import ToastBox from "../Components/ToastBox";
const Login = () => {
const [show, setShow] = useState(false);
const handleClick = () => setShow(!show);
const { isAuthenticate, setIsAuthenticate } = useContext(GlobalStateContext);
const toast = useToast();
// const { isAuthenticate } = useSelector((state) => state?.auth);
const [isLoading, setIsLoading] = useState(false);
const navigate = useNavigate();
const dispatch = useDispatch();
useEffect(() => {
if (isAuthenticate) {
navigate("/");
}
}, [navigate, isAuthenticate]);
const onSubmit = (value) => {
setIsLoading(true);
if (value.name === "Admin" && value.password === "Admin") {
return setTimeout(() => {
// dispatch(loginUser(true));
setIsAuthenticate(true);
setIsLoading(false);
toast({
render: () => (
<ToastBox status={"success"} message={"Login Successfully"} />
),
});
Cookies.set("isAuthenticated", true, { expires: 7 });
navigate("/");
}, 2000); // 3-second delay
} else {
return setTimeout(() => {
// dispatch(loginUser(true));
setIsAuthenticate(false);
setIsLoading(false);
toast({
render: () => (
<ToastBox status={"error"} message={"Invalid credentials"} />
),
});
reset();
navigate("/login");
}, 2000);
}
};
const {
register,
handleSubmit,
reset,
formState: { errors },
} = useForm({
resolver: yupResolver(validationSchema),
});
return (
<div
style={{
height: "100vh",
display: "flex",
alignItems: "center",
justifyContent: "center",
position: "relative",
overflow: "hidden",
backgroundColor:"#0041180A"
}}
className="rubix-primary"
>
<form
onSubmit={handleSubmit(onSubmit)}
style={{
width: "90%",
maxWidth: "450px",
height: "auto",
background: "#fff",
borderRadius: "10px",
padding: "1.5rem",
// boxShadow: "0 24px 64px #26214a1a",
boxShadow:
"rgba(50, 50, 93, 0.25) 0px 2px 5px -1px, rgba(0, 0, 0, 0.3) 0px 1px 3px -1px",
zIndex: 2,
}}
>
<div className="d-flex flex-column mb-4">
<img
style={{
left: "8px",
top: "8px",
// width: "10%",
maxWidth: "130px",
}}
src={logo}
alt="img"
className="mb-4"
/>
<span className="fw-bold fs-2 rubix-text-dark text-start">
Welcome back.
</span>
<span className="fw-500 web-text-large text-secondary text-start">
Login to manage tanami.
</span>
</div>
<FormControl className=" mb-3">
<FormLabel className="rubix-text-dark ps-1 web-text-medium fw-bold">
Owner name <span className="text-danger">*</span>
</FormLabel>
<Input
{...register("name")}
focusBorderColor="green.500"
type="text"
name="name"
variant="filled"
placeholder="Owner name"
size="lg"
className="web-text-medium"
/>
{errors.name && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " /> {errors.name.message}
</span>
)}
</FormControl>
<FormControl className="mb-4">
<FormLabel className="rubix-text-dark ps-1 web-text-medium fw-bold">
Password <span className="text-danger">*</span>
</FormLabel>
<InputGroup size="lg">
<Input
{...register("password")}
className="web-text-medium"
focusBorderColor="green.500"
variant="filled"
pr="4.5rem"
type={show ? "text" : "password"}
placeholder="Enter password"
/>
<InputRightElement width="4.5rem">
<Button
h="1.75rem"
size="sm"
fontSize={"xs"}
color={"green.800"}
onClick={handleClick}
>
{show ? "Hide" : "Show"}
</Button>
</InputRightElement>
</InputGroup>
{errors.password && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " /> {errors.password.message}
</span>
)}
</FormControl>
<Button
isLoading={isLoading}
spinner={<Loader01 />}
type="submit"
className="w-100 primary-btn"
color={"whitesmoke"}
colorScheme="green.500"
size="lg"
>
Log In
</Button>
</form>
<div
style={{
position: "absolute",
bottom: "0%",
fontSize: "13px",
color: "#919191",
textAlign: "center",
width: "100%",
zIndex: 2,
}}
>
Tanami v1.0.0
</div>
{/* <img
style={{
position: "absolute",
top: 0,
right: 0,
// width:100
}}
src={Asset1}
alt="bg-img"
/>
<img
style={{
position: "absolute",
top: 0,
right: 0,
// width:400
}}
src={Asset2}
alt="bg-img"
/> */}
<img
style={{
position: "absolute",
right: 0,
bottom: 0,
width: 150,
}}
src={Asset1}
alt="bg-img"
/>
{/* <img
style={{
position: "absolute",
top: 0,
left: 0,
}}
src={Asset1}
alt="bg-img"
/> */}
<img
style={{
position: "absolute",
top: 0,
left: 0,
}}
src={Asset2}
alt="bg-img"
/>
</div>
);
};
export default Login;

View File

@@ -0,0 +1,232 @@
import {
Box,
Button,
Image,
Menu,
MenuButton,
MenuItem,
MenuList,
Portal,
Switch,
Text,
Tooltip,
useToast,
} from "@chakra-ui/react";
import {
AlertDialog,
AlertDialogBody,
AlertDialogCloseButton,
AlertDialogContent,
AlertDialogFooter,
AlertDialogOverlay,
useDisclosure,
} from "@chakra-ui/react";
import { OPACITY_ON_LOAD } from "../../Layout/animations";
import { TABLE_PAGINATION } from "../../Constants/Paginations";
import {
useDeleteEmailMutation,
useGetNewsLetterEmailQuery,
useGetNewsLetterQuery,
} from "../../Services/api.service";
import { useState } from "react";
import TabularView from "../../Components/TabularView/TabularView";
import CustomAlertDialog from "../../Components/CustomAlertDialog";
import { HiDotsVertical } from "react-icons/hi";
import { Link } from "react-router-dom";
import { formatDate } from "../../Components/Functions/UTCConvertor";
import ToastBox from "../../Components/ToastBox";
import extractFilename from "../../Components/Functions/FileNameAlter";
import { DeleteIcon } from "@chakra-ui/icons";
const NewsLetter = () => {
const toast = useToast();
const [pageSize, setPageSize] = useState(TABLE_PAGINATION?.size);
const [currentPage, setCurrentPage] = useState(1);
const [searchTerm, setSearchTerm] = useState("");
const [statusFilter, setStatusFilter] = useState("all");
const [deleteAlert, setDeleteAlert] = useState(false);
const [actionId, setActionId] = useState(null);
const [actionStatus, setActionStatus] = useState(null);
const [deleteIsLoading, setDeleteIsLoading] = useState(false);
const { data } = useGetNewsLetterQuery({ page: currentPage, size: pageSize });
// // console.log(useGetNewsletterQuery);
const email = data?.data?.rows;
// console.log(email);
const [deleteEmail] = useDeleteEmailMutation();
// const [updateFaqStatus] = useUpdateFaqStatusMutation();
const handleDelete = async (communityId, status) => {
if (status) {
return toast({
render: () => (
<ToastBox
status={"warn"}
message={"Can't delete Email. Status is true."}
/>
),
});
}
try {
// Trigger the mutation
setDeleteIsLoading(true);
const response = await deleteEmail(communityId);
// Handle the response here
// console.log("Mutation response:", response?.data?.statusCode);
// console.log("Mutation response:", response?.data?.message);
if (response?.data?.statusCode === 200) {
toast({
render: () => (
<ToastBox status={"success"} message={response?.data?.message} />
),
});
} else {
toast({
render: () => (
<ToastBox
status={"error"}
message={"Failed to delete partner. Please try again."}
/>
),
});
}
} catch (error) {
// console.error("Error deleting community:", error);
toast({
render: () => (
<ToastBox
status={"error"}
message={"Error deleting partner. Please try again."}
/>
),
});
} finally {
// Ensure the loading state is reset and alert is closed
setDeleteIsLoading(false);
setDeleteAlert(false);
}
};
// ====================================================[Table Setup]================================================================
const tableHeadRow = [
"Title",
// "Content",
"Created At",
"Actions",
];
const extractedArray = email?.map((item, index) => {
return {
Title: (
<Tooltip
className="rounded-2 web-text-xsmall"
width={"fit-content"}
placement="top"
hasArrow
label={item?.email}
bg="blue.200"
>
<Box display={"flex"} alignItems={"center"} w={200}>
<Text as={"span"} isTruncated={true}>
{item?.email}
</Text>
</Box>
</Tooltip>
),
"Created At": (
<Box
w={200}
className="d-flex justify-content-between align-items-center"
>
<Text as={"span"} color={"gray.600"} className=" fw-bold">
{formatDate(item?.createdAt)}
</Text>
{/* <Menu>
<MenuButton className="link p-1 rounded-1">
<HiDotsVertical className="rubix-text-dark fs-6" />
</MenuButton>
<Portal>
<MenuList minWidth="80px">
<Link to={`/faq/edit/${item.id}`}>
<MenuItem className="web-text-medium">Edit</MenuItem>
</Link>
<Link to={`/faq/view/${item.id}`}>
<MenuItem className="web-text-medium">View</MenuItem>
</Link>
<MenuItem
onClick={() => {
setActionId(item.id);
setDeleteAlert(true);
setActionStatus(item.status);
}}
className="web-text-medium"
>
Delete
</MenuItem>
</MenuList>
</Portal>
</Menu> */}
</Box>
),
Actions: (
<Box w={200}>
<Button
size={"xs"}
colorScheme="red"
variant="ghost"
// onClick={() => {
// setDeleteAlert(true);
// }}
onClick={() => {
setActionId(item.id);
setDeleteAlert(true);
setActionStatus(item.status);
}}
>
<DeleteIcon me={2} /> Delete
</Button>
</Box>
),
};
});
// ====================================================[Functions]===================================================================
return (
<>
<TabularView
title={"News Letter"}
btnTitle={"Export email"}
link={"/faq/add-faq"}
apiData={email}
tableHeadRow={tableHeadRow}
extractedArray={extractedArray}
searchTerm={searchTerm}
setSearchTerm={setSearchTerm}
statusFilter={statusFilter}
setStatusFilter={setStatusFilter}
pageSize={pageSize}
setPageSize={setPageSize}
currentPage={currentPage}
setCurrentPage={setCurrentPage}
totalPages={email?.data?.data?.totalPages}
totalItems={email?.data?.data?.totalItems}
noDataTitle={"NewsLetter"}
/>
<CustomAlertDialog
onClose={() => setDeleteAlert(false)}
isOpen={deleteAlert}
alertHandler={() => handleDelete(actionId)}
message={"Are you sure you want to delete Email?"}
isLoading={deleteIsLoading}
/>
</>
);
};
export default NewsLetter;

424
src/Pages/News/AddNews.jsx Normal file
View File

@@ -0,0 +1,424 @@
import {
Box,
FormControl,
FormHelperText,
FormLabel,
Input,
Text,
Stack,
Textarea,
Heading,
Button,
useToast,
Divider,
Image,
} from "@chakra-ui/react";
import React, { useState } from "react";
import fallbackImage from "../../assets/ultp-fallback-img.webp";
import { TiWarning } from "react-icons/ti";
import { motion } from "framer-motion";
import { OPACITY_ON_LOAD } from "../../Layout/animations";
import { yupResolver } from "@hookform/resolvers/yup";
import { useForm } from "react-hook-form";
import {
addCommunityBannerSchema,
addCommunitySchema,
addNews,
} from "../../Validations/Validations";
import {
useCreateCommunityBannerMutation,
useCreateCommunityMutation,
useCreateNewsMutation,
useGetCommunityQuery,
} from "../../Services/api.service";
import { useNavigate } from "react-router-dom";
import Loader01 from "../../Components/Loaders/Loader01";
import Header from "../../Components/Header";
import ToastBox from "../../Components/ToastBox";
const AddNews = () => {
const toast = useToast();
const navigate = useNavigate();
const [createNews] = useCreateNewsMutation(); // Invoke the hook to get the mutation function
const [isLoading, setIsLoading] = useState(false);
const [selectedImage, setSelectedImage] = useState(fallbackImage);
const [imageData, setImageData] = useState(null);
const today = new Date().toISOString().split("T")[0];
const {
register,
handleSubmit,
reset,
watch,
formState: { errors },
} = useForm({
resolver: yupResolver(addNews),
});
const formData = watch()
const onSubmit = async (data) => {
const date = new Date(data?.release_date).toUTCString();
try {
setIsLoading(true);
const formData = new FormData();
formData.append("title", data.title);
formData.append("meta_description", data.meta_description);
formData.append("content", data.content);
formData.append("release_date", date);
if (data.banner_image[0]) {
formData.append("banner_image", data.banner_image[0]);
}
// Trigger the mutation
createNews(formData)
.then((response) => {
// Handle the response here
// // console.log("Mutation response:", response?.data?.statusCode);
// // console.log("Mutation response:", response?.data?.message);
if (response?.data?.statusCode === 200) {
setIsLoading(false);
toast({
render: () => (
<ToastBox
status={"success"}
message={response?.data?.message}
/>
),
});
reset();
navigate("/news");
} else if (response?.data?.statusCode === 500) {
setIsLoading(false);
toast({
render: () => (
<ToastBox
status={"success"}
message={response?.data?.message}
/>
),
});
}
})
.catch((error) => {
// Handle errors
// console.error("Error creating community:", error);
setIsLoading(false);
// Handle error notification if needed
});
} catch (error) {
// Handle errors
// console.error("Error creating community:", error);
setIsLoading(false);
}
};
const handleImageChange = (e) => {
const file = e.target.files[0];
setImageData(file);
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
setSelectedImage(reader.result);
};
reader.readAsDataURL(file);
}
};
return (
<Box
{...OPACITY_ON_LOAD}
w={"100%"}
h={"100vh"}
className="overflow-auto "
display={"flex"}
flexDirection={"column"}
>
<Header title={"News"} />
<Box className="d-flex">
<Box className="col-5 d-flex flex-column gap-2 pt-4">
<span className="web-text-large fw-bold rubix-text-dark">
Banner info
</span>
<span className="web-text-medium text-secondary">
Select the platform for which you need to create this campaign.
</span>
<Divider />
<span className="web-text-large fw-bold rubix-text-dark">
Banner image
</span>
<span className="web-text-medium text-secondary mb-4">
Below is the profile that will be displayed on the community page.
</span>
<Box
boxSize="sm"
className="d-flex w-100 justify-content-center flex-column align-items-center gap-3"
>
<Image
shadow={"md"}
rounded={8}
w={500}
h={240}
src={selectedImage}
alt="Selected Image"
/>
{selectedImage === fallbackImage || imageData === null ? (
""
) : (
<Box display={"flex"} flexDirection={"column"} w={"100%"}>
<span className="web-text-small">{imageData?.name}</span>
<span className="web-text-small text-secondary fst-italic">
{(imageData?.size / (1024 * 1024)).toFixed(2)} mb
</span>
</Box>
)}
<Button
onClick={() => setSelectedImage(fallbackImage)}
backgroundColor="red.400"
color={"whitesmoke"}
transition={"0.5s"}
_hover={{
backgroundColor: "red.500",
}}
size="xs"
>
Remove
</Button>
</Box>
</Box>
<form
onSubmit={handleSubmit(onSubmit)}
className="col-7 pt-4 mb-3 overflow-auto p-4"
>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Title
</FormLabel>
<Input
{...register("title")}
placeholder="Title"
className="web-text-medium"
size="sm"
errorBorderColor="crimson"
isInvalid={formData?.title?.length > 50}
// maxLength={51}
/>
<FormHelperText
color={formData?.title?.length > 50 ? "red" : "gray.500"}
className="web-text-small"
>
If description crosses 50 characters it will cause problem in
alignment on website.you have entered {formData?.title?.length}{" "}
characters
</FormHelperText>
{errors.title && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " /> {errors.title.message}
</span>
)}
</FormControl>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Meta description
</FormLabel>
<Textarea
{...register("meta_description")}
placeholder="Description"
className="web-text-medium"
size="sm"
errorBorderColor="crimson"
isInvalid={formData?.meta_description?.length > 160}
// maxLength={51}
/>
<FormHelperText
color={formData?.meta_description?.length > 160 ? "red" : "green.500"}
className="web-text-small"
>
If description crosses 160 characters it will cause problem in
alignment on website.you have entered {formData?.meta_description?.length}{" "}
characters
</FormHelperText>
{errors.meta_description && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.meta_description.message}
</span>
)}
</FormControl>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Content
</FormLabel>
<Textarea
{...register("content")}
placeholder="Content"
className="web-text-medium"
size="sm"
errorBorderColor="crimson"
isInvalid={formData?.content?.length > 230}
// maxLength={51}
/>
<FormHelperText
color={formData?.content?.length > 230 ? "red" : "gray.500"}
className="web-text-small"
>
If content crosses 230 characters it will cause problem in
alignment on website.you have entered {formData?.content?.length}{" "}
characters
</FormHelperText>
{errors.content && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " /> {errors.content.message}
</span>
)}
</FormControl>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Release date
</FormLabel>
<Input
type="date"
{...register("release_date")}
placeholder="Button link"
className="web-text-medium"
size="sm"
min={today} // Disable past dates
/>
<FormHelperText className="web-text-small">
Please share proper release date here.
</FormHelperText>
{errors.release_date && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.release_date.message}
</span>
)}
</FormControl>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Banner image
</FormLabel>
{/* <ImageDropBox /> */}
<Box
borderColor="gray.300"
borderStyle="dashed"
borderWidth="2px"
rounded="md"
shadow="sm"
role="group"
transition="all 150ms ease-in-out"
_hover={{
shadow: "md",
}}
as={motion.div}
initial="rest"
animate="rest"
whileHover="hover"
height={"105px"}
className="pointer"
>
<Box position="relative" height="100%" width="100%">
<Box
position="absolute"
top="0"
left="0"
height="100%"
width="100%"
display="flex"
flexDirection="column"
>
<Stack
height="100%"
width="100%"
display="flex"
alignItems="center"
justify="center"
>
<span
className="d-flex flex-column align-items-center pointer"
spacing="1"
>
<Heading
fontSize="lg"
color="gray.700"
fontWeight="bold"
cursor={"pointer"}
>
Drop images here
</Heading>
<span
fontWeight="light"
className="web-text-large text-secondary text-center pointer"
>
or click to upload
</span>
</span>
</Stack>
</Box>
<Input
{...register("banner_image")}
type="file"
height="100%"
width="100%"
position="absolute"
top="0"
left="0"
opacity="0"
aria-hidden="true"
accept="image/*"
onChange={handleImageChange}
onDrop={handleImageChange}
// onDragEnter={startAnimation}
// onDragLeave={stopAnimation}
/>
</Box>
</Box>
{errors.banner_image && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.banner_image.message}
</span>
)}
<FormHelperText className="web-text-small">
Maximum limit of image is 10MB.
</FormHelperText>
</FormControl>
<Box className=" d-flex justify-content-end mb-5">
<Button
isLoading={isLoading}
spinner={<Loader01 />}
color={"whitesmoke"}
backgroundColor={"purple.700"}
_hover={{
backgroundColor: "purple.800",
}}
type="submit"
size="sm"
rounded={"sm"}
>
Create
</Button>
</Box>
</form>
</Box>
</Box>
);
};
export default AddNews;

458
src/Pages/News/EditNews.jsx Normal file
View File

@@ -0,0 +1,458 @@
import {
Box,
Button,
Divider,
FormControl,
FormHelperText,
FormLabel,
Heading,
Image,
Input,
Stack,
Tag,
Textarea,
useToast,
} from "@chakra-ui/react";
import React, { useEffect, useState } from "react";
import { OPACITY_ON_LOAD } from "../../Layout/animations";
import Header from "../../Components/Header";
import { useNavigate, useParams } from "react-router-dom";
import {
useGetNewsByIdQuery,
useUpdateNewsMutation,
} from "../../Services/api.service";
import { addNews, editNews } from "../../Validations/Validations";
import { yupResolver } from "@hookform/resolvers/yup";
import { useForm } from "react-hook-form";
import fallbackImage from "../../assets/ultp-fallback-img.webp";
import { motion } from "framer-motion";
import Loader01 from "../../Components/Loaders/Loader01";
import { formatDate } from "../../Components/Functions/UTCConvertor";
import { TiWarning } from "react-icons/ti";
import FullscreenLoaders from "../../Components/Loaders/FullscreenLoaders";
import ToastBox from "../../Components/ToastBox";
const API_URL = import.meta.env.VITE_API_BASE_URL;
const EditNews = () => {
const { id } = useParams();
const toast = useToast();
const navigate = useNavigate();
const { data, error, isLoading, refetch } = useGetNewsByIdQuery(id);
const [isLoadingEdit, setIsLoadingEdit] = useState(false);
const [selectedImage, setSelectedImage] = useState(fallbackImage);
const [largeImageData, setLargeImageData] = useState(null);
const [updateNews] = useUpdateNewsMutation();
const {
register,
handleSubmit,
reset,
formState: { errors },
watch,
setValue,
} = useForm({
resolver: yupResolver(editNews),
defaultValues: {
title: "",
release_date: "",
meta_description: "",
content: "",
banner_image: null,
},
});
const formData = watch()
useEffect(() => {
if (data?.data) {
setSelectedImage(
`${API_URL}/${data?.data?.banner_image}`
);
setValue("title", data?.data?.title);
setValue("meta_description", data?.data?.meta_description);
setValue("release_date", data?.data?.release_date);
setValue("content", data?.data?.content);
setValue("banner_image", data?.data?.banner_image);
}
}, [data, setValue]);
// console.log(errors);
const onSubmit = async (data) => {
setIsLoadingEdit(true);
const form = new FormData();
form.append("title", data?.title);
form.append("meta_description", data?.meta_description);
form.append("content", data?.content);
form.append("release_date", data?.release_date);
if (largeImageData !== null) {
form.append("banner_image", largeImageData);
}
await updateNews({ id: id, data: form })
.then((response) => {
if (response?.data?.statusCode === 200) {
setIsLoadingEdit(false);
toast({
render: () => (
<ToastBox status={"success"} message={response?.data?.message} />
),
});
refetch();
navigate("/news");
// setDeleteAlert(false);
}
})
.catch((error) => {
// console.error("Error creating community:", error);
setIsLoadingEdit(false);
// setDeleteIsLoading(false);
// setDeleteAlert(false);
});
reset();
};
const handleImageChange = (e) => {
const file = e.target.files[0];
setLargeImageData(file);
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
setSelectedImage(reader.result);
};
reader.readAsDataURL(file);
}
};
return isLoading ? (
<FullscreenLoaders />
) : (
<Box
{...OPACITY_ON_LOAD}
overflowY={"scroll"}
paddingBottom={50}
height={"100vh"}
>
<Header title={"News"} />
<Box display={"flex"}>
<Box className="col-5 d-flex flex-column gap-2 pt-4">
<span className="web-text-large fw-bold rubix-text-dark">
Members Info
</span>
<span className="web-text-medium text-secondary">
Select the platform for which you need to create this campaign.
</span>
<Divider />
<span className="web-text-large fw-bold rubix-text-dark">
Display profile
</span>
<span className="web-text-medium text-secondary mb-4">
Below is the profile that will be displayed on the community page.
</span>
<Box
boxSize="sm"
className="d-flex w-100 justify-content-center flex-column align-items-center gap-3"
>
<Image
shadow={"md"}
rounded={8}
w={"100%"}
h={240}
src={selectedImage}
alt="Selected Image"
/>
{selectedImage === fallbackImage || largeImageData === null ? (
""
) : (
<Box display={"flex"} flexDirection={"column"} w={"100%"}>
<span className="web-text-small">
{largeImageData && largeImageData?.name}
</span>
<span className="web-text-small text-secondary fst-italic">
{largeImageData &&
(largeImageData?.size / (1024 * 1024)).toFixed(2)}{" "}
mb
</span>
</Box>
)}
<Button
onClick={() => setSelectedImage(fallbackImage)}
backgroundColor="red.400"
color={"whitesmoke"}
transition={"0.5s"}
_hover={{
backgroundColor: "red.500",
}}
size="xs"
>
Remove
</Button>
</Box>
</Box>
<form
className="col-7 pt-4 overflow-auto p-4"
onSubmit={handleSubmit(onSubmit)}
>
<Box className="web-text-large fw-bold mb-2 rubix-text-dark">
Status
</Box>
{data?.data?.status ? (
<Tag
position={"sticky"}
right={10}
size={"sm"}
variant="solid"
colorScheme="teal"
>
Active
</Tag>
) : (
<Tag
position={"sticky"}
right={10}
size={"sm"}
variant="solid"
colorScheme="red"
>
Inactive
</Tag>
)}
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Title
</FormLabel>
<Input
{...register("title")}
placeholder="Name"
className="web-text-medium"
size="sm"
name="title"
type="text"
id="title"
errorBorderColor="crimson"
isInvalid={formData?.title?.length > 50}
// maxLength={51}
/>
<FormHelperText
color={formData?.title?.length > 50 ? "red" : "gray.500"}
className="web-text-small"
>
If title crosses 50 characters it will cause problem in
alignment on website.you have entered {formData?.title?.length}{" "}
characters
</FormHelperText>
{errors.title && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " /> {errors.title.message}
</span>
)}
</FormControl>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Meta description
</FormLabel>
<Textarea
{...register("meta_description")}
placeholder="Name"
className="web-text-medium"
size="sm"
name="meta_description"
type="text"
id="meta_description"
errorBorderColor="crimson"
isInvalid={formData?.meta_description?.length > 160}
// maxLength={51}
/>
<FormHelperText
color={formData?.meta_description?.length > 160 ? "red" : "green.500"}
className="web-text-small"
>
If meta_description crosses 160 characters it will cause problem in
alignment on website.you have entered {formData?.meta_description?.length}{" "}
characters
</FormHelperText>
{errors.meta_description && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.meta_description.message}
</span>
)}
</FormControl>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Content
</FormLabel>
<Textarea
{...register("content")}
placeholder="Content"
className="web-text-medium"
size="sm"
errorBorderColor="crimson"
isInvalid={formData?.content?.length > 230}
// maxLength={51}
/>
<FormHelperText
color={formData?.content?.length > 230 ? "red" : "gray.500"}
className="web-text-small"
>
If content crosses 230 characters it will cause problem in
alignment on website.you have entered {formData?.content?.length}{" "}
characters
</FormHelperText>
{errors.content && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " /> {errors.content.message}
</span>
)}
</FormControl>
<FormControl isRequired className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Release date
</FormLabel>
<Input
type="date"
{...register("release_date")}
placeholder="Button link"
className="web-text-medium"
size="sm"
/>
<FormHelperText className="web-text-small">
{formatDate(data?.data?.release_date)}
</FormHelperText>
{errors.release_date && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.release_date.message}
</span>
)}
</FormControl>
<FormControl className="mb-3">
<FormLabel className="web-text-large fw-bold rubix-text-dark">
Display profile
</FormLabel>
{/* <ImageDropBox /> */}
<Box
borderColor="gray.300"
borderStyle="dashed"
borderWidth="2px"
rounded="md"
shadow="sm"
role="group"
transition="all 150ms ease-in-out"
_hover={{
shadow: "md",
}}
as={motion.div}
initial="rest"
animate="rest"
whileHover="hover"
height={"105px"}
className="pointer"
>
<Box position="relative" height="100%" width="100%">
<Box
position="absolute"
top="0"
left="0"
height="100%"
width="100%"
display="flex"
flexDirection="column"
>
<Stack
height="100%"
width="100%"
display="flex"
alignItems="center"
justify="center"
>
<span
className="d-flex flex-column align-items-center pointer"
spacing="1"
>
<Heading
fontSize="lg"
color="gray.700"
fontWeight="bold"
cursor={"pointer"}
>
Drop images here
</Heading>
<span
fontWeight="light"
className="web-text-large text-secondary text-center pointer"
>
or click to upload
</span>
</span>
</Stack>
</Box>
<Input
{...register("profile_image")}
type="file"
height="100%"
width="100%"
position="absolute"
top="0"
left="0"
opacity="0"
aria-hidden="true"
accept="image/*"
onChange={handleImageChange}
onDrop={handleImageChange}
// onDragEnter={startAnimation}
// onDragLeave={stopAnimation}
/>
</Box>
</Box>
{errors.banner_image && (
<span className="text-danger web-text-small fw-bold ps-2 d-flex align-items-center gap-1 mt-1">
<TiWarning className="fw-bold fs-5 " />{" "}
{errors.banner_image.message}
</span>
)}
<FormHelperText className="web-text-small">
Maximum limit of image should be 1mb to protect website from slow
loading.
</FormHelperText>
</FormControl>
<Box className=" d-flex justify-content-end ">
<Button
isLoading={isLoadingEdit}
spinner={<Loader01 />}
color={"whitesmoke"}
backgroundColor={"purple.900"}
_hover={{
backgroundColor: "purple.800",
}}
type="submit"
size="sm"
rounded={"sm"}
>
Save edit
</Button>
</Box>
</form>
</Box>
</Box>
);
};
export default EditNews;

Some files were not shown because too many files have changed in this diff Show More