feat: add filter review by brand filter button

This commit is contained in:
Mahen 2026-02-16 08:59:51 +07:00
parent 0fe22e8283
commit 644e299e14
6 changed files with 83 additions and 47 deletions

View File

@ -1,14 +1,11 @@
"use client"; "use client";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { BrandFilterProps } from "@/src/types";
import { useBrandFilter } from "@/src/hooks/useBrandFilter"; import { useBrandFilter } from "@/src/hooks/useBrandFilter";
export function BrandFilter({ export function BrandFilter() {
selectedBrand, const { brands, isLoading, totalCount, selectedBrand, handleSelect } =
onSelect, useBrandFilter();
}: Omit<BrandFilterProps, "brands">) {
const { brands, isLoading, totalCount } = useBrandFilter();
if (isLoading) { if (isLoading) {
return ( return (
@ -21,7 +18,7 @@ export function BrandFilter({
return ( return (
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<button <button
onClick={() => onSelect(null)} onClick={() => handleSelect(null)}
className={cn( className={cn(
"rounded-lg border px-4 py-2 text-sm font-medium transition-all", "rounded-lg border px-4 py-2 text-sm font-medium transition-all",
selectedBrand === null selectedBrand === null
@ -32,20 +29,25 @@ export function BrandFilter({
Semua ({totalCount.toLocaleString()}) Semua ({totalCount.toLocaleString()})
</button> </button>
{brands.map((brand) => ( {brands.map((brand) => {
<button const isActive =
key={brand.name} selectedBrand?.toLowerCase() === brand.name.toLowerCase();
onClick={() => onSelect(brand.name)}
className={cn( return (
"rounded-lg border px-4 py-2 text-sm font-medium transition-all", <button
selectedBrand === brand.name key={brand.name}
? "border-primary bg-primary text-primary-foreground" onClick={() => handleSelect(brand.name)}
: "border-border bg-card text-muted-foreground hover:border-primary/50 hover:text-foreground", className={cn(
)} "rounded-lg border px-4 py-2 text-sm font-medium transition-all",
> isActive
{brand.name} ({brand.count.toLocaleString()}) ? "border-primary bg-primary text-primary-foreground"
</button> : "border-border bg-card text-muted-foreground hover:border-primary/50 hover:text-foreground",
))} )}
>
{brand.name} ({brand.count.toLocaleString()})
</button>
);
})}
</div> </div>
); );
} }

View File

@ -3,11 +3,7 @@
import { Header } from "./Header"; import { Header } from "./Header";
import { Frown, Meh, MessageSquareText, Smile, TrendingUp } from "lucide-react"; import { Frown, Meh, MessageSquareText, Smile, TrendingUp } from "lucide-react";
import { StatCard } from "./StatCard"; import { StatCard } from "./StatCard";
import { import { sentimentDistribution, trendData } from "@/src/app/dashboard/lib/data";
brandData,
sentimentDistribution,
trendData,
} from "@/src/app/dashboard/lib/data";
import { ModelInfoSkeleton } from "../skeletons/ModelInfoSkeleton"; import { ModelInfoSkeleton } from "../skeletons/ModelInfoSkeleton";
import { ModelInfo } from "./ModelInfo"; import { ModelInfo } from "./ModelInfo";
import { BrandFilter } from "./BrandFilter"; import { BrandFilter } from "./BrandFilter";
@ -24,10 +20,8 @@ export default function DashboardClient() {
positiveCount, positiveCount,
negativeCount, negativeCount,
neutralCount, neutralCount,
selectedBrand,
loading, loading,
modelData, modelData,
setSelectedBrand,
percentage, percentage,
} = useDashboards(); } = useDashboards();
@ -142,10 +136,7 @@ export default function DashboardClient() {
Hasil klasifikasi sentimen ulasan produk laptop Hasil klasifikasi sentimen ulasan produk laptop
</p> </p>
</div> </div>
<BrandFilter <BrandFilter />
selectedBrand={selectedBrand}
onSelect={setSelectedBrand}
/>
</div> </div>
<ReviewTable /> <ReviewTable />
</div> </div>

View File

@ -26,9 +26,15 @@ import {
PaginationNext, PaginationNext,
PaginationPrevious, PaginationPrevious,
} from "../ui/pagination"; } from "../ui/pagination";
import { useSearchParams } from "next/navigation";
export function ReviewTable() { export function ReviewTable() {
const { currentData, isLoading, pagination } = useReviewTable(10); const searchParams = useSearchParams();
const selectedBrand = searchParams.get("brand");
const { currentData, isLoading, pagination } = useReviewTable(
10,
selectedBrand,
);
const { currentPage, totalPages, nextPage, prevPage, goToPage } = pagination; const { currentPage, totalPages, nextPage, prevPage, goToPage } = pagination;
if (isLoading) { if (isLoading) {
@ -41,7 +47,7 @@ export function ReviewTable() {
} }
return ( return (
<div className="rounded-xl border bg-card shadow-sm"> <div className="rounded-xl border bg-card">
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow className="hover:bg-transparent"> <TableRow className="hover:bg-transparent">

View File

@ -1,9 +1,11 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { getTotalBrandAnalysis } from "../app/dashboard/lib/actions"; import { getTotalBrandAnalysis } from "../app/dashboard/lib/actions";
import { useSelectSearch } from "./useSelectSearch";
export const useBrandFilter = () => { export const useBrandFilter = () => {
const [brands, setBrands] = useState<{ name: string; count: number }[]>([]); const [brands, setBrands] = useState<{ name: string; count: number }[]>([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const { selectedBrand, handleSelect } = useSelectSearch();
useEffect(() => { useEffect(() => {
const fetchBrands = async () => { const fetchBrands = async () => {
@ -24,5 +26,5 @@ export const useBrandFilter = () => {
const totalCount = brands.reduce((sum, b) => sum + (b?.count || 0), 0); const totalCount = brands.reduce((sum, b) => sum + (b?.count || 0), 0);
return { brands, isLoading, totalCount }; return { brands, isLoading, totalCount, selectedBrand, handleSelect };
}; };

View File

@ -1,7 +1,10 @@
import { useEffect, useState, useMemo } from "react"; import { useEffect, useState, useMemo } from "react";
import { ApiResponse, ReviewItem } from "../types"; import { ApiResponse, ReviewItem } from "../types";
export const useReviewTable = (itemsPerPage: number = 10) => { export const useReviewTable = (
itemsPerPage: number = 10,
selectedBrand: string | null = null,
) => {
const [data, setData] = useState<ReviewItem[]>([]); const [data, setData] = useState<ReviewItem[]>([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
@ -27,34 +30,43 @@ export const useReviewTable = (itemsPerPage: number = 10) => {
getReviewData(); getReviewData();
}, []); }, []);
useEffect(() => {
setCurrentPage(1);
}, [selectedBrand]);
const { currentData, totalPages } = useMemo(() => { const { currentData, totalPages } = useMemo(() => {
const total = Math.ceil(data.length / itemsPerPage); const filteredData = selectedBrand
const start = (currentPage - 1) * itemsPerPage; ? data.filter(
(review) =>
review.product?.brand?.toLowerCase() ===
selectedBrand.toLowerCase(),
)
: data;
const total = Math.ceil(filteredData.length / itemsPerPage) || 1;
const safePage = currentPage > total ? total : currentPage;
const start = (safePage - 1) * itemsPerPage;
const end = start + itemsPerPage; const end = start + itemsPerPage;
const slicedData = data.slice(start, end);
return { return {
currentData: slicedData, currentData: filteredData.slice(start, end),
totalPages: total, totalPages: total,
}; };
}, [data, currentPage, itemsPerPage]); }, [data, currentPage, itemsPerPage, selectedBrand]);
const nextPage = () => { const nextPage = () => {
if (currentPage < totalPages) setCurrentPage((prev) => prev + 1); if (currentPage < totalPages) setCurrentPage((prev) => prev + 1);
}; };
const prevPage = () => { const prevPage = () => {
if (currentPage > 1) setCurrentPage((prev) => prev - 1); if (currentPage > 1) setCurrentPage((prev) => prev - 1);
}; };
const goToPage = (pageNumber: number) => { const goToPage = (pageNumber: number) => {
if (pageNumber >= 1 && pageNumber <= totalPages) { if (pageNumber >= 1 && pageNumber <= totalPages) setCurrentPage(pageNumber);
setCurrentPage(pageNumber);
}
}; };
return { return {
data,
currentData, currentData,
isLoading, isLoading,
pagination: { pagination: {

View File

@ -0,0 +1,23 @@
import { usePathname, useRouter, useSearchParams } from "next/navigation";
export const useSelectSearch = () => {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const selectedBrand = searchParams.get("brand");
const handleSelect = (brandName: string | null) => {
const params = new URLSearchParams(searchParams.toString());
if (brandName) {
params.set("brand", brandName);
} else {
params.delete("brand");
}
router.push(`${pathname}?${params.toString()}`, { scroll: false });
};
return { selectedBrand, handleSelect };
};