feat: add filter review by brand filter button
This commit is contained in:
parent
0fe22e8283
commit
644e299e14
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue