This commit is contained in:
bipproduction
2025-10-13 17:27:10 +08:00
parent dd6ca462a9
commit 8b9abcdd03
4 changed files with 721 additions and 203 deletions

View File

@@ -1,7 +1,70 @@
import { Link } from "react-router-dom";
import { Button, Container, Stack, Title, Text, Group, Card, Divider } from "@mantine/core";
import { IconArrowRight, IconRocket, IconTerminal2 } from "@tabler/icons-react";
import clientRoutes from "@/clientRoutes";
export default function Home() {
return (
<div>
<h1>Home</h1>
</div>
<Container
mih="100vh"
px="md"
size={"full"}
w={"100%"}
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "radial-gradient(circle at top left, rgba(0,255,200,0.08), transparent 70%)",
}}
>
<Card
radius="xl"
p="xl"
withBorder
style={{
textAlign: "center",
background:
"linear-gradient(145deg, rgba(20,20,20,0.95), rgba(45,45,45,0.9))",
borderColor: "rgba(100,100,100,0.2)",
boxShadow: "0 0 25px rgba(0,255,200,0.08)",
backdropFilter: "blur(6px)",
}}
>
<Stack gap="lg" align="center">
<Group gap={8}>
<IconRocket size={28} color="teal" />
<Title order={1} c="gray.0">
Welcome to Jenna
</Title>
</Group>
<Text c="dimmed" size="md" maw={500}>
A futuristic dashboard experience built with Mantine and Bun designed for speed,
precision, and modern elegance. Navigate to your dashboard and start exploring.
</Text>
<Divider w="40%" mx="auto" />
<Button
component={Link}
to={clientRoutes["/scr/dashboard"]}
radius="md"
size="md"
variant="gradient"
gradient={{ from: "teal", to: "cyan", deg: 45 }}
rightSection={<IconArrowRight size={18} />}
style={{
boxShadow: "0 0 12px rgba(0,255,200,0.3)",
transition: "all 0.2s ease",
}}
>
Go to Dashboard
</Button>
<Group mt="xl" gap={4} c="dimmed">
<IconTerminal2 size={14} />
<Text size="xs" c="dimmed">
Built for developers optimized for 2025 workflows.
</Text>
</Group>
</Stack>
</Card>
</Container>
);
}

View File

