feat: implement dark/light mode toggle across dashboard and profile

This commit is contained in:
bipproduction
2026-02-09 11:24:34 +08:00
parent 5ed9637a88
commit df707fe29b
12 changed files with 167 additions and 140 deletions

View File

@@ -0,0 +1,34 @@
import {
ActionIcon,
Group,
rem,
useComputedColorScheme,
useMantineColorScheme,
} from "@mantine/core";
import { IconMoon, IconSun } from "@tabler/icons-react";
export function ColorSchemeToggle() {
const { setColorScheme } = useMantineColorScheme();
const computedColorScheme = useComputedColorScheme("light", {
getInitialValueInEffect: true,
});
return (
<Group justify="center">
<ActionIcon
onClick={() =>
setColorScheme(computedColorScheme === "light" ? "dark" : "light")
}
variant="default"
size="lg"
aria-label="Toggle color scheme"
>
{computedColorScheme === "light" ? (
<IconMoon style={{ width: rem(22), height: rem(22) }} stroke={1.5} />
) : (
<IconSun style={{ width: rem(22), height: rem(22) }} stroke={1.5} />
)}
</ActionIcon>
</Group>
);
}

View File

@@ -56,7 +56,7 @@ const app = (
}); });
}} }}
> >
<MantineProvider theme={theme}> <MantineProvider theme={theme} defaultColorScheme="dark">
<ModalsProvider> <ModalsProvider>
<RouterProvider router={router} /> <RouterProvider router={router} />
</ModalsProvider> </ModalsProvider>

View File

