feat:visitors table, fix:piee chart api, chart total views analytic

This commit is contained in:
Rama Priyanto 2025-07-09 12:02:51 +07:00
parent 70e2fb006e
commit 8a4a3b1d67
7 changed files with 410 additions and 28 deletions

View File

@ -57,7 +57,7 @@ export default function RootLayout({ children }: { children: ReactNode }) {
<Providers themeProps={{ attribute: "class", defaultTheme: "light" }}>
<main className="">{children}</main>
</Providers>
<LoadTawk />
{/* <LoadTawk /> */}
</body>
</html>
);

View File

@ -96,11 +96,21 @@ const ApexChartColumn = (props: {
const [seriesShare, setSeriesShare] = useState<number[]>([]);
const [years, setYear] = useState("");
const [datas, setDatas] = useState<any>([]);
const [totalChart, setTotalChart] = useState({
views: 0,
comment: 0,
share: 0,
});
useEffect(() => {
initFetch();
}, [date, type, view, range]);
const counting = (data: number[]) => {
const total = data.reduce((a: number, b: number) => a + b, 0);
return total;
};
const initFetch = async () => {
const splitDate = date.split(" ");
const splitDateDaily = String(range.start.year);
@ -147,6 +157,13 @@ const ApexChartColumn = (props: {
setSeriesComment(getDatas.comment);
setSeriesView(getDatas.view);
setSeriesShare(getDatas.share);
const totalChartNow = {
views: counting(getDatas.view),
share: counting(getDatas.share),
comment: counting(getDatas.comment),
};
setTotalChart(totalChartNow);
}
if (type === "daily") {
const mappedData = getRangeAcrossMonths(
@ -161,6 +178,12 @@ const ApexChartColumn = (props: {
setSeriesView(mappedData.view);
setSeriesShare(mappedData.share);
setCategories(mappedData.labels);
const totalChartNow = {
views: counting(mappedData.view),
share: counting(mappedData.share),
comment: counting(mappedData.comment),
};
setTotalChart(totalChartNow);
}
// if (type === "weekly") {
// const category = [];
@ -196,7 +219,7 @@ const ApexChartColumn = (props: {
}, [view, seriesShare, seriesView, seriesComment]);
return (
<div className="h-full">
<div className="h-full flex flex-col">
<div id="chart" className="h-full">
<ReactApexChart
options={{
@ -220,6 +243,14 @@ const ApexChartColumn = (props: {
/>
</div>
<div id="html-dist"></div>
<div className="flex flex-row gap-2 mt-5 border-1 p-5 rounded-lg">
<div className="w-1/5 text-center">Chart Total</div>
<div className="w-4/5 text-center">
{totalChart.views} {totalChart.views > 1 ? "Views" : "View"}{" "}
{totalChart.comment} {totalChart.comment > 1 ? "Comments" : "Comment"}{" "}
{totalChart.share} {totalChart.share > 1 ? "Shares" : "Share"}{" "}
</div>
</div>{" "}
</div>
);
};

View File

@ -9,6 +9,12 @@ const dummy = [
{ name: "Brave", value: 29 },
];
interface BrowserVisitor {
browserName: string;
totalVisitor: number;
}
import { getStatisticVisitorsBrowser } from "@/services/article";
import {
convertDateFormatNoTime,
convertDateFormatNoTimeV2,
@ -26,11 +32,11 @@ import ReactApexChart from "react-apexcharts";
export default function PieChartLoginBrowser() {
const [series, setSeries] = useState<number[]>([]);
const [labels, setLabels] = useState<string[]>([]);
const [data, setData] = useState<any>([]);
const [data, setData] = useState<BrowserVisitor[]>([]);
const [total, setTotal] = useState(0);
const [selectedLabel, setSelectedLabel] = useState<string>("");
const [topContentDate, setTopContentDate] = useState({
const [visitorBrowserDate, setVisitorBrowserDate] = useState({
startDate: parseDate(
convertDateFormatNoTimeV2(
new Date(new Date().setDate(new Date().getDate() - 7))
@ -41,13 +47,23 @@ export default function PieChartLoginBrowser() {
useEffect(() => {
fetchData();
}, [topContentDate.startDate, topContentDate.endDate]);
}, [visitorBrowserDate.startDate, visitorBrowserDate.endDate]);
const fetchData = async () => {
const data = dummy;
const getDate = (data: any) => {
return `${data.year}-${data.month < 10 ? `0${data.month}` : data.month}-${
data.day < 10 ? `0${data.day}` : data.day
}`;
};
const res = await getStatisticVisitorsBrowser(
getDate(visitorBrowserDate.startDate),
getDate(visitorBrowserDate.endDate)
);
const data: BrowserVisitor[] = res?.data?.data;
setData(data);
const label = data.map((a) => a.name);
const seriesNow = data.map((a) => a.value);
const label = data.map((a) => a.browserName);
const seriesNow = data.map((a) => a.totalVisitor);
const totalNow = seriesNow.reduce((a, c) => a + c, 0);
setTotal(totalNow);
setLabels(label);
@ -60,8 +76,8 @@ export default function PieChartLoginBrowser() {
};
return (
<div className="flex flex-row w-full mt-5 gap-4">
<div className="w-1/2 border-1 rounded-lg p-4">
<div className="flex flex-col lg:flex-row w-full mt-5 gap-4">
<div className="w-full lg:w-1/2 border-1 rounded-lg p-4">
<p className="font-bold">Browser Statistics</p>
<ReactApexChart
options={{
@ -101,7 +117,7 @@ export default function PieChartLoginBrowser() {
width={600}
/>
</div>
<div className="w-1/2 flex flex-col border-1 rounded-lg p-4">
<div className="w-full lg:w-1/2 flex flex-col border-1 rounded-lg p-4">
<div className="flex flex-row gap-1 justify-center">
<p> Browser Statistics from</p>
<Popover
@ -110,19 +126,19 @@ export default function PieChartLoginBrowser() {
>
<PopoverTrigger>
<a className="cursor-pointer underline">
{convertDateFormatNoTime(topContentDate.startDate)}
{convertDateFormatNoTime(visitorBrowserDate.startDate)}
</a>
</PopoverTrigger>
<PopoverContent className="bg-transparent">
<Calendar
value={topContentDate.startDate}
value={visitorBrowserDate.startDate}
onChange={(e) =>
setTopContentDate({
setVisitorBrowserDate({
startDate: e,
endDate: topContentDate.endDate,
endDate: visitorBrowserDate.endDate,
})
}
maxValue={topContentDate.endDate}
maxValue={visitorBrowserDate.endDate}
/>
</PopoverContent>
</Popover>
@ -133,19 +149,19 @@ export default function PieChartLoginBrowser() {
>
<PopoverTrigger>
<a className="cursor-pointer underline">
{convertDateFormatNoTime(topContentDate.endDate)}
{convertDateFormatNoTime(visitorBrowserDate.endDate)}
</a>
</PopoverTrigger>
<PopoverContent className="bg-transparent">
<Calendar
value={topContentDate.endDate}
value={visitorBrowserDate.endDate}
onChange={(e) =>
setTopContentDate({
startDate: topContentDate.startDate,
setVisitorBrowserDate({
startDate: visitorBrowserDate.startDate,
endDate: e,
})
}
minValue={topContentDate.startDate}
minValue={visitorBrowserDate.startDate}
/>
</PopoverContent>
</Popover>
@ -158,20 +174,20 @@ export default function PieChartLoginBrowser() {
</div>
{data &&
data?.map((list: any, index: number) => (
data?.map((list, index) => (
<div
key={list.name}
key={list.browserName}
className={`grid grid-cols-3 p-4 ${
selectedLabel === list.name
selectedLabel === list.browserName
? "bg-slate-600 text-white"
: index % 2 == 0
? "bg-gray-200"
: "bg-white"
}`}
>
<p className="font-semibold">{list.name}</p>
<p>{list.value}</p>
<p>{getPersentage(list.value)}</p>
<p className="font-semibold">{list.browserName}</p>
<p>{list.totalVisitor}</p>
<p>{getPersentage(list.totalVisitor)}</p>
</div>
))}
</div>

View File

@ -72,6 +72,7 @@ import IndonesiaMap from "@/components/ui/maps-charts";
import ApexChartDynamic from "./chart/dynamic-bar-char";
import ApexMultiLineChart from "./chart/multiline-chart";
import PieChartLoginBrowser from "./chart/pie-chart-browser";
import DashboardVisitorsTable from "@/components/table/dashboard-visitors-table";
type ArticleData = Article & {
no: number;
@ -1084,10 +1085,13 @@ export default function DashboardContainer() {
</div>
<div className="flex flex-row gap-2 mt-5 border-1 p-5 rounded-lg">
<div className="w-1/5 text-center">Chart Total</div>
<div className="w-4/5 text-center">{chartVisitorTotal}</div>
<div className="w-4/5 text-center">
{chartVisitorTotal} Visitors
</div>
</div>
</div>
<PieChartLoginBrowser />
<DashboardVisitorsTable />
</div>
)}
</div>

View File

@ -0,0 +1,308 @@
"use client";
import { getVisitorLog } from "@/services/activity-log";
import {
convertDateFormat,
convertDateFormatNoTime,
convertDateFormatNoTimeV2,
} from "@/utils/global";
import {
Button,
Calendar,
Pagination,
Popover,
PopoverContent,
PopoverTrigger,
Select,
SelectItem,
Spinner,
Table,
TableBody,
TableCell,
TableColumn,
TableHeader,
TableRow,
} from "@heroui/react";
import { Key, useCallback, useEffect, useState } from "react";
import { parseDate } from "@internationalized/date";
import Link from "next/link";
const columns = [
{ name: "No", uid: "no" },
{ name: "Browser", uid: "browserName" },
{ name: "Date & Time ", uid: "createdAt" },
{ name: "IP Visitors", uid: "visitorIp" },
{ name: "URL", uid: "url" },
{ name: "City", uid: "visitorCity" },
{ name: "Region", uid: "visitorRegion" },
// { name: "Country", uid: "visitorCountry" },
];
const provinces = [
{ engName: "ACEH", inName: "ACEH" },
{ engName: "NORTH SUMATRA", inName: "SUMATERA UTARA" },
{ engName: "WEST SUMATRA", inName: "SUMATERA BARAT" },
{ engName: "RIAU", inName: "RIAU" },
{ engName: "JAMBI", inName: "JAMBI" },
{ engName: "SOUTH SUMATRA", inName: "SUMATERA SELATAN" },
{ engName: "BENGKULU", inName: "BENGKULU" },
{ engName: "LAMPUNG", inName: "LAMPUNG" },
{ engName: "BANGKA BELITUNG ISLANDS", inName: "KEPULAUAN BANGKA BELITUNG" },
{ engName: "RIAU ISLANDS", inName: "KEPULAUAN RIAU" },
{ engName: "JAKARTA", inName: "DKI JAKARTA" },
{ engName: "WEST JAVA", inName: "JAWA BARAT" },
{ engName: "CENTRAL JAVA", inName: "JAWA TENGAH" },
{ engName: "YOGYAKARTA", inName: "DI YOGYAKARTA" },
{ engName: "EAST JAVA", inName: "JAWA TIMUR" },
{ engName: "BANTEN", inName: "BANTEN" },
{ engName: "BALI", inName: "BALI" },
{ engName: "WEST NUSA TENGGARA", inName: "NUSA TENGGARA BARAT" },
{ engName: "EAST NUSA TENGGARA", inName: "NUSA TENGGARA TIMUR" },
{ engName: "WEST KALIMANTAN", inName: "KALIMANTAN BARAT" },
{ engName: "CENTRAL KALIMANTAN", inName: "KALIMANTAN TENGAH" },
{ engName: "SOUTH KALIMANTAN", inName: "KALIMANTAN SELATAN" },
{ engName: "EAST KALIMANTAN", inName: "KALIMANTAN TIMUR" },
{ engName: "NORTH KALIMANTAN", inName: "KALIMANTAN UTARA" },
{ engName: "NORTH SULAWESI", inName: "SULAWESI UTARA" },
{ engName: "CENTRAL SULAWESI", inName: "SULAWESI TENGAH" },
{ engName: "SOUTH SULAWESI", inName: "SULAWESI SELATAN" },
{ engName: "SOUTHEAST SULAWESI", inName: "SULAWESI TENGGARA" },
{ engName: "GORONTALO", inName: "GORONTALO" },
{ engName: "WEST SULAWESI", inName: "SULAWESI BARAT" },
{ engName: "MALUKU", inName: "MALUKU" },
{ engName: "NORTH MALUKU", inName: "MALUKU UTARA" },
{ engName: "PAPUA", inName: "PAPUA" },
{ engName: "WEST PAPUA", inName: "PAPUA BARAT" },
{ engName: "SOUTH PAPUA", inName: "PAPUA SELATAN" },
{ engName: "CENTRAL PAPUA", inName: "PAPUA TENGAH" },
{ engName: "HIGHLAND PAPUA", inName: "PAPUA PEGUNUNGAN" },
{ engName: "SOUTHWEST PAPUA", inName: "PAPUA BARAT DAYA" },
];
const findRegion = (name: string): string => {
const find = provinces.find((a) => a.engName === name);
return find ? find.inName : "";
};
export default function DashboardVisitorsTable() {
const [page, setPage] = useState(0);
const [totalPage, setTotalPage] = useState(1);
const [showData, setShowData] = useState("10");
const [tableVisitorData, setTableVisitorData] = useState<any>([]);
const [visitorBrowserDate, setVisitorBrowserDate] = useState({
startDate: parseDate(
convertDateFormatNoTimeV2(
new Date(new Date().setDate(new Date().getDate() - 7))
)
),
endDate: parseDate(convertDateFormatNoTimeV2(new Date())),
});
useEffect(() => {
initFetch();
}, [
page,
visitorBrowserDate.startDate,
visitorBrowserDate.endDate,
showData,
]);
const initFetch = async () => {
const getDate = (data: any) => {
return `${data.year}-${data.month < 10 ? `0${data.month}` : data.month}-${
data.day < 10 ? `0${data.day}` : data.day
}`;
};
const res = await getVisitorLog({
page: page,
limit: showData,
startDate: getDate(visitorBrowserDate.startDate),
endDate: getDate(visitorBrowserDate.endDate),
});
console.log("resssss", res?.data?.data);
getTableNumber(Number(showData), res?.data?.data);
setTotalPage(res?.data?.meta?.totalPage);
};
const getTableNumber = (limit: number, data: any) => {
if (data) {
const startIndex = limit * page;
let iterate = 0;
const newData = data.map((value: any) => {
iterate++;
value.no = startIndex + iterate;
return value;
});
setTableVisitorData(newData);
} else {
setTableVisitorData([]);
}
};
const renderCell = useCallback(
(visitor: any, columnKey: Key) => {
const cellValue = visitor[columnKey as keyof any];
switch (columnKey) {
case "url":
return (
<Link
href={cellValue}
target="_blank"
className="hover:text-primary hover:underline"
>
{cellValue}
</Link>
);
case "browserName":
return (
<div>
{cellValue}{" "}
{cellValue !== null ? "Ver " + visitor.browserVersion : ""}
</div>
);
case "visitorRegion":
return (
<p className="capitalize">
{cellValue
? findRegion(cellValue.toUpperCase()).toLowerCase()
: ""}
</p>
);
case "createdAt":
return <p>{convertDateFormat(visitor.createdAt)}</p>;
default:
return cellValue;
}
},
[tableVisitorData]
);
return (
<div className="flex w-full mt-5 gap-4">
<div className="border p-3 rounded-lg flex flex-col gap-2 w-full">
<p className="font-bold">Visitors Table</p>
<div className="flex flex-row gap-2">
<div className="flex flex-col gap-1 w-full lg:w-[72px]">
<p className="font-semibold text-sm">Show Data</p>
<Select
label=""
variant="bordered"
labelPlacement="outside"
placeholder="Select"
selectedKeys={[showData]}
className="w-full"
classNames={{ trigger: "border-1" }}
onChange={(e) =>
e.target.value === "" ? "" : setShowData(e.target.value)
}
>
<SelectItem key="5">5</SelectItem>
<SelectItem key="10">10</SelectItem>
<SelectItem key="25">25</SelectItem>
<SelectItem key="50">50</SelectItem>
</Select>
</div>
<div className="flex flex-col gap-1">
<p className="font-semibold text-sm">Start Date</p>
<Popover
placement="bottom"
classNames={{ content: ["!bg-transparent", "p-0"] }}
>
<PopoverTrigger>
<Button className="border-1" variant="bordered">
{convertDateFormatNoTime(visitorBrowserDate.startDate)}
</Button>
</PopoverTrigger>
<PopoverContent className="bg-transparent">
<Calendar
value={visitorBrowserDate.startDate}
onChange={(e) =>
setVisitorBrowserDate({
startDate: e,
endDate: visitorBrowserDate.endDate,
})
}
maxValue={visitorBrowserDate.endDate}
/>
</PopoverContent>
</Popover>
</div>
<div className="flex flex-col gap-1">
<p className="font-semibold text-sm">End Date</p>
<Popover
placement="bottom"
classNames={{ content: ["!bg-transparent", "p-0"] }}
>
<PopoverTrigger>
<Button className="border-1" variant="bordered">
{convertDateFormatNoTime(visitorBrowserDate.endDate)}
</Button>
</PopoverTrigger>
<PopoverContent className="bg-transparent">
<Calendar
value={visitorBrowserDate.endDate}
onChange={(e) =>
setVisitorBrowserDate({
startDate: visitorBrowserDate.startDate,
endDate: e,
})
}
minValue={visitorBrowserDate.startDate}
/>
</PopoverContent>
</Popover>
</div>
</div>
<div className="flex flex-col items-start rounded-2xl gap-3">
<Table
aria-label="micro issue table"
className="rounded-3xl"
classNames={{
th: "bg-white dark:bg-black text-black dark:text-white border-b-1 text-md",
base: "bg-white dark:bg-black border",
wrapper:
"min-h-[50px] bg-transpararent text-black dark:text-white ",
}}
>
<TableHeader columns={columns}>
{(column) => (
<TableColumn key={column.uid}>{column.name}</TableColumn>
)}
</TableHeader>
<TableBody
items={tableVisitorData}
emptyContent={"No data to display."}
loadingContent={<Spinner label="Loading..." />}
>
{(item: any) => (
<TableRow key={item.id}>
{(columnKey) => (
<TableCell>{renderCell(item, columnKey)}</TableCell>
)}
</TableRow>
)}
</TableBody>
</Table>
<div className="my-2 w-full flex justify-center">
<Pagination
isCompact
showControls
showShadow
color="primary"
classNames={{
base: "bg-transparent",
wrapper: "bg-transparent",
item: "w-fit px-3",
cursor: "w-fit px-3",
}}
page={page}
total={totalPage}
onChange={(page) => setPage(page)}
/>
</div>
</div>
</div>
</div>
);
}

View File

@ -19,3 +19,12 @@ export async function getActivity() {
const pathUrl = `/activity-logs/statistics`;
return await httpGet(pathUrl, headers);
}
export async function getVisitorLog(data: any) {
const { page, limit, startDate, endDate } = data;
const headers = { "content-type": "application/json" };
const pathUrl = `/activity-logs?purpose=visitor-summary&page=${
page || 0
}&limit=${limit || 10}&startDate=${startDate || ""}&endDate=${endDate || ""}`;
return await httpGet(pathUrl, headers);
}

View File

@ -226,6 +226,20 @@ export async function getStatisticForMaps(startDate: string, endDate: string) {
headers
);
}
export async function getStatisticVisitorsBrowser(
startDate: string,
endDate: string
) {
const headers = {
"content-type": "application/json",
// Authorization: `Bearer ${token}`,
};
return await httpGet(
`/activity-logs/visitors-by-browser-stats?startDate=${startDate}&endDate=${endDate}`,
headers
);
}
export async function getStatisticMonthly(year: string) {
const headers = {
"content-type": "application/json",