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.
-
-
-
-
-
-
- );
+ 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 */}
-
-
-
- );
+ 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.
+
+
+
+
+
+
+ );
+}
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 */}
+
+
+
+ );
+}
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 };