@@ -171,8 +171,8 @@ if (!isProduction) {
const file = Bun.file(filePath); const file = Bun.file(filePath);
return new Response(file, { return new Response(file, {
headers: { headers: {
"Vary": "Accept-Encoding", Vary: "Accept-Encoding",
} },
}); });
} }
@@ -181,8 +181,8 @@ if (!isProduction) {
if (fs.existsSync(indexHtml)) { if (fs.existsSync(indexHtml)) {
return new Response(Bun.file(indexHtml), { return new Response(Bun.file(indexHtml), {
headers: { headers: {
"Vary": "Accept-Encoding", Vary: "Accept-Encoding",
} },
}); });
} }

View File

@@ -56,7 +56,11 @@ function DashboardComponent() {
return ( return (
<Container size="lg" py="xl"> <Container size="lg" py="xl">
<Title order={1} ta="center" className=" text-blue-600 p-4 rounded-lg mt-10 shadow-lg"> <Title
order={1}
ta="center"
className=" text-blue-600 p-4 rounded-lg mt-10 shadow-lg"
>
Dashboard Overview Dashboard Overview
</Title> </Title>
@@ -66,8 +70,7 @@ function DashboardComponent() {
p="xl" p="xl"
radius="md" radius="md"
mb="xl" mb="xl"
bg="rgba(251, 240, 223, 0.05)" style={{ border: "1px solid var(--mantine-color-default-border)" }}
style={{ border: "1px solid rgba(251, 240, 223, 0.1)" }}
> >
<Group justify="space-between"> <Group justify="space-between">
<Group> <Group>
@@ -77,14 +80,14 @@ function DashboardComponent() {
radius="xl" radius="xl"
style={{ style={{
cursor: "pointer", cursor: "pointer",
border: "2px solid rgba(251, 240, 223, 0.3)", border: "2px solid var(--mantine-color-orange-filled)",
}} }}
onClick={() => navigate({ to: "/profile" })} onClick={() => navigate({ to: "/profile" })}
> >
{snap.user?.name?.charAt(0).toUpperCase()} {snap.user?.name?.charAt(0).toUpperCase()}
</Avatar> </Avatar>
<div> <div>
<Text size="lg" fw={600} c="#fbf0df"> <Text size="lg" fw={600}>
{snap.user?.name} {snap.user?.name}
</Text> </Text>
<Text c="dimmed" size="sm"> <Text c="dimmed" size="sm">
@@ -104,23 +107,17 @@ function DashboardComponent() {
{/* Stats Grid */} {/* Stats Grid */}
<SimpleGrid cols={{ base: 1, sm: 2, md: 4 }} spacing="lg" mb="xl"> <SimpleGrid cols={{ base: 1, sm: 2, md: 4 }} spacing="lg" mb="xl">
{statsData.map((stat, index) => ( {statsData.map((stat, index) => (
<Card <Card key={index.toString()} withBorder p="lg" radius="md">
key={index.toString()}
withBorder
p="lg"
radius="md"
bg="rgba(251, 240, 223, 0.05)"
>
<Group justify="space-between"> <Group justify="space-between">
<Box> <Box>
<Text size="sm" c="dimmed"> <Text size="sm" c="dimmed">
{stat.title} {stat.title}
</Text> </Text>
<Text size="lg" fw={700} c="#fbf0df"> <Text size="lg" fw={700}>
{stat.value} {stat.value}
</Text> </Text>
</Box> </Box>
<Box c="#f3d5a3">{stat.icon}</Box> <Box c="orange.6">{stat.icon}</Box>
</Group> </Group>
</Card> </Card>
))} ))}
@@ -128,13 +125,7 @@ function DashboardComponent() {
<Grid gutter="lg"> <Grid gutter="lg">
<Grid.Col span={{ base: 12, md: 8 }}> <Grid.Col span={{ base: 12, md: 8 }}>
<Card <Card withBorder p="lg" radius="md" mb="lg">
withBorder
p="lg"
radius="md"
mb="lg"
bg="rgba(251, 240, 223, 0.05)"
>
<Title order={3} mb="md"> <Title order={3} mb="md">
System Performance System Performance
</Title> </Title>
@@ -171,7 +162,7 @@ function DashboardComponent() {
</Grid.Col> </Grid.Col>
<Grid.Col span={{ base: 12, md: 4 }}> <Grid.Col span={{ base: 12, md: 4 }}>
<Card withBorder p="lg" radius="md" bg="rgba(251, 240, 223, 0.05)"> <Card withBorder p="lg" radius="md">
<Title order={3} mb="md"> <Title order={3} mb="md">
Server Status Server Status
</Title> </Title>

View File

@@ -30,6 +30,7 @@ import {
useNavigate, useNavigate,
} from "@tanstack/react-router"; } from "@tanstack/react-router";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
import { ColorSchemeToggle } from "@/components/ColorSchemeToggle";
import { authClient } from "@/utils/auth-client"; import { authClient } from "@/utils/auth-client";
import { authStore } from "../../store/auth"; import { authStore } from "../../store/auth";
@@ -101,7 +102,9 @@ function DashboardLayout() {
header={{ height: 70 }} header={{ height: 70 }}
navbar={{ navbar={{
width: 280, width: 280,
breakpoint: "sm", breakpoint: "sm",
collapsed: { mobile: !mobileOpened, desktop: !desktopOpened }, collapsed: { mobile: !mobileOpened, desktop: !desktopOpened },
}} }}
padding="md" padding="md"
@@ -109,10 +112,10 @@ function DashboardLayout() {
transitionTimingFunction="ease" transitionTimingFunction="ease"
> >
<AppShell.Header <AppShell.Header
bg="rgba(26, 26, 26, 0.8)"
style={{ style={{
backdropFilter: "blur(10px)", backdropFilter: "blur(10px)",
borderBottom: "1px solid rgba(251, 240, 223, 0.1)",
borderBottom: "1px solid var(--mantine-color-default-border)",
}} }}
> >
<Group h="100%" px="md" justify="space-between"> <Group h="100%" px="md" justify="space-between">
@@ -122,24 +125,24 @@ function DashboardLayout() {
onClick={toggleMobile} onClick={toggleMobile}
hiddenFrom="sm" hiddenFrom="sm"
size="sm" size="sm"
color="#f3d5a3"
/> />
<Burger <Burger
opened={desktopOpened} opened={desktopOpened}
onClick={toggleDesktop} onClick={toggleDesktop}
visibleFrom="sm" visibleFrom="sm"
size="sm" size="sm"
color="#f3d5a3"
/> />
<Box visibleFrom="xs" ml="xs"> <Box visibleFrom="xs" ml="xs">
<Text <Text
fw={800} fw={800}
size="xl" size="xl"
c="#f3d5a3" c="orange.6"
style={{ letterSpacing: "-0.5px" }} style={{ letterSpacing: "-0.5px" }}
> >
ADMIN ADMIN
<Text span c="#fbf0df"> <Text span c="var(--mantine-color-text)">
PANEL PANEL
</Text> </Text>
</Text> </Text>
@@ -147,6 +150,8 @@ function DashboardLayout() {
</Group> </Group>
<Group gap="md"> <Group gap="md">
<ColorSchemeToggle />
<Menu <Menu
shadow="md" shadow="md"
width={200} width={200}
@@ -158,24 +163,28 @@ function DashboardLayout() {
gap="xs" gap="xs"
style={{ cursor: "pointer" }} style={{ cursor: "pointer" }}
p="xs" p="xs"
hover-bg="rgba(255,255,255,0.05)" className="hover:bg-gray-50 dark:hover:bg-white/5 rounded-md"
> >
<div <div
style={{ textAlign: "right" }} style={{ textAlign: "right" }}
className="visible-from-sm" className="visible-from-sm"
> >
<Text size="sm" fw={600} c="#fbf0df"> <Text size="sm" fw={600}>
{snap.user?.name} {snap.user?.name}
</Text> </Text>
<Text size="xs" c="dimmed"> <Text size="xs" c="dimmed">
Administrator Administrator
</Text> </Text>
</div> </div>
<Avatar <Avatar
src={snap.user?.image} src={snap.user?.image}
radius="xl" radius="xl"
size="md" size="md"
style={{ border: "2px solid #f3d5a3" }} style={{
border: "2px solid var(--mantine-color-orange-filled)",
}}
> >
{snap.user?.name?.charAt(0)} {snap.user?.name?.charAt(0)}
</Avatar> </Avatar>
@@ -184,6 +193,7 @@ function DashboardLayout() {
<Menu.Dropdown> <Menu.Dropdown>
<Menu.Label>Akun</Menu.Label> <Menu.Label>Akun</Menu.Label>
<Menu.Item <Menu.Item
leftSection={ leftSection={
<IconUser style={{ width: rem(14), height: rem(14) }} /> <IconUser style={{ width: rem(14), height: rem(14) }} />
@@ -192,6 +202,7 @@ function DashboardLayout() {
> >
Profil Saya Profil Saya
</Menu.Item> </Menu.Item>
<Menu.Item <Menu.Item
leftSection={ leftSection={
<IconSettings style={{ width: rem(14), height: rem(14) }} /> <IconSettings style={{ width: rem(14), height: rem(14) }} />
@@ -204,6 +215,7 @@ function DashboardLayout() {
<Menu.Divider /> <Menu.Divider />
<Menu.Label>Bahaya</Menu.Label> <Menu.Label>Bahaya</Menu.Label>
<Menu.Item <Menu.Item
color="red" color="red"
leftSection={ leftSection={
@@ -221,8 +233,7 @@ function DashboardLayout() {
<AppShell.Navbar <AppShell.Navbar
p="md" p="md"
bg="rgba(20, 20, 20, 1)" style={{ borderRight: "1px solid var(--mantine-color-default-border)" }}
style={{ borderRight: "1px solid rgba(251, 240, 223, 0.1)" }}
> >
<AppShell.Section grow component={ScrollArea} mx="-md" px="md"> <AppShell.Section grow component={ScrollArea} mx="-md" px="md">
<Stack gap="xs" mt="md"> <Stack gap="xs" mt="md">
@@ -237,6 +248,7 @@ function DashboardLayout() {
<NavLink <NavLink
onClick={() => { onClick={() => {
navigate({ to: item.to }); navigate({ to: item.to });
if (mobileOpened) toggleMobile(); if (mobileOpened) toggleMobile();
}} }}
leftSection={ leftSection={
@@ -254,20 +266,15 @@ function DashboardLayout() {
} }
rightSection={<IconChevronRight size="0.8rem" stroke={1.5} />} rightSection={<IconChevronRight size="0.8rem" stroke={1.5} />}
active={isActive(item.to)} active={isActive(item.to)}
variant="filled" variant="light"
color="orange" color="orange"
styles={{ styles={{
root: { root: {
borderRadius: rem(8), borderRadius: rem(8),
marginBottom: rem(4), marginBottom: rem(4),
backgroundColor: isActive(item.to)
? "rgba(243, 213, 163, 0.1)"
: "transparent",
color: isActive(item.to) ? "#f3d5a3" : "#fbf0df",
"&:hover": {
backgroundColor: "rgba(243, 213, 163, 0.05)",
},
}, },
label: { label: {
fontSize: rem(14), fontSize: rem(14),
}, },
@@ -279,7 +286,7 @@ function DashboardLayout() {
</AppShell.Section> </AppShell.Section>
<AppShell.Section <AppShell.Section
style={{ borderTop: "1px solid rgba(251, 240, 223, 0.1)" }} style={{ borderTop: "1px solid var(--mantine-color-default-border)" }}
pt="md" pt="md"
> >
<NavLink <NavLink
@@ -292,6 +299,7 @@ function DashboardLayout() {
} }
styles={{ root: { borderRadius: rem(8) } }} styles={{ root: { borderRadius: rem(8) } }}
/> />
<NavLink <NavLink
label="Keluar" label="Keluar"
onClick={handleLogout} onClick={handleLogout}
@@ -308,7 +316,7 @@ function DashboardLayout() {
</AppShell.Section> </AppShell.Section>
</AppShell.Navbar> </AppShell.Navbar>
<AppShell.Main bg="rgba(15, 15, 15, 1)"> <AppShell.Main>
<Box p="lg" style={{ minHeight: "calc(100vh - 100px)" }}> <Box p="lg" style={{ minHeight: "calc(100vh - 100px)" }}>
<Outlet /> <Outlet />
</Box> </Box>

View File

@@ -71,7 +71,7 @@ function EditProfile() {
<Stack gap="xl"> <Stack gap="xl">
<Group justify="space-between" align="center"> <Group justify="space-between" align="center">
<Box> <Box>
<Title order={1} c="#f3d5a3"> <Title order={1} c="orange.6">
Edit Profil Edit Profil
</Title> </Title>
<Text c="dimmed" size="sm"> <Text c="dimmed" size="sm">
@@ -88,14 +88,13 @@ function EditProfile() {
</Button> </Button>
</Group> </Group>
<Divider color="rgba(251, 240, 223, 0.1)" /> <Divider style={{ opacity: 0.1 }} />
<Card <Card
withBorder withBorder
radius="md" radius="md"
p="xl" p="xl"
bg="rgba(26, 26, 26, 0.5)" style={{ border: "1px solid var(--mantine-color-default-border)" }}
style={{ border: "1px solid rgba(251, 240, 223, 0.1)" }}
> >
<form onSubmit={form.onSubmit(handleUpdateProfile)}> <form onSubmit={form.onSubmit(handleUpdateProfile)}>
<Stack gap="md"> <Stack gap="md">
@@ -104,10 +103,9 @@ function EditProfile() {
placeholder="Masukkan nama lengkap Anda" placeholder="Masukkan nama lengkap Anda"
{...form.getInputProps("name")} {...form.getInputProps("name")}
styles={{ styles={{
label: { color: "#fbf0df", marginBottom: 8 }, label: { marginBottom: 8 },
input: { input: {
backgroundColor: "rgba(0,0,0,0.2)", backgroundColor: "var(--mantine-color-default-soft)",
color: "#fbf0df",
}, },
}} }}
/> />
@@ -116,10 +114,9 @@ function EditProfile() {
placeholder="https://example.com/photo.jpg" placeholder="https://example.com/photo.jpg"
{...form.getInputProps("image")} {...form.getInputProps("image")}
styles={{ styles={{
label: { color: "#fbf0df", marginBottom: 8 }, label: { marginBottom: 8 },
input: { input: {
backgroundColor: "rgba(0,0,0,0.2)", backgroundColor: "var(--mantine-color-default-soft)",
color: "#fbf0df",
}, },
}} }}
/> />

View File

@@ -34,6 +34,7 @@ import {
import { createFileRoute, useNavigate } from "@tanstack/react-router"; import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useState } from "react"; import { useState } from "react";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
import { ColorSchemeToggle } from "@/components/ColorSchemeToggle";
import { protectedRouteMiddleware } from "@/middleware/authMiddleware"; import { protectedRouteMiddleware } from "@/middleware/authMiddleware";
import { authClient } from "@/utils/auth-client"; import { authClient } from "@/utils/auth-client";
import { authStore } from "../../store/auth"; import { authStore } from "../../store/auth";
@@ -92,25 +93,22 @@ function Profile() {
withBorder withBorder
p="md" p="md"
radius="md" radius="md"
bg="rgba(251, 240, 223, 0.03)" style={{ border: "1px solid var(--mantine-color-default-border)" }}
style={{ border: "1px solid rgba(251, 240, 223, 0.1)" }}
> >
<Group wrap="nowrap" align="flex-start"> <Group wrap="nowrap" align="flex-start">
<Box mt={3}> <Box mt={3}>
<Icon size={20} stroke={1.5} color="#f3d5a3" /> <Icon
size={20}
stroke={1.5}
color="var(--mantine-color-orange-filled)"
/>
</Box> </Box>
<Box style={{ flex: 1 }}> <Box style={{ flex: 1 }}>
<Text size="xs" c="dimmed" tt="uppercase" fw={700} lts={0.5}> <Text size="xs" c="dimmed" tt="uppercase" fw={700} lts={0.5}>
{label} {label}
</Text> </Text>
<Group gap="xs" mt={4} wrap="nowrap"> <Group gap="xs" mt={4} wrap="nowrap">
<Text <Text fw={500} size="sm" truncate="end" style={{ flex: 1 }}>
fw={500}
size="sm"
c="#fbf0df"
truncate="end"
style={{ flex: 1 }}
>
{value || "N/A"} {value || "N/A"}
</Text> </Text>
{copyable && value && ( {copyable && value && (
@@ -145,7 +143,7 @@ function Profile() {
{/* Header Section */} {/* Header Section */}
<Group justify="space-between" align="center"> <Group justify="space-between" align="center">
<Box> <Box>
<Title order={1} c="#f3d5a3"> <Title order={1} c="orange.6">
Profil Saya Profil Saya
</Title> </Title>
<Text c="dimmed" size="sm"> <Text c="dimmed" size="sm">
@@ -153,6 +151,7 @@ function Profile() {
</Text> </Text>
</Box> </Box>
<Group> <Group>
<ColorSchemeToggle />
{snap.user?.role === "admin" && ( {snap.user?.role === "admin" && (
<Button <Button
variant="light" variant="light"
@@ -182,20 +181,17 @@ function Profile() {
</Group> </Group>
</Group> </Group>
<Divider color="rgba(251, 240, 223, 0.1)" /> <Divider style={{ opacity: 0.1 }} />
{/* Profile Overview Card */} {/* Profile Overview Card */}
<Card <Card withBorder radius="lg" p={0} style={{ overflow: "hidden" }}>
withBorder
radius="lg"
p={0}
bg="rgba(26, 26, 26, 0.5)"
style={{ overflow: "hidden" }}
>
<Box <Box
h={120} h={120}
bg="linear-gradient(45deg, #2c2c2c 0%, #1a1a1a 100%)" style={{
style={{ borderBottom: "1px solid rgba(251, 240, 223, 0.1)" }} background:
"linear-gradient(45deg, var(--mantine-color-gray-filled) 0%, var(--mantine-color-dark-filled) 100%)",
borderBottom: "1px solid var(--mantine-color-default-border)",
}}
/> />
<Box px="xl" pb="xl" style={{ marginTop: rem(-60) }}> <Box px="xl" pb="xl" style={{ marginTop: rem(-60) }}>
<Group align="flex-end" gap="xl" mb="md"> <Group align="flex-end" gap="xl" mb="md">
@@ -204,16 +200,14 @@ function Profile() {
size={120} size={120}
radius={120} radius={120}
style={{ style={{
border: "4px solid #1a1a1a", border: "4px solid var(--mantine-color-body)",
boxShadow: "0 4px 10px rgba(0,0,0,0.3)", boxShadow: "var(--mantine-shadow-md)",
}} }}
> >
{snap.user?.name?.charAt(0).toUpperCase()} {snap.user?.name?.charAt(0).toUpperCase()}
</Avatar> </Avatar>
<Stack gap={0} pb="md"> <Stack gap={0} pb="md">
<Title order={2} c="#fbf0df"> <Title order={2}>{snap.user?.name}</Title>
{snap.user?.name}
</Title>
<Group gap="xs"> <Group gap="xs">
<Text c="dimmed" size="sm"> <Text c="dimmed" size="sm">
{snap.user?.email} {snap.user?.email}
@@ -237,7 +231,7 @@ function Profile() {
<Grid gutter="lg"> <Grid gutter="lg">
<Grid.Col span={{ base: 12, md: 7 }}> <Grid.Col span={{ base: 12, md: 7 }}>
<Stack gap="md"> <Stack gap="md">
<Title order={4} c="#f3d5a3"> <Title order={4} c="orange.6">
Informasi Identitas Informasi Identitas
</Title> </Title>
<Grid gutter="sm"> <Grid gutter="sm">
@@ -279,15 +273,16 @@ function Profile() {
<Grid.Col span={{ base: 12, md: 5 }}> <Grid.Col span={{ base: 12, md: 5 }}>
<Stack gap="md"> <Stack gap="md">
<Title order={4} c="#f3d5a3"> <Title order={4} c="orange.6">
Keamanan & Sesi Keamanan & Sesi
</Title> </Title>
<Card <Card
withBorder withBorder
radius="md" radius="md"
p="lg" p="lg"
bg="rgba(251, 240, 223, 0.03)" style={{
style={{ border: "1px solid rgba(251, 240, 223, 0.1)" }} border: "1px solid var(--mantine-color-default-border)",
}}
> >
<Stack gap="md"> <Stack gap="md">
<Box> <Box>
@@ -312,9 +307,6 @@ function Profile() {
<Code <Code
block block
style={{ style={{
backgroundColor: "rgba(0,0,0,0.3)",
color: "#f3d5a3",
border: "1px solid rgba(251, 240, 223, 0.1)",
fontSize: rem(11), fontSize: rem(11),
flex: 1, flex: 1,
}} }}

View File

@@ -1,25 +1,24 @@
const CACHE_NAME = 'makuro-v1'; const CACHE_NAME = "makuro-v1";
const ASSETS = [ const ASSETS = ["/", "/index.html", "/logo.svg"];
'/',
'/index.html',
'/logo.svg'
];
self.addEventListener('install', (event) => { self.addEventListener("install", (event) => {
event.waitUntil( event.waitUntil(
caches.open(CACHE_NAME).then((cache) => { caches.open(CACHE_NAME).then((cache) => {
console.log('SW: Pre-caching assets'); console.log("SW: Pre-caching assets");
return cache.addAll(ASSETS).catch(err => { return cache.addAll(ASSETS).catch((err) => {
console.error('SW: Pre-cache failed (likely due to Vary header or missing file):', err); console.error(
"SW: Pre-cache failed (likely due to Vary header or missing file):",
err,
);
}); });
}) }),
); );
}); });
self.addEventListener('fetch', (event) => { self.addEventListener("fetch", (event) => {
event.respondWith( event.respondWith(
caches.match(event.request).then((response) => { caches.match(event.request).then((response) => {
return response || fetch(event.request); return response || fetch(event.request);
}) }),
); );
}); });

View File

@@ -1,7 +1,7 @@
import path from "node:path"; import path from "node:path";
import { inspectorServer } from "@react-dev-inspector/vite-plugin"; import { inspectorServer } from "@react-dev-inspector/vite-plugin";
import { tanstackRouter } from "@tanstack/router-vite-plugin";
import tailwindcss from "@tailwindcss/vite"; import tailwindcss from "@tailwindcss/vite";
import { tanstackRouter } from "@tanstack/router-vite-plugin";
import react from "@vitejs/plugin-react"; import react from "@vitejs/plugin-react";
import { createServer as createViteServer } from "vite"; import { createServer as createViteServer } from "vite";
@@ -36,7 +36,13 @@ export async function createVite() {
}, },
appType: "custom", appType: "custom",
optimizeDeps: { optimizeDeps: {
include: ["react", "react-dom", "@mantine/core", "manifest.json", "sw.js"], include: [
"react",
"react-dom",
"@mantine/core",
"manifest.json",
"sw.js",
],
}, },
}); });
} }