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() { export default function Home() {
return ( return (
<div> <Container
<h1>Home</h1> mih="100vh"
</div> 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, Table,
Text, Text,
TextInput, TextInput,
Title,
ScrollArea,
ActionIcon,
Tooltip,
Divider,
Loader,
Badge,
useMantineTheme,
} from "@mantine/core"; } from "@mantine/core";
import { useEffect, useState } from "react"; 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 { showNotification } from "@mantine/notifications";
import apiFetch from "@/lib/apiFetch";
export default function ApiKeyPage() { export default function ApiKeyPage() {
return ( return (
<Container size="md" w={"100%"}> <Container size="lg" py="xl" w="100%">
<Stack> <Stack gap="xl">
<Text>API Key</Text> <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 /> <CreateApiKey />
</Stack> </Stack>
</Container> </Container>
@@ -24,6 +47,7 @@ export default function ApiKeyPage() {
} }
function CreateApiKey() { function CreateApiKey() {
const theme = useMantineTheme();
const [name, setName] = useState(""); const [name, setName] = useState("");
const [description, setDescription] = useState(""); const [description, setDescription] = useState("");
const [expiredAt, setExpiredAt] = useState(""); const [expiredAt, setExpiredAt] = useState("");
@@ -31,6 +55,14 @@ function CreateApiKey() {
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!name || !expiredAt) {
showNotification({
color: "red",
title: "Missing Information",
message: "Please provide a name and expiration date.",
});
return;
}
setLoading(true); setLoading(true);
const res = await apiFetch.api.apikey.create.post({ const res = await apiFetch.api.apikey.create.post({
name, name,
@@ -42,52 +74,123 @@ function CreateApiKey() {
setDescription(""); setDescription("");
setExpiredAt(""); setExpiredAt("");
showNotification({ showNotification({
title: "Success", title: "API Key Created",
message: "API key created successfully", message: "Your new API key is now active and ready to use.",
color: "green", color: "teal",
});
} else {
showNotification({
title: "Error",
message: "Failed to create API key. Please try again.",
color: "red",
}); });
} }
setLoading(false); 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 /> <ListApiKey />
</Stack> </Stack>
</Card> </Card>
@@ -95,69 +198,169 @@ function CreateApiKey() {
} }
function ListApiKey() { function ListApiKey() {
const theme = useMantineTheme();
const [apiKeys, setApiKeys] = useState<any[]>([]); const [apiKeys, setApiKeys] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
const fetchApiKeys = async () => { const fetchApiKeys = async () => {
const res = await apiFetch.api.apikey.list.get(); const res = await apiFetch.api.apikey.list.get();
if (res.status === 200) { if (res.status === 200) {
setApiKeys(res.data?.apiKeys || []); setApiKeys(res.data?.apiKeys || []);
} }
setLoading(false);
}; };
fetchApiKeys(); 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 ( return (
<Card> <Card
<Stack> radius="lg"
<Text>API List</Text> withBorder
<Table> p="xl"
<thead> style={{
<tr> background:
<th>Name</th> "linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
<th>Description</th> borderColor: "rgba(100,100,100,0.2)",
<th>Expired At</th> boxShadow: "0 0 20px rgba(0,255,200,0.08)",
<th>Created At</th> }}
<th>Updated At</th> >
<th>Actions</th> <Stack gap="sm">
</tr> <Group justify="space-between" align="center">
</thead> <Group gap="xs">
<tbody> <IconDatabase size={22} color="var(--mantine-color-cyan-5)" />
{apiKeys.map((apiKey: any, index: number) => ( <Title order={3} c="gray.0">
<tr key={index}> Active API Keys
<td>{apiKey.name}</td> </Title>
<td>{apiKey.description}</td> </Group>
<td>{apiKey.expiredAt.toISOString().split("T")[0]}</td> <Badge
<td>{apiKey.createdAt.toISOString().split("T")[0]}</td> size="lg"
<td>{apiKey.updatedAt.toISOString().split("T")[0]}</td> radius="sm"
<td> color="teal"
<Button variant="light"
variant="outline" style={{ textTransform: "none" }}
onClick={() => { >
apiFetch.api.apikey.delete.delete({ id: apiKey.id }); {apiKeys.length} Active
setApiKeys( </Badge>
apiKeys.filter((api: any) => api.id !== apiKey.id), </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 <td>{apiKey.name}</td>
</Button> <td>{apiKey.description || "-"}</td>
<Button <td>
variant="outline" <Badge
onClick={() => { color={
navigator.clipboard.writeText(apiKey.key); new Date(apiKey.expiredAt) < new Date()
showNotification({ ? "red"
title: "Success", : "grape"
message: "API key copied to clipboard", }
color: "green", variant="light"
}); >
}} {new Date(apiKey.expiredAt).toLocaleDateString()}
> </Badge>
Copy </td>
</Button> <td>{new Date(apiKey.createdAt).toLocaleDateString()}</td>
</td> <td>{new Date(apiKey.updatedAt).toLocaleDateString()}</td>
</tr> <td>
))} <Group gap="xs" justify="center">
</tbody> <Tooltip label="Copy API Key">
</Table> <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> </Stack>
</Card> </Card>
); );

View File

@@ -1,33 +1,42 @@
import apiFetch from "@/lib/apiFetch"; import apiFetch from "@/lib/apiFetch";
import { import {
ActionIcon,
Button, Button,
Card, Card,
Container, Container,
Divider,
Flex, Flex,
Group, Group,
Stack, Stack,
Text, Text,
TextInput, TextInput,
Title, Title,
Tooltip,
LoadingOverlay,
Badge,
} from "@mantine/core"; } from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks"; import { useShallowEffect } from "@mantine/hooks";
import { showNotification } from "@mantine/notifications"; import { showNotification } from "@mantine/notifications";
import {
IconKey,
IconTrash,
IconPlus,
IconRefresh,
IconShieldLock,
} from "@tabler/icons-react";
import { useState } from "react"; import { useState } from "react";
import useSwr from "swr"; import useSwr from "swr";
import { proxy, subscribe } from "valtio"; import { proxy, subscribe } from "valtio";
const state = proxy({ const state = proxy({ reload: "" });
reload: "",
});
function reloadState() { function reloadState() {
state.reload = Math.random().toString(); state.reload = Math.random().toString();
} }
export default function CredentialPage() { export default function CredentialPage() {
return ( return (
<Container size={"md"} w={"100%"}> <Container size="md" w="100%">
<Stack> <Stack gap="xl" mt="xl">
<CredentialCreate /> <CredentialCreate />
<CredentialList /> <CredentialList />
</Stack> </Stack>
@@ -38,38 +47,115 @@ export default function CredentialPage() {
function CredentialCreate() { function CredentialCreate() {
const [name, setName] = useState(""); const [name, setName] = useState("");
const [apikey, setApikey] = useState(""); const [apikey, setApikey] = useState("");
const [loading, setLoading] = useState(false);
async function handleSubmit() { async function handleSubmit() {
const { data } = await apiFetch.api.credential.create.post({ if (!name || !apikey) {
name: name, showNotification({
value: apikey, color: "red",
}); title: "Missing Information",
message: "Please fill in all required fields before saving.",
setName(""); });
setApikey(""); return;
}
showNotification({ setLoading(true);
message: data?.message, try {
}); const { data } = await apiFetch.api.credential.create.post({
name,
reloadState(); 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 ( return (
<Card> <Card
<Stack> radius="lg"
<Title>Credential Create</Title> 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 <TextInput
placeholder="name" placeholder="Enter a friendly name for this credential"
label="Credential Name"
radius="md"
value={name} 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 <TextInput
placeholder="apikey" placeholder="Paste your API key or token here"
label="API Key"
radius="md"
value={apikey} 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> <Group justify="flex-end" mt="md">
<Button onClick={handleSubmit}>Save</Button> <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> </Group>
</Stack> </Stack>
</Card> </Card>
@@ -77,39 +163,133 @@ function CredentialCreate() {
} }
function CredentialList() { function CredentialList() {
const { data, mutate } = useSwr("/", () => const { data, mutate, isLoading } = useSwr("/", () =>
apiFetch.api.credential.list.get(), apiFetch.api.credential.list.get()
); );
useShallowEffect(() => { useShallowEffect(() => {
const unsubscribe = subscribe(state, async () => { const unsubscribe = subscribe(state, () => mutate());
console.log("state has changed to", state);
mutate();
});
return () => unsubscribe(); return () => unsubscribe();
}, []); }, []);
async function handleRm(id: string) { async function handleRemove(id: string) {
await apiFetch.api.credential.rm.delete({ try {
id: id, await apiFetch.api.credential.rm.delete({ id });
}); showNotification({
color: "teal",
reloadState(); 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 ( return (
<Card> <Card
<Stack> radius="lg"
{data?.data?.list.map((v, k) => ( p="xl"
<Stack key={k}> withBorder
<Flex justify={"space-between"}> style={{
<Text>{v.name}</Text> background:
<Group> "linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
<Button onClick={() => handleRm(v.id)}>delete</Button> borderColor: "rgba(100,100,100,0.2)",
</Group> boxShadow: "0 0 20px rgba(0,255,200,0.08)",
</Flex> }}
</Stack> >
))} <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> </Stack>
</Card> </Card>
); );

View File

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