0% found this document useful (0 votes)
3 views16 pages

3

Uploaded by

tarlemayuri74
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as TXT, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
3 views16 pages

3

Uploaded by

tarlemayuri74
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as TXT, PDF, TXT or read online on Scribd
You are on page 1/ 16

3

import dynamic from "next/dynamic";


import { useEffect, useRef, useState } from "react";
import Layout from "../../layouts";
import { Loader, Page } from "../../components";
// Add this import at the top of your file
import { Radio, message } from "antd";
// For Material-UI
import { ToggleButtonGroup, ToggleButton } from "@mui/material";

// @mui
import {
Container,
Grid,
Card,
Paper,
Typography,
styled,
Button,
TableRow,
TableCell,
Table,
TableHead,
TableBody,
Pagination,
TableContainer,
CardContent,
} from "@mui/material";
import axiosInstance from "../../utils/axios";
import { API_ENDPOINT } from "../../utils/constant";
import { RefreshOutlined, RefreshTwoTone } from "@mui/icons-material";
import ActiveUserCard from "../../components/ActiveUserCard";
import UserMetricsCard from "../../components/UserMetricsCard";
import ChartCard from "../../components/ChartCard";
import { List as BaseList } from "../../layouts/list/List";
import { fi } from "date-fns/locale";

const Chart = dynamic(() => import("react-apexcharts"), { ssr: false });

GeneralApp.getLayout = function getLayout(page) {


return <Layout variant="dashboard">{page}</Layout>;
};

// ----------------------------------------------------------------------

