Initial commit: Setup Bun, Elysia, Vite, React, TanStack Router, Mantine, and Biome
This commit is contained in:
485
src/routes/dashboard/apikey.tsx
Normal file
485
src/routes/dashboard/apikey.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
224
src/routes/dashboard/index.tsx
Normal file
224
src/routes/dashboard/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
122
src/routes/dashboard/route.tsx
Normal file
122
src/routes/dashboard/route.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
11
src/routes/dashboard/settings.tsx
Normal file
11
src/routes/dashboard/settings.tsx
Normal 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>;
|
||||
}
|
||||
11
src/routes/dashboard/users.tsx
Normal file
11
src/routes/dashboard/users.tsx
Normal 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>;
|
||||
}
|
||||
Reference in New Issue
Block a user