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;
|
||||
font-feature-settings: "cv02", "cv03", "cv04", "cv11";
|
||||
}
|
||||
|
||||
body[data-scroll-locked] {
|
||||
margin-right: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
overflow: overlay !important;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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> = {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ function PaginationLink({
|
|||
data-active={isActive}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: isActive ? "primary" : "ghost",
|
||||
variant: isActive ? "darkMode" : "ghost",
|
||||
size,
|
||||
}),
|
||||
className,
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ function SelectTrigger({
|
|||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className="size-4 opacity-50" />
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</SelectPrimitive.Icon>
|
||||
</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,
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -299,6 +299,7 @@ export interface ExtendedModalProps extends ProfileModalProps {
|
|||
userData: any;
|
||||
onOptimisticUpdate: (data: ProfileFormData) => void;
|
||||
router: any;
|
||||
darkMode: boolean;
|
||||
}
|
||||
|
||||
export interface ProfileState {
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue