From 9762069aa4bc676696dc972cecb994b4cec9e793 Mon Sep 17 00:00:00 2001 From: Mahen Date: Fri, 13 Feb 2026 10:20:15 +0700 Subject: [PATCH] feat: add optional product review URL --- src/app/analyze/page.tsx | 338 +----------------- src/components/dashboards/AnalysisClient.tsx | 165 +++++++++ src/components/dashboards/DashboardClient.tsx | 5 +- src/components/dashboards/Header.tsx | 2 +- src/components/dashboards/ResultSection.tsx | 212 +++++++++++ src/hooks/useAnalyzeText.ts | 23 +- src/types/index.ts | 4 + 7 files changed, 408 insertions(+), 341 deletions(-) create mode 100644 src/components/dashboards/AnalysisClient.tsx create mode 100644 src/components/dashboards/ResultSection.tsx diff --git a/src/app/analyze/page.tsx b/src/app/analyze/page.tsx index 6fbb8f0..4a99d29 100644 --- a/src/app/analyze/page.tsx +++ b/src/app/analyze/page.tsx @@ -1,339 +1,5 @@ -"use client"; - -import { Button } from "@/src/components/ui/button"; -import { Input } from "@/src/components/ui/input"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/src/components/ui/select"; -import { useAnalyseText } from "@/src/hooks/useAnalyzeText"; -import { CheckCircle2, Sparkles, Star, Trophy } from "lucide-react"; -import { motion } from "framer-motion"; +import AnalysisClient from "@/src/components/dashboards/AnalysisClient"; export default function AnalysisPage() { - const { - url1, - url2, - profession, - loading, - result, - disabled, - handleAnalyze, - setProfession, - setUrl1, - setUrl2, - setDisabled, - } = useAnalyseText(); - - const getSentimentTone = (score: number) => { - if (score >= 80) return "strong"; - if (score >= 60) return "light"; - return "neutral"; - }; - - return ( -
-
-
- -

Analisis Sentimen Real-time

