diff --git a/lib/withAuth.ts b/lib/withAuth.ts index ab4a311..2bd5bb4 100644 --- a/lib/withAuth.ts +++ b/lib/withAuth.ts @@ -1,5 +1,5 @@ import { authOptions } from "@/src/app/api/auth/[...nextauth]/route"; -import { ApiHandler } from "@/src/types"; +import { ApiHandler, ServerActionHandler } from "@/src/types"; import { getServerSession } from "next-auth"; import { NextResponse } from "next/server"; @@ -17,3 +17,18 @@ export function withAuth(handler: ApiHandler) { return handler(req, context, session); }; } + +export function withActionAuth( + handler: ServerActionHandler, +) { + return async (...args: Args): Promise => { + const session = await getServerSession(authOptions); + + if (!session?.user?.email) { + console.log("Unauthorized: User belum login"); + return null; + } + + return handler(session, ...args); + }; +} diff --git a/lib/withBody.ts b/lib/withBody.ts new file mode 100644 index 0000000..e29341a --- /dev/null +++ b/lib/withBody.ts @@ -0,0 +1,14 @@ +import { BodyData } from "@/src/types"; +import { NextResponse } from "next/server"; + +export function withBody(handler: BodyData) { + return async (req: Request) => { + const body = await req.json(); + const { url } = body; + + if (!url || !url.includes("tokopedia.com")) { + return NextResponse.json({ error: "URL tidak valid" }, { status: 400 }); + } + return handler(req, body); + }; +} diff --git a/src/app/api/scrape/route.ts b/src/app/api/scrape/route.ts index 69431d7..91a3918 100644 --- a/src/app/api/scrape/route.ts +++ b/src/app/api/scrape/route.ts @@ -1,16 +1,12 @@ +import { withBody } from "@/lib/withBody"; import { scrapeTokopediaProduct } from "@/src/services/scrape.service"; import { NextResponse } from "next/server"; -export async function POST(request: Request) { +export const POST = withBody(async (_req, body) => { try { - const body = await request.json(); - const { url } = body; + const result = await scrapeTokopediaProduct(body.url); - if (!url || !url.includes("tokopedia.com")) { - return NextResponse.json({ error: "URL tidak valid" }, { status: 400 }); - } - - const result = await scrapeTokopediaProduct(url); + console.log(result); return NextResponse.json({ success: true, @@ -19,4 +15,4 @@ export async function POST(request: Request) { } catch (error: any) { return NextResponse.json({ error: error.message }, { status: 500 }); } -} +}); diff --git a/src/app/dashboard/lib/actions.ts b/src/app/dashboard/lib/actions.ts index bc9ae67..e433665 100644 --- a/src/app/dashboard/lib/actions.ts +++ b/src/app/dashboard/lib/actions.ts @@ -1,29 +1,13 @@ "use server"; -import prisma from "@/lib/prisma"; -import { getServerSession } from "next-auth"; -import { notFound } from "next/navigation"; -import { authOptions } from "../../api/auth/[...nextauth]/route"; + +import { withActionAuth } from "@/lib/withAuth"; +import { getAnalysisData } from "@/src/services/analyze.service"; +import { formatBrandStats } from "@/src/services/brand.service"; +import { reportService } from "@/src/services/report.service"; export const getClassificationReport = async () => { try { - const response = await prisma.model.findMany({ - select: { - modelName: true, - description: true, - accuracy: true, - macroF1: true, - f1Negative: true, - f1Neutral: true, - isActive: true, - }, - orderBy: { - createdAt: "desc", - }, - }); - - if (!response || response.length === 0) { - return notFound(); - } + const response = await reportService(); return response; } catch (error) { @@ -32,85 +16,17 @@ export const getClassificationReport = async () => { } }; -export const getTotalBrandAnalysis = async () => { +export const getTotalBrandAnalysis = withActionAuth(async (session) => { try { - const session = await getServerSession(authOptions); + const email = session.user?.email as string; - if (!session?.user?.email) { - console.log("User belum login"); - return null; - } + const userAnalysis = await getAnalysisData(email); - const userAnalyses = await prisma.analysis.findMany({ - where: { - user: { - email: session.user.email, - }, - }, - include: { - product: { - select: { - id: true, - brand: true, - _count: { - select: { - reviews: { - where: { - user: { - email: session.user.email, - }, - }, - }, - }, - }, - }, - }, - }, - }); + const formattedBrands = formatBrandStats(userAnalysis); - const countedProductIds = new Set(); - - const brandCounts = userAnalyses.reduce( - (acc: Record, 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; + return { formattedBrands, userAnalysis }; } catch (error) { console.error("Gagal mengambil data review:", error); return []; } -}; +}); diff --git a/src/app/dashboard/lib/data.ts b/src/app/dashboard/lib/data.ts index 6f75735..da5c3e3 100644 --- a/src/app/dashboard/lib/data.ts +++ b/src/app/dashboard/lib/data.ts @@ -1,193 +1,193 @@ -// Sample data for the sentiment analysis dashboard -// Based on Tokopedia laptop reviews analysis +// // Sample data for the sentiment analysis dashboard +// // Based on Tokopedia laptop reviews analysis -import { CheckCircle2, Cpu, Target, Zap } from "lucide-react"; +// import { CheckCircle2, Cpu, Target, Zap } from "lucide-react"; -export const sentimentDistribution = [ - { name: "Positif", value: 2456, color: "hsl(158, 64%, 42%)" }, - { name: "Negatif", value: 607, color: "hsl(0, 72%, 51%)" }, - { name: "Netral", value: 382, color: "hsl(43, 74%, 49%)" }, -]; +// export const sentimentDistribution = [ +// { name: "Positif", value: 2456, color: "hsl(158, 64%, 42%)" }, +// { name: "Negatif", value: 607, color: "hsl(0, 72%, 51%)" }, +// { name: "Netral", value: 382, color: "hsl(43, 74%, 49%)" }, +// ]; -export const trendData = [ - { date: "Jan", positif: 580, negatif: 220, netral: 150 }, - { date: "Feb", positif: 620, negatif: 245, netral: 175 }, - { date: "Mar", positif: 750, negatif: 280, netral: 190 }, - { date: "Apr", positif: 690, negatif: 310, netral: 165 }, - { date: "Mei", positif: 820, negatif: 265, netral: 210 }, - { date: "Jun", positif: 780, negatif: 240, netral: 195 }, - { date: "Jul", positif: 850, negatif: 290, netral: 220 }, - { date: "Agu", positif: 720, negatif: 255, netral: 180 }, - { date: "Sep", positif: 680, negatif: 230, netral: 165 }, - { date: "Okt", positif: 540, negatif: 195, netral: 145 }, - { date: "Nov", positif: 520, negatif: 175, netral: 150 }, - { date: "Des", positif: 474, negatif: 140, netral: 136 }, -]; +// export const trendData = [ +// { date: "Jan", positif: 580, negatif: 220, netral: 150 }, +// { date: "Feb", positif: 620, negatif: 245, netral: 175 }, +// { date: "Mar", positif: 750, negatif: 280, netral: 190 }, +// { date: "Apr", positif: 690, negatif: 310, netral: 165 }, +// { date: "Mei", positif: 820, negatif: 265, netral: 210 }, +// { date: "Jun", positif: 780, negatif: 240, netral: 195 }, +// { date: "Jul", positif: 850, negatif: 290, netral: 220 }, +// { date: "Agu", positif: 720, negatif: 255, netral: 180 }, +// { date: "Sep", positif: 680, negatif: 230, netral: 165 }, +// { date: "Okt", positif: 540, negatif: 195, netral: 145 }, +// { date: "Nov", positif: 520, negatif: 175, netral: 150 }, +// { date: "Des", positif: 474, negatif: 140, netral: 136 }, +// ]; -export const wordCloudData = [ - { text: "barang", value: 1086, sentiment: "positive" as const }, - { text: "sesuai", value: 570, sentiment: "positive" as const }, - { text: "bagus", value: 523, sentiment: "positive" as const }, - { text: "seller", value: 478, sentiment: "positive" as const }, - { text: "cepat", value: 471, sentiment: "positive" as const }, - { text: "aman", value: 453, sentiment: "positive" as const }, - { text: "laptop", value: 451, sentiment: "positive" as const }, - { text: "kirim", value: 449, sentiment: "positive" as const }, - { text: "baik", value: 396, sentiment: "positive" as const }, - { text: "mulus", value: 364, sentiment: "positive" as const }, - { text: "barang", value: 181, sentiment: "negative" as const }, - { text: "kirim", value: 177, sentiment: "negative" as const }, - { text: "laptop", value: 129, sentiment: "negative" as const }, - { text: "beli", value: 124, sentiment: "negative" as const }, - { text: "lebih", value: 72, sentiment: "negative" as const }, - { text: "jual", value: 62, sentiment: "negative" as const }, - { text: "baru", value: 60, sentiment: "negative" as const }, - { text: "lalu", value: 59, sentiment: "negative" as const }, - { text: "sesuai", value: 56, sentiment: "negative" as const }, - { text: "tahun", value: 56, sentiment: "negative" as const }, - { text: "kirim", value: 106, sentiment: "neutral" as const }, - { text: "barang", value: 96, sentiment: "neutral" as const }, - { text: "laptop", value: 78, sentiment: "neutral" as const }, - { text: "sesuai", value: 48, sentiment: "neutral" as const }, - { text: "bagus", value: 47, sentiment: "neutral" as const }, - { text: "lebih", value: 45, sentiment: "neutral" as const }, - { text: "kurang", value: 44, sentiment: "neutral" as const }, - { text: "beli", value: 44, sentiment: "neutral" as const }, - { text: "baik", value: 33, sentiment: "neutral" as const }, - { text: "baru", value: 32, sentiment: "neutral" as const }, -]; +// export const wordCloudData = [ +// { text: "barang", value: 1086, sentiment: "positive" as const }, +// { text: "sesuai", value: 570, sentiment: "positive" as const }, +// { text: "bagus", value: 523, sentiment: "positive" as const }, +// { text: "seller", value: 478, sentiment: "positive" as const }, +// { text: "cepat", value: 471, sentiment: "positive" as const }, +// { text: "aman", value: 453, sentiment: "positive" as const }, +// { text: "laptop", value: 451, sentiment: "positive" as const }, +// { text: "kirim", value: 449, sentiment: "positive" as const }, +// { text: "baik", value: 396, sentiment: "positive" as const }, +// { text: "mulus", value: 364, sentiment: "positive" as const }, +// { text: "barang", value: 181, sentiment: "negative" as const }, +// { text: "kirim", value: 177, sentiment: "negative" as const }, +// { text: "laptop", value: 129, sentiment: "negative" as const }, +// { text: "beli", value: 124, sentiment: "negative" as const }, +// { text: "lebih", value: 72, sentiment: "negative" as const }, +// { text: "jual", value: 62, sentiment: "negative" as const }, +// { text: "baru", value: 60, sentiment: "negative" as const }, +// { text: "lalu", value: 59, sentiment: "negative" as const }, +// { text: "sesuai", value: 56, sentiment: "negative" as const }, +// { text: "tahun", value: 56, sentiment: "negative" as const }, +// { text: "kirim", value: 106, sentiment: "neutral" as const }, +// { text: "barang", value: 96, sentiment: "neutral" as const }, +// { text: "laptop", value: 78, sentiment: "neutral" as const }, +// { text: "sesuai", value: 48, sentiment: "neutral" as const }, +// { text: "bagus", value: 47, sentiment: "neutral" as const }, +// { text: "lebih", value: 45, sentiment: "neutral" as const }, +// { text: "kurang", value: 44, sentiment: "neutral" as const }, +// { text: "beli", value: 44, sentiment: "neutral" as const }, +// { text: "baik", value: 33, sentiment: "neutral" as const }, +// { text: "baru", value: 32, sentiment: "neutral" as const }, +// ]; -export const brandData = [ - { name: "ASUS", count: 3245 }, - { name: "Lenovo", count: 2890 }, - { name: "HP", count: 2456 }, - { name: "Acer", count: 2134 }, - { name: "Dell", count: 1725 }, -]; +// export const brandData = [ +// { name: "ASUS", count: 3245 }, +// { name: "Lenovo", count: 2890 }, +// { name: "HP", count: 2456 }, +// { name: "Acer", count: 2134 }, +// { name: "Dell", count: 1725 }, +// ]; -export const reviewData = [ - { - id: "1", - product: "ASUS VivoBook 15 X515EA Intel Core i5-1135G7", - brand: "ASUS", - review: - "Laptop sangat bagus, performa cepat untuk kerja kantoran. Layar jernih dan keyboard nyaman dipakai mengetik seharian. Pengiriman juga cepat dan aman.", - rating: 5, - sentiment: "positif" as const, - date: "2 hari yang lalu", - confidence: 0.945, - }, - { - id: "2", - product: "Lenovo IdeaPad Slim 3 AMD Ryzen 5 5500U", - brand: "Lenovo", - review: - "Produk sesuai deskripsi. Build quality oke, performa lancar untuk multitasking ringan. Baterai awet bisa 6-7 jam pemakaian normal.", - rating: 4, - sentiment: "positif" as const, - date: "3 hari yang lalu", - confidence: 0.887, - }, - { - id: "3", - product: "HP 14s-dq5001TU Intel Core i5-1235U", - brand: "HP", - review: - "Kecewa dengan produk ini. Baru dipakai 2 minggu sudah sering hang dan restart sendiri. Kipas juga berisik sekali padahal hanya buka browser.", - rating: 2, - sentiment: "negatif" as const, - date: "4 hari yang lalu", - confidence: 0.923, - }, - { - id: "4", - product: "Acer Aspire 5 A515-57 Intel Core i5-1235U", - brand: "Acer", - review: - "Laptop oke lah untuk harga segini. Tidak terlalu cepat tapi juga tidak lemot. Cocok untuk mahasiswa dengan budget terbatas.", - rating: 3, - sentiment: "netral" as const, - date: "5 hari yang lalu", - confidence: 0.812, - }, - { - id: "5", - product: "Dell Inspiron 15 3520 Intel Core i3-1215U", - brand: "Dell", - review: - "Sangat puas dengan pembelian ini! Laptop premium dengan harga terjangkau. Build quality solid, keyboard backlit, dan layar anti-glare sangat membantu.", - rating: 5, - sentiment: "positif" as const, - date: "1 minggu yang lalu", - confidence: 0.956, - }, - { - id: "6", - product: "ASUS TUF Gaming F15 FX506HF RTX 2050", - brand: "ASUS", - review: - "Gaming laptop yang worth it! Main game AAA lancar di medium-high setting. Thermal management bagus, tidak terlalu panas saat gaming marathon.", - rating: 5, - sentiment: "positif" as const, - date: "1 minggu yang lalu", - confidence: 0.934, - }, - { - id: "7", - product: "Lenovo V14 G3 AMD Ryzen 3 5300U", - brand: "Lenovo", - review: - "Laptop datang dalam kondisi rusak, layar ada garis horizontal. Sudah komplain ke seller tapi respon lambat. Sangat mengecewakan.", - rating: 1, - sentiment: "negatif" as const, - date: "1 minggu yang lalu", - confidence: 0.967, - }, - { - id: "8", - product: "HP Pavilion 14-dv2045TX Intel Core i5", - brand: "HP", - review: - "Desain elegan dan performa mumpuni. Cocok untuk pekerja mobile yang butuh laptop stylish. Speaker B&O juga keren suaranya.", - rating: 4, - sentiment: "positif" as const, - date: "2 minggu yang lalu", - confidence: 0.891, - }, -]; +// export const reviewData = [ +// { +// id: "1", +// product: "ASUS VivoBook 15 X515EA Intel Core i5-1135G7", +// brand: "ASUS", +// review: +// "Laptop sangat bagus, performa cepat untuk kerja kantoran. Layar jernih dan keyboard nyaman dipakai mengetik seharian. Pengiriman juga cepat dan aman.", +// rating: 5, +// sentiment: "positif" as const, +// date: "2 hari yang lalu", +// confidence: 0.945, +// }, +// { +// id: "2", +// product: "Lenovo IdeaPad Slim 3 AMD Ryzen 5 5500U", +// brand: "Lenovo", +// review: +// "Produk sesuai deskripsi. Build quality oke, performa lancar untuk multitasking ringan. Baterai awet bisa 6-7 jam pemakaian normal.", +// rating: 4, +// sentiment: "positif" as const, +// date: "3 hari yang lalu", +// confidence: 0.887, +// }, +// { +// id: "3", +// product: "HP 14s-dq5001TU Intel Core i5-1235U", +// brand: "HP", +// review: +// "Kecewa dengan produk ini. Baru dipakai 2 minggu sudah sering hang dan restart sendiri. Kipas juga berisik sekali padahal hanya buka browser.", +// rating: 2, +// sentiment: "negatif" as const, +// date: "4 hari yang lalu", +// confidence: 0.923, +// }, +// { +// id: "4", +// product: "Acer Aspire 5 A515-57 Intel Core i5-1235U", +// brand: "Acer", +// review: +// "Laptop oke lah untuk harga segini. Tidak terlalu cepat tapi juga tidak lemot. Cocok untuk mahasiswa dengan budget terbatas.", +// rating: 3, +// sentiment: "netral" as const, +// date: "5 hari yang lalu", +// confidence: 0.812, +// }, +// { +// id: "5", +// product: "Dell Inspiron 15 3520 Intel Core i3-1215U", +// brand: "Dell", +// review: +// "Sangat puas dengan pembelian ini! Laptop premium dengan harga terjangkau. Build quality solid, keyboard backlit, dan layar anti-glare sangat membantu.", +// rating: 5, +// sentiment: "positif" as const, +// date: "1 minggu yang lalu", +// confidence: 0.956, +// }, +// { +// id: "6", +// product: "ASUS TUF Gaming F15 FX506HF RTX 2050", +// brand: "ASUS", +// review: +// "Gaming laptop yang worth it! Main game AAA lancar di medium-high setting. Thermal management bagus, tidak terlalu panas saat gaming marathon.", +// rating: 5, +// sentiment: "positif" as const, +// date: "1 minggu yang lalu", +// confidence: 0.934, +// }, +// { +// id: "7", +// product: "Lenovo V14 G3 AMD Ryzen 3 5300U", +// brand: "Lenovo", +// review: +// "Laptop datang dalam kondisi rusak, layar ada garis horizontal. Sudah komplain ke seller tapi respon lambat. Sangat mengecewakan.", +// rating: 1, +// sentiment: "negatif" as const, +// date: "1 minggu yang lalu", +// confidence: 0.967, +// }, +// { +// id: "8", +// product: "HP Pavilion 14-dv2045TX Intel Core i5", +// brand: "HP", +// review: +// "Desain elegan dan performa mumpuni. Cocok untuk pekerja mobile yang butuh laptop stylish. Speaker B&O juga keren suaranya.", +// rating: 4, +// sentiment: "positif" as const, +// date: "2 minggu yang lalu", +// confidence: 0.891, +// }, +// ]; -export const modelData = { - baseline: { - name: "Model XGBoost (Baseline)", - metrics: [ - { label: "Accuracy", value: "80.0%", icon: Target }, - { label: "Macro F1-Score", value: "56.0%", icon: Cpu }, - { label: "F1-Negatif", value: "61.0%", icon: CheckCircle2 }, - { label: "F1-Netral", value: "16.0%", icon: Zap }, - ], - description: - "Model awal menggunakan parameter default XGBoost (learning_rate=0.3, max_depth=6) pada dataset yang tidak seimbang.", - }, - tuned: { - name: "Model XGBoost (Tuned)", - metrics: [ - { label: "Accuracy", value: "81.0%", icon: Target }, - { label: "Macro F1-Score", value: "58.0%", icon: Cpu }, - { label: "F1-Negatif", value: "65.0%", icon: CheckCircle2 }, - { label: "F1-Netral", value: "17.0%", icon: Zap }, - ], - description: - "Model dengan optimasi Hyperparameter menggunakan Grid Search untuk mencari kombinasi learning_rate dan max_depth terbaik.", - }, - optimized: { - name: "Model XGBoost (Optimized)", - metrics: [ - { label: "Accuracy", value: "82.0%", icon: Target }, - { label: "Macro F1-Score", value: "61.0%", icon: Cpu }, - { label: "F1-Negatif", value: "65.0%", icon: CheckCircle2 }, - { label: "F1-Netral", value: "27.0%", icon: Zap }, - ], - description: - "Model final menggunakan teknik SMOTE untuk menyeimbangkan kelas, seleksi fitur Chi-Square, dan optimasi Grid Search.", - }, -}; +// export const modelData = { +// baseline: { +// name: "Model XGBoost (Baseline)", +// metrics: [ +// { label: "Accuracy", value: "80.0%", icon: Target }, +// { label: "Macro F1-Score", value: "56.0%", icon: Cpu }, +// { label: "F1-Negatif", value: "61.0%", icon: CheckCircle2 }, +// { label: "F1-Netral", value: "16.0%", icon: Zap }, +// ], +// description: +// "Model awal menggunakan parameter default XGBoost (learning_rate=0.3, max_depth=6) pada dataset yang tidak seimbang.", +// }, +// tuned: { +// name: "Model XGBoost (Tuned)", +// metrics: [ +// { label: "Accuracy", value: "81.0%", icon: Target }, +// { label: "Macro F1-Score", value: "58.0%", icon: Cpu }, +// { label: "F1-Negatif", value: "65.0%", icon: CheckCircle2 }, +// { label: "F1-Netral", value: "17.0%", icon: Zap }, +// ], +// description: +// "Model dengan optimasi Hyperparameter menggunakan Grid Search untuk mencari kombinasi learning_rate dan max_depth terbaik.", +// }, +// optimized: { +// name: "Model XGBoost (Optimized)", +// metrics: [ +// { label: "Accuracy", value: "82.0%", icon: Target }, +// { label: "Macro F1-Score", value: "61.0%", icon: Cpu }, +// { label: "F1-Negatif", value: "65.0%", icon: CheckCircle2 }, +// { label: "F1-Netral", value: "27.0%", icon: Zap }, +// ], +// description: +// "Model final menggunakan teknik SMOTE untuk menyeimbangkan kelas, seleksi fitur Chi-Square, dan optimasi Grid Search.", +// }, +// }; diff --git a/src/app/profile/lib/action.ts b/src/app/profile/lib/action.ts index 76f0c63..f57ea2e 100644 --- a/src/app/profile/lib/action.ts +++ b/src/app/profile/lib/action.ts @@ -1,37 +1,17 @@ "use server"; -import { getServerSession } from "next-auth/next"; -import { authOptions } from "../../api/auth/[...nextauth]/route"; -import prisma from "@/lib/prisma"; -export const getAnotherUserData = async () => { +import { withActionAuth } from "@/lib/withAuth"; +import { getAnotherUserDataService } from "@/src/services/users.service"; + +export const getAnotherUserData = withActionAuth(async (session) => { try { - const session = await getServerSession(authOptions); + const email = (await session.user?.email) as string; - if (!session?.user?.email) return null; - - const userData = await prisma.user.findUnique({ - where: { - email: session.user.email, - }, - select: { - name: true, - bio: true, - preference: { - select: { - id: true, - profession: true, - preferredBrand: true, - preferredOS: true, - budgetMin: true, - budgetMax: true, - }, - }, - }, - }); + const userData = await getAnotherUserDataService(email); return userData; } catch (error) { console.error("Error fetching user data:", error); return null; } -}; +}); diff --git a/src/hooks/useBrandFilter.ts b/src/hooks/useBrandFilter.ts index 625fb6b..5590b2c 100644 --- a/src/hooks/useBrandFilter.ts +++ b/src/hooks/useBrandFilter.ts @@ -11,8 +11,10 @@ export const useBrandFilter = () => { const fetchBrands = async () => { try { const data = await getTotalBrandAnalysis(); - if (data) { - setBrands(data); + if (data && "formattedBrands" in data) { + setBrands(data.formattedBrands); + } else { + setBrands([]); } } catch (error) { console.error("Gagal memuat filter brand", error); diff --git a/src/services/analyze.service.ts b/src/services/analyze.service.ts index c5b2c5e..2a4a9b8 100644 --- a/src/services/analyze.service.ts +++ b/src/services/analyze.service.ts @@ -1,3 +1,5 @@ +import prisma from "@/lib/prisma"; + export const scrapeProduct = async (url: string) => { const res = await fetch("/api/scrape", { method: "POST", @@ -31,3 +33,34 @@ export const getAIRecommendation = async (payload: { return await aiRes.json(); }; + +export const getAnalysisData = async (email: string) => { + + const userAnalyses = await prisma.analysis.findMany({ + where: { + user: { + email: email, + }, + }, + include: { + product: { + select: { + id: true, + brand: true, + _count: { + select: { + reviews: { + where: { + user: { + email: email, + }, + }, + }, + }, + }, + }, + }, + }, + }); + return userAnalyses; +}; diff --git a/src/services/brand.service.ts b/src/services/brand.service.ts new file mode 100644 index 0000000..f977925 --- /dev/null +++ b/src/services/brand.service.ts @@ -0,0 +1,44 @@ +import { AnalysisData } from "../types"; + +export const formatBrandStats = (userAnalysis: AnalysisData[]) => { + const countedProductIds = new Set(); + + const brandCounts = userAnalysis.reduce( + (acc: Record, 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; +}; diff --git a/src/services/report.service.ts b/src/services/report.service.ts new file mode 100644 index 0000000..1d09aae --- /dev/null +++ b/src/services/report.service.ts @@ -0,0 +1,25 @@ +import prisma from "@/lib/prisma"; +import { notFound } from "next/navigation"; + +export const reportService = async () => { + const response = await prisma.model.findMany({ + select: { + modelName: true, + description: true, + accuracy: true, + macroF1: true, + f1Negative: true, + f1Neutral: true, + isActive: true, + }, + orderBy: { + createdAt: "desc", + }, + }); + + if (!response || response.length === 0) { + return notFound(); + } + + return response; +}; diff --git a/src/services/users.service.ts b/src/services/users.service.ts new file mode 100644 index 0000000..8803785 --- /dev/null +++ b/src/services/users.service.ts @@ -0,0 +1,25 @@ +import prisma from "@/lib/prisma"; + +export const getAnotherUserDataService = async (email: string) => { + const userData = await prisma.user.findUnique({ + where: { + email: email, + }, + select: { + name: true, + bio: true, + preference: { + select: { + id: true, + profession: true, + preferredBrand: true, + preferredOS: true, + budgetMin: true, + budgetMax: true, + }, + }, + }, + }); + + return userData; +}; diff --git a/src/types/index.ts b/src/types/index.ts index 4c11acf..6e85ee5 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -268,3 +268,20 @@ export type WordCloudReview = { keywords: string[]; sentiment: Sentiment; }; + +export type ServerActionHandler = ( + session: Session, + ...args: Args +) => Promise; + +export type AnalysisData = { + product?: { + id: number; + brand: string | null; + _count?: { + reviews: number; + }; + }; +}; + +export type BodyData = (req: Request, body: any) => Promise;