export default function GeneralApp() {


const [error, setError] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [activeUsers, setActiveUsers] = useState({});
const [unprocessedCount, setUnproccesedCount] = useState(0);
const [userMetrics, setUserMetrics] = useState({});
const [isButtonLoading, setIsButtonLoading] = useState(false);
const [isRefreshing, setIsRefreshing] = useState(false);
const [activeUsersData, setActiveUsersData] = useState([]);
const [pageMeta, setPageMeta] = useState({ page: 1, limit: 10 });

const [notificationsData, setNotificationsData] = useState(() => []);


const [notificationsMeta, setNotificationsMeta] = useState({
page: 1,
limit: 10,
total: 0,
});
const [readFilter, setReadFilter] = useState(null);
const [duration, setDuration] = useState(240);
const [userDataLoading, setUserDataLoading] = useState(false);
const isInitialRender = useRef(true);

const NOTIFICATION_ROWS = [
{ id: "empty1", label: "" },
{ id: "title", label: "Title" },
{ id: "empty2", label: "" },
{ id: "type", label: "Type" },
{ id: "empty3", label: "" },
{ id: "createdAt", label: "Created At" },
{ id: "status", label: "Status" },
];

// And then replace the Radio.Group with


<ToggleButtonGroup
value={readFilter}
exclusive
onChange={(_, newValue) => setReadFilter(newValue)}
>
<ToggleButton value={null}>All</ToggleButton>
<ToggleButton value={true}>Read</ToggleButton>
<ToggleButton value={false}>Unread</ToggleButton>
</ToggleButtonGroup>;

// Fetch Notifications Function


const getNotificationsData = async () => {
setIsLoading(true);
try {
const params = new URLSearchParams();
params.append("page", notificationsMeta.page);
params.append("limit", notificationsMeta.limit);
if (readFilter !== null) {
params.append("isRead", readFilter);
}

const response = await axiosInstance.get(


`${API_ENDPOINT.NOTIFICATIONS.GET_ALL}?${params.toString()}`
);
setNotificationsData(response.data.notifications || []);
setNotificationsMeta((prev) => ({
...prev,
total: response.data.total || 0,
}));
} catch (error) {
console.error("Failed to fetch notifications:", error);
message.error("Failed to load notifications");
setNotificationsData([]);
} finally {
setIsLoading(false);
}
};

// Fetch Notifications on Page/Filter Change


useEffect(() => {
getNotificationsData();
}, [notificationsMeta.page, readFilter]);

// Mark Single Notification as Read


const markAsRead = async (notificationId) => {
try {
await axiosInstance.post(API_ENDPOINT.NOTIFICATIONS.MARK_AS_READ, {
notificationId,
});
setNotificationsData((prev) =>
prev.map((notif) =>
notif._id === notificationId ? { ...notif, hasBeenRead: true } : notif
)
);
message.success("Notification marked as read");
} catch (error) {
console.error("Failed to mark notification as read:", error);
message.error("Failed to update notification");
}
};

// Mark All Notifications as Read


const markAllAsRead = async () => {
try {
await axiosInstance.post(API_ENDPOINT.NOTIFICATIONS.MARK_ALL_READ);
getNotificationsData();
message.success("All notifications marked as read");
} catch (error) {
console.error("Failed to mark all notifications as read:", error);
message.error("Failed to update notifications");
}
};

const ROWS = [
{ id: "username", label: "Username" },
{ id: "", label: "" },
{ id: "mobileNumber", label: "Mobile Number" },
{ id: "", label: "" },
{ id: "lastActiveAt", label: "Last Active" },
];

const [languageBarChart, setLanguageBarChart] = useState({


series: [
{
data: [], // Holds the data for each language
name: "Audios", // The name of the data series
},
],
options: {
chart: {
type: "bar", // Change to bar chart type
height: 400, // Set custom height for the bar chart
},
xaxis: {
categories: [], // Holds the labels for each language
},
dataLabels: {
enabled: true, // Enable data labels to show on top of bars
formatter: function (value) {
return value; // Display the count value on top of each bar
},
style: {
fontSize: "12px", // Customize the font size of the label
fontWeight: "bold", // Make the label bold
colors: ["#fff"], // Set the color for the data label text
},
offsetY: -10, // Adjust the vertical positioning of the data label
(negative to position it above)
},
},
});

// Bar Chart Data for Audios by Category


const [categoriesLineChart, setCategoriesLineChart] = useState({
series: [{ name: "Audios Count", data: [] }],
options: {
chart: { type: "bar", height: 350 },
xaxis: { categories: [] },
stroke: { curve: "smooth" },
dataLabels: {
enabled: true, // Enable data labels to show on top of bars
formatter: function (value) {
return value; // Display the count value on top of each bar
},
style: {
fontSize: "12px", // Customize the font size of the label
fontWeight: "bold", // Make the label bold
colors: ["#fff"], // Set the color for the data label text
},
offsetY: -10, // Adjust the vertical positioning of the data label
(negative to position it above)
},
},
});

// New State for Monthly User Growth Bar Chart


const [userGrowthBarChart, setUserGrowthBarChart] = useState({
series: [{ name: "User Count", data: [] }],
options: {
chart: { type: "bar", height: 350 },
xaxis: { categories: [] },
dataLabels: {
enabled: true, // Enable data labels to show on top of bars
formatter: function (value) {
return value; // Display the count value on top of each bar
},
style: {
fontSize: "12px", // Customize the font size of the label
fontWeight: "bold", // Make the label bold
colors: ["#fff"], // Set the color for the data label text
},
offsetY: -10, // Adjust the vertical positioning of the data label
(negative to position it above)
},
},
});

const [dayWiseUserGrowthBarChart, setDayWiseUserGrowthBarChart] = useState({


series: [{ name: "User Count", data: [] }],
options: {
chart: { type: "bar", height: 350 },
xaxis: { categories: [] },
dataLabels: {
enabled: true, // Enable data labels to show on top of bars
formatter: function (value) {
return value; // Display the count value on top of each bar
},
style: {
fontSize: "12px", // Customize the font size of the label
fontWeight: "bold", // Make the label bold
colors: ["#fff"], // Set the color for the data label text
},
offsetY: -10, // Adjust the vertical positioning of the data label
(negative to position it above)
},
},
});

const [audiosCreatedMonthwiseBarChart, setAudiosCreatedMonthwise] = useState({


series: [{ name: "Audios Monthwise Count", data: [] }],
options: {
chart: { type: "bar", height: 350 },
xaxis: { categories: [] },
dataLabels: {
enabled: true, // Enable data labels to show on top of bars
formatter: function (value) {
return value; // Display the count value on top of each bar
},
style: {
fontSize: "12px", // Customize the font size of the label
fontWeight: "bold", // Make the label bold
colors: ["#fff"], // Set the color for the data label text
},
offsetY: -10, // Adjust the vertical positioning of the data label
(negative to position it above)
},
},
});

const [audiosCreatedLast30DaysBarChart, setAudiosCreatedLast30Dayswise] =


useState({
series: [{ name: "Audios Last 30 days Count", data: [] }],
options: {
chart: { type: "bar", height: 350 },
xaxis: { categories: [] },
dataLabels: {
enabled: true, // Enable data labels to show on top of bars
formatter: function (value) {
return value; // Display the count value on top of each bar
},
style: {
fontSize: "12px", // Customize the font size of the label
fontWeight: "bold", // Make the label bold
colors: ["#fff"], // Set the color for the data label text
},
offsetY: -10, // Adjust the vertical positioning of the data label
(negative to position it above)
},
},
});

// Fetch Stats Data


const getStatsData = async () => {
try {
setIsLoading(true);

const [
audiosByCategoryResponse,
audiosByLanguageResponse,
activeUsersResponse,
userGrowthStatsResponse,
audiosCreatedByMonthResponse,
audiosCreatedLast30DaysResponse,
unprocessedAudiosResponse,
userMatricesResponse,
] = await Promise.all([
axiosInstance.get(API_ENDPOINT.DASHBOARD.AUDIOS_BY_CATEGORY),
axiosInstance.get(API_ENDPOINT.DASHBOARD.AUDIOS_BY_LANGUAGE),
axiosInstance.get(API_ENDPOINT.DASHBOARD.ACTIVE_USERS),
axiosInstance.get(API_ENDPOINT.DASHBOARD.USER_GROWTH),
axiosInstance.get(API_ENDPOINT.DASHBOARD.AUDIOS_CREATED_BY_MONTH),
axiosInstance.get(API_ENDPOINT.DASHBOARD.AUDIOS_CREATED_30_DAYS),
axiosInstance.get(API_ENDPOINT.DASHBOARD.UNPROCESSED_AUDIOS),
axiosInstance.get(API_ENDPOINT.METRICS.USER_METRICS),
]);

const userMetricsData = userMatricesResponse?.data;


setUserMetrics(userMetricsData);
const unprocessedCount =
unprocessedAudiosResponse?.data?.unprocessedAudiosCount || 0;
setUnproccesedCount(unprocessedCount);
const monthWiseCountsData =
userGrowthStatsResponse?.data?.monthWiseRegisteredUsers || [];
const dayWiseRegisteredUsersLast30Days =
userGrowthStatsResponse?.data?.dayWiseRegisteredUsersLast30Days || [];

const dayWiseDates = dayWiseRegisteredUsersLast30Days.map(


(item) => item.date
);
const dayWiseCounts = dayWiseRegisteredUsersLast30Days.map(
(item) => item.count
);
setDayWiseUserGrowthBarChart({
series: [{ name: "User Count", data: dayWiseCounts }],
options: {
...dayWiseUserGrowthBarChart.options,
xaxis: { categories: dayWiseDates },
},
});

// Prepare data for the User Growth Bar Chart


const months = monthWiseCountsData.map(
(item) => `${item.year}-${item.month.toString().padStart(2, "0")}`
);
const counts = monthWiseCountsData.map((item) => item.count);
setUserGrowthBarChart({
series: [{ name: "User Count", data: counts }],
options: {
...userGrowthBarChart.options,
xaxis: { categories: months },
},
});

// Set Active Users Data


setActiveUsers(activeUsersResponse.data);

// Set Audios by Category Data


const categoryData = audiosByCategoryResponse.data.data || [];
const categoryLabels = categoryData.map(
(item) => item.category || "Unknown"
);
const categoryCounts = categoryData.map((item) => item.count);
setCategoriesLineChart({
series: [{ name: "Audios Count", data: categoryCounts }],
options: {
...categoriesLineChart.options,
xaxis: { categories: categoryLabels },
},
});

// Set Audios by Language Data


const languageData = audiosByLanguageResponse.data.data || [];
const languageLabels = languageData.map(
(item) => item.language || "Unknown"
);
const languageCounts = languageData.map((item) => item.count);
setLanguageBarChart({
series: [
{
...languageBarChart.series[0],
data: languageCounts, // Set data for each language count
},
],
options: {
...languageBarChart.options,
xaxis: { categories: languageLabels }, // Set categories for each
language label
},
});

const audiosCreatedMonthWiseDates =
audiosCreatedByMonthResponse?.data?.map((item) => item.month) || [];
const audiosCreatedMonthWiseCounts =
audiosCreatedByMonthResponse?.data?.map((item) => item.count) || [];
setAudiosCreatedMonthwise({
series: [
{ name: "Audios Created Count", data: audiosCreatedMonthWiseCounts },
],
options: {
...audiosCreatedMonthwiseBarChart.options,
xaxis: { categories: audiosCreatedMonthWiseDates },
},
});

const audiosCreatedLast30Dates =
audiosCreatedLast30DaysResponse?.data?.map((item) => item.date) || [];
const audiosCreatedLast30Counts =
audiosCreatedLast30DaysResponse?.data?.map((item) => item.count) || [];
setAudiosCreatedLast30Dayswise({
series: [
{ name: "Audios Created Count", data: audiosCreatedLast30Counts },
],
options: {
...audiosCreatedMonthwiseBarChart.options,
xaxis: { categories: audiosCreatedLast30Dates },
},
});

setError(false);
} catch (error) {
console.error(error);
setError(true);
} finally {
setIsLoading(false);
}
};

const handleRefresh = async () => {


setIsLoading(true);
await getStatsData();
setIsLoading(false);
};

useEffect(() => {
getStatsData();
getActiveUserData(duration, pageMeta?.limit, pageMeta?.page);
}, []);
useEffect(() => {
setUserDataLoading(true);
getActiveUserData(duration, pageMeta?.limit, pageMeta?.page);
setUserDataLoading(false);
}, [pageMeta]);

const handleClick = async () => {


const url = `${API_ENDPOINT.BASE_URL}files/trigger-sqs`;

try {
setIsButtonLoading(true); // Disable button
const response = await axiosInstance.post(url, null, {
headers: {
"Content-Type": "multipart/form-data",
},
});

if (response.status === 200) {


throw new error("Request successful!");
} else {
throw new error(
"Request failed:",
response.status,
response.statusText
);
}
} catch (error) {
throw new error("An error occurred:", error);
} finally {
setIsButtonLoading(false);
}
};
const refreshUnprocessedCount = async () => {
try {
setIsRefreshing(true);
const unprocessedAudiosResponse = await axiosInstance.get(
API_ENDPOINT.DASHBOARD.UNPROCESSED_AUDIOS
);
const unprocessedCount =
unprocessedAudiosResponse?.data?.unprocessedAudiosCount || 0;
setUnproccesedCount(unprocessedCount);
} catch (error) {
throw new error("Error refreshing unprocessed audios count:", error);
} finally {
setIsRefreshing(false);
}
};

const getActiveUserData = async (durationValue, limit, page) => {


setUserDataLoading(true);
try {
const response = await axiosInstance.get(
API_ENDPOINT.DASHBOARD.ACTIVE_USERS_DETAILS,
{
params: { duration: durationValue, limit, page },
}
);
setActiveUsersData(response?.data || []);
} catch (error) {
throw new error("Error fetching active user data:", error);
setUserDataLoading(false);
} finally {
setUserDataLoading(false);
}
};

useEffect(() => {
if (!isInitialRender.current) {
setUserDataLoading(true);
getActiveUserData(duration, pageMeta?.limit, pageMeta?.page);
setUserDataLoading(false);
}
}, [pageMeta, duration]);

return (
<Page title="Audioshots | Admin">
{isLoading ? (
<Loader />
) : (
<>
<Container maxWidth="xl">
<Grid
container
alignItems="center"
justifyContent="space-between"
sx={{ mb: 3 }}
>
<Typography
variant="h5"
sx={{ fontWeight: "bold", color: "#34bf49" }}
>
{unprocessedCount}
</Typography>

<RefreshOutlined
onClick={handleRefresh}
color="primary"
sx={{
fontSize: 30,
cursor: "pointer",
"&:hover": {
color: "#1565c0",
},
}}
/>
</Grid>

{/* Active Users Section */}


<Typography
variant="h5"
sx={{
mb: 3,
fontWeight: "bold",

borderBottom: "2px solidrgb(113, 244, 90)",


pb: 1,
}}
>
Active Users
</Typography>
{activeUsers && Object.entries(activeUsers).length > 0 && (
<Grid container spacing={2} sx={{ mb: 3 }}>
{Object.entries(activeUsers).map(
([timeFrame, count], index) => (
<Grid item xs={6} sm={3} key={timeFrame}>
<ActiveUserCard
timeFrame={timeFrame}
count={count}
//backgroundColor={["#e8f5e9", "#e8f5e9", "#e8f5e9",
"#e8f5e9"][index]}
//textColor={["#34bf49", "#34bf49", "#34bf49", "#34bf49"]
[index]}
/>
</Grid>
)
)}
</Grid>
)}

<Typography
variant="h5"
sx={{
mb: 3,
fontWeight: "bold",
pb: 1,
}}
>
User Metrics
</Typography>
<Grid container spacing={2} sx={{ mb: 3 }}>
<Grid item xs={6} sm={3}>
<UserMetricsCard label="DAU" value={userMetrics?.DAU} />
</Grid>
<Grid item xs={6} sm={3}>
<UserMetricsCard label="WAU" value={userMetrics?.WAU} />
</Grid>
<Grid item xs={6} sm={3}>
<UserMetricsCard label="MAU" value={userMetrics?.MAU} />
</Grid>
</Grid>

{/* Active Users Section */}


<Typography
variant="h5"
sx={{
mb: 3,
fontWeight: "bold",
pb: 1,
}}
>
Audios
</Typography>
<Grid container spacing={2} sx={{ mb: 3 }}>
<Grid item xs={6} sm={3}>
<Paper
sx={{
p: 3,
textAlign: "center",
borderRadius: 3,
boxShadow: 4,
position: "relative",
}}
>
<Typography
variant="h4"
sx={{ fontWeight: "bold", color: "#34bf49" }}
>
{unprocessedCount}
</Typography>
<Typography variant="body2" color="textSecondary">
Unprocessed Audios
</Typography>
<RefreshTwoTone
onClick={refreshUnprocessedCount}
color="primary"
sx={{
position: "absolute",
top: 10,
right: 10,
fontSize: 30,
cursor: "pointer",
color: isRefreshing ? "#B0B0B0" : "primary.main",
transition: "color 0.3s ease",
}}
/>
</Paper>
</Grid>

<Grid item>
<Button
onClick={handleClick}
disabled={unprocessedCount <= 0}
style={{
padding: "12px 24px",
backgroundColor:
unprocessedCount > 0 ? "#28a745" : "#e0e0e0",
color: unprocessedCount > 0 ? "#ffffff" : "#9e9e9e",
fontSize: "16px",
fontWeight: "500",
border: "1px solid",
borderColor: unprocessedCount > 0 ? "#28a745" : "#ccc",
borderRadius: "8px",
cursor: unprocessedCount > 0 ? "pointer" : "not-allowed",
transition: "all 0.1s ease",
boxShadow:
unprocessedCount > 0
? "0 2px 4px rgba(0, 0, 0, 0.2)"
: "none",
}}
onMouseOver={(e) => {
if (unprocessedCount > 0) {
e.target.style.backgroundColor = "#218838";
e.target.style.boxShadow = "0 4px 8px rgba(0, 0, 0, 0.2)";
}
}}
onMouseOut={(e) => {
if (unprocessedCount > 0) {
e.target.style.backgroundColor = "#28a745";
e.target.style.boxShadow = "0 2px 4px rgba(0, 0, 0, 0.2)";
}
}}
>
{isButtonLoading ? "Processing..." : "Trigger Request"}{" "}
</Button>
</Grid>
</Grid>

{/* Charts Section - Two Columns for Larger Screens */}


<Typography
variant="h5"
sx={{
mb: 3,
fontWeight: "bold",

pb: 1,
}}
>
Audios Analytics
</Typography>
<Grid container spacing={4} sx={{ mb: 3 }}>
<Grid item xs={12} sm={6}>
<ChartCard
title="Audios by Language"
chartData={languageBarChart}
/>
</Grid>

<Grid item xs={12} sm={6}>


<ChartCard
title="Audios by Category"
chartData={categoriesLineChart}
/>
</Grid>
<Grid item xs={12} sm={6}>
<ChartCard title="User Growth" chartData={userGrowthBarChart} />
</Grid>
<Grid item xs={12} sm={6}>
<ChartCard
title="Day Wise User Growth"
chartData={dayWiseUserGrowthBarChart}
/>
</Grid>
</Grid>

{userDataLoading ? (
<Loader />
) : (
<>
<BaseList
title="Active Users"
countries={[]}
hideSearch={true}
elements={activeUsersData?.users || []}
rows={ROWS}
onChange={(newMeta) => {
setPageMeta(newMeta);
if (newMeta.duration !== duration) {
setDuration(newMeta.duration);
getActiveUserData(
newMeta.duration,
newMeta.limit,
newMeta.page
);
}
}}
meta={pageMeta}
total={activeUsersData?.total || 0}
showUserFilter={false}
durationFilter={true}
renderBody={(elements) => (
<>
{elements.map((user) => (
<TableRow hover key={user._id}>
<TableCell>{}</TableCell>
<TableCell>{user?.userName}</TableCell>
<TableCell>{}</TableCell>
<TableCell>{user?.mobileNumber}</TableCell>
<TableCell>{}</TableCell>
<TableCell>
{user?.lastActiveAt
? new Date(user.lastActiveAt).toLocaleString()
: "N/A"}
</TableCell>
</TableRow>
))}
</>
)}
/>
</>
)}

<div>
<Typography variant="h5" sx={{ mb: 3, fontWeight: "bold" }}>
Notifications
</Typography>

{/* Read Filter */}


<ToggleButtonGroup
value={readFilter}
exclusive
onChange={(_, newValue) => setReadFilter(newValue)}
>
<ToggleButton value={null}>All</ToggleButton>
<ToggleButton value={true}>Read</ToggleButton>
<ToggleButton value={false}>Unread</ToggleButton>
</ToggleButtonGroup>

<Button
onClick={markAllAsRead}
disabled={
!Array.isArray(notificationsData) ||
notificationsData.length === 0 ||
!notificationsData.some((n) => !n.hasBeenRead)
}
sx={{ ml: 2 }}
variant="contained"
color="primary"
>
Mark All as Read
</Button>

{/* Notifications Table */}


<TableContainer component={Paper} sx={{ mt: 3 }}>
<Table>
<TableHead>
<TableRow>
<TableCell>Title</TableCell>
<TableCell>Type</TableCell>
<TableCell>User</TableCell>
<TableCell>Mobile Number</TableCell>
<TableCell>Created At</TableCell>
<TableCell>Status</TableCell>
</TableRow>
</TableHead>
<TableBody>
{isLoading ? ( // ✅ Use isLoading instead of loading
<TableRow>
<TableCell colSpan={6} align="center">
Loading...
</TableCell>
</TableRow>
) : !Array.isArray(notificationsData) ||
notificationsData.length === 0 ? (
<TableRow>
<TableCell colSpan={6} align="center">
{readFilter === false
? "No unread notifications"
: "No notifications found"}
</TableCell>
</TableRow>
) : (
notificationsData.map((notification) => (
<TableRow
key={notification._id}
style={{
backgroundColor: notification.hasBeenRead
? "inherit"
: "#f0f5ff",
}}
>
<TableCell>{notification.title}</TableCell>
<TableCell>{notification.notificationType}</TableCell>
<TableCell>
{notification.user?.[0]?.username || "N/A"}
</TableCell>
<TableCell>
{notification.user?.[0]?.mobileNumber || "N/A"}
</TableCell>
<TableCell>
{new Date(notification.createdAt).toLocaleString()}
</TableCell>
<TableCell>
{notification.hasBeenRead ? (
"Read"
) : (
<Button
onClick={() => markAsRead(notification._id)}
variant="outlined"
>
Mark as Read
</Button>
)}
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</TableContainer>

{/* Pagination */}


<Pagination
count={Math.ceil(
notificationsMeta.total / notificationsMeta.limit
)}
page={notificationsMeta.page}
onChange={(event, value) =>
setNotificationsMeta((prev) => ({ ...prev, page: value }))
}
color="primary"
sx={{ mt: 3 }}
/>
</div>
</Container>
</>
)}
</Page>
);
}

You might also like

pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy