style: enhance final dark mode visibiliity for all pages

This commit is contained in:
Mahen 2026-04-13 11:58:08 +07:00
parent f367b1c3ad
commit b73c3669f7
25 changed files with 542 additions and 351 deletions

View File

@ -93,6 +93,12 @@
@apply bg-background text-foreground antialiased;
font-feature-settings: "cv02", "cv03", "cv04", "cv11";
}
body[data-scroll-locked] {
margin-right: 0 !important;
padding-right: 0 !important;
overflow: overlay !important;
}
}
@layer utilities {

View File

@ -2,6 +2,7 @@ import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import Providers from "./providers";
import { ThemeProvider } from "../context/ThemeContext";
export const metadata: Metadata = {
title: "Create Next App",
@ -18,7 +19,9 @@ export default function RootLayout({
return (
<html lang="en">
<body className={`${inter.variable} font-sans`}>
<Providers>{children}</Providers>
<Providers>
<ThemeProvider>{children}</ThemeProvider>
</Providers>
</body>
</html>
);

View File

@ -9,11 +9,5 @@ export default async function Home() {
if (session) {
redirect("/dashboard");
}
return (
<div className="flex min-h-svh w-full bg-[#F8FBFF] items-center justify-center p-6 md:p-10 ">
<div className="w-full max-w-sm ">
<LoginForm />
</div>
</div>
);
return <LoginForm />;
}

View File

@ -1,13 +1,17 @@
import Footer from "@/src/components/dashboards/Footer";
import { Header } from "@/src/components/dashboards/Header";
import ProfileClient from "@/src/components/dashboards/ProfileClient";
import { getAnotherUserData } from "./lib/action";
export default async function ProfilePage() {
const user = await getAnotherUserData();
return (
<div className="min-h-screen bg-[#F8FBFF]">
<Header />
<ProfileClient />
</div>
<ProfileClient
name={user?.name || ""}
bio={user?.bio || "None"}
profession={user?.preference?.profession || ""}
preferenceBrand={user?.preference?.brand?.name || ""}
preferenceOS={user?.preference?.preferredOS || ""}
budgetMax={user?.preference?.budgetMax || 0}
budgetMin={user?.preference?.budgetMin || 0}
/>
);
}

View File

@ -2,46 +2,116 @@
import { signIn } from "next-auth/react";
import { Button } from "../ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "../ui/card";
import { Field, FieldGroup } from "../ui/field";
import { FcGoogle } from "react-icons/fc";
import { BarChart3 } from "lucide-react";
import { BarChart3, Moon, Sun } from "lucide-react";
import { useTheme } from "@/src/context/ThemeContext";
export function LoginForm() {
const { darkMode, toggleDarkMode } = useTheme();
return (
<Card>
<CardHeader className="text-center">
<div className="mx-auto flex h-10 w-10 items-center justify-center rounded-xl bg-primary text-primary-foreground">
<BarChart3 className="h-5 w-5" />
</div>
<CardTitle>Login to SENTILAISES.</CardTitle>
<CardDescription className="mt-2">
Masuk dengan menggunakan akun Google Anda untuk mengakses beranda
SENTILAISES.
</CardDescription>
</CardHeader>
<CardContent>
<form>
<FieldGroup>
<Field>
<Button
variant="outline"
type="button"
onClick={() => signIn("google", { callbackUrl: "/dashboard" })}
<div
className={`${darkMode ? "min-h-screen bg-gray-900" : "min-h-screen bg-[#F8FBFF]"} flex items-center justify-center p-4 w-full mx-auto transition-all duration-500`}
>
<div className="w-full max-w-md">
{/* <div className="flex justify-center gap-3 mb-6">
{[
{ icon: TrendingUp, label: "Real-time Analysis" },
{ icon: Star, label: "Sentiment AI" },
].map(({ icon: Icon, label }) => (
<div
key={label}
className="flex items-center gap-1.5 bg-white border border-border rounded-full px-3 py-1.5 text-xs text-muted-foreground shadow-sm"
>
<Icon className="w-3 h-3 text-primary" />
{label}
</div>
))}
</div> */}
<div
className={`rounded-3xl border border-border overflow-hidden ${darkMode ? "bg-gray-800 text-white border-transparent shadow-lg" : "bg-card shadow-sm"} transition-all duration-500`}
>
<div
className={`h-1 w-full bg-linear-to-r from-primary via-primary/60 to-primary/20 ${darkMode ? "from-white via-white/60 to-white/20" : ""} transition-all duration-500`}
/>
<div className="px-4 py-8 sm:py-8 sm:px-4">
<div className="flex flex-col items-center text-center mb-8">
{darkMode ? (
<Sun
onClick={toggleDarkMode}
className={`h-5 w-5 ml-auto -mt-4 sm:-mt-4 cursor-pointer ${darkMode ? "text-white" : "text-black"} transition-all duration-500`}
/>
) : (
<Moon
onClick={toggleDarkMode}
className={`h-5 w-5 ml-auto -mt-4 sm:-mt-4 cursor-pointer ${darkMode ? "text-white" : "text-black"} transition-all duration-500`}
/>
)}
<div
className={`flex h-14 w-14 items-center justify-center rounded-2xl shadow-md mb-4 ${darkMode ? "bg-gray-900" : "text-primary-foreground bg-primary"} transition-all duration-500`}
>
<FcGoogle />
Login with Google
</Button>
</Field>
</FieldGroup>
</form>
</CardContent>
</Card>
<BarChart3 className="h-7 w-7" />
</div>
<h1 className="text-2xl font-bold tracking-tight text-foreground">
SENTILAISES<span className="text-primary">.</span>
</h1>
<p className="mt-2 text-sm text-muted-foreground max-w-xs leading-relaxed">
Platform analisis sentimen ulasan laptop berbasis AI. Masuk
untuk mulai mengeksplorasi data.
</p>
</div>
<div className="relative mb-6">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-border" />
</div>
<div className="relative flex justify-center text-xs">
<span
className={`bg-muted-background px-3 text-muted-foreground ${darkMode ? "bg-gray-800" : "bg-[#F8FBFF]"} transition-all duration-500`}
>
Masuk dengan
</span>
</div>
</div>
<Button
variant="outline"
type="button"
onClick={() => signIn("google", { callbackUrl: "/dashboard" })}
className={`w-full h-11 rounded-xl border-border transition-all duration-200 font-medium text-sm gap-3 ${darkMode ? "bg-gray-800 text-white" : "bg-white text-black"} transition-all duration-500`}
>
<FcGoogle className="w-5 h-5" />
Lanjutkan dengan Google
</Button>
<p className="mt-6 text-center text-xs text-muted-foreground leading-relaxed">
Dengan masuk, kamu menyetujui{" "}
<span
className={`${darkMode ? "text-white hover:text-white/80" : "text-primary hover:text-primary/80"} underline underline-offset-2 cursor-pointer transition-all duration-500`}
>
Syarat & Ketentuan
</span>{" "}
dan{" "}
<span
className={`${darkMode ? "text-white hover:text-white/80" : "text-primary hover:text-primary/80"} underline underline-offset-2 cursor-pointertransition-all duration-500`}
>
Kebijakan Privasi
</span>{" "}
kami.
</p>
</div>
</div>
<p
className={`text-center text-xs text-muted-foreground mt-5 ${darkMode ? "text-card" : ""} transition-all duration-500`}
>
© {new Date().getFullYear()} SENTILAISES. Dibuat untuk analisis yang
lebih cerdas.
</p>
</div>
</div>
// </div>
// </div>
);
}

View File

@ -28,7 +28,7 @@ export default function AnalysisClient({ isDark }: { isDark: boolean }) {
<div className="w-full mx-auto">
<form
onSubmit={handleSubmit(onSubmit)}
className={`p-6 rounded-lg mb-8 ${isDark ? "bg-gray-800" : "bg-white border border-gray-200"} transition-all duration-500`}
className={`p-6 rounded-lg mb-8 ${isDark ? "bg-gray-800 border-transparent" : "bg-white border border-gray-200"} transition-all duration-500`}
>
<div className="mb-4 flex items-center gap-2">
<Sparkles
@ -125,7 +125,7 @@ export default function AnalysisClient({ isDark }: { isDark: boolean }) {
</div>
<div className="w-full bg-gray-200 rounded-full h-2.5">
<div
className="bg-primary h-2.5 rounded-full transition-all duration-500"
className={`h-2.5 rounded-full transition-all duration-500 ${isDark ? "bg-gray-900" : "bg-primary"}`}
style={{ width: `${progress.percent}%` }}
></div>
</div>
@ -138,7 +138,7 @@ export default function AnalysisClient({ isDark }: { isDark: boolean }) {
type="button"
variant="destructive"
onClick={handleCancel}
className="w-full bg-sentiment-negative text-white md:w-max px-6 py-3 mt-6 rounded-md transition-colors disabled:bg-gray-400"
className="w-full bg-sentiment-negative/10 text-sentiment-negative md:w-max px-6 py-3 mt-6 rounded-md transition-colors disabled:bg-gray-400"
>
<X className="h-4 w-4" />
<span>Cancel</span>
@ -148,7 +148,7 @@ export default function AnalysisClient({ isDark }: { isDark: boolean }) {
type="submit"
hidden={loading}
disabled={!isValid}
className={`w-full md:w-max bg-primary text-white px-6 py-3 mt-6 rounded-md transition-colors disabled:bg-gray-400`}
className={`w-full md:w-max ${isDark ? "bg-gray-900 hover:bg-card hover:text-black" : "bg-primary text-white"} px-6 py-3 mt-6 rounded-md transition-colors disabled:bg-gray-400`}
>
<Sparkles className="h-4 w-4" />
{loading ? "Menganalisis..." : "Analisis Sekarang"}

View File

@ -10,7 +10,7 @@ import {
SelectValue,
} from "../ui/select";
export function BrandFilter() {
export function BrandFilter({ isDark }: { isDark: boolean }) {
const { isLoading, totalCount, selectedBrand, validBrands, handleSelect } =
useBrandFilter();
@ -36,12 +36,12 @@ export function BrandFilter() {
<SelectValue placeholder="Pilih Brand" />
</SelectTrigger>
<SelectContent
className="bg-card shadow-lg"
className={`bg-card shadow-lg ${isDark ? "bg-gray-900 text-white" : "bg-white"}`}
position="popper"
>
<SelectItem
value="__all__"
className="cursor-pointer hover:bg-primary hover:text-card focus:bg-primary focus:text-card"
className={`cursor-pointer hover:bg-primary hover:text-card focus:text-card ${isDark ? "text-white focus:bg-gray-800" : "text-black focus:bg-primary"}`}
>
Semua ({totalCount.toLocaleString()})
</SelectItem>
@ -49,7 +49,7 @@ export function BrandFilter() {
<SelectItem
key={brand.name}
value={brand.name}
className="cursor-pointer hover:bg-primary hover:text-card focus:bg-primary focus:text-card"
className={`cursor-pointer hover:bg-primary hover:text-card focus:text-card ${isDark ? "text-white focus:bg-gray-800" : "text-black focus:bg-primary"}`}
>
{brand.name} ({brand.count.toLocaleString()})
</SelectItem>

View File

@ -7,7 +7,6 @@ import {
Frown,
Meh,
MessageSquareText,
Moon,
Smile,
Sparkles,
} from "lucide-react";
@ -22,6 +21,7 @@ import AnalysisClient from "./AnalysisClient";
import Footer from "./Footer";
import { Button } from "../ui/button";
import ExportExcel from "./ExportExcel";
import { useTheme } from "@/src/context/ThemeContext";
export default function DashboardClient() {
const {
@ -31,15 +31,10 @@ export default function DashboardClient() {
neutralCount,
loading,
modelData,
darkMode,
setDarkMode,
percentage,
scrollToResult,
} = useDashboards();
const toggleDarkMode = () => {
setDarkMode((prevMode) => !prevMode);
};
const { darkMode, toggleDarkMode } = useTheme();
return (
<div
@ -49,8 +44,7 @@ export default function DashboardClient() {
<main className="container mx-auto px-4 py-8">
<div
className="mb-8 rounded-2xl p-8 text-center"
style={{ background: "hsl(var(--primary))" }}
className={`mb-8 rounded-2xl p-8 text-center ${darkMode ? "bg-gray-800 text-white" : "bg-primary"} transition-all duration-500`}
>
<h2 className="mb-2 text-3xl font-bold text-white md:text-4xl">
Analisis Sentimen Ulasan Laptop
@ -62,7 +56,7 @@ export default function DashboardClient() {
<div className="flex items-center justify-center gap-4 text-sm text-white/70">
<Button
onClick={scrollToResult}
className="bg-[#F8FBFF] cursor-pointer hover:bg-card hover:text-primary text-black mt-4"
className={`bg-[#F8FBFF] cursor-pointer hover:bg-card hover:text-primary text-black mt-4 ${darkMode ? "bg-gray-900 text-white" : "bg-white text-black"} transition-all duration-500`}
>
<Sparkles className="h-5 w-5" />
<span>Coba Analisis Sentimen</span>
@ -147,7 +141,7 @@ export default function DashboardClient() {
</div>
{loading ? (
<ModelInfoSkeleton />
<ModelInfoSkeleton isDark={darkMode} />
) : modelData.length > 0 ? (
<ModelInfo data={modelData} isDark={darkMode} />
) : (
@ -168,18 +162,18 @@ export default function DashboardClient() {
<div>
<div className="flex items-center gap-2 mb-2">
<ChartNoAxesGantt className="w-5 h-5" />
<h3 className="text-lg font-semibold">Ulasan Terbaru</h3>
<h3 className="text-lg font-semibold">Riwayat Ulasan</h3>
</div>
<p className="text-sm text-muted-foreground">
Hasil klasifikasi sentimen ulasan produk laptop
</p>
</div>
<div className="flex gap-2 items-center">
<BrandFilter />
<ExportExcel />
<BrandFilter isDark={darkMode} />
<ExportExcel isDark={darkMode} />
</div>
</div>
<ReviewTable />
<ReviewTable isDark={darkMode} />
</div>
<Footer />

View File

@ -4,7 +4,7 @@ import { useReviewTable } from "@/src/hooks/useReviewTable";
import { useSearchParams } from "next/navigation";
import { downloadAllData } from "@/src/services/report.service";
export default function ExportExcel() {
export default function ExportExcel({ isDark }: { isDark: boolean }) {
const searchParams = useSearchParams();
const selectedBrand = searchParams.get("brand");
const { isLoading, data } = useReviewTable(10, selectedBrand);
@ -13,7 +13,7 @@ export default function ExportExcel() {
<Button
onClick={() => downloadAllData(data)}
variant="outline"
className="flex items-center gap-2 border-primary/20 text-primary hover:bg-primary hover:text-card"
className={`flex items-center gap-2 border-primary/20 text-primary hover:bg-primary hover:text-card ${isDark ? "text-white hover:bg-gray-800 border-transparent" : "text-black hover:bg-primary"}`}
disabled={isLoading}
>
<Download className="h-4 w-4" />

View File

@ -5,6 +5,7 @@ import {
Laptop,
LogOut,
Moon,
Sun,
User,
UserCircle,
} from "lucide-react";
@ -21,6 +22,7 @@ import Link from "next/link";
import { useHeader } from "@/src/hooks/useHeader";
import { useDashboards } from "@/src/hooks/useDashboard";
import { useState } from "react";
import { is } from "zod/v4/locales";
export function Header({
onToggle,
@ -40,7 +42,9 @@ export function Header({
<div className="container mx-auto px-4 py-4">
<div className="flex items-center justify-between">
<Link href="/" className="flex items-center gap-3 cursor-pointer">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-primary text-primary-foreground">
<div
className={`flex h-10 w-10 items-center justify-center rounded-xl ${isDark ? "bg-gray-700 text-white" : "bg-primary text-primary-foreground"}`}
>
<BarChart3 className="h-5 w-5" />
</div>
<div>
@ -75,10 +79,12 @@ export function Header({
<DropdownMenuContent
align="end"
className="w-max bg-card border-border shadow-md"
className={`bg-card ${isDark ? "bg-gray-800 text-white" : "bg-white"} transition-all duration-500`}
>
<Link href="/profile">
<DropdownMenuItem className="cursor-pointer gap-2 focus:bg-primary focus:text-card transition-colors hover:text-primary">
<DropdownMenuItem
className={`cursor-pointer gap-2 transition-colors hover:text-primary ${isDark ? "text-white hover:bg-gray-900 hover:text-card" : "text-black hover:bg-primary hover:text-card"} transition-all duration-500`}
>
<UserCircle className="h-4 w-4 text-muted-foreground" />
<span>Menu Profil</span>
</DropdownMenuItem>
@ -87,7 +93,7 @@ export function Header({
<DropdownMenuSeparator className="bg-border" />
<DropdownMenuItem
className="cursor-pointer gap-2 text-destructive focus:bg-red-500 focus:text-card transition-colors"
className={`cursor-pointer gap-2 text-destructive transition-colors ${isDark ? "text-white focus:bg-sentiment-negative/10 focus:text-sentiment-negative" : "text-black focus:bg-sentiment-negative-light focus:text-sentiment-negative"} transition-all duration-500`}
onClick={() => signOut({ callbackUrl: "/" })}
>
<LogOut className="h-4 w-4" />
@ -96,7 +102,17 @@ export function Header({
</DropdownMenuContent>
</DropdownMenu>
</div>
<Moon onClick={onToggle} className="h-4 w-4 cursor-pointer" />
{isDark ? (
<Sun
onClick={onToggle}
className={`h-4 w-4 cursor-pointer ${isDark ? "text-white" : "text-black"} `}
/>
) : (
<Moon
onClick={onToggle}
className={`h-4 w-4 cursor-pointer ${isDark ? "text-white" : "text-black"} `}
/>
)}
</div>
</div>
</div>

View File

@ -54,7 +54,7 @@ export function ModelInfo({
<SelectItem
key={model.modelName + index}
value={index.toString()}
className={`cursor-pointer hover:bg-primary hover:text-card focus:bg-primary focus:text-card ${isDark ? "bg-gray-900 text-white" : "bg-white"} transition-all duration-500`}
className={`cursor-pointer hover:bg-primary hover:text-card focus:bg-primary focus:text-card ${isDark ? "text-white focus:bg-gray-800" : "text-black focus:bg-primary"} transition-all duration-500`}
>
{model.modelName}
</SelectItem>

View File

@ -1,187 +0,0 @@
"use client";
import { motion } from "framer-motion";
import Image from "next/image";
import { Pencil, Wallet, Laptop, User, Monitor, Fan } from "lucide-react";
import { ProfileClientProps } from "@/src/types";
import { Button } from "../ui/button";
import { Separator } from "../ui/separator";
import { formatRupiah, toTitleCase } from "@/src/utils/datas";
import { brandItems, OSItems, professionItems } from "@/src/utils/const";
import { ProfileModal } from "./ProfileModal";
import { useProfileClient } from "@/src/hooks/useProfileClient";
export default function ProfileCard(props: ProfileClientProps) {
const {
session,
router,
showModal,
name,
bio,
profession,
brands,
preferenceOS,
profileDatas,
budgetMin,
budgetMax,
setShowModal,
handleOptimisticUpdate,
} = useProfileClient(props);
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, ease: "circOut" }}
className="mx-auto w-full max-w-xl overflow-hidden rounded-2xl border bg-card mt-4"
>
<div className="relative bg-linier-to-r from-primary/5 via-primary/10 to-transparent p-6 sm:p-8">
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<div className="flex items-center gap-5">
<div className="relative">
<Image
src={session?.data?.user?.image ?? "/default-avatar.svg"}
alt="User Avatar"
width={88}
height={88}
className="h-20 w-20 rounded-full border-4 border-background object-cover shadow-sm"
/>
<div className="absolute bottom-1 right-1 h-4 w-4 rounded-full border-2 border-background bg-green-500"></div>
</div>
<div>
{name && (
<h1 className="text-2xl font-bold text-card-foreground tracking-tight">
{/* {session?.data?.user?.name || "Guest User"} */}
{name || "Guest User"}
</h1>
)}
<p className="text-sm font-medium text-muted-foreground mb-2">
{session?.data?.user?.email || "Belum ada email"}
</p>
{profession && (
<span className="inline-flex items-center gap-1.5 rounded-full bg-primary/10 px-2.5 py-0.5 text-xs font-semibold text-primary">
<Fan className="w-3.5 h-3.5" />
<span className="capitalize">{toTitleCase(profession)}</span>
</span>
)}
</div>
</div>
<Button
size="sm"
className="w-full sm:w-auto gap-2 shadow-sm"
onClick={() => setShowModal(true)}
>
<Pencil className="h-4 w-4" />
Edit Profile
</Button>
</div>
</div>
<Separator />
<div className="p-6 sm:p-8 space-y-6">
<div className="space-y-2 -mt-2">
<div className="flex items-center gap-1 text-sm font-semibold text-muted-foreground">
<User className="w-4 h-4" />
Tentang Saya
</div>
<div className="rounded-xl bg-muted/50 p-4 border border-muted">
<p
className={`text-sm ${!bio ? "text-gray-400" : "text-card-foreground"}`}
>
{bio ||
"Belum ada deskripsi profil. Ceritakan sedikit tentang aktivitas dan kebutuhan laptop Anda."}
</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-5 rounded-xl border p-5">
<div>
<div className="flex items-center gap-2 text-sm font-semibold text-muted-foreground mb-3">
<Laptop className="w-4 h-4 text-primary" />
Preferensi Merek
</div>
<div className="flex flex-wrap gap-2">
{brands.length > 0 ? (
brands.map((brand, i) => (
<span
key={i}
className="rounded-full bg-secondary px-2.5 py-1 text-xs font-semibold text-secondary-foreground border"
>
{brand}
</span>
))
) : (
<span className="bg-card rounded-full text-sm text-gray-400 border px-3 py-1">
None
</span>
)}
</div>
</div>
<Separator />
<div>
<div className="flex items-center gap-2 text-sm font-semibold text-muted-foreground mb-3">
<Monitor className="w-4 h-4 text-primary" />
Sistem Operasi
</div>
<div>
{preferenceOS ? (
<span className="rounded-full bg-secondary px-2.5 py-1 text-xs font-semibold text-secondary-foreground border">
{preferenceOS}
</span>
) : (
<span className="bg-card rounded-full text-sm text-gray-400 border px-3 py-1">
None
</span>
)}
</div>
</div>
</div>
<div className="space-y-3 rounded-xl border bg-linier-to-br from-green-50 to-emerald-50/30 p-5">
<div className="flex items-center gap-2 text-sm font-semibold">
<Wallet className="w-4 h-4" />
Rentang Anggaran
</div>
{budgetMin || budgetMax ? (
<div className="flex flex-col justify-center h-[calc(100%-2rem)]">
<p className="text-sm text-muted-foreground mb-1">Dari</p>
<p className="text-xl font-semibold">
{formatRupiah(budgetMin)}
</p>
<div className="my-2 h-px w-full bg-border"></div>
<p className="text-sm text-muted-foreground mb-1">Hingga</p>
<p className="text-xl font-semibold text-green-600">
{formatRupiah(budgetMax)}
</p>
</div>
) : (
<div className="flex h-[calc(100%-2rem)] justify-center items-center">
<p className="text-sm text-gray-400">Anggaran Belum Diatur.</p>
</div>
)}
</div>
</div>
</div>
{showModal && (
<ProfileModal
setShowModal={setShowModal}
professionItems={professionItems}
brandItems={brandItems}
OSItems={OSItems}
userData={profileDatas}
onOptimisticUpdate={handleOptimisticUpdate}
router={router}
/>
)}
</motion.div>
);
}

View File

@ -1,34 +1,236 @@
"use client";
import { ArrowLeft } from "lucide-react";
import Link from "next/link";
import { getAnotherUserData } from "@/src/app/profile/lib/action";
import ProfileCard from "./ProfileCard";
import Footer from "./Footer";
import { motion } from "framer-motion";
import Image from "next/image";
import { Pencil, Wallet, Laptop, User, Monitor, Fan } from "lucide-react";
import { ProfileClientProps } from "@/src/types";
import { Button } from "../ui/button";
import { Separator } from "../ui/separator";
import { formatRupiah, toTitleCase } from "@/src/utils/datas";
import { brandItems, OSItems, professionItems } from "@/src/utils/const";
import { ProfileModal } from "./ProfileModal";
import { useProfileClient } from "@/src/hooks/useProfileClient";
import { Header } from "./Header";
import { useDashboards } from "@/src/hooks/useDashboard";
import { useTheme } from "@/src/context/ThemeContext";
export default async function ProfileClient() {
const user = await getAnotherUserData();
export default function ProfileClient(props: ProfileClientProps) {
const {
session,
router,
showModal,
name,
bio,
profession,
brands,
preferenceOS,
profileDatas,
budgetMin,
budgetMax,
setShowModal,
handleOptimisticUpdate,
} = useProfileClient(props);
const { darkMode, toggleDarkMode, mounted } = useTheme();
const isDark = mounted && darkMode;
return (
<div className="container mx-auto px-4 py-8">
<div className="flex max-w-xl mx-auto">
<Link
href="/"
className="flex items-center gap-2 text-md text-primary max-w-xl w-max mr-auto hover:text-primary/80 transition-colors"
>
<ArrowLeft className="w-4 h-4" />
<span>Back to Dashboard</span>
</Link>
</div>
<div
className={`min-h-screen bg-[#F8FBFF] ${isDark ? "bg-gray-900 text-white" : "bg-[#F8FBFF]"} transition-all duration-500`}
>
<Header onToggle={toggleDarkMode} isDark={isDark} />
<ProfileCard
name={user?.name || ""}
bio={user?.bio || "None"}
profession={user?.preference?.profession || ""}
preferenceBrand={user?.preference?.brand?.name || ""}
preferenceOS={user?.preference?.preferredOS || ""}
budgetMax={user?.preference?.budgetMax || 0}
budgetMin={user?.preference?.budgetMin || 0}
/>
<Footer />
<div className="container mx-auto px-4 py-8">
<div className="flex max-w-xl mx-auto">
<Link
href="/"
className="flex items-center gap-2 text-md text-primary max-w-xl w-max mr-auto hover:text-primary/80 transition-all duration-500"
>
<ArrowLeft
className={`w-4 h-4 ${isDark ? "text-white" : "text-primary"} transition-all duration-500`}
/>
<span
className={`text-md ${isDark ? "text-white" : "text-primary"} transition-all duration-500`}
>
Back to Dashboard
</span>
</Link>
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, ease: "circOut" }}
className={`${isDark ? "bg-gray-800" : "bg-card"} mx-auto w-full max-w-xl overflow-hidden rounded-2xl border mt-4 transition-all duration-500`}
>
<div className="relative bg-linier-to-r from-primary/5 via-primary/10 to-transparent p-6 sm:p-8">
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<div className="flex items-center gap-5">
<div className="relative">
<Image
src={session?.data?.user?.image ?? "/default-avatar.svg"}
alt="User Avatar"
width={88}
height={88}
className={`h-20 w-20 rounded-full border-4 object-cover shadow-sm ${isDark ? "border-gray-900" : "border-card"} transition-all duration-500`}
/>
<div
className={`absolute bottom-1 right-1 h-4 w-4 rounded-full border-2 bg-green-500 ${isDark ? "border-gray-900" : "border-card"} transition-all duration-500s`}
></div>
</div>
<div>
{name && (
<h1
className={`text-2xl font-bold ${isDark ? "text-white" : "text-card-foreground"} tracking-tight transition-all duration-500`}
>
{name || "Guest User"}
</h1>
)}
<p
className={`text-sm font-medium ${isDark ? "text-gray-400" : "text-muted-foreground"} mb-2 transition-all duration-500`}
>
{session?.data?.user?.email || "Belum ada email"}
</p>
{profession && (
<span
className={`inline-flex items-center gap-1.5 rounded-full ${isDark ? "bg-gray-900 text-white" : "bg-primary/10"} px-2.5 py-0.5 text-xs font-semibold ${isDark ? "text-primary" : "text-primary"} transition-all duration-500`}
>
<Fan className="w-3.5 h-3.5" />
<span className="capitalize">
{toTitleCase(profession)}
</span>
</span>
)}
</div>
</div>
<Button
size="sm"
className={`${isDark ? "bg-gray-900 hover:bg-gray-200 hover:text-black" : "bg-primary hover:bg-primary/80"} w-full sm:w-auto gap-2 shadow-sm`}
onClick={() => setShowModal(true)}
>
<Pencil className="h-4 w-4" />
Edit Profile
</Button>
</div>
</div>
<Separator />
<div className="p-6 sm:p-8 space-y-6">
<div className="space-y-2 -mt-2">
<div className="flex items-center gap-1 text-sm font-semibold text-muted-foreground">
<User className="w-4 h-4" />
Tentang Saya
</div>
<div className="rounded-xl bg-muted/50 p-4 border border-muted">
<p
className={`text-sm ${!bio ? "text-gray-400" : "text-card-foreground"} ${isDark ? "text-gray-400" : "text-card-foreground"} transition-all duration-500`}
>
{bio ||
"Belum ada deskripsi profil. Ceritakan sedikit tentang aktivitas dan kebutuhan laptop Anda."}
</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-5 rounded-xl border p-5">
<div>
<div className="flex items-center gap-2 text-sm font-semibold text-muted-foreground mb-3">
<Laptop
className={`w-4 h-4 text-primary ${isDark ? "text-white" : "text-muted-foreground"} transition-all duration-500`}
/>
Preferensi Merek
</div>
<div className="flex flex-wrap gap-2">
{brands.length > 0 ? (
brands.map((brand, i) => (
<span
key={i}
className="rounded-full bg-secondary px-2.5 py-1 text-xs font-semibold text-secondary-foreground border"
>
{brand}
</span>
))
) : (
<span className="bg-card rounded-full text-sm text-gray-400 border px-3 py-1">
None
</span>
)}
</div>
</div>
<Separator />
<div>
<div className="flex items-center gap-2 text-sm font-semibold text-muted-foreground mb-3">
<Monitor
className={`w-4 h-4 text-primary ${isDark ? "text-white" : "text-muted-foreground"} transition-all duration-500`}
/>
Sistem Operasi
</div>
<div>
{preferenceOS ? (
<span className="rounded-full bg-secondary px-2.5 py-1 text-xs font-semibold text-secondary-foreground border">
{preferenceOS}
</span>
) : (
<span className="bg-card rounded-full text-sm text-gray-400 border px-3 py-1">
None
</span>
)}
</div>
</div>
</div>
<div className="space-y-3 rounded-xl border bg-linier-to-br from-green-50 to-emerald-50/30 p-5">
<div className="flex items-center gap-2 text-sm font-semibold">
<Wallet className="w-4 h-4" />
Rentang Anggaran
</div>
{budgetMin || budgetMax ? (
<div className="flex flex-col justify-center h-[calc(100%-2rem)]">
<p className="text-sm text-muted-foreground mb-1">Dari</p>
<p className="text-xl font-semibold">
{formatRupiah(budgetMin)}
</p>
<div className="my-2 h-px w-full bg-border"></div>
<p className="text-sm text-muted-foreground mb-1">Hingga</p>
<p className="text-xl font-semibold text-green-600">
{formatRupiah(budgetMax)}
</p>
</div>
) : (
<div className="flex h-[calc(100%-2rem)] justify-center items-center">
<p className="text-sm text-gray-400">
Anggaran Belum Diatur.
</p>
</div>
)}
</div>
</div>
</div>
{showModal && (
<ProfileModal
setShowModal={setShowModal}
professionItems={professionItems}
brandItems={brandItems}
OSItems={OSItems}
userData={profileDatas}
onOptimisticUpdate={handleOptimisticUpdate}
router={router}
darkMode={isDark}
/>
)}
</motion.div>
<Footer />
</div>
</div>
);
}

View File

@ -15,6 +15,7 @@ import {
} from "../ui/select";
import { Button } from "../ui/button";
import { useProfileModal } from "@/src/hooks/useProfileModal";
import { useEffect } from "react";
export const ProfileModal = ({
setShowModal,
@ -24,6 +25,7 @@ export const ProfileModal = ({
userData,
onOptimisticUpdate,
router,
darkMode,
}: ExtendedModalProps) => {
const { control, errors, isSubmitting, onSubmit, register, handleSubmit } =
useProfileModal({
@ -33,16 +35,36 @@ export const ProfileModal = ({
setShowModal,
});
useEffect(() => {
const observer = new MutationObserver(() => {
if (document.body.style.marginRight) {
document.body.style.marginRight = "0px";
}
if (document.body.style.paddingRight) {
document.body.style.paddingRight = "0px";
}
});
observer.observe(document.body, {
attributes: true,
attributeFilter: ["style"],
});
return () => observer.disconnect();
}, []);
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2, ease: "circOut" }}
className="fixed inset-0 flex items-center justify-center bg-primary/30 z-1"
className={`${darkMode ? "bg-gray-500/20" : "bg-primary/30"} fixed inset-0 flex items-center justify-center z-10 backdrop-blur-xs`}
onMouseDown={(e) => e.stopPropagation()}
style={{ isolation: "isolate" }}
>
<form
onSubmit={handleSubmit(onSubmit)}
className="flex flex-col bg-card w-xs sm:w-sm lg:w-lg md:w-md p-6 rounded-2xl border relative gap-4 "
className={`flex flex-col w-xs sm:w-sm lg:w-lg md:w-md p-6 rounded-2xl border relative gap-4 max-h-[90vh] overflow-y-auto ${darkMode ? "bg-gray-800 border-gray-600" : "bg-card border-border"}`}
>
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
@ -93,12 +115,12 @@ export const ProfileModal = ({
onValueChange={field.onChange}
>
<SelectTrigger
className={`w-full ${!field.value ? "text-gray-500" : "text-black"}`}
className={`w-full ${!field.value ? "text-gray-500" : "text-black"} ${darkMode ? "text-card" : "bg-white"} border border-gray-200 transition-all duration-500`}
>
<SelectValue placeholder="Pilih Profesi/Kebutuhan" />
</SelectTrigger>
<SelectContent
className="bg-card border-border shadow-lg"
className={`${darkMode ? "bg-gray-900 text-white" : "bg-white"}`}
position="popper"
>
{professionItems.map((item) => {
@ -107,7 +129,7 @@ export const ProfileModal = ({
<SelectItem
key={item.value}
value={item.value}
className="cursor-pointer hover:bg-primary hover:text-card focus:bg-primary focus:text-card"
className={`cursor-pointer hover:bg-primary hover:text-card focus:bg-primary focus:text-card ${darkMode ? "text-white focus:bg-gray-800" : "text-black focus:bg-primary"} transition-all duration-500`}
>
<div className="flex gap-2 items-center">
<PIcon className="h-4 w-4 text-muted-foreground" />
@ -139,14 +161,12 @@ export const ProfileModal = ({
onValueChange={field.onChange}
>
<SelectTrigger
className={`w-full ${
!field.value ? "text-gray-500" : "text-black"
}`}
className={`w-full ${!field.value ? "text-gray-500" : "text-black"} ${darkMode ? "text-card" : "bg-white"} border border-gray-200 transition-all duration-500`}
>
<SelectValue placeholder="Pilih Merek Laptop" />
</SelectTrigger>
<SelectContent
className="bg-card border-border shadow-lg"
className={`${darkMode ? "bg-gray-900 text-white" : "bg-white"}`}
position="popper"
>
{brandItems.map((item) => {
@ -155,7 +175,7 @@ export const ProfileModal = ({
<SelectItem
key={item.value}
value={item.value}
className="cursor-pointer hover:bg-primary hover:text-card focus:bg-primary focus:text-card"
className={`cursor-pointer hover:bg-primary hover:text-card focus:bg-primary focus:text-card ${darkMode ? "text-white focus:bg-gray-800" : "text-black focus:bg-primary"} transition-all duration-500`}
>
<div className="flex gap-2 items-center">
<PIcon className="h-4 w-4 text-muted-foreground" />
@ -189,14 +209,12 @@ export const ProfileModal = ({
onValueChange={field.onChange}
>
<SelectTrigger
className={`w-full ${
!field.value ? "text-gray-500" : "text-black"
}`}
className={`w-full ${!field.value ? "text-gray-500" : "text-black"} ${darkMode ? "text-card" : "bg-white"} border border-gray-200 transition-all duration-500`}
>
<SelectValue placeholder="Pilih Sistem Operasi" />
</SelectTrigger>
<SelectContent
className="bg-card border-border shadow-lg"
className={`${darkMode ? "bg-gray-900 text-white" : "bg-white"}`}
position="popper"
>
{OSItems.map((item) => {
@ -205,7 +223,7 @@ export const ProfileModal = ({
<SelectItem
key={item.value}
value={item.value}
className="cursor-pointer hover:bg-primary hover:text-card focus:bg-primary focus:text-card"
className={`cursor-pointer hover:bg-primary hover:text-card focus:bg-primary focus:text-card ${darkMode ? "text-white focus:bg-gray-800" : "text-black focus:bg-primary"} transition-all duration-500`}
>
<div className="flex gap-2 items-center">
<PIcon className="h-4 w-4 text-muted-foreground" />
@ -273,7 +291,11 @@ export const ProfileModal = ({
<X className="mr-2" />
<span>Cancel</span>
</Button>
<Button type="submit" disabled={isSubmitting}>
<Button
type="submit"
disabled={isSubmitting}
className={`${darkMode ? "bg-gray-900 hover:bg-card hover:text-black" : "bg-primary text-white"} transition-all duration-500`}
>
<Save className="mr-2" />
<span>Simpan</span>
</Button>

View File

@ -24,7 +24,7 @@ import {
import { useSearchParams } from "next/navigation";
import { getVisiblePages } from "@/src/utils/datas";
export function ReviewTable() {
export function ReviewTable({ isDark }: { isDark: boolean }) {
const searchParams = useSearchParams();
const selectedBrand = searchParams.get("brand");
const { currentData, isLoading, pagination } = useReviewTable(
@ -36,8 +36,10 @@ export function ReviewTable() {
if (isLoading) {
return (
<div className="flex h-75 w-full flex-col items-center justify-center gap-2 rounded-xl border bg-card text-muted-foreground">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
<div
className={`flex h-75 w-full flex-col items-center justify-center gap-2 rounded-xl border bg-card text-muted-foreground ${isDark ? "bg-gray-800 border-transparent" : "border-gray-200"} transition-all duration-500`}
>
<Loader2 className={`h-8 w-8 animate-spin ${isDark ? "text-white" : "text-black"}`} />
<p className="text-sm">Memuat data ulasan...</p>
</div>
);
@ -45,10 +47,14 @@ export function ReviewTable() {
return (
<div className="space-y-4">
<div className="rounded-xl border border-gray-200 bg-card">
<Table>
<div
className={`rounded-xl border ${isDark ? " bg-gray-800 border-transparent" : "border-gray-200 bg-card"} transition-all duration-500`}
>
<Table className="transition-all duration-500">
<TableHeader>
<TableRow className="hover:bg-transparent">
<TableRow
className={`hover:bg-transparent transition-all duration-500 ${isDark ? "text-white" : "text-neutral"}`}
>
<TableHead className="w-62.5">Produk</TableHead>
<TableHead className="w-auto min-w-75 text-center">
Ulasan & Kata Kunci
@ -90,11 +96,13 @@ export function ReviewTable() {
<TableCell className="align-top">
<div className="flex flex-col gap-1.5">
<div className="flex items-center gap-2">
<span className="inline-flex items-center rounded-md bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary ring-1 ring-inset ring-primary/20">
<span
className={`inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium ${isDark ? "bg-gray-900 text-white ring-1 ring-inset ring-primary/20" : "bg-primary/10 text-primary ring-1 ring-inset ring-primary/20"} transition-all duration-500`}
>
{review.product?.brand?.name || "Generic"}
</span>
</div>
<span className="text-sm font-medium leading-tight text-foreground line-clamp-2">
<span className="text-sm font-medium leading-tight text-foreground line-clamp-2 transition-all duration-500">
{review.product?.name || "Unknown Product"}
</span>
</div>
@ -102,7 +110,7 @@ export function ReviewTable() {
<TableCell className="align-top">
<div className="flex flex-col gap-3">
<p className="text-sm leading-relaxed text-muted-foreground line-clamp-3 group-hover:text-foreground transition-colors">
<p className="text-sm leading-relaxed text-muted-foreground line-clamp-3 group-hover:text-foreground transition-all duration-500">
{review.content}
</p>
@ -112,7 +120,7 @@ export function ReviewTable() {
<Badge
key={i}
variant="secondary"
className="h-5 px-1.5 text-[10px] font-normal text-muted-foreground border-border bg-muted group-hover:bg-background transition-all"
className="h-5 px-1.5 text-[10px] font-normal text-muted-foreground border-border bg-muted group-hover:bg-background transition-all duration-500"
>
{k}
</Badge>
@ -123,7 +131,7 @@ export function ReviewTable() {
</TableCell>
<TableCell className="align-top whitespace-nowrap">
<span className="text-xs text-muted-foreground font-medium">
<span className="text-xs text-muted-foreground font-medium transition-all duration-500">
{review.createdAt
? new Date(review.createdAt).toLocaleDateString(
"id-ID",
@ -138,11 +146,11 @@ export function ReviewTable() {
</TableCell>
<TableCell className="align-top text-center">
{getSentimentBadge(review.sentiment ?? null)}
{getSentimentBadge(review.sentiment ?? null, isDark)}
</TableCell>
<TableCell className="align-top text-center">
<span className="font-mono text-sm font-semibold text-foreground">
<span className="font-mono text-sm font-semibold text-foreground transition-all duration-500">
{review.confidenceScore
? `${(review.confidenceScore * 100).toFixed(1)}%`
: "-"}
@ -168,7 +176,7 @@ export function ReviewTable() {
className={
currentPage === 1
? "pointer-events-none opacity-50"
: "cursor-pointer hover:bg-[#F8FBFF] hover:text-primary"
: `text-card cursor-pointer hover:bg-[#F8FBFF] hover:text-primary ${isDark ? "hover:bg-gray-900 hover:text-card" : "text-black"} transition-all duration-500`
}
/>
</PaginationItem>
@ -185,6 +193,7 @@ export function ReviewTable() {
pagination.goToPage(page as number);
}}
isActive={currentPage === page}
className={`transition-all duration-500 ${isDark ? "text-white hover:bg-gray-200 hover:text-black focus:bg-gray-200 focus:text-black" : "text-black hover:bg-primary hover:text-card bg-transparent"}`}
>
{page}
</PaginationLink>
@ -202,7 +211,7 @@ export function ReviewTable() {
className={
currentPage === totalPages
? "pointer-events-none opacity-50"
: "cursor-pointer hover:bg-primary hover:text-card"
: `cursor-pointer ${isDark ? "text-white hover:bg-gray-200 hover:text-black" : "text-black hover:bg-primary hover:text-card"} transition-all duration-500`
}
/>
</PaginationItem>

View File

@ -2,11 +2,11 @@ import { Review } from "@/src/types";
import { Badge } from "../ui/badge";
import { cn } from "@/lib/utils";
const getSentimentBadge = (sentiment: Review["sentiment"]) => {
const getSentimentBadge = (sentiment: Review["sentiment"], isDark: boolean) => {
const styles: Record<Review["sentiment"], string> = {
POSITIVE: "sentiment-positive",
NEGATIVE: "sentiment-negative",
NEUTRAL: "sentiment-neutral",
POSITIVE: ` ${isDark ? "text-sentiment-positive bg-sentiment-positive/10" : "text-sentiment-positive bg-sentiment-positive/10"}`,
NEGATIVE: ` ${isDark ? "text-sentiment-negative bg-sentiment-negative/10" : "text-sentiment-negative bg-sentiment-negative/10"}`,
NEUTRAL: ` ${isDark ? "text-sentiment-neutral bg-sentiment-neutral/10" : "text-sentiment-neutral bg-sentiment-neutral/10"}`,
};
const labels: Record<Review["sentiment"], string> = {

View File

@ -10,7 +10,9 @@ export function WordCloud({ isDark }: { isDark: boolean }) {
return (
<div className="flex flex-wrap items-center justify-center gap-2 p-4">
{isEmpty ? (
<div className="flex flex-col gap-2 items-center py-22 text-muted-foreground">
<div
className={`flex flex-col gap-2 items-center py-22 ${isDark ? "text-gray-400" : "text-muted-foreground"}`}
>
<div className="rounded-full bg-muted">
<Inbox className="h-8 w-8" />
</div>

View File

@ -1,8 +1,10 @@
import { ChevronDown } from "lucide-react";
export function ModelInfoSkeleton() {
export function ModelInfoSkeleton({ isDark }: { isDark: boolean }) {
return (
<div className="rounded-xl border bg-card p-6">
<div
className={`rounded-xl border ${isDark ? "bg-gray-800 border-none" : "bg-card"} p-6`}
>
<div className="mb-4 flex items-center justify-between gap-4">
<div className="flex items-center gap-3">
<div className="flex items-center pr-3 h-9 w-64 rounded-md border border-border">
@ -10,12 +12,18 @@ export function ModelInfoSkeleton() {
</div>
</div>
<div className="h-4.5 w-13.5 rounded-full bg-gray-200" />
<div
className={`h-4.5 w-13.5 rounded-full bg-gray-200 ${isDark ? "bg-gray-600" : ""}`}
/>
</div>
<div className="mb-6 min-h-10 space-y-2">
<div className="h-4 w-3/4 rounded bg-gray-100" />
<div className="h-4 w-1/2 rounded bg-gray-100" />
<div
className={`h-4 w-3/4 rounded bg-gray-100 ${isDark ? "bg-gray-600" : ""}`}
/>
<div
className={`h-4 w-1/2 rounded bg-gray-100 ${isDark ? "bg-gray-600" : ""}`}
/>
</div>
<div className="grid grid-cols-2 gap-4 mb-2">
@ -25,25 +33,32 @@ export function ModelInfoSkeleton() {
className="flex items-center gap-3 rounded-lg border border-border/40 bg-secondary/50 p-3"
>
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-muted/60">
<div className="h-7 w-7 rounded bg-gray-200" />
<div
className={`h-7 w-7 rounded bg-gray-200 ${isDark ? "bg-gray-600" : ""}`}
/>
</div>
<div className="flex-1 space-y-2">
<div className="h-3 w-20 rounded bg-gray-100" />
<div className="h-4 w-15 rounded bg-gray-200" />
<div
className={`h-3 w-20 rounded bg-gray-100 ${isDark ? "bg-gray-600" : ""}`}
/>
<div
className={`h-4 w-15 rounded bg-gray-200 ${isDark ? "bg-gray-600" : ""}`}
/>
</div>
</div>
))}
</div>
{/* Footer info */}
<div className="mt-8 space-y-3 border-t pt-4">
{[1, 2, 3].map((i) => (
<div key={i} className="flex items-center justify-between">
{/* label */}
<div className="h-4 w-32 rounded bg-gray-100" />
{/* value */}
<div className="h-4 w-44 rounded bg-gray-100" />
<div
className={`h-4 w-32 rounded bg-gray-100 ${isDark ? "bg-gray-600" : ""}`}
/>
<div
className={`h-4 w-44 rounded bg-gray-100 ${isDark ? "bg-gray-600" : ""}`}
/>
</div>
))}
</div>

View File

@ -19,7 +19,9 @@ const buttonVariants = cva(
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
primary: "bg-primary text-card"
primary: "bg-primary text-card",
darkMode:
"bg-gray-800 text-white hover:bg-gray-900 border border-gray-700",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",

View File

@ -55,7 +55,7 @@ function PaginationLink({
data-active={isActive}
className={cn(
buttonVariants({
variant: isActive ? "primary" : "ghost",
variant: isActive ? "darkMode" : "ghost",
size,
}),
className,

View File

@ -44,7 +44,7 @@ function SelectTrigger({
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
<ChevronDownIcon className="size-4" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
);

View File

@ -0,0 +1,41 @@
"use client";
import { createContext, useContext, useEffect, useState } from "react";
interface ThemeContextType {
darkMode: boolean;
toggleDarkMode: () => void;
mounted: boolean;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
const [darkMode, setDarkMode] = useState(false);
const [mounted, setMounted] = useState(false);
useEffect(() => {
const stored = localStorage.getItem("theme");
setDarkMode(stored === "dark");
setMounted(true);
}, []);
useEffect(() => {
if (!mounted) return;
localStorage.setItem("theme", darkMode ? "dark" : "light");
}, [darkMode, mounted]);
const toggleDarkMode = () => setDarkMode((prev) => !prev);
return (
<ThemeContext.Provider value={{ darkMode, toggleDarkMode, mounted }}>
{children}
</ThemeContext.Provider>
);
};
export const useTheme = () => {
const context = useContext(ThemeContext);
if (!context) throw new Error("useTheme must be used within ThemeProvider");
return context;
};

View File

@ -16,7 +16,6 @@ export const useDashboards = () => {
negative: 0,
neutral: 0,
});
const [darkMode, setDarkMode] = useState(false);
useEffect(() => {
async function fetchStats() {
@ -85,8 +84,6 @@ export const useDashboards = () => {
selectedBrand,
loading,
modelData,
darkMode,
setDarkMode,
setSelectedBrand,
percentage,
scrollToResult,

View File

@ -299,6 +299,7 @@ export interface ExtendedModalProps extends ProfileModalProps {
userData: any;
onOptimisticUpdate: (data: ProfileFormData) => void;
router: any;
darkMode: boolean;
}
export interface ProfileState {

View File

@ -19,11 +19,11 @@ export const setWordCloud = ({ maxValue, minValue }: WordCloudConfig) => {
const getColor = (sentiment: WordItem["sentiment"]) => {
switch (sentiment) {
case "POSITIVE":
return "text-sentiment-positive hover:bg-sentiment-positive-light";
return "text-sentiment-positive hover:bg-sentiment-positive/10";
case "NEGATIVE":
return "text-sentiment-negative hover:bg-sentiment-negative-light";
return "text-sentiment-negative hover:bg-sentiment-negative/10";
case "NEUTRAL":
return "text-sentiment-neutral hover:bg-sentiment-neutral-light";
return "text-sentiment-neutral hover:bg-sentiment-neutral/10";
default:
return "hover:bg-primary hover:text-card";
}