style: enhance final dark mode visibiliity for all pages
This commit is contained in:
parent
f367b1c3ad
commit
b73c3669f7
|
|
@ -93,6 +93,12 @@
|
||||||
@apply bg-background text-foreground antialiased;
|
@apply bg-background text-foreground antialiased;
|
||||||
font-feature-settings: "cv02", "cv03", "cv04", "cv11";
|
font-feature-settings: "cv02", "cv03", "cv04", "cv11";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body[data-scroll-locked] {
|
||||||
|
margin-right: 0 !important;
|
||||||
|
padding-right: 0 !important;
|
||||||
|
overflow: overlay !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import type { Metadata } from "next";
|
||||||
import { Inter } from "next/font/google";
|
import { Inter } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import Providers from "./providers";
|
import Providers from "./providers";
|
||||||
|
import { ThemeProvider } from "../context/ThemeContext";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Create Next App",
|
title: "Create Next App",
|
||||||
|
|
@ -18,7 +19,9 @@ export default function RootLayout({
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body className={`${inter.variable} font-sans`}>
|
<body className={`${inter.variable} font-sans`}>
|
||||||
<Providers>{children}</Providers>
|
<Providers>
|
||||||
|
<ThemeProvider>{children}</ThemeProvider>
|
||||||
|
</Providers>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,5 @@ export default async function Home() {
|
||||||
if (session) {
|
if (session) {
|
||||||
redirect("/dashboard");
|
redirect("/dashboard");
|
||||||
}
|
}
|
||||||
return (
|
return <LoginForm />;
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 ProfileClient from "@/src/components/dashboards/ProfileClient";
|
||||||
|
import { getAnotherUserData } from "./lib/action";
|
||||||
|
|
||||||
export default async function ProfilePage() {
|
export default async function ProfilePage() {
|
||||||
|
const user = await getAnotherUserData();
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-[#F8FBFF]">
|
<ProfileClient
|
||||||
<Header />
|
name={user?.name || ""}
|
||||||
<ProfileClient />
|
bio={user?.bio || "None"}
|
||||||
|
profession={user?.preference?.profession || ""}
|
||||||
</div>
|
preferenceBrand={user?.preference?.brand?.name || ""}
|
||||||
|
preferenceOS={user?.preference?.preferredOS || ""}
|
||||||
|
budgetMax={user?.preference?.budgetMax || 0}
|
||||||
|
budgetMin={user?.preference?.budgetMin || 0}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,46 +2,116 @@
|
||||||
|
|
||||||
import { signIn } from "next-auth/react";
|
import { signIn } from "next-auth/react";
|
||||||
import { Button } from "../ui/button";
|
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 { 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() {
|
export function LoginForm() {
|
||||||
|
const { darkMode, toggleDarkMode } = useTheme();
|
||||||
return (
|
return (
|
||||||
<Card>
|
<div
|
||||||
<CardHeader className="text-center">
|
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="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 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>
|
||||||
<CardTitle>Login to SENTILAISES.</CardTitle>
|
))}
|
||||||
<CardDescription className="mt-2">
|
</div> */}
|
||||||
Masuk dengan menggunakan akun Google Anda untuk mengakses beranda
|
|
||||||
SENTILAISES.
|
<div
|
||||||
</CardDescription>
|
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`}
|
||||||
</CardHeader>
|
>
|
||||||
<CardContent>
|
<div
|
||||||
<form>
|
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`}
|
||||||
<FieldGroup>
|
/>
|
||||||
<Field>
|
|
||||||
|
<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`}
|
||||||
|
>
|
||||||
|
<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
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => signIn("google", { callbackUrl: "/dashboard" })}
|
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 />
|
<FcGoogle className="w-5 h-5" />
|
||||||
Login with Google
|
Lanjutkan dengan Google
|
||||||
</Button>
|
</Button>
|
||||||
</Field>
|
|
||||||
</FieldGroup>
|
<p className="mt-6 text-center text-xs text-muted-foreground leading-relaxed">
|
||||||
</form>
|
Dengan masuk, kamu menyetujui{" "}
|
||||||
</CardContent>
|
<span
|
||||||
</Card>
|
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ export default function AnalysisClient({ isDark }: { isDark: boolean }) {
|
||||||
<div className="w-full mx-auto">
|
<div className="w-full mx-auto">
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
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">
|
<div className="mb-4 flex items-center gap-2">
|
||||||
<Sparkles
|
<Sparkles
|
||||||
|
|
@ -125,7 +125,7 @@ export default function AnalysisClient({ isDark }: { isDark: boolean }) {
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full bg-gray-200 rounded-full h-2.5">
|
<div className="w-full bg-gray-200 rounded-full h-2.5">
|
||||||
<div
|
<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}%` }}
|
style={{ width: `${progress.percent}%` }}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -138,7 +138,7 @@ export default function AnalysisClient({ isDark }: { isDark: boolean }) {
|
||||||
type="button"
|
type="button"
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
onClick={handleCancel}
|
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" />
|
<X className="h-4 w-4" />
|
||||||
<span>Cancel</span>
|
<span>Cancel</span>
|
||||||
|
|
@ -148,7 +148,7 @@ export default function AnalysisClient({ isDark }: { isDark: boolean }) {
|
||||||
type="submit"
|
type="submit"
|
||||||
hidden={loading}
|
hidden={loading}
|
||||||
disabled={!isValid}
|
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" />
|
<Sparkles className="h-4 w-4" />
|
||||||
{loading ? "Menganalisis..." : "Analisis Sekarang"}
|
{loading ? "Menganalisis..." : "Analisis Sekarang"}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import {
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "../ui/select";
|
} from "../ui/select";
|
||||||
|
|
||||||
export function BrandFilter() {
|
export function BrandFilter({ isDark }: { isDark: boolean }) {
|
||||||
const { isLoading, totalCount, selectedBrand, validBrands, handleSelect } =
|
const { isLoading, totalCount, selectedBrand, validBrands, handleSelect } =
|
||||||
useBrandFilter();
|
useBrandFilter();
|
||||||
|
|
||||||
|
|
@ -36,12 +36,12 @@ export function BrandFilter() {
|
||||||
<SelectValue placeholder="Pilih Brand" />
|
<SelectValue placeholder="Pilih Brand" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent
|
<SelectContent
|
||||||
className="bg-card shadow-lg"
|
className={`bg-card shadow-lg ${isDark ? "bg-gray-900 text-white" : "bg-white"}`}
|
||||||
position="popper"
|
position="popper"
|
||||||
>
|
>
|
||||||
<SelectItem
|
<SelectItem
|
||||||
value="__all__"
|
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()})
|
Semua ({totalCount.toLocaleString()})
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
|
@ -49,7 +49,7 @@ export function BrandFilter() {
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={brand.name}
|
key={brand.name}
|
||||||
value={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()})
|
{brand.name} ({brand.count.toLocaleString()})
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import {
|
||||||
Frown,
|
Frown,
|
||||||
Meh,
|
Meh,
|
||||||
MessageSquareText,
|
MessageSquareText,
|
||||||
Moon,
|
|
||||||
Smile,
|
Smile,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
@ -22,6 +21,7 @@ import AnalysisClient from "./AnalysisClient";
|
||||||
import Footer from "./Footer";
|
import Footer from "./Footer";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import ExportExcel from "./ExportExcel";
|
import ExportExcel from "./ExportExcel";
|
||||||
|
import { useTheme } from "@/src/context/ThemeContext";
|
||||||
|
|
||||||
export default function DashboardClient() {
|
export default function DashboardClient() {
|
||||||
const {
|
const {
|
||||||
|
|
@ -31,15 +31,10 @@ export default function DashboardClient() {
|
||||||
neutralCount,
|
neutralCount,
|
||||||
loading,
|
loading,
|
||||||
modelData,
|
modelData,
|
||||||
darkMode,
|
|
||||||
setDarkMode,
|
|
||||||
percentage,
|
percentage,
|
||||||
scrollToResult,
|
scrollToResult,
|
||||||
} = useDashboards();
|
} = useDashboards();
|
||||||
|
const { darkMode, toggleDarkMode } = useTheme();
|
||||||
const toggleDarkMode = () => {
|
|
||||||
setDarkMode((prevMode) => !prevMode);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -49,8 +44,7 @@ export default function DashboardClient() {
|
||||||
|
|
||||||
<main className="container mx-auto px-4 py-8">
|
<main className="container mx-auto px-4 py-8">
|
||||||
<div
|
<div
|
||||||
className="mb-8 rounded-2xl p-8 text-center"
|
className={`mb-8 rounded-2xl p-8 text-center ${darkMode ? "bg-gray-800 text-white" : "bg-primary"} transition-all duration-500`}
|
||||||
style={{ background: "hsl(var(--primary))" }}
|
|
||||||
>
|
>
|
||||||
<h2 className="mb-2 text-3xl font-bold text-white md:text-4xl">
|
<h2 className="mb-2 text-3xl font-bold text-white md:text-4xl">
|
||||||
Analisis Sentimen Ulasan Laptop
|
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">
|
<div className="flex items-center justify-center gap-4 text-sm text-white/70">
|
||||||
<Button
|
<Button
|
||||||
onClick={scrollToResult}
|
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" />
|
<Sparkles className="h-5 w-5" />
|
||||||
<span>Coba Analisis Sentimen</span>
|
<span>Coba Analisis Sentimen</span>
|
||||||
|
|
@ -147,7 +141,7 @@ export default function DashboardClient() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<ModelInfoSkeleton />
|
<ModelInfoSkeleton isDark={darkMode} />
|
||||||
) : modelData.length > 0 ? (
|
) : modelData.length > 0 ? (
|
||||||
<ModelInfo data={modelData} isDark={darkMode} />
|
<ModelInfo data={modelData} isDark={darkMode} />
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -168,18 +162,18 @@ export default function DashboardClient() {
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<ChartNoAxesGantt className="w-5 h-5" />
|
<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>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Hasil klasifikasi sentimen ulasan produk laptop
|
Hasil klasifikasi sentimen ulasan produk laptop
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<BrandFilter />
|
<BrandFilter isDark={darkMode} />
|
||||||
<ExportExcel />
|
<ExportExcel isDark={darkMode} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ReviewTable />
|
<ReviewTable isDark={darkMode} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { useReviewTable } from "@/src/hooks/useReviewTable";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import { downloadAllData } from "@/src/services/report.service";
|
import { downloadAllData } from "@/src/services/report.service";
|
||||||
|
|
||||||
export default function ExportExcel() {
|
export default function ExportExcel({ isDark }: { isDark: boolean }) {
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const selectedBrand = searchParams.get("brand");
|
const selectedBrand = searchParams.get("brand");
|
||||||
const { isLoading, data } = useReviewTable(10, selectedBrand);
|
const { isLoading, data } = useReviewTable(10, selectedBrand);
|
||||||
|
|
@ -13,7 +13,7 @@ export default function ExportExcel() {
|
||||||
<Button
|
<Button
|
||||||
onClick={() => downloadAllData(data)}
|
onClick={() => downloadAllData(data)}
|
||||||
variant="outline"
|
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}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
<Download className="h-4 w-4" />
|
<Download className="h-4 w-4" />
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import {
|
||||||
Laptop,
|
Laptop,
|
||||||
LogOut,
|
LogOut,
|
||||||
Moon,
|
Moon,
|
||||||
|
Sun,
|
||||||
User,
|
User,
|
||||||
UserCircle,
|
UserCircle,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
@ -21,6 +22,7 @@ import Link from "next/link";
|
||||||
import { useHeader } from "@/src/hooks/useHeader";
|
import { useHeader } from "@/src/hooks/useHeader";
|
||||||
import { useDashboards } from "@/src/hooks/useDashboard";
|
import { useDashboards } from "@/src/hooks/useDashboard";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { is } from "zod/v4/locales";
|
||||||
|
|
||||||
export function Header({
|
export function Header({
|
||||||
onToggle,
|
onToggle,
|
||||||
|
|
@ -40,7 +42,9 @@ export function Header({
|
||||||
<div className="container mx-auto px-4 py-4">
|
<div className="container mx-auto px-4 py-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Link href="/" className="flex items-center gap-3 cursor-pointer">
|
<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" />
|
<BarChart3 className="h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -75,10 +79,12 @@ export function Header({
|
||||||
|
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
align="end"
|
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">
|
<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" />
|
<UserCircle className="h-4 w-4 text-muted-foreground" />
|
||||||
<span>Menu Profil</span>
|
<span>Menu Profil</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
@ -87,7 +93,7 @@ export function Header({
|
||||||
<DropdownMenuSeparator className="bg-border" />
|
<DropdownMenuSeparator className="bg-border" />
|
||||||
|
|
||||||
<DropdownMenuItem
|
<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: "/" })}
|
onClick={() => signOut({ callbackUrl: "/" })}
|
||||||
>
|
>
|
||||||
<LogOut className="h-4 w-4" />
|
<LogOut className="h-4 w-4" />
|
||||||
|
|
@ -96,7 +102,17 @@ export function Header({
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ export function ModelInfo({
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={model.modelName + index}
|
key={model.modelName + index}
|
||||||
value={index.toString()}
|
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}
|
{model.modelName}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,34 +1,236 @@
|
||||||
|
"use client";
|
||||||
import { ArrowLeft } from "lucide-react";
|
import { ArrowLeft } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { getAnotherUserData } from "@/src/app/profile/lib/action";
|
|
||||||
import ProfileCard from "./ProfileCard";
|
|
||||||
import Footer from "./Footer";
|
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() {
|
export default function ProfileClient(props: ProfileClientProps) {
|
||||||
const user = await getAnotherUserData();
|
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 (
|
return (
|
||||||
|
<div
|
||||||
|
className={`min-h-screen bg-[#F8FBFF] ${isDark ? "bg-gray-900 text-white" : "bg-[#F8FBFF]"} transition-all duration-500`}
|
||||||
|
>
|
||||||
|
<Header onToggle={toggleDarkMode} isDark={isDark} />
|
||||||
|
|
||||||
<div className="container mx-auto px-4 py-8">
|
<div className="container mx-auto px-4 py-8">
|
||||||
<div className="flex max-w-xl mx-auto">
|
<div className="flex max-w-xl mx-auto">
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href="/"
|
||||||
className="flex items-center gap-2 text-md text-primary max-w-xl w-max mr-auto hover:text-primary/80 transition-colors"
|
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" />
|
<ArrowLeft
|
||||||
<span>Back to Dashboard</span>
|
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>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ProfileCard
|
<motion.div
|
||||||
name={user?.name || ""}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
bio={user?.bio || "None"}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
profession={user?.preference?.profession || ""}
|
transition={{ duration: 0.5, ease: "circOut" }}
|
||||||
preferenceBrand={user?.preference?.brand?.name || ""}
|
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`}
|
||||||
preferenceOS={user?.preference?.preferredOS || ""}
|
>
|
||||||
budgetMax={user?.preference?.budgetMax || 0}
|
<div className="relative bg-linier-to-r from-primary/5 via-primary/10 to-transparent p-6 sm:p-8">
|
||||||
budgetMin={user?.preference?.budgetMin || 0}
|
<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 />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import {
|
||||||
} from "../ui/select";
|
} from "../ui/select";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { useProfileModal } from "@/src/hooks/useProfileModal";
|
import { useProfileModal } from "@/src/hooks/useProfileModal";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
export const ProfileModal = ({
|
export const ProfileModal = ({
|
||||||
setShowModal,
|
setShowModal,
|
||||||
|
|
@ -24,6 +25,7 @@ export const ProfileModal = ({
|
||||||
userData,
|
userData,
|
||||||
onOptimisticUpdate,
|
onOptimisticUpdate,
|
||||||
router,
|
router,
|
||||||
|
darkMode,
|
||||||
}: ExtendedModalProps) => {
|
}: ExtendedModalProps) => {
|
||||||
const { control, errors, isSubmitting, onSubmit, register, handleSubmit } =
|
const { control, errors, isSubmitting, onSubmit, register, handleSubmit } =
|
||||||
useProfileModal({
|
useProfileModal({
|
||||||
|
|
@ -33,16 +35,36 @@ export const ProfileModal = ({
|
||||||
setShowModal,
|
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 (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.2, ease: "circOut" }}
|
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
|
<form
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
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 flex-col gap-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|
@ -93,12 +115,12 @@ export const ProfileModal = ({
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
>
|
>
|
||||||
<SelectTrigger
|
<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" />
|
<SelectValue placeholder="Pilih Profesi/Kebutuhan" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent
|
<SelectContent
|
||||||
className="bg-card border-border shadow-lg"
|
className={`${darkMode ? "bg-gray-900 text-white" : "bg-white"}`}
|
||||||
position="popper"
|
position="popper"
|
||||||
>
|
>
|
||||||
{professionItems.map((item) => {
|
{professionItems.map((item) => {
|
||||||
|
|
@ -107,7 +129,7 @@ export const ProfileModal = ({
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={item.value}
|
key={item.value}
|
||||||
value={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">
|
<div className="flex gap-2 items-center">
|
||||||
<PIcon className="h-4 w-4 text-muted-foreground" />
|
<PIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
|
@ -139,14 +161,12 @@ export const ProfileModal = ({
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
>
|
>
|
||||||
<SelectTrigger
|
<SelectTrigger
|
||||||
className={`w-full ${
|
className={`w-full ${!field.value ? "text-gray-500" : "text-black"} ${darkMode ? "text-card" : "bg-white"} border border-gray-200 transition-all duration-500`}
|
||||||
!field.value ? "text-gray-500" : "text-black"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<SelectValue placeholder="Pilih Merek Laptop" />
|
<SelectValue placeholder="Pilih Merek Laptop" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent
|
<SelectContent
|
||||||
className="bg-card border-border shadow-lg"
|
className={`${darkMode ? "bg-gray-900 text-white" : "bg-white"}`}
|
||||||
position="popper"
|
position="popper"
|
||||||
>
|
>
|
||||||
{brandItems.map((item) => {
|
{brandItems.map((item) => {
|
||||||
|
|
@ -155,7 +175,7 @@ export const ProfileModal = ({
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={item.value}
|
key={item.value}
|
||||||
value={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">
|
<div className="flex gap-2 items-center">
|
||||||
<PIcon className="h-4 w-4 text-muted-foreground" />
|
<PIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
|
@ -189,14 +209,12 @@ export const ProfileModal = ({
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
>
|
>
|
||||||
<SelectTrigger
|
<SelectTrigger
|
||||||
className={`w-full ${
|
className={`w-full ${!field.value ? "text-gray-500" : "text-black"} ${darkMode ? "text-card" : "bg-white"} border border-gray-200 transition-all duration-500`}
|
||||||
!field.value ? "text-gray-500" : "text-black"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<SelectValue placeholder="Pilih Sistem Operasi" />
|
<SelectValue placeholder="Pilih Sistem Operasi" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent
|
<SelectContent
|
||||||
className="bg-card border-border shadow-lg"
|
className={`${darkMode ? "bg-gray-900 text-white" : "bg-white"}`}
|
||||||
position="popper"
|
position="popper"
|
||||||
>
|
>
|
||||||
{OSItems.map((item) => {
|
{OSItems.map((item) => {
|
||||||
|
|
@ -205,7 +223,7 @@ export const ProfileModal = ({
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={item.value}
|
key={item.value}
|
||||||
value={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">
|
<div className="flex gap-2 items-center">
|
||||||
<PIcon className="h-4 w-4 text-muted-foreground" />
|
<PIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
|
@ -273,7 +291,11 @@ export const ProfileModal = ({
|
||||||
<X className="mr-2" />
|
<X className="mr-2" />
|
||||||
<span>Cancel</span>
|
<span>Cancel</span>
|
||||||
</Button>
|
</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" />
|
<Save className="mr-2" />
|
||||||
<span>Simpan</span>
|
<span>Simpan</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ import {
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import { getVisiblePages } from "@/src/utils/datas";
|
import { getVisiblePages } from "@/src/utils/datas";
|
||||||
|
|
||||||
export function ReviewTable() {
|
export function ReviewTable({ isDark }: { isDark: boolean }) {
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const selectedBrand = searchParams.get("brand");
|
const selectedBrand = searchParams.get("brand");
|
||||||
const { currentData, isLoading, pagination } = useReviewTable(
|
const { currentData, isLoading, pagination } = useReviewTable(
|
||||||
|
|
@ -36,8 +36,10 @@ export function ReviewTable() {
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-75 w-full flex-col items-center justify-center gap-2 rounded-xl border bg-card text-muted-foreground">
|
<div
|
||||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
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>
|
<p className="text-sm">Memuat data ulasan...</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -45,10 +47,14 @@ export function ReviewTable() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="rounded-xl border border-gray-200 bg-card">
|
<div
|
||||||
<Table>
|
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>
|
<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-62.5">Produk</TableHead>
|
||||||
<TableHead className="w-auto min-w-75 text-center">
|
<TableHead className="w-auto min-w-75 text-center">
|
||||||
Ulasan & Kata Kunci
|
Ulasan & Kata Kunci
|
||||||
|
|
@ -90,11 +96,13 @@ export function ReviewTable() {
|
||||||
<TableCell className="align-top">
|
<TableCell className="align-top">
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<div className="flex items-center gap-2">
|
<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"}
|
{review.product?.brand?.name || "Generic"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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"}
|
{review.product?.name || "Unknown Product"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -102,7 +110,7 @@ export function ReviewTable() {
|
||||||
|
|
||||||
<TableCell className="align-top">
|
<TableCell className="align-top">
|
||||||
<div className="flex flex-col gap-3">
|
<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}
|
{review.content}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
@ -112,7 +120,7 @@ export function ReviewTable() {
|
||||||
<Badge
|
<Badge
|
||||||
key={i}
|
key={i}
|
||||||
variant="secondary"
|
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}
|
{k}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|
@ -123,7 +131,7 @@ export function ReviewTable() {
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell className="align-top whitespace-nowrap">
|
<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
|
{review.createdAt
|
||||||
? new Date(review.createdAt).toLocaleDateString(
|
? new Date(review.createdAt).toLocaleDateString(
|
||||||
"id-ID",
|
"id-ID",
|
||||||
|
|
@ -138,11 +146,11 @@ export function ReviewTable() {
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell className="align-top text-center">
|
<TableCell className="align-top text-center">
|
||||||
{getSentimentBadge(review.sentiment ?? null)}
|
{getSentimentBadge(review.sentiment ?? null, isDark)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell className="align-top text-center">
|
<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
|
||||||
? `${(review.confidenceScore * 100).toFixed(1)}%`
|
? `${(review.confidenceScore * 100).toFixed(1)}%`
|
||||||
: "-"}
|
: "-"}
|
||||||
|
|
@ -168,7 +176,7 @@ export function ReviewTable() {
|
||||||
className={
|
className={
|
||||||
currentPage === 1
|
currentPage === 1
|
||||||
? "pointer-events-none opacity-50"
|
? "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>
|
</PaginationItem>
|
||||||
|
|
@ -185,6 +193,7 @@ export function ReviewTable() {
|
||||||
pagination.goToPage(page as number);
|
pagination.goToPage(page as number);
|
||||||
}}
|
}}
|
||||||
isActive={currentPage === page}
|
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}
|
{page}
|
||||||
</PaginationLink>
|
</PaginationLink>
|
||||||
|
|
@ -202,7 +211,7 @@ export function ReviewTable() {
|
||||||
className={
|
className={
|
||||||
currentPage === totalPages
|
currentPage === totalPages
|
||||||
? "pointer-events-none opacity-50"
|
? "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>
|
</PaginationItem>
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@ import { Review } from "@/src/types";
|
||||||
import { Badge } from "../ui/badge";
|
import { Badge } from "../ui/badge";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const getSentimentBadge = (sentiment: Review["sentiment"]) => {
|
const getSentimentBadge = (sentiment: Review["sentiment"], isDark: boolean) => {
|
||||||
const styles: Record<Review["sentiment"], string> = {
|
const styles: Record<Review["sentiment"], string> = {
|
||||||
POSITIVE: "sentiment-positive",
|
POSITIVE: ` ${isDark ? "text-sentiment-positive bg-sentiment-positive/10" : "text-sentiment-positive bg-sentiment-positive/10"}`,
|
||||||
NEGATIVE: "sentiment-negative",
|
NEGATIVE: ` ${isDark ? "text-sentiment-negative bg-sentiment-negative/10" : "text-sentiment-negative bg-sentiment-negative/10"}`,
|
||||||
NEUTRAL: "sentiment-neutral",
|
NEUTRAL: ` ${isDark ? "text-sentiment-neutral bg-sentiment-neutral/10" : "text-sentiment-neutral bg-sentiment-neutral/10"}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
const labels: Record<Review["sentiment"], string> = {
|
const labels: Record<Review["sentiment"], string> = {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,9 @@ export function WordCloud({ isDark }: { isDark: boolean }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap items-center justify-center gap-2 p-4">
|
<div className="flex flex-wrap items-center justify-center gap-2 p-4">
|
||||||
{isEmpty ? (
|
{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">
|
<div className="rounded-full bg-muted">
|
||||||
<Inbox className="h-8 w-8" />
|
<Inbox className="h-8 w-8" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import { ChevronDown } from "lucide-react";
|
import { ChevronDown } from "lucide-react";
|
||||||
|
|
||||||
export function ModelInfoSkeleton() {
|
export function ModelInfoSkeleton({ isDark }: { isDark: boolean }) {
|
||||||
return (
|
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="mb-4 flex items-center justify-between gap-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex items-center pr-3 h-9 w-64 rounded-md border border-border">
|
<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>
|
</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>
|
||||||
|
|
||||||
<div className="mb-6 min-h-10 space-y-2">
|
<div className="mb-6 min-h-10 space-y-2">
|
||||||
<div className="h-4 w-3/4 rounded bg-gray-100" />
|
<div
|
||||||
<div className="h-4 w-1/2 rounded bg-gray-100" />
|
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>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4 mb-2">
|
<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"
|
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="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>
|
||||||
|
|
||||||
<div className="flex-1 space-y-2">
|
<div className="flex-1 space-y-2">
|
||||||
<div className="h-3 w-20 rounded bg-gray-100" />
|
<div
|
||||||
<div className="h-4 w-15 rounded bg-gray-200" />
|
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>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer info */}
|
|
||||||
<div className="mt-8 space-y-3 border-t pt-4">
|
<div className="mt-8 space-y-3 border-t pt-4">
|
||||||
{[1, 2, 3].map((i) => (
|
{[1, 2, 3].map((i) => (
|
||||||
<div key={i} className="flex items-center justify-between">
|
<div key={i} className="flex items-center justify-between">
|
||||||
{/* label */}
|
<div
|
||||||
<div className="h-4 w-32 rounded bg-gray-100" />
|
className={`h-4 w-32 rounded bg-gray-100 ${isDark ? "bg-gray-600" : ""}`}
|
||||||
{/* value */}
|
/>
|
||||||
<div className="h-4 w-44 rounded bg-gray-100" />
|
<div
|
||||||
|
className={`h-4 w-44 rounded bg-gray-100 ${isDark ? "bg-gray-600" : ""}`}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,9 @@ const buttonVariants = cva(
|
||||||
ghost:
|
ghost:
|
||||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
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: {
|
size: {
|
||||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ function PaginationLink({
|
||||||
data-active={isActive}
|
data-active={isActive}
|
||||||
className={cn(
|
className={cn(
|
||||||
buttonVariants({
|
buttonVariants({
|
||||||
variant: isActive ? "primary" : "ghost",
|
variant: isActive ? "darkMode" : "ghost",
|
||||||
size,
|
size,
|
||||||
}),
|
}),
|
||||||
className,
|
className,
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ function SelectTrigger({
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<SelectPrimitive.Icon asChild>
|
<SelectPrimitive.Icon asChild>
|
||||||
<ChevronDownIcon className="size-4 opacity-50" />
|
<ChevronDownIcon className="size-4" />
|
||||||
</SelectPrimitive.Icon>
|
</SelectPrimitive.Icon>
|
||||||
</SelectPrimitive.Trigger>
|
</SelectPrimitive.Trigger>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
@ -16,7 +16,6 @@ export const useDashboards = () => {
|
||||||
negative: 0,
|
negative: 0,
|
||||||
neutral: 0,
|
neutral: 0,
|
||||||
});
|
});
|
||||||
const [darkMode, setDarkMode] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchStats() {
|
async function fetchStats() {
|
||||||
|
|
@ -85,8 +84,6 @@ export const useDashboards = () => {
|
||||||
selectedBrand,
|
selectedBrand,
|
||||||
loading,
|
loading,
|
||||||
modelData,
|
modelData,
|
||||||
darkMode,
|
|
||||||
setDarkMode,
|
|
||||||
setSelectedBrand,
|
setSelectedBrand,
|
||||||
percentage,
|
percentage,
|
||||||
scrollToResult,
|
scrollToResult,
|
||||||
|
|
|
||||||
|
|
@ -299,6 +299,7 @@ export interface ExtendedModalProps extends ProfileModalProps {
|
||||||
userData: any;
|
userData: any;
|
||||||
onOptimisticUpdate: (data: ProfileFormData) => void;
|
onOptimisticUpdate: (data: ProfileFormData) => void;
|
||||||
router: any;
|
router: any;
|
||||||
|
darkMode: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProfileState {
|
export interface ProfileState {
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,11 @@ export const setWordCloud = ({ maxValue, minValue }: WordCloudConfig) => {
|
||||||
const getColor = (sentiment: WordItem["sentiment"]) => {
|
const getColor = (sentiment: WordItem["sentiment"]) => {
|
||||||
switch (sentiment) {
|
switch (sentiment) {
|
||||||
case "POSITIVE":
|
case "POSITIVE":
|
||||||
return "text-sentiment-positive hover:bg-sentiment-positive-light";
|
return "text-sentiment-positive hover:bg-sentiment-positive/10";
|
||||||
case "NEGATIVE":
|
case "NEGATIVE":
|
||||||
return "text-sentiment-negative hover:bg-sentiment-negative-light";
|
return "text-sentiment-negative hover:bg-sentiment-negative/10";
|
||||||
case "NEUTRAL":
|
case "NEUTRAL":
|
||||||
return "text-sentiment-neutral hover:bg-sentiment-neutral-light";
|
return "text-sentiment-neutral hover:bg-sentiment-neutral/10";
|
||||||
default:
|
default:
|
||||||
return "hover:bg-primary hover:text-card";
|
return "hover:bg-primary hover:text-card";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue