style: make night mode visibility

This commit is contained in:
Mahen 2026-04-12 22:24:37 +07:00
parent be2b6d7fcd
commit f367b1c3ad
14 changed files with 134 additions and 49 deletions

View File

@ -7,7 +7,7 @@ import { Button } from "../ui/button";
import ResultSection from "./ResultSection";
import UrlInputList from "./UrlInputList";
export default function AnalysisClient() {
export default function AnalysisClient({ isDark }: { isDark: boolean }) {
const {
isValid,
errors,
@ -28,23 +28,31 @@ export default function AnalysisClient() {
<div className="w-full mx-auto">
<form
onSubmit={handleSubmit(onSubmit)}
className="bg-white p-6 rounded-lg border mb-8"
className={`p-6 rounded-lg mb-8 ${isDark ? "bg-gray-800" : "bg-white border border-gray-200"} transition-all duration-500`}
>
<div className="mb-4 flex items-center gap-2">
<Sparkles className="h-5 w-5 text-primary" />
<h3 className="text-lg font-semibold">Analisis Sentimen Real-time</h3>
<Sparkles
className={`h-5 w-5 text-primary ${isDark ? "text-white" : "text-black"} transition-all duration-500`}
/>
<h3
className={`text-lg font-semibold ${isDark ? "text-white" : "text-black"} transition-all duration-500`}
>
Analisis Sentimen Real-time
</h3>
</div>
<div className="flex flex-col gap-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="w-full">
<label className="block mb-1 text-sm font-medium text-gray-700">
<label
className={`block mb-1 text-sm font-medium ${isDark ? "text-white" : "text-gray-700"} transition-all duration-500`}
>
Tautan Produk 1
</label>
<Input
type="url"
placeholder="Contoh: https://tokopedia.com/..."
className={`${errors.url1 ? "border-sentiment-negative" : "focus:ring-primary"}`}
className={`${errors.url1 ? "border-sentiment-negative" : "focus:ring-primary"} ${isDark ? "bg-gray-800 text-white" : "bg-white"} transition-all duration-500 w-full`}
{...register("url1")}
/>
{errors.url1 && (
@ -55,13 +63,15 @@ export default function AnalysisClient() {
</div>
<div className="w-full">
<label className="block mb-1 text-sm font-medium text-gray-700">
<label
className={`block mb-1 text-sm font-medium ${isDark ? "text-white" : "text-gray-700"} transition-all duration-500`}
>
Tautan Produk 2
</label>
<Input
type="url"
placeholder="Contoh: https://tokopedia.com/..."
className={`w-full ${errors.url2 ? "border-sentiment-negative" : "focus:ring-primary"}`}
className={`${errors.url2 ? "border-sentiment-negative" : "focus:ring-primary"} ${isDark ? "bg-gray-800 text-white" : "bg-white"} transition-all duration-500 w-full`}
{...register("url2")}
/>
{errors.url2 && (
@ -77,6 +87,7 @@ export default function AnalysisClient() {
urlDatas={urlDatas}
visibleFields={visibleFields}
setVisibleFields={setVisibleFields}
isDark={isDark}
/>
{visibleFields < 2 && (
@ -90,10 +101,11 @@ export default function AnalysisClient() {
type="button"
onClick={() => setVisibleFields((prev) => prev + 1)}
className={`
h-max bg-card text-primary hover:bg-[#F8FBFF]
h-max bg-card text-primary
border-dashed border border-primary/20 shadow-xs
transition-all duration-500 animate-in fade-in zoom-in-95
${visibleFields === 0 ? "w-full md:w-1/2" : "w-full"}
${isDark ? "bg-gray-800 text-white hover:bg-gray-900 border-dashed border border-white" : "text-black hover:bg-[#F8FBFF] "}
`}
>
{visibleFields === 0
@ -136,7 +148,7 @@ export default function AnalysisClient() {
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 bg-primary text-white px-6 py-3 mt-6 rounded-md transition-colors disabled:bg-gray-400`}
>
<Sparkles className="h-4 w-4" />
{loading ? "Menganalisis..." : "Analisis Sekarang"}

View File

@ -36,7 +36,7 @@ export function BrandFilter() {
<SelectValue placeholder="Pilih Brand" />
</SelectTrigger>
<SelectContent
className="bg-card border-border shadow-lg"
className="bg-card shadow-lg"
position="popper"
>
<SelectItem

View File

@ -7,6 +7,7 @@ import {
Frown,
Meh,
MessageSquareText,
Moon,
Smile,
Sparkles,
} from "lucide-react";
@ -30,13 +31,21 @@ export default function DashboardClient() {
neutralCount,
loading,
modelData,
darkMode,
setDarkMode,
percentage,
scrollToResult,
} = useDashboards();
const toggleDarkMode = () => {
setDarkMode((prevMode) => !prevMode);
};
return (
<div className="min-h-screen bg-[#F8FBFF]" suppressHydrationWarning>
<Header />
<div
className={`min-h-screen ${darkMode ? "bg-gray-900 text-white" : "bg-[#F8FBFF]"} suppressHydrationWarning} transition-all duration-500`}
>
<Header onToggle={toggleDarkMode} isDark={darkMode} />
<main className="container mx-auto px-4 py-8">
<div
@ -67,6 +76,7 @@ export default function DashboardClient() {
value={totalReviews}
icon={MessageSquareText}
delay={0}
isDark={darkMode}
/>
<StatCard
@ -76,6 +86,7 @@ export default function DashboardClient() {
icon={Smile}
variant="positive"
delay={100}
isDark={darkMode}
/>
<StatCard
@ -85,6 +96,7 @@ export default function DashboardClient() {
icon={Frown}
variant="negative"
delay={200}
isDark={darkMode}
/>
<StatCard
@ -94,6 +106,7 @@ export default function DashboardClient() {
icon={Meh}
variant="neutral"
delay={300}
isDark={darkMode}
/>
</div>
@ -111,24 +124,34 @@ export default function DashboardClient() {
</div> */}
<div className="mb-8 grid gap-4 lg:grid-cols-2">
<div className="rounded-xl border bg-card p-6">
<div
className={`rounded-xl border ${darkMode ? "border-transparent" : "border-gray-200"} bg-card p-6 ${darkMode ? "bg-gray-800" : "bg-white"} transition-all duration-500`}
>
<div className="flex items-center gap-2 mb-2">
<FileLock className="h-5 w-5 text-primary" />
<h3 className="text-lg font-semibold">Kata Kunci Populer</h3>
<FileLock
className={`h-5 w-5 text-primary ${darkMode ? "text-white" : "text-black"} transition-all duration-500`}
/>
<h3
className={`text-lg font-semibold ${darkMode ? "text-white" : "text-neutral"} transition-all duration-500`}
>
Kata Kunci Populer
</h3>
</div>
<p className="mb-4 text-sm text-muted-foreground">
<p
className={`mb-4 text-sm ${darkMode ? "text-white" : "text-neutral"} transition-all duration-500`}
>
Kata-kata yang sering muncul dalam ulasan berdasarkan kategori
sentimen
</p>
<WordCloud />
<WordCloud isDark={darkMode} />
</div>
{loading ? (
<ModelInfoSkeleton />
) : modelData.length > 0 ? (
<ModelInfo data={modelData} />
<ModelInfo data={modelData} isDark={darkMode} />
) : (
<div className="rounded-xl border bg-card p-6 text-center text-muted-foreground">
<div className="rounded-xl border border-gray-200 bg-card p-6 text-center text-muted-foreground">
Data model tidak tersedia.
</div>
)}
@ -136,7 +159,7 @@ export default function DashboardClient() {
<section id="analysis-form" className="scroll-mt-60">
<div className="mb-8 ">
<AnalysisClient />
<AnalysisClient isDark={darkMode} />
</div>
</section>

View File

@ -4,6 +4,7 @@ import {
Database,
Laptop,
LogOut,
Moon,
User,
UserCircle,
} from "lucide-react";
@ -19,14 +20,23 @@ import { signOut } from "next-auth/react";
import Link from "next/link";
import { useHeader } from "@/src/hooks/useHeader";
import { useDashboards } from "@/src/hooks/useDashboard";
import { useState } from "react";
export function Header() {
export function Header({
onToggle,
isDark,
}: {
onToggle: () => void;
isDark: boolean;
}) {
const { open, setOpen, session, mounted, productCount } = useHeader();
const { totalReviews } = useDashboards();
if (!mounted) return null;
return (
<header className="border-b bg-[#F8FBFF]/50 backdrop-blur-sm sticky top-0 z-1">
<header
className={`border-b ${isDark ? "bg-gray-900 text-white" : "bg-[#F8FBFF]/50"} backdrop-blur-sm sticky top-0 z-10 transition-all duration-500`}
>
<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">
@ -86,6 +96,7 @@ export function Header() {
</DropdownMenuContent>
</DropdownMenu>
</div>
<Moon onClick={onToggle} className="h-4 w-4 cursor-pointer" />
</div>
</div>
</div>

View File

@ -11,7 +11,13 @@ import {
import { ModelDB } from "@/src/types";
import { useModelInfo } from "@/src/hooks/useModelInfo";
export function ModelInfo({ data }: { data: ModelDB[] }) {
export function ModelInfo({
data,
isDark,
}: {
data: ModelDB[];
isDark: boolean;
}) {
const { selectedIndex, metrics, setSelectedIndex, currentModel } =
useModelInfo({ data });
@ -26,25 +32,29 @@ export function ModelInfo({ data }: { data: ModelDB[] }) {
}
return (
<div className="rounded-xl border bg-card p-6">
<div
className={`rounded-xl border ${isDark ? "border-transparent" : "border-gray-200"} bg-card p-6 ${isDark ? "bg-gray-800" : "bg-white"} transition-all duration-500`}
>
<div className="mb-4 flex items-center justify-between gap-4">
<Select
value={selectedIndex.toString()}
onValueChange={(val) => setSelectedIndex(parseInt(val))}
>
<SelectTrigger className="w-fit justify-start gap-3 text-md font-semibold border-border bg-card shadow-sm">
<SelectTrigger
className={`w-fit justify-start gap-3 text-md font-semibold border border-gray-200 bg-card ${isDark ? "bg-gray-900" : "bg-white"} transition-all duration-500`}
>
<SelectValue placeholder="Pilih Model" />
</SelectTrigger>
<SelectContent
className="bg-card border-border shadow-lg"
className={`bg-card shadow-lg ${isDark ? "bg-gray-900 text-white" : "bg-white"}`}
position="popper"
>
{data.map((model, index) => (
<SelectItem
key={model.modelName + index}
value={index.toString()}
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 ${isDark ? "bg-gray-900 text-white" : "bg-white"} transition-all duration-500`}
>
{model.modelName}
</SelectItem>
@ -54,7 +64,7 @@ export function ModelInfo({ data }: { data: ModelDB[] }) {
<Badge
variant="secondary"
className={`bg-sentiment-positive-light text-sentiment-positive ${currentModel.isActive ? "bg-sentiment-positive-light text-sentiment-positive" : "bg-primary/10 text-primary"}`}
className={`${currentModel.isActive ? `${isDark ? "bg-sentiment-positive/10 text-sentiment-positive" : "bg-sentiment-positive-light text-sentiment-positive"}` : `${isDark ? "bg-gray-900 text-white" : "bg-primary/10 text-primary"}`} transition-all duration-500`}
>
{currentModel.isActive === true ? "Active" : "Inactive"}
</Badge>
@ -70,8 +80,12 @@ export function ModelInfo({ data }: { data: ModelDB[] }) {
key={metric.label}
className="flex items-center gap-3 rounded-lg p-3 bg-secondary/50 border border-border/40"
>
<div className="rounded-lg bg-primary/10 p-2">
<metric.icon className="h-4 w-4 text-primary" />
<div
className={`rounded-lg p-2 ${isDark ? "bg-gray-600" : "bg-primary/10 "} transition-all duration-500`}
>
<metric.icon
className={`h-4 w-4 text-primary ${isDark ? "text-white" : "text-black"} transition-all duration-500`}
/>
</div>
<div>
<p className="text-xs text-muted-foreground">{metric.label}</p>
@ -85,7 +99,9 @@ export function ModelInfo({ data }: { data: ModelDB[] }) {
))}
</div>
<div className="mt-6 space-y-2 text-sm text-muted-foreground border-t pt-4">
<div
className={`mt-6 space-y-2 text-sm text-muted-foreground border-t border-gray-200 pt-4`}
>
<div className="flex justify-between">
<span>Preprocessing</span>
<span className="text-foreground">

View File

@ -45,7 +45,7 @@ export function ReviewTable() {
return (
<div className="space-y-4">
<div className="rounded-xl border bg-card">
<div className="rounded-xl border border-gray-200 bg-card">
<Table>
<TableHeader>
<TableRow className="hover:bg-transparent">
@ -155,7 +155,7 @@ export function ReviewTable() {
</Table>
{totalPages > 1 && (
<div className="border-t bg-muted/20 px-6 py-4">
<div className="border-t border-t-gray-200 bg-muted/20 px-6 py-4">
<Pagination className="justify-center sm:justify-end">
<PaginationContent>
<PaginationItem>

View File

@ -11,6 +11,7 @@ export function StatCard({
trend,
variant = "default",
delay = 0,
isDark,
}: StatCardProps) {
const { isVisible, displayValue } = useStatCard({ value, delay });
@ -20,18 +21,28 @@ export function StatCard({
"relative overflow-hidden rounded-xl border p-6 card-elevated transition-all duration-500",
variantStyles[variant],
isVisible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-4",
isDark ? "bg-gray-800" : "bg-white",
isDark ? "border-transparent" : "border-gray-200",
)}
>
<div className="flex items-start justify-between">
<div className="space-y-2">
<p className="text-sm font-medium text-muted-foreground">{title}</p>
<p
className={`text-sm font-medium text-muted-foreground ${isDark ? "text-white" : "text-neutral"}`}
>
{title}
</p>
<div className="flex items-baseline gap-1">
<span className="text-3xl font-bold tracking-tight">
<span
className={`text-3xl font-bold tracking-tight ${isDark ? "text-white" : "text-neutral"}`}
>
{displayValue.toLocaleString()}
</span>
{suffix && (
<span className="text-lg font-medium text-muted-foreground">
<span
className={`text-lg font-medium ${isDark ? "text-white" : "text-neutral"}`}
>
{suffix}
</span>
)}
@ -55,7 +66,7 @@ export function StatCard({
)}
</div>
<div className={cn("rounded-xl p-3", iconStyles[variant])}>
<div className={cn("rounded-xl p-3 transition-all duration-500", iconStyles(isDark)[variant])}>
<Icon className="h-6 w-6" />
</div>
</div>

View File

@ -7,10 +7,13 @@ const UrlInputItem = ({
index,
visibleFields,
onRemove,
isDark,
}: UrlInputItemProps) => {
return (
<div className="animate-in fade-in slide-in-from-bottom-2 duration-300">
<label className="block mb-1 text-sm font-medium text-gray-700">
<label
className={`block mb-1 text-sm font-medium ${isDark ? "text-white" : "text-gray-700"} transition-all duration-500`}
>
{item.labels}
</label>
<div className="flex gap-2">
@ -18,16 +21,15 @@ const UrlInputItem = ({
<Input
type="url"
placeholder="Contoh: https://tokopedia.com/..."
className={`${item.errors ? "border-sentiment-negative" : "focus:ring-primary"}`}
className={`${item.errors ? "border-sentiment-negative" : "focus:ring-primary"} ${isDark ? "bg-gray-800 text-white" : "bg-white"} transition-all duration-500 w-full`}
{...item.title}
/>
</div>
{index === visibleFields - 1 && (
<Button
type="button"
variant="ghost"
onClick={onRemove}
className="text-sentiment-negative hover:text-sentiment-negative hover:bg-sentiment-negative-light shrink-0"
className={`shrink-0 ${isDark ? "text-sentiment-negative bg-transparent hover:text-sentiment-negative hover:bg-sentiment-negative/10" : "text-sentiment-negative bg-card hover:text-sentiment-negative hover:bg-sentiment-negative-light"} transition-all duration-500`}
>
</Button>

View File

@ -5,6 +5,7 @@ const UrlInputList = ({
urlDatas,
visibleFields,
setVisibleFields,
isDark,
}: UrlInputListProps) => {
return (
<>
@ -15,6 +16,7 @@ const UrlInputList = ({
index={index}
visibleFields={visibleFields}
onRemove={() => setVisibleFields((prev) => prev - 1)}
isDark={isDark}
/>
))}
</>

View File

@ -4,7 +4,7 @@ import { useWordCloud } from "@/src/hooks/useWordCloud";
import WordCloudItem from "./WordCloudItem";
import { Inbox } from "lucide-react";
export function WordCloud() {
export function WordCloud({ isDark }: { isDark: boolean }) {
const { maxValue, minValue, shuffledWords, isEmpty } = useWordCloud();
return (

View File

@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border border-gray-200 bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className

View File

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

View File

@ -90,6 +90,7 @@ export interface StatCardProps {
};
variant?: "default" | "positive" | "negative" | "neutral";
delay?: number;
isDark: boolean;
}
interface TrendData {
@ -385,10 +386,12 @@ export type UrlInputItemProps = {
index: number;
visibleFields: number;
onRemove: () => void;
isDark: boolean;
};
export type UrlInputListProps = {
urlDatas: UrlData[];
visibleFields: number;
setVisibleFields: React.Dispatch<React.SetStateAction<number>>;
isDark: boolean;
};

View File

@ -5,11 +5,13 @@ const variantStyles = {
neutral: "bg-sentiment-neutral-light border-sentiment-neutral/20",
};
const iconStyles = {
default: "bg-primary/10 text-primary",
positive: "bg-sentiment-positive/10 text-sentiment-positive",
negative: "bg-sentiment-negative/10 text-sentiment-negative",
neutral: "bg-sentiment-neutral/10 text-sentiment-neutral",
const iconStyles = (isDark: boolean) => {
return {
default: `bg-primary/10 text-primary ${isDark ? "text-white" : "text-neutral"} ${isDark ? "bg-gray-900" : "bg-primary/10"}`,
positive: "bg-sentiment-positive/10 text-sentiment-positive",
negative: "bg-sentiment-negative/10 text-sentiment-negative",
neutral: "bg-sentiment-neutral/10 text-sentiment-neutral",
};
};
export { variantStyles, iconStyles };