diff --git a/src/app/api/product/route.ts b/src/app/api/product/route.ts
new file mode 100644
index 0000000..d061941
--- /dev/null
+++ b/src/app/api/product/route.ts
@@ -0,0 +1,42 @@
+import prisma from "@/lib/prisma";
+import { NextResponse } from "next/server";
+
+export const dynamic = "force-dynamic";
+
+export async function POST(request: Request) {
+ try {
+ // const body = await request.json();
+
+ // const { name, brand } = body;
+ // if (!name || !brand) {
+ // return NextResponse.json(
+ // { error: "Missing required fields" },
+ // { status: 400 },
+ // );
+ // }
+
+ const products = [
+ { name: "ZenBook 14", brand: "ASUS" },
+ { name: "Swift 3", brand: "Acer" },
+ { name: "Surface Laptop 5", brand: "Microsoft" },
+ ];
+
+ const result = await prisma.product.createMany({
+ data: products,
+ });
+
+ return NextResponse.json(
+ {
+ message: "Booking successful",
+ data: result,
+ },
+ { status: 201 },
+ );
+ } catch (error: any) {
+ console.error("Create product Error:", error);
+ return NextResponse.json(
+ { error: "Internal Server Error" },
+ { status: 500 },
+ );
+ }
+}
diff --git a/src/app/api/review/route.ts b/src/app/api/review/route.ts
new file mode 100644
index 0000000..babcd58
--- /dev/null
+++ b/src/app/api/review/route.ts
@@ -0,0 +1,62 @@
+import prisma from "@/lib/prisma";
+import { Sentiment } from "@prisma/client";
+import { NextResponse } from "next/server";
+
+export const dynamic = "force-dynamic";
+
+export async function POST(request: Request) {
+ try {
+ // const body = await request.json();
+
+ // const { name, brand } = body;
+ // if (!name || !brand) {
+ // return NextResponse.json(
+ // { error: "Missing required fields" },
+ // { status: 400 },
+ // );
+ // }
+
+ const reviews = [
+ {
+ productId: 2,
+ content:
+ "Laptop ini sangat ringan dan performanya cepat untuk kerja harian.",
+ keywords: ["ringan", "cepat", "kerja"],
+ sentiment: Sentiment.positive,
+ confidenceScore: 0.92,
+ },
+ {
+ productId: 3,
+ content: "Baterainya awet, tapi harganya cukup mahal.",
+ keywords: ["baterai", "awet", "mahal"],
+ sentiment: Sentiment.neutral,
+ confidenceScore: 0.75,
+ },
+ {
+ productId: 4,
+ content: "Performa kurang stabil dan sering panas.",
+ keywords: ["performa", "panas", "stabil"],
+ sentiment: Sentiment.negative,
+ confidenceScore: 0.88,
+ },
+ ];
+
+ const result = await prisma.review.createMany({
+ data: reviews,
+ });
+
+ return NextResponse.json(
+ {
+ message: "Booking successful",
+ data: result,
+ },
+ { status: 201 },
+ );
+ } catch (error: any) {
+ console.error("Create product Error:", error);
+ return NextResponse.json(
+ { error: "Internal Server Error" },
+ { status: 500 },
+ );
+ }
+}
diff --git a/src/app/api/review/sentiment-stats/route.ts b/src/app/api/review/sentiment-stats/route.ts
new file mode 100644
index 0000000..e9d51a1
--- /dev/null
+++ b/src/app/api/review/sentiment-stats/route.ts
@@ -0,0 +1,29 @@
+// app/api/reviews/sentiment-stats/route.ts
+import prisma from "@/lib/prisma";
+import { NextResponse } from "next/server";
+
+export async function GET() {
+ try {
+ const grouped = await prisma.review.groupBy({
+ by: ["sentiment"],
+ _count: { _all: true },
+ });
+
+ const result = {
+ positive: 0,
+ negative: 0,
+ neutral: 0,
+ };
+
+ grouped.forEach((item) => {
+ if (item.sentiment === "positive") result.positive = item._count._all;
+ if (item.sentiment === "negative") result.negative = item._count._all;
+ if (item.sentiment === "neutral") result.neutral = item._count._all;
+ });
+
+ return NextResponse.json(result);
+ } catch (error) {
+ console.log(error);
+ return [];
+ }
+}
diff --git a/src/components/dashboards/DashboardClient.tsx b/src/components/dashboards/DashboardClient.tsx
index 76db7fd..5128d4d 100644
--- a/src/components/dashboards/DashboardClient.tsx
+++ b/src/components/dashboards/DashboardClient.tsx
@@ -20,7 +20,7 @@ import { SentimentAnalyzer } from "./SentimentAnalyzer";
import { BrandFilter } from "./BrandFilter";
import { ReviewTable } from "./ReviewTable";
import { SentimentChart, TrendChart, WordCloud } from "@/src/utils/dImports";
-import { useDashboard } from "@/src/hooks/useDashboard";
+import { useDashboards } from "@/src/hooks/useDashboard";
export default function DashboardClient() {
const {
@@ -30,10 +30,11 @@ export default function DashboardClient() {
neutralCount,
filteredReviews,
selectedBrand,
- setSelectedBrand,
loading,
modelData,
- } = useDashboard();
+ setSelectedBrand,
+ percentage,
+ } = useDashboards();
return (
@@ -71,26 +72,29 @@ export default function DashboardClient() {
trend={{ value: 12.5, isPositive: true }}
delay={0}
/>
+
+
+
diff --git a/src/components/dashboards/ModelInfo.tsx b/src/components/dashboards/ModelInfo.tsx
index f1f5f48..f9b7bd7 100644
--- a/src/components/dashboards/ModelInfo.tsx
+++ b/src/components/dashboards/ModelInfo.tsx
@@ -36,7 +36,7 @@ export function ModelInfo({ data }: { data: ModelDB[] }) {
-
+
{data.map((model, index) => (
{title}
+
{displayValue.toLocaleString()}
@@ -35,6 +36,7 @@ export function StatCard({
)}
+
{trend && (
)}
+
diff --git a/src/hooks/useDashboard.ts b/src/hooks/useDashboard.ts
index 9ef66eb..df8ab6c 100644
--- a/src/hooks/useDashboard.ts
+++ b/src/hooks/useDashboard.ts
@@ -1,32 +1,44 @@
"use client";
-import { useEffect, useState } from "react";
-import { ModelDB } from "../types";
-import { reviewData, sentimentDistribution } from "../app/dashboard/lib/data";
+
+import { useState, useEffect, useMemo } from "react";
+import { ModelDB, Review, StatCounts } from "@/src/types";
import { getClassificationReport } from "../app/dashboard/lib/actions";
-export const useDashboard = () => {
+export const useDashboards = () => {
const [selectedBrand, setSelectedBrand] = useState
(null);
const [loading, setLoading] = useState(true);
const [modelData, setModelData] = useState([]);
-
- const totalReviews = sentimentDistribution.reduce(
- (sum, s) => sum + s.value,
- 0,
- );
-
- const positiveCount =
- sentimentDistribution.find((s) => s.name === "Positif")?.value || 0;
- const negativeCount =
- sentimentDistribution.find((s) => s.name === "Negatif")?.value || 0;
- const neutralCount =
- sentimentDistribution.find((s) => s.name === "Netral")?.value || 0;
-
- const filteredReviews = selectedBrand
- ? reviewData.filter((r) => r.brand === selectedBrand)
- : reviewData;
+ const [reviews, setReviews] = useState([]);
+ const [stats, setStats] = useState({
+ totalReviews: 0,
+ positive: 0,
+ negative: 0,
+ neutral: 0,
+ });
useEffect(() => {
- async function fetchData() {
+ async function fetchStats() {
+ setLoading(true);
+ const res = await fetch("/api/review/sentiment-stats");
+ const data = await res.json();
+
+ const total = data.positive + data.negative + data.neutral;
+
+ setStats({
+ totalReviews: total ?? 0,
+ positive: data.positive ?? 0,
+ negative: data.negative ?? 0,
+ neutral: data.neutral ?? 0,
+ });
+
+ setLoading(false);
+ }
+
+ fetchStats();
+ }, []);
+
+ useEffect(() => {
+ async function fetchModelData() {
try {
const data = await getClassificationReport();
setModelData(data);
@@ -36,18 +48,29 @@ export const useDashboard = () => {
setLoading(false);
}
}
- fetchData();
+
+ fetchModelData();
}, []);
+ const filteredReviews = useMemo(() => {
+ return selectedBrand
+ ? reviews.filter((r) => r.brand === selectedBrand)
+ : reviews;
+ }, [reviews, selectedBrand]);
+
+ const percentage = (value: number, total: number) =>
+ total > 0 ? ((value / total) * 100).toFixed(1) : "0.0";
+
return {
- totalReviews,
- positiveCount,
- negativeCount,
- neutralCount,
+ totalReviews: stats.totalReviews,
+ positiveCount: stats.positive,
+ negativeCount: stats.negative,
+ neutralCount: stats.neutral,
filteredReviews,
selectedBrand,
loading,
modelData,
setSelectedBrand,
+ percentage,
};
};
diff --git a/src/hooks/useHeader.ts b/src/hooks/useHeader.ts
index a9e7383..ffc4aba 100644
--- a/src/hooks/useHeader.ts
+++ b/src/hooks/useHeader.ts
@@ -1,15 +1,20 @@
import { useSession } from "next-auth/react";
-import { useState } from "react";
+import { useEffect, useState } from "react";
export const useHeader = () => {
const [isRefreshing, setIsRefreshing] = useState(false);
const [open, setOpen] = useState(false);
const session = useSession();
+ const [mounted, setMounted] = useState(false);
const handleRefresh = () => {
setIsRefreshing(true);
setTimeout(() => setIsRefreshing(false), 1500);
};
- return { open, setOpen, session };
+ useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ return { open, setOpen, session, isRefreshing, handleRefresh, mounted };
};
diff --git a/src/hooks/useStatCard.ts b/src/hooks/useStatCard.ts
index 7ab6b2d..f5d8f15 100644
--- a/src/hooks/useStatCard.ts
+++ b/src/hooks/useStatCard.ts
@@ -1,50 +1,32 @@
import { useEffect, useState } from "react";
-import { StatCardProps } from "../types";
+import { UseStatCardProps } from "../types";
-export const useStatCard = ({
- title,
- value,
- suffix = "",
- icon: Icon,
- trend,
- variant = "default",
- delay = 0,
-}: StatCardProps) => {
- const [displayValue, setDisplayValue] = useState(0);
+export function useStatCard({ value, delay = 0 }: UseStatCardProps) {
const [isVisible, setIsVisible] = useState(false);
+ const [displayValue, setDisplayValue] = useState(0);
useEffect(() => {
- const timer = setTimeout(() => {
+ const timeout = setTimeout(() => {
setIsVisible(true);
+
+ let start = 0;
+ const duration = 800;
+ const stepTime = 16;
+ const increment = value / (duration / stepTime);
+
+ const counter = setInterval(() => {
+ start += increment;
+ if (start >= value) {
+ setDisplayValue(value);
+ clearInterval(counter);
+ } else {
+ setDisplayValue(Math.floor(start));
+ }
+ }, stepTime);
}, delay);
- return () => clearTimeout(timer);
- }, [delay]);
- useEffect(() => {
- if (!isVisible) return;
-
- const duration = 1200;
- const steps = 40;
- const stepValue = value / steps;
- let current = 0;
- let step = 0;
-
- const timer = setInterval(() => {
- step++;
- const progress = step / steps;
- const eased = 1 - Math.pow(1 - progress, 3);
- current = value * eased;
-
- if (step >= steps) {
- setDisplayValue(value);
- clearInterval(timer);
- } else {
- setDisplayValue(Math.floor(current));
- }
- }, duration / steps);
-
- return () => clearInterval(timer);
- }, [value, isVisible]);
+ return () => clearTimeout(timeout);
+ }, [value, delay]);
return { isVisible, displayValue };
-};
+}
diff --git a/src/types/index.ts b/src/types/index.ts
index 4dd9fc1..2b4b3d3 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -1,4 +1,4 @@
-import { UserGender } from "@prisma/client";
+import { Sentiment, UserGender } from "@prisma/client";
import { LucideIcon } from "lucide-react";
export interface ModelDB {
@@ -28,12 +28,12 @@ export interface BrandFilterProps {
}
export interface Review {
- id: string;
+ id: number;
product: string;
brand: string;
review: string;
- rating: number;
- sentiment: "positif" | "negatif" | "netral";
+ rating?: number | null;
+ sentiment: Sentiment;
date: string;
confidence: number;
}
@@ -110,3 +110,15 @@ export interface WordCloudItemProps {
maxValue: number;
minValue: number;
}
+
+export interface StatCounts {
+ totalReviews: number;
+ positive: number;
+ negative: number;
+ neutral: number;
+}
+
+export interface UseStatCardProps {
+ value: number;
+ delay?: number;
+}