From 8a4a3b1d6742cadfeebe8ee866e5455c2a75daf2 Mon Sep 17 00:00:00 2001 From: Rama Priyanto Date: Wed, 9 Jul 2025 12:02:51 +0700 Subject: [PATCH] feat:visitors table, fix:piee chart api, chart total views analytic --- app/layout.tsx | 2 +- .../main/dashboard/chart/column-chart.tsx | 33 +- .../dashboard/chart/pie-chart-browser.tsx | 66 ++-- .../main/dashboard/dashboard-container.tsx | 6 +- components/table/dashboard-visitors-table.tsx | 308 ++++++++++++++++++ services/activity-log.ts | 9 + services/article.ts | 14 + 7 files changed, 410 insertions(+), 28 deletions(-) create mode 100644 components/table/dashboard-visitors-table.tsx diff --git a/app/layout.tsx b/app/layout.tsx index ef41878..6e3a51c 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -57,7 +57,7 @@ export default function RootLayout({ children }: { children: ReactNode }) {
{children}
- + {/* */} ); diff --git a/components/main/dashboard/chart/column-chart.tsx b/components/main/dashboard/chart/column-chart.tsx index 1ffbfcc..9e19dd0 100644 --- a/components/main/dashboard/chart/column-chart.tsx +++ b/components/main/dashboard/chart/column-chart.tsx @@ -96,11 +96,21 @@ const ApexChartColumn = (props: { const [seriesShare, setSeriesShare] = useState([]); const [years, setYear] = useState(""); const [datas, setDatas] = useState([]); + 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 ( -
+
+
+
Chart Total
+
+ {totalChart.views} {totalChart.views > 1 ? "Views" : "View"}{" "} + {totalChart.comment} {totalChart.comment > 1 ? "Comments" : "Comment"}{" "} + {totalChart.share} {totalChart.share > 1 ? "Shares" : "Share"}{" "} +
+
{" "}
); }; diff --git a/components/main/dashboard/chart/pie-chart-browser.tsx b/components/main/dashboard/chart/pie-chart-browser.tsx index edb7b1a..2d41765 100644 --- a/components/main/dashboard/chart/pie-chart-browser.tsx +++ b/components/main/dashboard/chart/pie-chart-browser.tsx @@ -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([]); const [labels, setLabels] = useState([]); - const [data, setData] = useState([]); + const [data, setData] = useState([]); const [total, setTotal] = useState(0); const [selectedLabel, setSelectedLabel] = useState(""); - 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 ( -
-
+
+

Browser Statistics

-
+

Browser Statistics from

- {convertDateFormatNoTime(topContentDate.startDate)} + {convertDateFormatNoTime(visitorBrowserDate.startDate)} - setTopContentDate({ + setVisitorBrowserDate({ startDate: e, - endDate: topContentDate.endDate, + endDate: visitorBrowserDate.endDate, }) } - maxValue={topContentDate.endDate} + maxValue={visitorBrowserDate.endDate} /> @@ -133,19 +149,19 @@ export default function PieChartLoginBrowser() { > - {convertDateFormatNoTime(topContentDate.endDate)} + {convertDateFormatNoTime(visitorBrowserDate.endDate)} - setTopContentDate({ - startDate: topContentDate.startDate, + setVisitorBrowserDate({ + startDate: visitorBrowserDate.startDate, endDate: e, }) } - minValue={topContentDate.startDate} + minValue={visitorBrowserDate.startDate} /> @@ -158,20 +174,20 @@ export default function PieChartLoginBrowser() {
{data && - data?.map((list: any, index: number) => ( + data?.map((list, index) => (
-

{list.name}

-

{list.value}

-

{getPersentage(list.value)}

+

{list.browserName}

+

{list.totalVisitor}

+

{getPersentage(list.totalVisitor)}

))}
diff --git a/components/main/dashboard/dashboard-container.tsx b/components/main/dashboard/dashboard-container.tsx index 008508c..3d909e2 100644 --- a/components/main/dashboard/dashboard-container.tsx +++ b/components/main/dashboard/dashboard-container.tsx @@ -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() {
Chart Total
-
{chartVisitorTotal}
+
+ {chartVisitorTotal} Visitors +
+
)}
diff --git a/components/table/dashboard-visitors-table.tsx b/components/table/dashboard-visitors-table.tsx new file mode 100644 index 0000000..dd2f07a --- /dev/null +++ b/components/table/dashboard-visitors-table.tsx @@ -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([]); + 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 ( + + {cellValue} + + ); + case "browserName": + return ( +
+ {cellValue}{" "} + {cellValue !== null ? "Ver " + visitor.browserVersion : ""} +
+ ); + case "visitorRegion": + return ( +

+ {cellValue + ? findRegion(cellValue.toUpperCase()).toLowerCase() + : ""} +

+ ); + case "createdAt": + return

{convertDateFormat(visitor.createdAt)}

; + + default: + return cellValue; + } + }, + [tableVisitorData] + ); + return ( +
+
+

Visitors Table

+
+
+

Show Data

+ +
+
+

Start Date

+ + + + + + + + setVisitorBrowserDate({ + startDate: e, + endDate: visitorBrowserDate.endDate, + }) + } + maxValue={visitorBrowserDate.endDate} + /> + + +
+
+

End Date

+ + + + + + + + setVisitorBrowserDate({ + startDate: visitorBrowserDate.startDate, + endDate: e, + }) + } + minValue={visitorBrowserDate.startDate} + /> + + +
+
+
+ + + {(column) => ( + {column.name} + )} + + } + > + {(item: any) => ( + + {(columnKey) => ( + {renderCell(item, columnKey)} + )} + + )} + +
+
+ setPage(page)} + /> +
+
+
+
+ ); +} diff --git a/services/activity-log.ts b/services/activity-log.ts index 2056d6f..3f685c8 100644 --- a/services/activity-log.ts +++ b/services/activity-log.ts @@ -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); +} diff --git a/services/article.ts b/services/article.ts index bbabf95..23e6f57 100644 --- a/services/article.ts +++ b/services/article.ts @@ -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",