From 431b56b7795fd3a8bd228d9119f6eb7294b62ce8 Mon Sep 17 00:00:00 2001 From: Mahen Date: Fri, 6 Feb 2026 16:02:10 +0700 Subject: [PATCH] feat: add login action & adjust auth middleware --- package-lock.json | 154 ++++++++++++ package.json | 1 + src/app/api/auth/[...nextauth]/route.ts | 15 ++ src/app/auth/login/page.tsx | 78 +------ src/app/dashboard/page.tsx | 210 +---------------- src/app/layout.tsx | 5 +- src/app/page.tsx | 15 +- src/app/providers.tsx | 7 + src/components/auth/LoginForm.tsx | 70 ++++++ src/components/dashboards/DashboardClient.tsx | 219 ++++++++++++++++++ src/components/dashboards/Header.tsx | 4 +- src/components/dashboards/ReviewTable.tsx | 20 +- src/components/dashboards/TrendChart.tsx | 2 +- src/components/ui/button.tsx | 22 +- src/components/ui/table.tsx | 32 +-- src/components/ui/textarea.tsx | 10 +- 16 files changed, 551 insertions(+), 313 deletions(-) create mode 100644 src/app/api/auth/[...nextauth]/route.ts create mode 100644 src/app/providers.tsx create mode 100644 src/components/auth/LoginForm.tsx create mode 100644 src/components/dashboards/DashboardClient.tsx diff --git a/package-lock.json b/package-lock.json index 92cc641..0e838a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "framer-motion": "^12.31.0", "lucide-react": "^0.563.0", "next": "16.1.6", + "next-auth": "^4.24.13", "pg": "^8.18.0", "radix-ui": "^1.4.3", "react": "19.2.3", @@ -1903,6 +1904,15 @@ "node": ">=12.4.0" } }, + "node_modules/@panva/hkdf": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz", + "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/@prisma/adapter-neon": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/@prisma/adapter-neon/-/adapter-neon-7.3.0.tgz", @@ -6856,6 +6866,15 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -9121,6 +9140,15 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -9827,6 +9855,38 @@ } } }, + "node_modules/next-auth": { + "version": "4.24.13", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.13.tgz", + "integrity": "sha512-sgObCfcfL7BzIK76SS5TnQtc3yo2Oifp/yIpfv6fMfeBOiBJkDWF3A2y9+yqnmJ4JKc2C+nMjSjmgDeTwgN1rQ==", + "license": "ISC", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@panva/hkdf": "^1.0.2", + "cookie": "^0.7.0", + "jose": "^4.15.5", + "oauth": "^0.9.15", + "openid-client": "^5.4.0", + "preact": "^10.6.3", + "preact-render-to-string": "^5.1.19", + "uuid": "^8.3.2" + }, + "peerDependencies": { + "@auth/core": "0.34.3", + "next": "^12.2.5 || ^13 || ^14 || ^15 || ^16", + "nodemailer": "^7.0.7", + "react": "^17.0.2 || ^18 || ^19", + "react-dom": "^17.0.2 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "@auth/core": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -9894,6 +9954,12 @@ "devOptional": true, "license": "MIT" }, + "node_modules/oauth": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", + "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -9904,6 +9970,15 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -10024,6 +10099,48 @@ "devOptional": true, "license": "MIT" }, + "node_modules/oidc-token-hash": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.2.0.tgz", + "integrity": "sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw==", + "license": "MIT", + "engines": { + "node": "^10.13.0 || >=12.0.0" + } + }, + "node_modules/openid-client": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz", + "integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==", + "license": "MIT", + "dependencies": { + "jose": "^4.15.9", + "lru-cache": "^6.0.0", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/openid-client/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/openid-client/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -10367,6 +10484,28 @@ "node": ">=0.10.0" } }, + "node_modules/preact": { + "version": "10.28.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.3.tgz", + "integrity": "sha512-tCmoRkPQLpBeWzpmbhryairGnhW9tKV6c6gr/w+RhoRoKEJwsjzipwp//1oCpGPOchvSLaAPlpcJi9MwMmoPyA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz", + "integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==", + "license": "MIT", + "dependencies": { + "pretty-format": "^3.8.0" + }, + "peerDependencies": { + "preact": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -10377,6 +10516,12 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", + "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==", + "license": "MIT" + }, "node_modules/prisma": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.3.0.tgz", @@ -12059,6 +12204,15 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", diff --git a/package.json b/package.json index abc1c11..075c36e 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "framer-motion": "^12.31.0", "lucide-react": "^0.563.0", "next": "16.1.6", + "next-auth": "^4.24.13", "pg": "^8.18.0", "radix-ui": "^1.4.3", "react": "19.2.3", diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..06596ea --- /dev/null +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,15 @@ +import NextAuth from "next-auth"; +import GoogleProvider from "next-auth/providers/google"; + +export const authOptions = { + providers: [ + GoogleProvider({ + clientId: process.env.GOOGLE_CLIENT_ID!, + clientSecret: process.env.GOOGLE_CLIENT_SECRET!, + }), + ], +}; + +const handler = NextAuth(authOptions); + +export { handler as GET, handler as POST }; diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx index 35854ea..e64f9e1 100644 --- a/src/app/auth/login/page.tsx +++ b/src/app/auth/login/page.tsx @@ -1,71 +1,17 @@ -import { cn } from "@/lib/utils"; -import { Button } from "../../../components/ui/button"; -import { - Card, - CardContent, - CardHeader, - CardTitle, -} from "../../../components/ui/card"; -import { - Field, - FieldDescription, - FieldGroup, - FieldLabel, -} from "../../../components/ui/field"; -import { Input } from "../../../components/ui/input"; +import { redirect } from "next/navigation"; +import { LoginForm } from "@/src/components/auth/LoginForm"; +import { getServerSession } from "next-auth"; +import { authOptions } from "../../api/auth/[...nextauth]/route"; -export function LoginPage({ +export default async function LoginPage({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- - - Login to SENTILAISES. - - -
- - - Email - - - - - - - - - - - Don't have an account? Sign up - - - -
-
-
-
- ); + const session = await getServerSession(authOptions); + + if (session) { + redirect("/dashboard"); + } + + return ; } diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index d6a3307..3afcfbd 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -1,205 +1,13 @@ -"use client"; -import { Suspense, useEffect, useState } from "react"; -import { - brandData, - reviewData, - sentimentDistribution, - trendData, - wordCloudData, -} from "./lib/data"; -import { Header } from "../../components/dashboards/Header"; -import { - MessageSquareText, - Minus, - ThumbsDown, - ThumbsUp, - TrendingUp, -} from "lucide-react"; -import { StatCard } from "../../components/dashboards/StatCard"; -import { TrendChart } from "../../components/dashboards/TrendChart"; -import { SentimentChart } from "../../components/dashboards/SentimentChart"; -import { WordCloud } from "../../components/dashboards/WordCloud"; -import { ModelInfo } from "../../components/dashboards/ModelInfo"; -import { SentimentAnalyzer } from "../../components/dashboards/SentimentAnalyzer"; -import { BrandFilter } from "../../components/dashboards/BrandFilter"; -import { ReviewTable } from "../../components/dashboards/ReviewTable"; -import { getClassificationReport } from "./lib/actions"; -import { ModelDB } from "@/src/types"; -import { ModelInfoSkeleton } from "../../components/skeletons/ModelInfoSkeleton"; +import { redirect } from "next/navigation"; +import { getServerSession } from "next-auth"; +import DashboardClient from "@/src/components/dashboards/DashboardClient"; -export default function DashboardPage() { - const [selectedBrand, setSelectedBrand] = useState(null); - const [loading, setLoading] = useState(true); - const [modelData, setModelData] = useState([]); - const totalReviews = sentimentDistribution.reduce( - (sum, s) => sum + s.value, - 0, - ); - const positiveCount = - sentimentDistribution.find((s) => s.name === "Positif")?.value || 0; - const negativeCount = - sentimentDistribution.find((s) => s.name === "Negatif")?.value || 0; - const neutralCount = - sentimentDistribution.find((s) => s.name === "Netral")?.value || 0; +export default async function DashboardPage() { + const session = await getServerSession(); - const filteredReviews = selectedBrand - ? reviewData.filter((r) => r.brand === selectedBrand) - : reviewData; + if (!session) { + redirect("/"); + } - useEffect(() => { - async function fetchData() { - try { - const data = await getClassificationReport(); - setModelData(data); - } catch (error) { - console.error("Failed to fetch model data", error); - } finally { - setLoading(false); - } - } - fetchData(); - }, []); - - return ( -
-
- -
- {/* Hero Section */} -
-

- Analisis Sentimen Ulasan Laptop -

-

- Sistem klasifikasi sentimen menggunakan algoritma XGBoost untuk - menganalisis ulasan produk laptop pada platform Tokopedia -

-
- - - Akurasi 92.4% - - - XGBoost + TF-IDF - - Real-time Analysis -
-
- - {/* Stats Grid */} -
- - - - -
- - {/* Charts Section */} -
-
-

- Tren Sentimen Bulanan -

- -
-
-

Distribusi Sentimen

- -
-
- - {/* Word Cloud & Model Info */} -
- {/* Slot Kata Kunci */} -
-

Kata Kunci Populer

-

- Kata-kata yang sering muncul dalam ulasan berdasarkan kategori - sentimen -

- -
- - {loading ? ( - - ) : modelData.length > 0 ? ( - - ) : ( -
- Data model tidak tersedia. -
- )} -
- - {/* Sentiment Analyzer */} -
- -
- - {/* Reviews Section */} -
-
-
-

Ulasan Terbaru

-

- Hasil klasifikasi sentimen ulasan produk laptop -

-
- -
- -
- - {/* Footer */} -
-
-
-

- SentiLaptop - Analisis Sentimen -

-

Skripsi oleh Syafrizal Wd Mahendra (E41222719)

-
-
-

Politeknik Negeri Jember

-

PSDKU Teknik Informatika Kampus 3 Nganjuk

-
-
-
-
-
- ); + return ; } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 301a097..13509e1 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono, Inter } from "next/font/google"; import "./globals.css"; +import Providers from "./providers"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -27,7 +28,9 @@ export default function RootLayout({ }>) { return ( - {children} + + {children} + ); } diff --git a/src/app/page.tsx b/src/app/page.tsx index e03eb4b..037b505 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,11 +1,18 @@ -'use client";'; -import { LoginPage } from "./auth/login/page"; +import { redirect } from "next/navigation"; +import { LoginForm } from "../components/auth/LoginForm"; +import { getServerSession } from "next-auth"; +import { authOptions } from "./api/auth/[...nextauth]/route"; -export default function Home() { +export default async function Home() { + const session = await getServerSession(authOptions); + + if (session) { + redirect("/dashboard"); + } return (
- +
); diff --git a/src/app/providers.tsx b/src/app/providers.tsx new file mode 100644 index 0000000..0dfb823 --- /dev/null +++ b/src/app/providers.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { SessionProvider } from "next-auth/react"; + +export default function Providers({ children }: { children: React.ReactNode }) { + return {children}; +} diff --git a/src/components/auth/LoginForm.tsx b/src/components/auth/LoginForm.tsx new file mode 100644 index 0000000..fd2da21 --- /dev/null +++ b/src/components/auth/LoginForm.tsx @@ -0,0 +1,70 @@ +"use client"; + +import { signIn } from "next-auth/react"; +import { Input } from "../ui/input"; +import { Button } from "../ui/button"; +import { cn } from "@/lib/utils"; +import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"; +import { Field, FieldDescription, FieldGroup, FieldLabel } from "../ui/field"; + +export function LoginForm({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ + + Login to SENTILAISES. + + +
+ + + Email + + + + + + + + + + + Don't have an account? Sign up + + + +
+
+
+
+ ); +} diff --git a/src/components/dashboards/DashboardClient.tsx b/src/components/dashboards/DashboardClient.tsx new file mode 100644 index 0000000..34e8008 --- /dev/null +++ b/src/components/dashboards/DashboardClient.tsx @@ -0,0 +1,219 @@ +"use client"; +import { useEffect, useState } from "react"; +import { Header } from "./Header"; +import { + MessageSquareText, + Minus, + ThumbsDown, + ThumbsUp, + TrendingUp, +} from "lucide-react"; +import { StatCard } from "./StatCard"; +import { ModelDB } from "@/src/types"; +import { + brandData, + reviewData, + sentimentDistribution, + trendData, + wordCloudData, +} from "@/src/app/dashboard/lib/data"; +import { getClassificationReport } from "@/src/app/dashboard/lib/actions"; +import { ModelInfoSkeleton } from "../skeletons/ModelInfoSkeleton"; +import { ModelInfo } from "./ModelInfo"; +import { SentimentAnalyzer } from "./SentimentAnalyzer"; +import { BrandFilter } from "./BrandFilter"; +import { ReviewTable } from "./ReviewTable"; +import dynamic from "next/dynamic"; + +const TrendChart = dynamic( + () => import("./TrendChart").then((mod) => ({ default: mod.TrendChart })), + { ssr: false }, +); +const WordCloud = dynamic( + () => import("./WordCloud").then((mod) => ({ default: mod.WordCloud })), + { ssr: false }, +); +const SentimentChart = dynamic( + () => + import("./SentimentChart").then((mod) => ({ default: mod.SentimentChart })), + { ssr: false }, +); + +export default function DashboardClient() { + const [selectedBrand, setSelectedBrand] = useState(null); + const [loading, setLoading] = useState(true); + const [modelData, setModelData] = useState([]); + + const totalReviews = sentimentDistribution.reduce( + (sum, s) => sum + s.value, + 0, + ); + + const positiveCount = + sentimentDistribution.find((s) => s.name === "Positif")?.value || 0; + const negativeCount = + sentimentDistribution.find((s) => s.name === "Negatif")?.value || 0; + const neutralCount = + sentimentDistribution.find((s) => s.name === "Netral")?.value || 0; + + const filteredReviews = selectedBrand + ? reviewData.filter((r) => r.brand === selectedBrand) + : reviewData; + + useEffect(() => { + async function fetchData() { + try { + const data = await getClassificationReport(); + setModelData(data); + } catch (error) { + console.error("Failed to fetch model data", error); + } finally { + setLoading(false); + } + } + fetchData(); + }, []); + + return ( +
+
+ +
+ {/* Hero Section */} +
+

+ Analisis Sentimen Ulasan Laptop +

+

+ Sistem klasifikasi sentimen menggunakan algoritma XGBoost untuk + menganalisis ulasan produk laptop pada platform Tokopedia +

+
+ + + Akurasi 92.4% + + + XGBoost + TF-IDF + + Real-time Analysis +
+
+ + {/* Stats Grid */} +
+ + + + +
+ + {/* Charts Section */} +
+
+

+ Tren Sentimen Bulanan +

+ +
+
+

Distribusi Sentimen

+ +
+
+ + {/* Word Cloud & Model Info */} +
+ {/* Slot Kata Kunci */} +
+

Kata Kunci Populer

+

+ Kata-kata yang sering muncul dalam ulasan berdasarkan kategori + sentimen +

+ +
+ + {loading ? ( + + ) : modelData.length > 0 ? ( + + ) : ( +
+ Data model tidak tersedia. +
+ )} +
+ + {/* Sentiment Analyzer */} +
+ +
+ + {/* Reviews Section */} +
+
+
+

Ulasan Terbaru

+

+ Hasil klasifikasi sentimen ulasan produk laptop +

+
+ +
+ +
+ + {/* Footer */} +
+
+
+

+ SentiLaptop - Analisis Sentimen +

+

Skripsi oleh Syafrizal Wd Mahendra (E41222719)

+
+
+

Politeknik Negeri Jember

+

PSDKU Teknik Informatika Kampus 3 Nganjuk

+
+
+
+
+
+ ); +} diff --git a/src/components/dashboards/Header.tsx b/src/components/dashboards/Header.tsx index ae89339..88d67bd 100644 --- a/src/components/dashboards/Header.tsx +++ b/src/components/dashboards/Header.tsx @@ -1,3 +1,4 @@ +"use client"; import { BarChart3, Database, @@ -16,6 +17,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "../ui/dropdown-menu"; +import { signOut, useSession } from "next-auth/react"; import { redirect } from "next/navigation"; export function Header() { @@ -84,7 +86,7 @@ export function Header() { redirect("/")} + onClick={() => signOut({ callbackUrl: "/" })} > Logout diff --git a/src/components/dashboards/ReviewTable.tsx b/src/components/dashboards/ReviewTable.tsx index b7c4587..47b8ab3 100644 --- a/src/components/dashboards/ReviewTable.tsx +++ b/src/components/dashboards/ReviewTable.tsx @@ -86,20 +86,26 @@ export function ReviewTable({ reviews }: ReviewTableProps) { className="animate-fade-in" style={{ animationDelay: `${index * 50}ms` }} > - -
-

{review.brand}

-

+ +

+

+ {review.brand} +

+

{review.product}

- -

{review.review}

-

+ + +

+ {review.review} +

+

{review.date}

+ {renderStars(review.rating)} {getSentimentBadge(review.sentiment)} diff --git a/src/components/dashboards/TrendChart.tsx b/src/components/dashboards/TrendChart.tsx index 1252bcc..d678e02 100644 --- a/src/components/dashboards/TrendChart.tsx +++ b/src/components/dashboards/TrendChart.tsx @@ -54,7 +54,7 @@ export function TrendChart({ data }: TrendChartProps) { } return ( -
+
& VariantProps & { - asChild?: boolean + asChild?: boolean; }) { - const Comp = asChild ? Slot : "button" + const Comp = asChild ? Slot : "button"; return ( - ) + ); } -export { Button, buttonVariants } +export { Button, buttonVariants }; diff --git a/src/components/ui/table.tsx b/src/components/ui/table.tsx index 51b74dd..4b3c98e 100644 --- a/src/components/ui/table.tsx +++ b/src/components/ui/table.tsx @@ -1,8 +1,8 @@ -"use client" +"use client"; -import * as React from "react" +import * as React from "react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; function Table({ className, ...props }: React.ComponentProps<"table">) { return ( @@ -16,7 +16,7 @@ function Table({ className, ...props }: React.ComponentProps<"table">) { {...props} />
- ) + ); } function TableHeader({ className, ...props }: React.ComponentProps<"thead">) { @@ -26,7 +26,7 @@ function TableHeader({ className, ...props }: React.ComponentProps<"thead">) { className={cn("[&_tr]:border-b", className)} {...props} /> - ) + ); } function TableBody({ className, ...props }: React.ComponentProps<"tbody">) { @@ -36,7 +36,7 @@ function TableBody({ className, ...props }: React.ComponentProps<"tbody">) { className={cn("[&_tr:last-child]:border-0", className)} {...props} /> - ) + ); } function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) { @@ -45,11 +45,11 @@ function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) { data-slot="table-footer" className={cn( "bg-muted/50 border-t font-medium [&>tr]:last:border-b-0", - className + className, )} {...props} /> - ) + ); } function TableRow({ className, ...props }: React.ComponentProps<"tr">) { @@ -58,11 +58,11 @@ function TableRow({ className, ...props }: React.ComponentProps<"tr">) { data-slot="table-row" className={cn( "hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors", - className + className, )} {...props} /> - ) + ); } function TableHead({ className, ...props }: React.ComponentProps<"th">) { @@ -71,11 +71,11 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) { data-slot="table-head" className={cn( "text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", - className + className, )} {...props} /> - ) + ); } function TableCell({ className, ...props }: React.ComponentProps<"td">) { @@ -84,11 +84,11 @@ function TableCell({ className, ...props }: React.ComponentProps<"td">) { data-slot="table-cell" className={cn( "p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", - className + className, )} {...props} /> - ) + ); } function TableCaption({ @@ -101,7 +101,7 @@ function TableCaption({ className={cn("text-muted-foreground mt-4 text-sm", className)} {...props} /> - ) + ); } export { @@ -113,4 +113,4 @@ export { TableRow, TableCell, TableCaption, -} +}; diff --git a/src/components/ui/textarea.tsx b/src/components/ui/textarea.tsx index 7f21b5e..0735a8c 100644 --- a/src/components/ui/textarea.tsx +++ b/src/components/ui/textarea.tsx @@ -1,6 +1,6 @@ -import * as React from "react" +import * as React from "react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { return ( @@ -8,11 +8,11 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { data-slot="textarea" className={cn( "border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", - className + className, )} {...props} /> - ) + ); } -export { Textarea } +export { Textarea };