feat: add get brand review total count

This commit is contained in:
Mahen 2026-02-15 10:31:29 +07:00
parent 5c4749e853
commit cba88c53e1
9 changed files with 175 additions and 47 deletions

View File

@ -1,41 +1,40 @@
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
export async function POST(_request: Request) { // export async function POST(_request: Request) {
try { // try {
const products = [ // const products = [
{ name: "ZenBook 14", brand: "ASUS" }, // { name: "ZenBook 14", brand: "ASUS" },
{ name: "Swift 3", brand: "Acer" }, // { name: "Swift 3", brand: "Acer" },
{ name: "Surface Laptop 5", brand: "Microsoft" }, // { name: "Surface Laptop 5", brand: "Microsoft" },
]; // ];
const result = await prisma.product.createMany({ // const result = await prisma.product.createMany({
data: products, // data: products,
}); // });
return NextResponse.json( // return NextResponse.json(
{ // {
message: "Booking successful", // message: "Booking successful",
data: result, // data: result,
}, // },
{ status: 201 }, // { status: 201 },
); // );
} catch (error: unknown) { // } catch (error: unknown) {
console.error("Create product error:", error); // console.error("Create product error:", error);
if (error instanceof Prisma.PrismaClientKnownRequestError) { // if (error instanceof Prisma.PrismaClientKnownRequestError) {
return NextResponse.json({ error: error.message }, { status: 400 }); // return NextResponse.json({ error: error.message }, { status: 400 });
} // }
return NextResponse.json( // return NextResponse.json(
{ error: "Internal Server Error" }, // { error: "Internal Server Error" },
{ status: 500 }, // { status: 500 },
); // );
} // }
} // }
export async function GET() { export async function GET() {
try { try {

View File

@ -13,7 +13,7 @@ export async function POST(_request: Request) {
content: content:
"Laptop ini sangat ringan dan performanya cepat untuk kerja harian.", "Laptop ini sangat ringan dan performanya cepat untuk kerja harian.",
keywords: ["ringan", "cepat", "kerja"], keywords: ["ringan", "cepat", "kerja"],
sentiment: Sentiment.positive, sentiment: Sentiment.POSITIVE,
confidenceScore: 0.92, confidenceScore: 0.92,
}, },
{ {
@ -21,7 +21,7 @@ export async function POST(_request: Request) {
modelId: 1, modelId: 1,
content: "Baterainya awet, tapi harganya cukup mahal.", content: "Baterainya awet, tapi harganya cukup mahal.",
keywords: ["baterai", "awet", "mahal"], keywords: ["baterai", "awet", "mahal"],
sentiment: Sentiment.neutral, sentiment: Sentiment.NEUTRAL,
confidenceScore: 0.75, confidenceScore: 0.75,
}, },
{ {
@ -29,7 +29,7 @@ export async function POST(_request: Request) {
modelId: 1, modelId: 1,
content: "Performa kurang stabil dan sering panas.", content: "Performa kurang stabil dan sering panas.",
keywords: ["performa", "panas", "stabil"], keywords: ["performa", "panas", "stabil"],
sentiment: Sentiment.negative, sentiment: Sentiment.NEGATIVE,
confidenceScore: 0.88, confidenceScore: 0.88,
}, },
]; ];

View File

@ -1,6 +1,8 @@
"use server"; "use server";
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { getServerSession } from "next-auth";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { authOptions } from "../../api/auth/[...nextauth]/route";
export const getClassificationReport = async () => { export const getClassificationReport = async () => {
try { try {
@ -28,3 +30,80 @@ export const getClassificationReport = async () => {
throw error; throw error;
} }
}; };
export const getTotalBrandAnalysis = async () => {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.email) {
console.log("User belum login");
return null;
}
const userAnalyses = await prisma.analysis.findMany({
where: {
user: {
email: session.user.email,
},
},
include: {
product: {
select: {
id: true,
brand: true,
_count: {
select: {
reviews: true,
},
},
},
},
},
});
const countedProductIds = new Set<number>();
const brandCounts = userAnalyses.reduce(
(acc: Record<string, number>, analysis) => {
const productId = analysis.product?.id;
const rawBrand = analysis.product?.brand || "Unknown";
const reviewCount = analysis.product?._count?.reviews || 0;
if (productId && countedProductIds.has(productId)) {
return acc;
}
if (productId) {
countedProductIds.add(productId);
}
const formattedBrand = rawBrand
.trim()
.toLowerCase()
.replace(/\b\w/g, (char) => char.toUpperCase());
if (!acc[formattedBrand]) {
acc[formattedBrand] = 0;
}
acc[formattedBrand] += reviewCount;
return acc;
},
{},
);
const formattedBrands = Object.entries(brandCounts).map(
([name, count]) => ({
name,
count,
}),
);
formattedBrands.sort((a, b) => b.count - a.count);
return formattedBrands;
} catch (error) {
console.error("Gagal mengambil data review:", error);
return [];
}
};

View File

@ -1,11 +1,23 @@
"use client";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { BrandFilterProps } from "@/src/types"; import { BrandFilterProps } from "@/src/types";
import { useBrandFilter } from "@/src/hooks/useBrandFilter";
export function BrandFilter({ export function BrandFilter({
brands,
selectedBrand, selectedBrand,
onSelect, onSelect,
}: BrandFilterProps) { }: Omit<BrandFilterProps, "brands">) {
const { brands, isLoading, totalCount } = useBrandFilter();
if (isLoading) {
return (
<div className="text-sm text-muted-foreground animate-pulse">
Memuat brand...
</div>
);
}
return ( return (
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<button <button
@ -17,8 +29,9 @@ export function BrandFilter({
: "border-border bg-card text-muted-foreground hover:border-primary/50 hover:text-foreground", : "border-border bg-card text-muted-foreground hover:border-primary/50 hover:text-foreground",
)} )}
> >
Semua ({brands.reduce((sum, b) => sum + b.count, 0).toLocaleString()}) Semua ({totalCount.toLocaleString()})
</button> </button>
{brands.map((brand) => ( {brands.map((brand) => (
<button <button
key={brand.name} key={brand.name}

View File

@ -143,7 +143,7 @@ export default function DashboardClient() {
</p> </p>
</div> </div>
<BrandFilter <BrandFilter
brands={brandData} // brands={brandData}
selectedBrand={selectedBrand} selectedBrand={selectedBrand}
onSelect={setSelectedBrand} onSelect={setSelectedBrand}
/> />

View File

@ -4,15 +4,15 @@ import { cn } from "@/lib/utils";
const getSentimentBadge = (sentiment: Review["sentiment"]) => { const getSentimentBadge = (sentiment: Review["sentiment"]) => {
const styles: Record<Review["sentiment"], string> = { const styles: Record<Review["sentiment"], string> = {
positive: "sentiment-positive", POSITIVE: "sentiment-positive",
negative: "sentiment-negative", NEGATIVE: "sentiment-negative",
neutral: "sentiment-neutral", NEUTRAL: "sentiment-neutral",
}; };
const labels: Record<Review["sentiment"], string> = { const labels: Record<Review["sentiment"], string> = {
positive: "Positif", POSITIVE: "Positif",
negative: "Negatif", NEGATIVE: "Negatif",
neutral: "Netral", NEUTRAL: "Netral",
}; };
return ( return (

View File

@ -1,7 +1,10 @@
import { useState } from "react"; import { useState } from "react";
import { useSession } from "next-auth/react";
import { AnalysisResults } from "../types"; import { AnalysisResults } from "../types";
export const useAnalyseText = () => { export const useAnalyseText = () => {
const { data: session } = useSession();
const [url1, setUrl1] = useState(""); const [url1, setUrl1] = useState("");
const [url2, setUrl2] = useState(""); const [url2, setUrl2] = useState("");
const [url3, setUrl3] = useState(""); const [url3, setUrl3] = useState("");
@ -12,6 +15,13 @@ export const useAnalyseText = () => {
const [showField, setShowField] = useState(false); const [showField, setShowField] = useState(false);
const handleAnalyze = async () => { const handleAnalyze = async () => {
if (!session?.user?.email) {
alert(
"Anda harus login terlebih dahulu untuk menyimpan riwayat analisis.",
);
return;
}
setLoading(true); setLoading(true);
setResult(null); setResult(null);
@ -53,6 +63,7 @@ export const useAnalyseText = () => {
})); }));
const payload = { const payload = {
user_email: session.user.email,
profession: profession, profession: profession,
candidates: candidates, candidates: candidates,
}; };

View File

@ -0,0 +1,28 @@
import { useEffect, useState } from "react";
import { getTotalBrandAnalysis } from "../app/dashboard/lib/actions";
export const useBrandFilter = () => {
const [brands, setBrands] = useState<{ name: string; count: number }[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const fetchBrands = async () => {
try {
const data = await getTotalBrandAnalysis();
if (data) {
setBrands(data);
}
} catch (error) {
console.error("Gagal memuat filter brand", error);
} finally {
setIsLoading(false);
}
};
fetchBrands();
}, []);
const totalCount = brands.reduce((sum, b) => sum + (b?.count || 0), 0);
return { brands, isLoading, totalCount };
};

View File

@ -1,5 +1,5 @@
import { LucideIcon } from "lucide-react"; import { LucideIcon } from "lucide-react";
import Brand from "@prisma/client"; import Brand, { Sentiment } from "@prisma/client";
export interface ModelDB { export interface ModelDB {
modelName: string; modelName: string;
@ -27,7 +27,7 @@ interface Brand {
} }
export interface BrandFilterProps { export interface BrandFilterProps {
brands: Brand[]; // brands: Brand[];
selectedBrand: string | null; selectedBrand: string | null;
onSelect: (brand: string | null) => void; onSelect: (brand: string | null) => void;
} }
@ -50,7 +50,7 @@ export interface ReviewTableProps {
} }
export interface AnalysisResult { export interface AnalysisResult {
sentiment: "positif" | "negatif" | "netral"; sentiment: Sentiment;
confidence: number; confidence: number;
keywords: string[]; keywords: string[];
} }
@ -142,8 +142,6 @@ export interface ApiResponse {
data: ReviewItem[]; data: ReviewItem[];
} }
export type Sentiment = "positive" | "negative" | "neutral";
export type WordItem = { export type WordItem = {
text: string; text: string;
value: number; value: number;