Initial commit: Setup Bun, Elysia, Vite, React, TanStack Router, Mantine, and Biome

This commit is contained in:
bipproduction
2026-02-07 02:15:29 +08:00
commit b9abcaadde
46 changed files with 5742 additions and 0 deletions

View File

@@ -0,0 +1,485 @@
/** biome-ignore-all lint/suspicious/noExplicitAny: <explanation */
import {
ActionIcon,
Alert,
Badge,
Button,
Card,
Container,
CopyButton,
Group,
LoadingOverlay,
Modal,
Stack,
Switch,
Table,
Text,
TextInput,
Title,
Tooltip,
} from "@mantine/core";
import { DatePicker, type DatePickerValue } from "@mantine/dates";
import {
IconCalendar,
IconCircleCheck,
IconCircleX,
IconClock,
IconCopy,
IconEye,
IconEyeOff,
IconInfoCircle,
IconKey,
IconPlus,
IconTrash,
} from "@tabler/icons-react";
import { createFileRoute } from "@tanstack/react-router";
import dayjs from "dayjs";
import { useCallback, useEffect, useState } from "react";
import { protectedRouteMiddleware } from "../../middleware/authMiddleware";
import { apiClient } from "../../utils/api-client";
export const Route = createFileRoute("/dashboard/apikey")({
beforeLoad: protectedRouteMiddleware,
component: DashboardApikeyComponent,
});
interface ApiKey {
id: string;
name: string;
key: string;
isActive: boolean;
expiresAt: string | null;
createdAt: string;
updatedAt: string;
}
function DashboardApikeyComponent() {
const [apiKeys, setApiKeys] = useState<ApiKey[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [createModalOpen, setCreateModalOpen] = useState(false);
const [newKeyName, setNewKeyName] = useState("");
const [newKeyExpiresAt, setNewKeyExpiresAt] = useState<DatePickerValue>(null);
const [creating, setCreating] = useState(false);
const [showKey, setShowKey] = useState<{ [key: string]: boolean }>({});
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
const [keyToDelete, setKeyToDelete] = useState<string | null>(null);
const fetchApiKeys = useCallback(async () => {
try {
setLoading(true);
const response = await apiClient.api.apikey.get();
if (response.data) {
setApiKeys((response.data.apiKeys as any) || []);
}
} catch (err) {
setError("Failed to load API keys");
console.error(err);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchApiKeys();
}, [fetchApiKeys]);
const handleCreateApiKey = async () => {
if (!newKeyName.trim()) {
setError("API key name is required");
return;
}
try {
setCreating(true);
const response = await apiClient.api.apikey.post({
name: newKeyName,
expiresAt: newKeyExpiresAt ? dayjs(newKeyExpiresAt).toISOString() : undefined,
});
if (response.data) {
setApiKeys([...apiKeys, response.data.apiKey as any]);
setNewKeyName("");
setNewKeyExpiresAt(null);
setCreateModalOpen(false);
}
} catch (err) {
setError("Failed to create API key");
console.error(err);
} finally {
setCreating(false);
}
};
const handleToggleApiKey = async (id: string, currentStatus: boolean) => {
try {
if (!id) {
setError("API key ID is required");
return;
}
const response = await apiClient.api.apikey.update.post({
id,
isActive: !currentStatus,
});
if (response.data) {
setApiKeys(
apiKeys.map((key) =>
key.id === id ? { ...key, isActive: !currentStatus } : key,
),
);
}
} catch (err) {
setError("Failed to update API key status");
console.error(err);
}
};
const handleDeleteApiKey = async (id: string) => {
// Store the key ID and open the confirmation modal
setKeyToDelete(id);
setDeleteModalOpen(true);
};
const confirmDeleteApiKey = async () => {
if (!keyToDelete) return;
try {
await apiClient.api.apikey.delete.post({
id: keyToDelete,
});
setApiKeys(apiKeys.filter((key: ApiKey) => key.id !== keyToDelete));
setDeleteModalOpen(false);
setKeyToDelete(null);
} catch (err) {
setError("Failed to delete API key");
console.error(err);
}
};
const toggleShowKey = (id: string) => {
setShowKey((prev) => ({
...prev,
[id]: !prev[id],
}));
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString();
};
const formatTime = (dateString: string) => {
return new Date(dateString).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
});
};
return (
<Container size="lg" py="xl">
<Title order={1} mb="lg" ta="center">
API Keys Management
</Title>
{error && (
<Alert title="Error" color="red" mb="md">
{error}
</Alert>
)}
<Card
withBorder
p="xl"
radius="md"
bg="rgba(251, 240, 223, 0.05)"
style={{ border: "1px solid rgba(251, 240, 223, 0.1)" }}
>
<Group justify="space-between" mb="md">
<Stack gap={0}>
<Title order={3}>Your API Keys</Title>
<Text size="sm" c="dimmed">
Manage your API keys for secure access to our services
</Text>
</Stack>
<Button
leftSection={<IconPlus size={16} />}
onClick={() => setCreateModalOpen(true)}
variant="light"
color="blue"
>
Create New API Key
</Button>
</Group>
<Table striped highlightOnHover mt="md" verticalSpacing="md">
<Table.Thead>
<Table.Tr>
<Table.Th>
<Group gap={6}>
<IconKey size={16} stroke={1.5} /> Name
</Group>
</Table.Th>
<Table.Th>
<Group gap={6}>
<IconKey size={16} stroke={1.5} /> Key
</Group>
</Table.Th>
<Table.Th>
<Group gap={6}>
<IconCircleCheck size={16} stroke={1.5} /> Status
</Group>
</Table.Th>
<Table.Th>
<Group gap={6}>
<IconCalendar size={16} stroke={1.5} /> Expiration
</Group>
</Table.Th>
<Table.Th>
<Group gap={6}>
<IconClock size={16} stroke={1.5} /> Created
</Group>
</Table.Th>
<Table.Th>
<Group gap={6}>
<IconInfoCircle size={16} stroke={1.5} /> Actions
</Group>
</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{apiKeys.map((apiKey) => (
<Table.Tr
key={apiKey.id}
style={{ backgroundColor: "rgba(251, 240, 223, 0.02)" }}
>
<Table.Td>
<Text fw={500} c="#fbf0df">
{apiKey.name}
</Text>
</Table.Td>
<Table.Td>
<Group gap={6}>
{showKey[apiKey.id] ? (
<Text
c="#f3d5a3"
style={{ fontFamily: "monospace", fontSize: "0.85rem" }}
>
{apiKey.key}
</Text>
) : (
<Text
c="dimmed"
style={{ fontFamily: "monospace", fontSize: "0.85rem" }}
>
</Text>
)}
<CopyButton value={apiKey.key}>
{({ copied, copy }) => (
<Tooltip label={copied ? "Copied" : "Copy"}>
<ActionIcon
color={copied ? "green" : "gray"}
onClick={copy}
variant="subtle"
size="sm"
>
<IconCopy size={16} />
</ActionIcon>
</Tooltip>
)}
</CopyButton>
<Tooltip
label={showKey[apiKey.id] ? "Hide key" : "Show key"}
>
<ActionIcon
color="gray"
onClick={() => toggleShowKey(apiKey.id)}
variant="subtle"
size="sm"
>
{showKey[apiKey.id] ? (
<IconEyeOff size={16} />
) : (
<IconEye size={16} />
)}
</ActionIcon>
</Tooltip>
</Group>
</Table.Td>
<Table.Td>
<Group>
<Tooltip
label={`API Key is ${apiKey.isActive ? "Active" : "Inactive"}`}
>
<Switch
checked={apiKey.isActive}
onChange={() =>
handleToggleApiKey(apiKey.id, apiKey.isActive)
}
size="md"
color={apiKey.isActive ? "green" : "gray"}
onLabel={<IconCircleCheck size={12} stroke={1.5} />}
offLabel={<IconCircleX size={12} stroke={1.5} />}
/>
</Tooltip>
</Group>
</Table.Td>
<Table.Td>
{apiKey.expiresAt ? (
<Group>
<Text>{formatDate(apiKey.expiresAt)}</Text>
<Text c="dimmed" size="sm">
{formatTime(apiKey.expiresAt)}
</Text>
</Group>
) : (
<Badge variant="outline" color="blue">
Never Expires
</Badge>
)}
</Table.Td>
<Table.Td>
<Group>
<Text>{formatDate(apiKey.createdAt)}</Text>
<Text c="dimmed" size="sm">
{formatTime(apiKey.createdAt)}
</Text>
</Group>
</Table.Td>
<Table.Td>
<Group>
<Tooltip label="Delete API Key">
<ActionIcon
color="red"
onClick={() => handleDeleteApiKey(apiKey.id)}
variant="light"
size="lg"
>
<IconTrash size={16} />
</ActionIcon>
</Tooltip>
</Group>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
{apiKeys.length === 0 && !loading && (
<Card
p="xl"
radius="md"
withBorder
mt="xl"
bg="rgba(251, 240, 223, 0.03)"
>
<Group justify="center" align="center">
<Stack align="center" gap="md">
<IconKey
size={48}
stroke={1.2}
color="rgba(251, 240, 223, 0.3)"
/>
<Text ta="center" c="dimmed" fz="lg">
No API keys created yet
</Text>
<Text ta="center" c="dimmed" size="sm">
Get started by creating your first API key
</Text>
<Button
leftSection={<IconPlus size={16} />}
onClick={() => setCreateModalOpen(true)}
variant="light"
color="blue"
mt="md"
>
Create New API Key
</Button>
</Stack>
</Group>
</Card>
)}
</Card>
<Modal
opened={createModalOpen}
onClose={() => {
setCreateModalOpen(false);
setError(null);
}}
title="Create New API Key"
centered
size="md"
>
<LoadingOverlay
visible={creating}
zIndex={1000}
overlayProps={{ radius: "sm", blur: 2 }}
/>
<TextInput
label="API Key Name"
placeholder="Enter a descriptive name for your API key"
value={newKeyName}
onChange={(e) => setNewKeyName(e.currentTarget.value)}
mb="md"
description="Choose a name that identifies the purpose of this API key"
/>
<DatePicker
value={newKeyExpiresAt}
onChange={setNewKeyExpiresAt}
mb="md"
/>
<Group justify="flex-end" mt="xl">
<Button
variant="subtle"
color="gray"
onClick={() => {
setCreateModalOpen(false);
setError(null);
}}
>
Cancel
</Button>
<Button
leftSection={<IconPlus size={16} />}
onClick={handleCreateApiKey}
color="blue"
>
Create API Key
</Button>
</Group>
</Modal>
<Modal
opened={deleteModalOpen}
onClose={() => setDeleteModalOpen(false)}
title="Confirm Delete"
centered
size="md"
>
<Stack>
<Text>Are you sure you want to delete this API key?</Text>
<Text size="sm" c="dimmed">
This action cannot be undone.
</Text>
<Group justify="flex-end" mt="xl">
<Button
variant="subtle"
color="gray"
onClick={() => setDeleteModalOpen(false)}
>
Cancel
</Button>
<Button color="red" onClick={confirmDeleteApiKey}>
Delete API Key
</Button>
</Group>
</Stack>
</Modal>
</Container>
);
}

View File

@@ -0,0 +1,224 @@
import {
Avatar,
Badge,
Box,
Button,
Card,
Container,
Grid,
Group,
Modal,
Progress,
SimpleGrid,
Stack,
Text,
Title,
} from "@mantine/core";
import {
IconClock,
IconDatabase,
IconServer,
IconUserCheck,
} from "@tabler/icons-react";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useState } from "react";
import { useSnapshot } from "valtio";
import { authClient } from "@/utils/auth-client";
import { authStore } from "../../store/auth";
export const Route = createFileRoute("/dashboard/")({
component: DashboardComponent,
});
function DashboardComponent() {
const snap = useSnapshot(authStore);
const navigate = useNavigate();
const [logoutModalOpen, setLogoutModalOpen] = useState(false);
// Mock data for dashboard stats
const statsData = [
{ title: "Total Users", value: "1,234", icon: <IconUserCheck size={24} /> },
{ title: "Server Uptime", value: "99.9%", icon: <IconClock size={24} /> },
{ title: "Database Load", value: "42%", icon: <IconDatabase size={24} /> },
{ title: "Active Sessions", value: "128", icon: <IconServer size={24} /> },
];
return (
<Container size="lg" py="xl">
<Title order={1} mb="lg" ta="center">
Dashboard Overview
</Title>
{/* User Profile Card */}
<Card
withBorder
p="xl"
radius="md"
mb="xl"
bg="rgba(251, 240, 223, 0.05)"
style={{ border: "1px solid rgba(251, 240, 223, 0.1)" }}
>
<Group justify="space-between">
<Group>
<Avatar
src={snap.user?.image}
size={80}
radius="xl"
style={{
cursor: "pointer",
border: "2px solid rgba(251, 240, 223, 0.3)",
}}
onClick={() => navigate({ to: "/profile" })}
>
{snap.user?.name?.charAt(0).toUpperCase()}
</Avatar>
<div>
<Text size="lg" fw={600} c="#fbf0df">
{snap.user?.name}
</Text>
<Text c="dimmed" size="sm">
{snap.user?.email}
</Text>
<Badge mt="xs" variant="light" color="green">
Verified Account
</Badge>
</div>
</Group>
<Button
variant="outline"
color="red"
onClick={() => setLogoutModalOpen(true)}
>
Sign Out
</Button>
</Group>
</Card>
{/* Stats Grid */}
<SimpleGrid cols={{ base: 1, sm: 2, md: 4 }} spacing="lg" mb="xl">
{statsData.map((stat, index) => (
<Card
key={index.toString()}
withBorder
p="lg"
radius="md"
bg="rgba(251, 240, 223, 0.05)"
>
<Group justify="space-between">
<Box>
<Text size="sm" c="dimmed">
{stat.title}
</Text>
<Text size="lg" fw={700} c="#fbf0df">
{stat.value}
</Text>
</Box>
<Box c="#f3d5a3">{stat.icon}</Box>
</Group>
</Card>
))}
</SimpleGrid>
<Grid gutter="lg">
<Grid.Col span={{ base: 12, md: 8 }}>
<Card
withBorder
p="lg"
radius="md"
mb="lg"
bg="rgba(251, 240, 223, 0.05)"
>
<Title order={3} mb="md">
System Performance
</Title>
<Stack gap="md">
<Box>
<Group justify="space-between" mb="xs">
<Text size="sm">CPU Usage</Text>
<Text size="sm" fw={500}>
32%
</Text>
</Group>
<Progress value={32} color="green" />
</Box>
<Box>
<Group justify="space-between" mb="xs">
<Text size="sm">Memory Usage</Text>
<Text size="sm" fw={500}>
64%
</Text>
</Group>
<Progress value={64} color="blue" />
</Box>
<Box>
<Group justify="space-between" mb="xs">
<Text size="sm">Disk Usage</Text>
<Text size="sm" fw={500}>
45%
</Text>
</Group>
<Progress value={45} color="yellow" />
</Box>
</Stack>
</Card>
</Grid.Col>
<Grid.Col span={{ base: 12, md: 4 }}>
<Card withBorder p="lg" radius="md" bg="rgba(251, 240, 223, 0.05)">
<Title order={3} mb="md">
Server Status
</Title>
<Stack gap="sm">
<Group justify="space-between">
<Text size="sm">Main Server</Text>
<Badge color="green" variant="light">
Online
</Badge>
</Group>
<Group justify="space-between">
<Text size="sm">Database</Text>
<Badge color="green" variant="light">
Connected
</Badge>
</Group>
<Group justify="space-between">
<Text size="sm">Cache</Text>
<Badge color="green" variant="light">
Running
</Badge>
</Group>
<Group justify="space-between">
<Text size="sm">Backup</Text>
<Badge color="orange" variant="light">
Pending
</Badge>
</Group>
</Stack>
</Card>
</Grid.Col>
</Grid>
<Modal
opened={logoutModalOpen}
onClose={() => setLogoutModalOpen(false)}
title="Confirm Logout"
centered
>
<Text mb="md">Are you sure you want to log out?</Text>
<Group justify="flex-end">
<Button variant="outline" onClick={() => setLogoutModalOpen(false)}>
Cancel
</Button>
<Button
color="red"
onClick={() => {
authClient.signOut();
setLogoutModalOpen(false);
}}
>
Logout
</Button>
</Group>
</Modal>
</Container>
);
}

View File

@@ -0,0 +1,122 @@
import {
ActionIcon,
AppShell,
Burger,
Group,
NavLink,
rem,
ScrollArea,
Text,
Title
} from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import {
IconHome,
IconKey,
IconSettings,
IconUsers
} from "@tabler/icons-react";
import {
createFileRoute,
Outlet,
useLocation,
useNavigate,
} from "@tanstack/react-router";
export const Route = createFileRoute("/dashboard")({
component: DashboardLayout,
});
function DashboardLayout() {
const location = useLocation();
const [mobileOpened, { toggle: toggleMobile }] = useDisclosure();
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
const navigate = useNavigate();
const navItems = [
{ icon: IconHome, label: "Beranda", to: "/dashboard" },
{ icon: IconUsers, label: "Pengguna", to: "/dashboard/users" },
{ icon: IconKey, label: "API Key", to: "/dashboard/apikey" },
{ icon: IconSettings, label: "Pengaturan", to: "/dashboard/settings" },
];
const isActive = (path: string) => {
const current = location.pathname;
if (path === "/dashboard") {
return current === "/dashboard";
}
return current === path || current.startsWith(`${path}/`);
};
return (
<AppShell
navbar={{
width: 300,
breakpoint: "sm",
collapsed: { mobile: !mobileOpened, desktop: !desktopOpened },
}}
padding="md"
header={{ height: 60 }}
>
<AppShell.Header>
<Group h="100%" px="md" justify="space-between">
<Group>
<Burger
opened={mobileOpened}
onClick={toggleMobile}
hiddenFrom="sm"
size="sm"
/>
<Burger
opened={desktopOpened}
onClick={toggleDesktop}
visibleFrom="sm"
size="sm"
/>
<Title order={3}>Dashboard</Title>
</Group>
<Group>
<ActionIcon variant="subtle" size="lg">
<IconSettings
style={{ width: rem(20), height: rem(20) }}
stroke={1.5}
/>
</ActionIcon>
</Group>
</Group>
</AppShell.Header>
<AppShell.Navbar p="md">
<ScrollArea h="calc(100vh - 120px)">
<Group mb="lg">
<Text fw={500} size="lg">
Navigasi Utama
</Text>
</Group>
{navItems.map((item) => (
<NavLink
key={item.to}
onClick={() => {
navigate({ to: item.to });
}}
leftSection={
<item.icon
style={{ width: rem(18), height: rem(18) }}
stroke={1.5}
/>
}
label={item.label}
active={isActive(item.to)}
/>
))}
</ScrollArea>
</AppShell.Navbar>
<AppShell.Main>
<Outlet />
</AppShell.Main>
</AppShell>
);
}

View File

@@ -0,0 +1,11 @@
import { createFileRoute } from "@tanstack/react-router";
import { protectedRouteMiddleware } from "../../middleware/authMiddleware";
export const Route = createFileRoute("/dashboard/settings")({
beforeLoad: protectedRouteMiddleware,
component: DashboardSettingsComponent,
});
function DashboardSettingsComponent() {
return <div>Hello from /dashboard/settings!</div>;
}

View File

@@ -0,0 +1,11 @@
import { createFileRoute } from "@tanstack/react-router";
import { protectedRouteMiddleware } from "../../middleware/authMiddleware";
export const Route = createFileRoute("/dashboard/users")({
beforeLoad: protectedRouteMiddleware,
component: DashboardUsersComponent,
});
function DashboardUsersComponent() {
return <div>Hello from /dashboard/users!</div>;
}