-
-
-
- - -
-
- - setUrl1(e.target.value)} - className="border p-2 rounded-md focus:ring-2 focus:ring-green-500" - required - /> -
-
-
-
- - setUrl2(e.target.value)} - className="border p-2 rounded-md focus:ring-2 focus:ring-green-500" - required - /> -
-
- -
-
- -
- - {result && ( - - {/* ================= HEADER WINNER ================= */} - -
- -
-
- - - Rekomendasi Terbaik - -
- -

- {result.winning_product} -

- -

- Pilihan paling tepat untuk{" "} - - {result.profession_target} - -

-
-
- - {/* ================= CARDS GRID ================= */} -
- {result.details.map((item, index) => { - const isWinner = item.name === result.winning_product; - - const getSentimentTone = (score: number) => { - if (score >= 80) return "strong"; - if (score >= 60) return "light"; - return "neutral"; - }; - - const sentimentTone = getSentimentTone( - item.general_sentiment_score, - ); - - return ( - - {/* WINNER BADGE */} - {isWinner && ( -
- - - Top Choice - -
- )} - -
- {/* PRODUCT NAME */} -
-

- {item.name} -

- - - Lihat di Tokopedia ↗ - -
- - {/* COMPATIBILITY SCORE */} -
-
- - Kecocokan Profesi - - - {item.profession_compatibility_score}% - -
- -
- -
-
- - {/* SENTIMENT SCORE */} -
-
- - Sentimen Publik - - - {item.general_sentiment_score}% Positif - -
- -
- -
-
- - {/* KEYWORDS */} -
-

- Kata Kunci Dominan -

- -
- {item.top_keywords.map((kw, i) => ( - - #{kw} - - ))} -
-
-
-
- ); - })} -
-
- )} -
- ); + return ; } diff --git a/src/components/dashboards/AnalysisClient.tsx b/src/components/dashboards/AnalysisClient.tsx new file mode 100644 index 0000000..0db9408 --- /dev/null +++ b/src/components/dashboards/AnalysisClient.tsx @@ -0,0 +1,165 @@ +"use client"; + +import { useAnalyseText } from "@/src/hooks/useAnalyzeText"; +import { Sparkles, Trophy } from "lucide-react"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "../ui/select"; +import { Input } from "../ui/input"; +import { Button } from "../ui/button"; +import { motion } from "framer-motion"; +import { useState } from "react"; +import ResultSection from "./ResultSection"; + +export default function AnalysisClient() { + const { + url1, + url2, + url3, + profession, + loading, + result, + disabled, + handleAnalyze, + setProfession, + setUrl1, + setUrl2, + setDisabled, + setUrl3, + } = useAnalyseText(); + const [showField, setShowField] = useState(false); + + const getSentimentTone = (score: number) => { + if (score >= 80) return "strong"; + if (score >= 60) return "light"; + return "neutral"; + }; + + return ( +
+
+
+ +

Analisis Sentimen Real-time

+
+
+
+ + +
+
+ + setUrl1(e.target.value)} + className="border rounded-md focus:ring-2 focus:ring-green-500" + required + /> +
+
+ +
+
+ + setUrl2(e.target.value)} + className="border rounded-md focus:ring-2 focus:ring-green-500 w-full" + required + /> +
+ + {showField ? ( +
+ +
+ setUrl3(e.target.value)} + className="border p-2 rounded-md focus:ring-2 focus:ring-green-500 w-full" + required + /> + +
+
+ ) : ( +
+ +
+ )} +
+ + +
+ + +
+ ); +} diff --git a/src/components/dashboards/DashboardClient.tsx b/src/components/dashboards/DashboardClient.tsx index e53cb8c..b389a2e 100644 --- a/src/components/dashboards/DashboardClient.tsx +++ b/src/components/dashboards/DashboardClient.tsx @@ -18,6 +18,7 @@ import SentimentForm from "./SentimentAnalyzer"; import { WordCloud } from "./WordCloud"; import AnalysisPage from "@/src/app/analyze/page"; import SentimentAnalyzer from "./SentimentAnalyzer"; +import AnalysisClient from "./AnalysisClient"; export default function DashboardClient() { const { @@ -33,7 +34,7 @@ export default function DashboardClient() { } = useDashboards(); return ( -
+
@@ -133,7 +134,7 @@ export default function DashboardClient() {
{/* */} - +
diff --git a/src/components/dashboards/Header.tsx b/src/components/dashboards/Header.tsx index 49011d4..e412c40 100644 --- a/src/components/dashboards/Header.tsx +++ b/src/components/dashboards/Header.tsx @@ -27,7 +27,7 @@ export function Header() { if (!mounted) return null; return ( -
+
diff --git a/src/components/dashboards/ResultSection.tsx b/src/components/dashboards/ResultSection.tsx new file mode 100644 index 0000000..273a99c --- /dev/null +++ b/src/components/dashboards/ResultSection.tsx @@ -0,0 +1,212 @@ +import { AnalysisResults, ResultProps } from "@/src/types"; +import { motion } from "framer-motion"; +import { Trophy, ExternalLink, CheckCircle2, TrendingUp } from "lucide-react"; + +export default function ResultSection({ result }: ResultProps) { + if (!result) return null; + + const getGridClass = (count: number) => { + if (count === 1) return "max-w-md mx-auto"; + if (count === 2) return "grid-cols-1 md:grid-cols-2"; + return "grid-cols-1 md:grid-cols-2 lg:grid-cols-3"; + }; + + return ( + + {/* ================= HEADER WINNER ================= */} + + {/* Dekorasi Background Abstrak */} +
+
+ +
+
+ + + Rekomendasi Terbaik + +
+ +

+ {result.winning_product} +

+ +

+ Pilihan paling tepat dan efisien untuk kebutuhan{" "} + + {result.profession_target} + +

+
+
+ + {/* ================= CARDS GRID ================= */} +
+ {result.details.map((item, index) => { + const isWinner = item.name === result.winning_product; + + return ( + + {/* WINNER BADGE (Floating) */} + {isWinner && ( +
+ + + Pemenang + +
+ )} + +
+ {/* 1. HEADER KARTU */} +
+
+

+ {item.name} +

+ + {item.verdict.split("")}{" "} + +
+ + + Lihat Produk{" "} + + +
+ + {/* 2. STATISTIK UTAMA (Progress Bars) */} +
+ {/* Kecocokan Profesi */} +
+
+ + + Kecocokan + + + {item.profession_compatibility_score}% + +
+
+ +
+
+ + {/* Sentimen Publik */} +
+
+ + + Sentimen + + + {item.general_sentiment_score}% Positif + +
+
+ +
+
+
+ + {/* 3. FOOTER (Keywords) */} +
+

+ Kata Kunci +

+
+ {item.top_keywords.map((kw, i) => ( + + #{kw} + + ))} +
+
+
+
+ ); + })} +
+
+ ); +} diff --git a/src/hooks/useAnalyzeText.ts b/src/hooks/useAnalyzeText.ts index 9c8f996..9cf6cfa 100644 --- a/src/hooks/useAnalyzeText.ts +++ b/src/hooks/useAnalyzeText.ts @@ -4,6 +4,7 @@ import { AnalysisResults } from "../types"; export const useAnalyseText = () => { const [url1, setUrl1] = useState(""); const [url2, setUrl2] = useState(""); + const [url3, setUrl3] = useState(""); const [profession, setProfession] = useState(""); const [loading, setLoading] = useState(false); const [result, setResult] = useState(null); @@ -14,11 +15,27 @@ export const useAnalyseText = () => { setResult(null); try { - const scrapePromises = [url1, url2].map((u) => + const urlsToScrape = [url1, url2, url3].filter( + (url) => url && url.trim() !== "", + ); + + if (urlsToScrape.length < 2) { + alert("Produk Utama dan minimal 1 Produk Pembanding wajib diisi!"); + setLoading(false); + return; + } + + const scrapePromises = urlsToScrape.map((u) => fetch("/api/scrape", { method: "POST", + headers: { + "Content-Type": "application/json", + }, body: JSON.stringify({ url: u }), - }).then((res) => res.json()), + }).then((res) => { + if (!res.ok) throw new Error(`Gagal scraping: ${u}`); + return res.json(); + }), ); const scrapeResults = await Promise.all(scrapePromises); @@ -59,6 +76,7 @@ export const useAnalyseText = () => { return { url1, url2, + url3, profession, loading, result, @@ -67,6 +85,7 @@ export const useAnalyseText = () => { setProfession, setUrl1, setUrl2, + setUrl3, setDisabled, }; }; diff --git a/src/types/index.ts b/src/types/index.ts index b97d60c..1c5d66f 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -194,3 +194,7 @@ export interface AnalysisResults { winning_product: string; details: ProductDetail[]; } + +export interface ResultProps { + result: AnalysisResults | null; +}