@@ -7,16 +7,39 @@ import {
Table,
Text,
TextInput,
Title,
ScrollArea,
ActionIcon,
Tooltip,
Divider,
Loader,
Badge,
useMantineTheme,
} from "@mantine/core";
import { useEffect, useState } from "react";
import apiFetch from "@/lib/apiFetch";
import {
IconPlus,
IconCopy,
IconTrash,
IconKey,
IconDatabase,
IconLock,
} from "@tabler/icons-react";
import { showNotification } from "@mantine/notifications";
import apiFetch from "@/lib/apiFetch";
export default function ApiKeyPage() {
return (
<Container size="md" w={"100%"}>
<Stack>
<Text>API Key</Text>
<Container size="lg" py="xl" w="100%">
<Stack gap="xl">
<Group justify="space-between" align="center">
<Group gap="xs">
<IconKey size={28} color="var(--mantine-color-cyan-5)" />
<Title order={2} fw={700} c="gray.0">
API Key Management
</Title>
</Group>
</Group>
<CreateApiKey />
</Stack>
</Container>
@@ -24,6 +47,7 @@ export default function ApiKeyPage() {
}
function CreateApiKey() {
const theme = useMantineTheme();
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [expiredAt, setExpiredAt] = useState("");
@@ -31,6 +55,14 @@ function CreateApiKey() {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!name || !expiredAt) {
showNotification({
color: "red",
title: "Missing Information",
message: "Please provide a name and expiration date.",
});
return;
}
setLoading(true);
const res = await apiFetch.api.apikey.create.post({
name,
@@ -42,52 +74,123 @@ function CreateApiKey() {
setDescription("");
setExpiredAt("");
showNotification({
title: "Success",
message: "API key created successfully",
color: "green",
title: "API Key Created",
message: "Your new API key is now active and ready to use.",
color: "teal",
});
} else {
showNotification({
title: "Error",
message: "Failed to create API key. Please try again.",
color: "red",
});
}
setLoading(false);
};
return (
<Card>
<Stack>
<Text>API Create</Text>
<TextInput
label="Name"
placeholder="Name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<TextInput
label="Description"
placeholder="Description"
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
<TextInput
label="Expired At"
placeholder="Expired At"
type="date"
value={expiredAt}
onChange={(e) => setExpiredAt(e.target.value)}
/>
<Group>
<Button
variant="outline"
onClick={() => {
setName("");
setDescription("");
setExpiredAt("");
}}
>
Cancel
</Button>
<Button onClick={handleSubmit} type="submit" loading={loading}>
Save
</Button>
</Group>
return (
<Card
radius="lg"
p="xl"
withBorder
style={{
background:
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
borderColor: "rgba(100,100,100,0.2)",
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
}}
>
<Stack gap="md">
<Group justify="space-between" align="center">
<Title order={3} c="gray.0">
Create New API Key
</Title>
<Tooltip label="Generate a new API key for your app">
<ActionIcon
variant="light"
radius="md"
size="lg"
style={{ boxShadow: "0 0 10px rgba(0,255,200,0.2)" }}
>
<IconPlus size={20} />
</ActionIcon>
</Tooltip>
</Group>
<Divider my="sm" />
<form onSubmit={handleSubmit}>
<Stack gap="md">
<TextInput
label="Key Name"
placeholder="Enter a name for this key"
required
radius="md"
value={name}
onChange={(e) => setName(e.target.value)}
styles={{
input: {
backgroundColor: "rgba(255,255,255,0.05)",
borderColor: "rgba(255,255,255,0.1)",
},
}}
/>
<TextInput
label="Description"
placeholder="Describe this keys purpose (optional)"
radius="md"
value={description}
onChange={(e) => setDescription(e.target.value)}
styles={{
input: {
backgroundColor: "rgba(255,255,255,0.05)",
borderColor: "rgba(255,255,255,0.1)",
},
}}
/>
<TextInput
label="Expiration Date"
type="date"
required
radius="md"
value={expiredAt}
onChange={(e) => setExpiredAt(e.target.value)}
styles={{
input: {
backgroundColor: "rgba(255,255,255,0.05)",
borderColor: "rgba(255,255,255,0.1)",
},
}}
/>
<Group justify="flex-end" mt="md">
<Button
variant="light"
color="gray"
radius="md"
onClick={() => {
setName("");
setDescription("");
setExpiredAt("");
}}
>
Clear
</Button>
<Button
type="submit"
loading={loading}
radius="md"
size="md"
variant="gradient"
gradient={{ from: "teal", to: "cyan", deg: 45 }}
style={{
boxShadow: "0 0 12px rgba(0,255,200,0.3)",
transition: "all 0.2s ease",
}}
>
Create Key
</Button>
</Group>
</Stack>
</form>
<Divider my="lg" />
<ListApiKey />
</Stack>
</Card>
@@ -95,69 +198,169 @@ function CreateApiKey() {
}
function ListApiKey() {
const theme = useMantineTheme();
const [apiKeys, setApiKeys] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchApiKeys = async () => {
const res = await apiFetch.api.apikey.list.get();
if (res.status === 200) {
setApiKeys(res.data?.apiKeys || []);
}
setLoading(false);
};
fetchApiKeys();
}, []);
const handleDelete = async (id: string) => {
const res = await apiFetch.api.apikey.delete.delete({ id });
if (res.status === 200) {
setApiKeys(apiKeys.filter((api: any) => api.id !== id));
showNotification({
title: "API Key Deleted",
message: "The API key has been successfully removed.",
color: "teal",
});
}
};
const handleCopy = (key: string) => {
navigator.clipboard.writeText(key);
showNotification({
title: "Copied",
message: "API key copied to clipboard.",
color: "cyan",
});
};
return (
<Card>
<Stack>
<Text>API List</Text>
<Table>
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Expired At</th>
<th>Created At</th>
<th>Updated At</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{apiKeys.map((apiKey: any, index: number) => (
<tr key={index}>
<td>{apiKey.name}</td>
<td>{apiKey.description}</td>
<td>{apiKey.expiredAt.toISOString().split("T")[0]}</td>
<td>{apiKey.createdAt.toISOString().split("T")[0]}</td>
<td>{apiKey.updatedAt.toISOString().split("T")[0]}</td>
<td>
<Button
variant="outline"
onClick={() => {
apiFetch.api.apikey.delete.delete({ id: apiKey.id });
setApiKeys(
apiKeys.filter((api: any) => api.id !== apiKey.id),
);
<Card
radius="lg"
withBorder
p="xl"
style={{
background:
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
borderColor: "rgba(100,100,100,0.2)",
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
}}
>
<Stack gap="sm">
<Group justify="space-between" align="center">
<Group gap="xs">
<IconDatabase size={22} color="var(--mantine-color-cyan-5)" />
<Title order={3} c="gray.0">
Active API Keys
</Title>
</Group>
<Badge
size="lg"
radius="sm"
color="teal"
variant="light"
style={{ textTransform: "none" }}
>
{apiKeys.length} Active
</Badge>
</Group>
<Divider my="sm" />
{loading ? (
<Group justify="center" py="xl">
<Loader color="teal" size="lg" />
</Group>
) : apiKeys.length === 0 ? (
<Stack align="center" justify="center" py="xl" gap={4}>
<IconLock size={32} color="gray" />
<Text c="dimmed" size="sm">
No API keys created yet.
</Text>
</Stack>
) : (
<ScrollArea>
<Table
highlightOnHover
withTableBorder
withColumnBorders
style={{
borderColor: "rgba(255,255,255,0.1)",
color: "var(--mantine-color-gray-0)",
fontSize: "0.9rem",
}}
>
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Expires</th>
<th>Created</th>
<th>Updated</th>
<th style={{ textAlign: "center" }}>Actions</th>
</tr>
</thead>
<tbody>
{apiKeys.map((apiKey: any, index: number) => (
<tr
key={index}
style={{
backgroundColor: "rgba(255,255,255,0.03)",
transition: "background 0.2s ease",
}}
onMouseEnter={(e) =>
(e.currentTarget.style.backgroundColor =
"rgba(0,255,200,0.05)")
}
onMouseLeave={(e) =>
(e.currentTarget.style.backgroundColor =
"rgba(255,255,255,0.03)")
}
>
Delete
</Button>
<Button
variant="outline"
onClick={() => {
navigator.clipboard.writeText(apiKey.key);
showNotification({
title: "Success",
message: "API key copied to clipboard",
color: "green",
});
}}
>
Copy
</Button>
</td>
</tr>
))}
</tbody>
</Table>
<td>{apiKey.name}</td>
<td>{apiKey.description || "-"}</td>
<td>
<Badge
color={
new Date(apiKey.expiredAt) < new Date()
? "red"
: "grape"
}
variant="light"
>
{new Date(apiKey.expiredAt).toLocaleDateString()}
</Badge>
</td>
<td>{new Date(apiKey.createdAt).toLocaleDateString()}</td>
<td>{new Date(apiKey.updatedAt).toLocaleDateString()}</td>
<td>
<Group gap="xs" justify="center">
<Tooltip label="Copy API Key">
<ActionIcon
variant="light"
color="teal"
onClick={() => handleCopy(apiKey.key)}
radius="md"
>
<IconCopy size={18} />
</ActionIcon>
</Tooltip>
<Tooltip label="Delete API Key">
<ActionIcon
variant="light"
color="red"
onClick={() => handleDelete(apiKey.id)}
radius="md"
>
<IconTrash size={18} />
</ActionIcon>
</Tooltip>
</Group>
</td>
</tr>
))}
</tbody>
</Table>
</ScrollArea>
)}
</Stack>
</Card>
);

View File

@@ -1,33 +1,42 @@
import apiFetch from "@/lib/apiFetch";
import {
ActionIcon,
Button,
Card,
Container,
Divider,
Flex,
Group,
Stack,
Text,
TextInput,
Title,
Tooltip,
LoadingOverlay,
Badge,
} from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks";
import { showNotification } from "@mantine/notifications";
import {
IconKey,
IconTrash,
IconPlus,
IconRefresh,
IconShieldLock,
} from "@tabler/icons-react";
import { useState } from "react";
import useSwr from "swr";
import { proxy, subscribe } from "valtio";
const state = proxy({
reload: "",
});
const state = proxy({ reload: "" });
function reloadState() {
state.reload = Math.random().toString();
}
export default function CredentialPage() {
return (
<Container size={"md"} w={"100%"}>
<Stack>
<Container size="md" w="100%">
<Stack gap="xl" mt="xl">
<CredentialCreate />
<CredentialList />
</Stack>
@@ -38,38 +47,115 @@ export default function CredentialPage() {
function CredentialCreate() {
const [name, setName] = useState("");
const [apikey, setApikey] = useState("");
const [loading, setLoading] = useState(false);
async function handleSubmit() {
const { data } = await apiFetch.api.credential.create.post({
name: name,
value: apikey,
});
setName("");
setApikey("");
showNotification({
message: data?.message,
});
reloadState();
if (!name || !apikey) {
showNotification({
color: "red",
title: "Missing Information",
message: "Please fill in all required fields before saving.",
});
return;
}
setLoading(true);
try {
const { data } = await apiFetch.api.credential.create.post({
name,
value: apikey,
});
setName("");
setApikey("");
showNotification({
color: "teal",
title: "Credential Saved",
message: data?.message || "Your credential has been successfully added.",
});
reloadState();
} catch {
showNotification({
color: "red",
title: "Error",
message: "Failed to create credential. Please try again.",
});
} finally {
setLoading(false);
}
}
return (
<Card>
<Stack>
<Title>Credential Create</Title>
<Card
radius="lg"
p="xl"
withBorder
style={{
background:
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
borderColor: "rgba(100,100,100,0.2)",
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
}}
>
<LoadingOverlay visible={loading} />
<Stack gap="md">
<Flex align="center" justify="space-between">
<Title order={3} c="gray.0">
Create New Credential
</Title>
<Tooltip label="Reload saved credentials">
<ActionIcon
variant="light"
size="lg"
radius="md"
onClick={reloadState}
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
>
<IconRefresh size={20} />
</ActionIcon>
</Tooltip>
</Flex>
<Divider my="sm" />
<TextInput
placeholder="name"
placeholder="Enter a friendly name for this credential"
label="Credential Name"
radius="md"
value={name}
onChange={(e) => setName(e.target.value)}
onChange={(e) => setName(e.currentTarget.value)}
styles={{
input: {
backgroundColor: "rgba(255,255,255,0.05)",
borderColor: "rgba(255,255,255,0.1)",
},
}}
/>
<TextInput
placeholder="apikey"
placeholder="Paste your API key or token here"
label="API Key"
radius="md"
value={apikey}
onChange={(e) => setApikey(e.target.value)}
onChange={(e) => setApikey(e.currentTarget.value)}
leftSection={<IconKey size={18} />}
styles={{
input: {
backgroundColor: "rgba(255,255,255,0.05)",
borderColor: "rgba(255,255,255,0.1)",
},
}}
/>
<Group>
<Button onClick={handleSubmit}>Save</Button>
<Group justify="flex-end" mt="md">
<Button
leftSection={<IconPlus size={18} />}
radius="md"
size="md"
variant="gradient"
gradient={{ from: "teal", to: "cyan", deg: 45 }}
onClick={handleSubmit}
style={{
boxShadow: "0 0 12px rgba(0,255,200,0.3)",
transition: "all 0.2s ease",
}}
>
Save Credential
</Button>
</Group>
</Stack>
</Card>
@@ -77,39 +163,133 @@ function CredentialCreate() {
}
function CredentialList() {
const { data, mutate } = useSwr("/", () =>
apiFetch.api.credential.list.get(),
const { data, mutate, isLoading } = useSwr("/", () =>
apiFetch.api.credential.list.get()
);
useShallowEffect(() => {
const unsubscribe = subscribe(state, async () => {
console.log("state has changed to", state);
mutate();
});
const unsubscribe = subscribe(state, () => mutate());
return () => unsubscribe();
}, []);
async function handleRm(id: string) {
await apiFetch.api.credential.rm.delete({
id: id,
});
reloadState();
async function handleRemove(id: string) {
try {
await apiFetch.api.credential.rm.delete({ id });
showNotification({
color: "teal",
title: "Credential Deleted",
message: "The credential was successfully removed.",
});
reloadState();
} catch {
showNotification({
color: "red",
title: "Error",
message: "Failed to delete credential. Please try again.",
});
}
}
if (isLoading)
return (
<Card
radius="lg"
p="xl"
withBorder
style={{
background:
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
}}
>
<Text size="sm" c="dimmed">
Loading credentials...
</Text>
</Card>
);
const list = data?.data?.list || [];
return (
<Card>
<Stack>
{data?.data?.list.map((v, k) => (
<Stack key={k}>
<Flex justify={"space-between"}>
<Text>{v.name}</Text>
<Group>
<Button onClick={() => handleRm(v.id)}>delete</Button>
</Group>
</Flex>
</Stack>
))}
<Card
radius="lg"
p="xl"
withBorder
style={{
background:
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
borderColor: "rgba(100,100,100,0.2)",
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
}}
>
<Stack gap="md">
<Flex align="center" justify="space-between">
<Title order={3} c="gray.0">
Saved Credentials
</Title>
<Badge
size="lg"
variant="light"
radius="sm"
color="teal"
style={{ textTransform: "none" }}
>
{list.length} Active
</Badge>
</Flex>
<Divider my="sm" />
{list.length === 0 ? (
<Flex justify="center" align="center" h={120}>
<Stack gap={4} align="center">
<IconShieldLock size={32} color="gray" />
<Text c="dimmed" size="sm">
No credentials have been added yet.
</Text>
</Stack>
</Flex>
) : (
list.map((v: any) => (
<Card
key={v.id}
radius="md"
p="md"
withBorder
style={{
background:
"linear-gradient(135deg, rgba(35,35,35,0.9), rgba(55,55,55,0.9))",
borderColor: "rgba(100,100,100,0.2)",
transition: "transform 0.15s ease, box-shadow 0.15s ease",
}}
onMouseEnter={(e) =>
(e.currentTarget.style.boxShadow =
"0 0 10px rgba(0,255,200,0.2)")
}
onMouseLeave={(e) =>
(e.currentTarget.style.boxShadow = "none")
}
>
<Flex align="center" justify="space-between">
<Stack gap={2}>
<Text fw={600} c="gray.0">
{v.name}
</Text>
<Text size="xs" c="dimmed">
ID: {v.id}
</Text>
</Stack>
<Tooltip label="Delete credential permanently">
<ActionIcon
variant="light"
color="red"
radius="md"
onClick={() => handleRemove(v.id)}
>
<IconTrash size={18} />
</ActionIcon>
</Tooltip>
</Flex>
</Card>
))
)}
</Stack>
</Card>
);

View File

@@ -1,9 +1,9 @@
import { useEffect, useState } from "react";
import {
ActionIcon,
AppShell,
Avatar,
Badge,
Button,
Card,
Divider,
@@ -24,29 +24,32 @@ import {
IconDashboard,
IconKey,
IconLock,
IconUser,
} from "@tabler/icons-react";
import type { User } from "generated/prisma";
import { Outlet, useLocation, useNavigate } from "react-router-dom";
import {
default as clientRoute,
default as clientRoutes,
} from "@/clientRoutes";
import { default as clientRoute, default as clientRoutes } from "@/clientRoutes";
import apiFetch from "@/lib/apiFetch";
function Logout() {
return (
<Group>
<Group justify="center" mt="sm">
<Button
variant="transparent"
size="compact-xs"
variant="gradient"
gradient={{ from: "red", to: "orange", deg: 60 }}
size="xs"
radius="md"
onClick={async () => {
await apiFetch.auth.logout.delete();
localStorage.removeItem("token");
window.location.href = "/login";
}}
style={{
boxShadow: "0 0 10px rgba(255,100,100,0.25)",
transition: "transform 0.2s ease",
}}
>
Logout
Log Out
</Button>
</Group>
);
@@ -60,14 +63,24 @@ export default function DashboardLayout() {
return (
<AppShell
padding="md"
padding="lg"
navbar={{
width: 260,
breakpoint: "sm",
collapsed: { mobile: !opened, desktop: !opened },
}}
style={{
background: "radial-gradient(circle at top, #0a0a0a, #101010 70%)",
color: "#f8f9fa",
}}
>
<AppShell.Navbar>
<AppShell.Navbar
style={{
background: "linear-gradient(180deg, #141414, #1e1e1e)",
borderRight: "1px solid rgba(255,255,255,0.05)",
boxShadow: "2px 0 10px rgba(0,0,0,0.6)",
}}
>
<AppShell.Section>
<Group justify="flex-end" p="xs">
<Tooltip
@@ -78,8 +91,12 @@ export default function DashboardLayout() {
variant="light"
color="gray"
onClick={() => setOpened((v) => !v)}
aria-label="Toggle navigation"
radius="xl"
size="lg"
style={{
backgroundColor: "rgba(255,255,255,0.05)",
boxShadow: "0 0 6px rgba(0,255,200,0.2)",
}}
>
{opened ? <IconChevronLeft /> : <IconChevronRight />}
</ActionIcon>
@@ -97,8 +114,17 @@ export default function DashboardLayout() {
</AppShell.Navbar>
<AppShell.Main>
<Stack>
<Paper withBorder shadow="md" radius="lg" p="md">
<Stack gap="md">
<Paper
withBorder
radius="lg"
p="md"
style={{
background: "linear-gradient(145deg, #181818, #202020)",
border: "1px solid rgba(255,255,255,0.05)",
boxShadow: "0 0 15px rgba(0,255,200,0.08)",
}}
>
<Flex align="center" gap="md">
{!opened && (
<Tooltip label="Open navigation menu" withArrow>
@@ -106,15 +132,14 @@ export default function DashboardLayout() {
variant="light"
color="gray"
onClick={() => setOpened(true)}
aria-label="Open navigation"
radius="xl"
>
<IconChevronRight />
</ActionIcon>
</Tooltip>
)}
<Title order={3} fw={600}>
App Dashboard
<Title order={3} fw={600} c="gray.0">
Dashboard Control Panel
</Title>
</Flex>
</Paper>
@@ -125,7 +150,6 @@ export default function DashboardLayout() {
);
}
/* ----------------------- Host Info ----------------------- */
function HostView() {
const [host, setHost] = useState<User | null>(null);
@@ -138,33 +162,46 @@ function HostView() {
}, []);
return (
<Card radius="lg" withBorder shadow="sm" p="md">
<Card
radius="lg"
withBorder
shadow="sm"
p="md"
style={{
background: "linear-gradient(145deg, #181818, #212121)",
borderColor: "rgba(255,255,255,0.05)",
}}
>
{host ? (
<Stack>
<Stack gap="sm">
<Flex gap="md" align="center">
<Avatar size="md" radius="xl" color="blue">
{host.name?.[0]}
<Avatar size="md" radius="xl" color="teal" variant="filled">
{host.name?.[0]?.toUpperCase()}
</Avatar>
<Stack gap={2}>
<Text fw={600}>{host.name}</Text>
<Text fw={600} c="gray.0">
{host.name}
</Text>
<Text size="sm" c="dimmed">
{host.email}
</Text>
</Stack>
</Flex>
<Divider />
<Divider my="xs" color="rgba(255,255,255,0.1)" />
<Logout />
</Stack>
) : (
<Text size="sm" c="dimmed" ta="center">
No host information available
</Text>
<Flex align="center" justify="center" direction="column" p="md">
<IconUser size={28} color="gray" />
<Text size="sm" c="dimmed" mt={4}>
No user information available
</Text>
</Flex>
)}
</Card>
);
}
/* ----------------------- Navigation ----------------------- */
function NavigationDashboard() {
const navigate = useNavigate();
const location = useLocation();
@@ -172,31 +209,66 @@ function NavigationDashboard() {
const isActive = (path: keyof typeof clientRoute) =>
location.pathname.startsWith(clientRoute[path]);
const navItems = [
{
path: "/scr/dashboard/dashboard-home",
icon: <IconDashboard size={20} />,
label: "Dashboard Overview",
description: "Quick summary and insights",
},
{
path: "/scr/dashboard/apikey/apikey",
icon: <IconKey size={20} />,
label: "API Key Manager",
description: "Create and manage API keys",
},
{
path: "/scr/dashboard/credential/credential",
icon: <IconLock size={20} />,
label: "Credentials",
description: "Manage service credentials",
},
];
return (
<Stack gap="xs" p="sm">
<NavLink
active={isActive("/scr/dashboard/dashboard-home")}
leftSection={<IconDashboard size={20} />}
label="Dashboard Overview"
description="Quick summary and activity highlights"
onClick={() => navigate(clientRoutes["/scr/dashboard/dashboard-home"])}
/>
<NavLink
active={isActive("/scr/dashboard/apikey/apikey")}
leftSection={<IconKey size={20} />}
label="API Key"
description="API Key Management and Generation"
onClick={() => navigate(clientRoutes["/scr/dashboard/apikey/apikey"])}
/>
<NavLink
active={isActive("/scr/dashboard/credential/credential")}
leftSection={<IconLock size={20} />}
label="Credential"
description="Credential Management"
onClick={() =>
navigate(clientRoutes["/scr/dashboard/credential/credential"])
}
/>
{navItems.map((item) => (
<NavLink
key={item.path}
active={isActive(item.path as keyof typeof clientRoute)}
leftSection={item.icon}
label={
<Flex align="center" gap={6}>
<Text fw={500}>{item.label}</Text>
{isActive(item.path as keyof typeof clientRoute) && (
<Badge
variant="light"
color="teal"
radius="sm"
size="xs"
style={{ textTransform: "none" }}
>
Active
</Badge>
)}
</Flex>
}
description={item.description}
onClick={() => navigate(clientRoutes[item.path as keyof typeof clientRoute])}
style={{
backgroundColor: isActive(item.path as keyof typeof clientRoute)
? "rgba(0,255,200,0.1)"
: "transparent",
borderRadius: "8px",
transition: "all 0.2s ease",
}}
styles={{
label: { color: "white" },
description: { color: "#aaa" },
section: { color: "teal" },
}}
/>
))}
</Stack>
);
}