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

19
src/routes/__root.tsx Normal file
View File

@@ -0,0 +1,19 @@
/** biome-ignore-all lint/suspicious/noExplicitAny: <explanation */
import { protectedRouteMiddleware } from "@/middleware/authMiddleware";
import { authStore } from "@/store/auth";
import "@mantine/core/styles.css";
import "@mantine/dates/styles.css";
import { createRootRoute, Outlet } from "@tanstack/react-router";
export const Route = createRootRoute({
component: RootComponent,
beforeLoad: protectedRouteMiddleware,
onEnter({ context }) {
authStore.user = context?.user as any;
authStore.session = context?.session as any;
},
});
function RootComponent() {
return <Outlet />;
}

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>;
}

788
src/routes/index.tsx Normal file
View File

@@ -0,0 +1,788 @@
import {
ActionIcon,
Avatar,
Box,
Button,
Card,
Container,
Grid,
Group,
Image,
Paper,
rem,
SimpleGrid,
Stack,
Text,
ThemeIcon,
Title,
Transition,
useMantineColorScheme,
} from "@mantine/core";
import {
IconApi,
IconBolt,
IconBrandGithub,
IconBrandLinkedin,
IconBrandTwitter,
IconChevronRight,
IconLock,
IconMoon,
IconRocket,
IconShield,
IconStack2,
IconSun,
} from "@tabler/icons-react";
import { createFileRoute, Link } from "@tanstack/react-router";
import { useEffect, useState } from "react";
export const Route = createFileRoute("/")({
component: HomePage,
});
// Navigation items
const NAV_ITEMS = [
{ label: "Home", link: "/" },
{ label: "Features", link: "#features" },
{ label: "Testimonials", link: "#testimonials" },
{ label: "Pricing", link: "/pricing" },
{ label: "Contact", link: "/contact" },
];
// Features data
const FEATURES = [
{
icon: IconBolt,
title: "Lightning Fast",
description: "Built on Bun runtime for exceptional performance and speed.",
},
{
icon: IconShield,
title: "Secure by Design",
description:
"Enterprise-grade authentication with Better Auth integration.",
},
{
icon: IconApi,
title: "RESTful API",
description:
"Full-featured API with Elysia.js for seamless backend operations.",
},
{
icon: IconStack2,
title: "Modern Stack",
description: "React 19, TanStack Router, and Mantine UI for the best DX.",
},
{
icon: IconLock,
title: "API Key Auth",
description: "Secure API key management for external integrations.",
},
{
icon: IconRocket,
title: "Production Ready",
description: "Type-safe, tested, and optimized for production deployment.",
},
];
// Testimonials data
const TESTIMONIALS = [
{
id: "testimonial-1",
name: "Alex Johnson",
role: "Lead Developer",
content:
"This template saved us weeks of setup time. The architecture is clean and well-thought-out.",
avatar:
"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=200&q=80",
},
{
id: "testimonial-2",
name: "Sarah Williams",
role: "CTO",
content:
"The performance improvements we saw after switching to this stack were remarkable. Highly recommended!",
avatar:
"https://images.unsplash.com/photo-1494790108377-be9c29b29330?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=200&q=80",
},
{
id: "testimonial-3",
name: "Michael Chen",
role: "Product Manager",
content:
"The developer experience is top-notch. Everything is well-documented and easy to extend.",
avatar:
"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=200&q=80",
},
];
function NavigationBar() {
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
const [scrolled, setScrolled] = useState(false);
useEffect(() => {
const handleScroll = () => {
setScrolled(window.scrollY > 20);
};
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
return (
<Box
h={70}
px="md"
style={{
borderBottom: "1px solid var(--mantine-color-gray-2)",
transition: "all 0.3s ease",
boxShadow: scrolled ? "0 2px 10px rgba(0,0,0,0.1)" : "none",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<Group h="100%" justify="space-between">
<Group>
<Link to="/" style={{ textDecoration: "none" }}>
<Title order={3} c="blue">
BunStack
</Title>
</Link>
<Group ml={50} visibleFrom="sm" gap="lg">
{NAV_ITEMS.map((item) => {
const isActive = window.location.pathname === item.link;
return (
<Box
key={item.label}
component={Link}
to={item.link}
style={{
textDecoration: "none",
fontSize: rem(16),
padding: `${rem(8)} ${rem(12)}`,
borderRadius: rem(6),
transition: "all 0.2s ease",
color: isActive
? "var(--mantine-color-blue-6)"
: "var(--mantine-color-dimmed)",
fontWeight: 500,
cursor: "pointer",
display: "block",
}}
className="nav-item"
>
{item.label}
</Box>
);
})}
</Group>
</Group>
<Group>
<ActionIcon
variant="default"
onClick={() => toggleColorScheme()}
size="lg"
>
{colorScheme === "dark" ? (
<IconSun size={18} />
) : (
<IconMoon size={18} />
)}
</ActionIcon>
<Button component={Link} to="/signin" variant="light" size="sm">
Sign In
</Button>
<Button component={Link} to="/signup" size="sm">
Get Started
</Button>
</Group>
</Group>
</Box>
);
}
function HeroSection() {
const [loaded, setLoaded] = useState(false);
const [imageLoaded, setImageLoaded] = useState(false);
useEffect(() => {
setLoaded(true);
}, []);
// Simulate delay for image transition
useEffect(() => {
const timer = setTimeout(() => {
setImageLoaded(true);
}, 200);
return () => clearTimeout(timer);
}, []);
return (
<Box
pt={rem(140)} // Adjusted padding for simpler header
pb={rem(60)}
>
<Container size="lg">
<Grid gutter={{ base: rem(40), md: rem(80) }} align="center">
<Grid.Col span={{ base: 12, md: 6 }}>
<Transition
mounted={loaded}
transition="slide-up"
duration={600}
timingFunction="ease"
>
{(styles) => (
<Stack gap="xl" style={styles}>
<Title
order={1}
style={{
fontSize: rem(48),
fontWeight: 900,
lineHeight: 1.2,
}}
>
Build Faster with{" "}
<Text span c="blue" inherit>
Bun Stack
</Text>
</Title>
<Text size="xl" c="dimmed">
A modern, full-stack React template powered by Bun,
Elysia.js, and TanStack Router. Ship your ideas faster than
ever.
</Text>
<Group gap="md">
<Button
component={Link}
to="/dashboard"
size="lg"
variant="filled"
rightSection={<IconRocket size="1.25rem" />}
>
Get Started
</Button>
<Button
component={Link}
to="/docs"
size="lg"
variant="outline"
>
Learn More
</Button>
</Group>
</Stack>
)}
</Transition>
</Grid.Col>
<Grid.Col span={{ base: 12, md: 6 }}>
<Transition
mounted={imageLoaded}
transition="slide-left"
duration={800}
timingFunction="ease"
>
{(styles) => (
<Paper shadow="xl" radius="lg" p="md" withBorder style={styles}>
<Image
src="https://images.unsplash.com/photo-1555066931-4365d14bab8c?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80"
alt="Code editor showing Bun Stack code"
radius="md"
/>
</Paper>
)}
</Transition>
</Grid.Col>
</Grid>
</Container>
</Box>
);
}
function AnimatedFeatureCard({
feature,
index,
isVisible,
}: {
feature: (typeof FEATURES)[number];
index: number;
isVisible: boolean;
}) {
const [isDelayedVisible, setIsDelayedVisible] = useState(isVisible);
useEffect(() => {
if (isVisible) {
const timer = setTimeout(() => {
setIsDelayedVisible(true);
}, index * 100);
return () => clearTimeout(timer);
}
}, [isVisible, index]);
return (
<Transition
mounted={isDelayedVisible}
transition="slide-up"
duration={500}
timingFunction="ease"
>
{(styles) => (
<Card
className="feature-card"
padding="lg"
radius="md"
withBorder
shadow="sm"
style={styles}
>
<ThemeIcon variant="light" color="blue" size={60} radius="md">
<feature.icon size="1.75rem" />
</ThemeIcon>
<Stack gap={8} mt="md">
<Title order={4}>{feature.title}</Title>
<Text size="sm" c="dimmed" lh={1.5}>
{feature.description}
</Text>
</Stack>
</Card>
)}
</Transition>
);
}
function FeaturesSection() {
const [visibleFeatures, setVisibleFeatures] = useState(
Array(FEATURES.length).fill(false),
);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry, index) => {
if (entry.isIntersecting) {
setVisibleFeatures((prev) => {
const newVisible = [...prev];
newVisible[index] = true;
return newVisible;
});
}
});
},
{ threshold: 0.1 },
);
const elements = document.querySelectorAll(".feature-card");
elements.forEach((el) => {
observer.observe(el);
});
return () => observer.disconnect();
}, []);
return (
<Container size="lg" py={rem(80)}>
<Stack gap="xl" align="center" mb={rem(50)}>
<Transition
mounted={true}
transition="fade"
duration={600}
timingFunction="ease"
>
{(styles) => (
<div style={styles}>
<Title order={2} ta="center">
Everything You Need
</Title>
<Text c="dimmed" size="lg" ta="center" maw={600}>
A complete toolkit for building modern web applications with
best practices built-in.
</Text>
</div>
)}
</Transition>
</Stack>
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="lg">
{FEATURES.map((feature, index) => (
<AnimatedFeatureCard
key={feature.title}
feature={feature}
index={index}
isVisible={visibleFeatures[index]}
/>
))}
</SimpleGrid>
</Container>
);
}
function AnimatedTestimonialCard({
testimonial,
index,
isVisible,
}: {
testimonial: (typeof TESTIMONIALS)[number];
index: number;
isVisible: boolean;
}) {
const [isDelayedVisible, setIsDelayedVisible] = useState(isVisible);
useEffect(() => {
if (isVisible) {
const timer = setTimeout(() => {
setIsDelayedVisible(true);
}, index * 150);
return () => clearTimeout(timer);
}
}, [isVisible, index]);
return (
<Transition
mounted={isDelayedVisible}
transition="slide-up"
duration={500}
timingFunction="ease"
>
{(styles) => (
<Card
padding="lg"
radius="md"
withBorder
shadow="sm"
className="testimonial-card"
style={styles}
>
<Text c="dimmed" mb="md">
"{testimonial.content}"
</Text>
<Group>
<Avatar src={testimonial.avatar} size="md" radius="xl" />
<Stack gap={0}>
<Text fw={600}>{testimonial.name}</Text>
<Text size="sm" c="dimmed">
{testimonial.role}
</Text>
</Stack>
</Group>
</Card>
)}
</Transition>
);
}
function TestimonialsSection() {
const [visibleTestimonials, setVisibleTestimonials] = useState(
Array(TESTIMONIALS.length).fill(false),
);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry, index) => {
if (entry.isIntersecting) {
setVisibleTestimonials((prev) => {
const newVisible = [...prev];
newVisible[index] = true;
return newVisible;
});
}
});
},
{ threshold: 0.1 },
);
const elements = document.querySelectorAll(".testimonial-card");
elements.forEach((el) => {
observer.observe(el);
});
return () => observer.disconnect();
}, []);
return (
<Box py={rem(80)}>
<Container size="lg">
<Stack gap="xl" align="center" mb={rem(50)}>
<Transition
mounted={true}
transition="fade"
duration={600}
timingFunction="ease"
>
{(styles) => (
<div style={styles}>
<Title order={2} ta="center">
Loved by Developers
</Title>
<Text c="dimmed" size="lg" ta="center" maw={600}>
Join thousands of satisfied developers who have accelerated
their projects with Bun Stack.
</Text>
</div>
)}
</Transition>
</Stack>
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="lg">
{TESTIMONIALS.map((testimonial, index) => (
<AnimatedTestimonialCard
key={testimonial.id}
testimonial={testimonial}
index={index}
isVisible={visibleTestimonials[index]}
/>
))}
</SimpleGrid>
</Container>
</Box>
);
}
function CtaSection() {
const [loaded, setLoaded] = useState(false);
useEffect(() => {
setLoaded(true);
}, []);
return (
<Container size="lg" py={rem(80)}>
<Transition
mounted={loaded}
transition="slide-up"
duration={600}
timingFunction="ease"
>
{(styles) => (
<Paper
radius="lg"
p={rem(60)}
bg="blue"
style={{
...styles,
background:
"linear-gradient(135deg, var(--mantine-color-blue-6), var(--mantine-color-indigo-6))",
}}
>
<Stack align="center" gap="xl" ta="center">
<Title c="white" order={2}>
Ready to get started?
</Title>
<Text c="white" size="lg" maw={600}>
Join thousands of developers who are building faster and more
reliable applications with Bun Stack.
</Text>
<Group>
<Button
component={Link}
to="/signup"
size="lg"
variant="white"
color="dark"
rightSection={<IconChevronRight size="1.125rem" />}
>
Create Account
</Button>
<Button
component={Link}
to="/docs"
size="lg"
variant="outline"
color="white"
>
View Documentation
</Button>
</Group>
</Stack>
</Paper>
)}
</Transition>
</Container>
);
}
function Footer() {
const [loaded, setLoaded] = useState(false);
useEffect(() => {
const timer = setTimeout(() => {
setLoaded(true);
}, 300);
return () => clearTimeout(timer);
}, []);
return (
<Transition
mounted={loaded}
transition="slide-up"
duration={600}
timingFunction="ease"
>
{(styles) => (
<Box
py={rem(40)}
style={{
...styles,
borderTop: "1px solid var(--mantine-color-gray-2)",
}}
>
<Container size="lg">
<Grid gutter={{ base: rem(40), md: rem(80) }}>
<Grid.Col span={{ base: 12, md: 4 }}>
<Stack gap="md">
<Title order={3}>BunStack</Title>
<Text size="sm" c="dimmed">
The ultimate full-stack solution for modern web
applications.
</Text>
<Group>
<ActionIcon size="lg" variant="subtle" color="gray">
<IconBrandGithub size="1.25rem" />
</ActionIcon>
<ActionIcon size="lg" variant="subtle" color="gray">
<IconBrandTwitter size="1.25rem" />
</ActionIcon>
<ActionIcon size="lg" variant="subtle" color="gray">
<IconBrandLinkedin size="1.25rem" />
</ActionIcon>
</Group>
</Stack>
</Grid.Col>
<Grid.Col span={{ base: 12, md: 2 }}>
<Stack gap="xs">
<Title order={4}>Product</Title>
<Text
size="sm"
c="dimmed"
component={Link}
to="/features"
td="none"
>
Features
</Text>
<Text
size="sm"
c="dimmed"
component={Link}
to="/pricing"
td="none"
>
Pricing
</Text>
<Text
size="sm"
c="dimmed"
component={Link}
to="/docs"
td="none"
>
Documentation
</Text>
</Stack>
</Grid.Col>
<Grid.Col span={{ base: 12, md: 2 }}>
<Stack gap="xs">
<Title order={4}>Company</Title>
<Text
size="sm"
c="dimmed"
component={Link}
to="/about"
td="none"
>
About
</Text>
<Text
size="sm"
c="dimmed"
component={Link}
to="/blog"
td="none"
>
Blog
</Text>
<Text
size="sm"
c="dimmed"
component={Link}
to="/careers"
td="none"
>
Careers
</Text>
</Stack>
</Grid.Col>
<Grid.Col span={{ base: 12, md: 4 }}>
<Stack gap="xs">
<Title order={4}>Subscribe to our newsletter</Title>
<Text size="sm" c="dimmed">
Get the latest news and updates
</Text>
<Group>
<input
type="email"
placeholder="Your email"
style={{
padding: "8px 12px",
borderRadius: "4px",
border: "1px solid var(--mantine-color-gray-3)",
flex: 1,
}}
/>
<Button>Subscribe</Button>
</Group>
</Stack>
</Grid.Col>
</Grid>
<Box
pt={rem(40)}
style={{ borderTop: "1px solid var(--mantine-color-gray-2)" }}
>
<Group justify="space-between" align="center">
<Text size="sm" c="dimmed">
© 2024 Bun Stack. Built with Bun, Elysia, and React.
</Text>
<Group gap="lg">
<Text
component={Link}
to="/privacy"
size="sm"
c="dimmed"
style={{ textDecoration: "none" }}
>
Privacy Policy
</Text>
<Text
component={Link}
to="/terms"
size="sm"
c="dimmed"
style={{ textDecoration: "none" }}
>
Terms of Service
</Text>
</Group>
</Group>
</Box>
</Container>
</Box>
)}
</Transition>
);
}
function HomePage() {
return (
<Box>
<NavigationBar />
<HeroSection />
<FeaturesSection />
<TestimonialsSection />
<CtaSection />
<Footer />
</Box>
);
}

317
src/routes/profile.tsx Normal file
View File

@@ -0,0 +1,317 @@
import {
ActionIcon,
Avatar,
Badge,
Button,
Card,
Code,
Container,
Group,
Modal,
SimpleGrid,
Stack,
Text,
Title,
Tooltip,
} from "@mantine/core";
import {
IconAt,
IconCheck,
IconCopy,
IconDashboard,
IconId,
IconLogout,
IconShield,
IconUser,
} 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("/profile")({
component: Profile,
});
function Profile() {
const snap = useSnapshot(authStore);
const navigate = useNavigate();
const [opened, setOpened] = useState(false);
const [copied, setCopied] = useState(false);
async function logout() {
await authClient.signOut();
navigate({ to: "/signin" });
}
const copyToClipboard = (text: string) => {
if (navigator.clipboard) {
navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
return (
<Container size="lg" py="xl">
<Title order={1} mb="lg" ta="center">
User Profile
</Title>
{/* Profile Header 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="center" align="flex-start" gap="xl">
<Avatar
src={snap.user?.image}
size={120}
radius="xl"
style={{ border: "2px solid rgba(251, 240, 223, 0.3)" }}
>
{snap.user?.name?.charAt(0).toUpperCase()}
</Avatar>
<Stack gap="xs" justify="center">
<Text size="xl" fw={700} c="#fbf0df">
{snap.user?.name}
</Text>
<Group gap="sm">
<IconAt size={16} stroke={1.5} color="rgba(255, 255, 255, 0.6)" />
<Text c="dimmed">{snap.user?.email}</Text>
</Group>
<Group gap="sm">
<IconShield
size={16}
stroke={1.5}
color="rgba(255, 255, 255, 0.6)"
/>
<Badge
variant="light"
color={snap.user?.role === "admin" ? "green" : "blue"}
>
{snap.user?.role || "user"}
</Badge>
</Group>
</Stack>
</Group>
</Card>
<Title order={2} mb="md" ta="center">
Account Information
</Title>
<SimpleGrid cols={{ base: 1, sm: 2, md: 4 }} spacing="md" mb="xl">
<Card withBorder p="lg" radius="md" bg="rgba(251, 240, 223, 0.05)">
<Group>
<IconId size={24} stroke={1.5} color="#f3d5a3" />
<div>
<Text size="sm" c="dimmed">
User ID
</Text>
<Group gap="xs" mt="xs">
<Text fw={500} truncate="end" miw={0} c="#fbf0df">
{snap.user?.id || "N/A"}
</Text>
<Tooltip
label={copied ? "Copied!" : "Copy to clipboard"}
position="top"
>
<ActionIcon
variant="subtle"
color="gray"
size="sm"
onClick={() =>
snap.user?.id && copyToClipboard(snap.user.id)
}
>
{copied ? <IconCheck size={16} /> : <IconCopy size={16} />}
</ActionIcon>
</Tooltip>
</Group>
</div>
</Group>
</Card>
<Card withBorder p="lg" radius="md" bg="rgba(251, 240, 223, 0.05)">
<Group>
<IconAt size={24} stroke={1.5} color="#f3d5a3" />
<div>
<Text size="sm" c="dimmed">
Email
</Text>
<Group gap="xs" mt="xs">
<Text fw={500} truncate="end" miw={0} c="#fbf0df">
{snap.user?.email || "N/A"}
</Text>
<Tooltip
label={copied ? "Copied!" : "Copy to clipboard"}
position="top"
>
<ActionIcon
variant="subtle"
color="gray"
size="sm"
onClick={() =>
snap.user?.email && copyToClipboard(snap.user.email)
}
>
{copied ? <IconCheck size={16} /> : <IconCopy size={16} />}
</ActionIcon>
</Tooltip>
</Group>
</div>
</Group>
</Card>
<Card withBorder p="lg" radius="md" bg="rgba(251, 240, 223, 0.05)">
<Group>
<IconUser size={24} stroke={1.5} color="#f3d5a3" />
<div>
<Text size="sm" c="dimmed">
Name
</Text>
<Group gap="xs" mt="xs">
<Text fw={500} truncate="end" miw={0} c="#fbf0df">
{snap.user?.name || "N/A"}
</Text>
<Tooltip
label={copied ? "Copied!" : "Copy to clipboard"}
position="top"
>
<ActionIcon
variant="subtle"
color="gray"
size="sm"
onClick={() =>
snap.user?.name && copyToClipboard(snap.user.name)
}
>
{copied ? <IconCheck size={16} /> : <IconCopy size={16} />}
</ActionIcon>
</Tooltip>
</Group>
</div>
</Group>
</Card>
<Card withBorder p="lg" radius="md" bg="rgba(251, 240, 223, 0.05)">
<Group>
<IconShield size={24} stroke={1.5} color="#f3d5a3" />
<div>
<Text size="sm" c="dimmed">
Role
</Text>
<Text fw={500} mt="xs" c="#fbf0df">
{snap.user?.role || "user"}
</Text>
</div>
</Group>
</Card>
</SimpleGrid>
<Card
withBorder
p="lg"
radius="md"
bg="rgba(251, 240, 223, 0.05)"
mb="xl"
>
<Group justify="space-between" align="center">
<Title order={3}>Session Information</Title>
<Group>
{snap.user?.role === "admin" && (
<Button
leftSection={<IconDashboard size={16} />}
variant="light"
color="blue"
onClick={() => navigate({ to: "/dashboard" })}
>
Dashboard
</Button>
)}
<Button
leftSection={<IconLogout size={16} />}
variant="outline"
color="red"
onClick={() => setOpened(true)}
>
Sign Out
</Button>
</Group>
</Group>
<Group mt="md" justify="space-between">
<div>
<Text size="sm" c="dimmed" mb="xs">
Session Token
</Text>
<Group gap="xs">
<Code
block
style={{
fontSize: "0.8rem",
padding: "0.5rem 0.75rem",
backgroundColor: "rgba(26, 26, 26, 0.7)",
color: "#f3d5a3",
}}
>
{snap.session?.token
? `${snap.session.token.substring(0, 30)}...`
: "N/A"}
</Code>
<Tooltip
label={copied ? "Copied!" : "Copy to clipboard"}
position="top"
>
<ActionIcon
variant="light"
color="gray"
size="md"
onClick={() =>
snap.session?.token && copyToClipboard(snap.session.token)
}
>
{copied ? <IconCheck size={16} /> : <IconCopy size={16} />}
</ActionIcon>
</Tooltip>
</Group>
</div>
</Group>
</Card>
<Modal
opened={opened}
onClose={() => setOpened(false)}
title="Confirm Sign Out"
centered
size="sm"
>
<Text mb="md">
Are you sure you want to sign out? You will need to sign in again to
access your account.
</Text>
<Group justify="flex-end">
<Button
variant="subtle"
color="gray"
onClick={() => setOpened(false)}
>
Cancel
</Button>
<Button
leftSection={<IconLogout size={16} />}
color="red"
onClick={async () => {
await logout();
setOpened(false);
}}
>
Sign Out
</Button>
</Group>
</Modal>
</Container>
);
}

125
src/routes/signin.tsx Normal file
View File

@@ -0,0 +1,125 @@
import {
Anchor,
Button,
Checkbox,
Container,
Paper,
PasswordInput,
Text,
TextInput,
Title,
} from "@mantine/core";
import { IconBrandGithub } from "@tabler/icons-react";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useState } from "react";
import { authClient } from "../utils/auth-client";
export const Route = createFileRoute("/signin")({
component: SigninComponent,
});
function SigninComponent() {
const navigate = useNavigate();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError("");
try {
const result = await authClient.signIn.email(
{
email,
password,
},
{
onRequest: () => {
console.log("Sign in request started");
},
onSuccess: async () => {
console.log("Sign in successful, navigating to dashboard");
navigate({ to: "/profile", replace: true });
},
onError: (ctx) => {
setError(ctx.error.message || "Failed to sign in");
},
},
);
// If using callbacks, result will be undefined
if (result?.error) {
setError(result.error.message || "Failed to sign in");
}
} catch {
setError("An unexpected error occurred");
} finally {
setLoading(false);
}
};
return (
<Container size={420} my={40}>
<Title ta="center" c="dimmed">
Welcome back!
</Title>
<Text c="dimmed" size="sm" ta="center" mt={5}>
Do not have an account yet?{" "}
<Anchor
size="sm"
component="button"
onClick={() => navigate({ to: "/signup" })}
>
Create account
</Anchor>
</Text>
<Paper withBorder shadow="md" p={30} mt={30} radius="md">
<form onSubmit={handleSubmit}>
<TextInput
label="Email"
placeholder="your@email.com"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<PasswordInput
label="Password"
placeholder="Your password"
required
mt="md"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<Checkbox label="Remember me" mt="md" />
{error && (
<Text c="red" size="sm" mt="md">
{error}
</Text>
)}
<Button fullWidth mt="xl" type="submit" loading={loading}>
Sign in
</Button>
</form>
<Button
variant="outline"
fullWidth
mt="md"
leftSection={<IconBrandGithub size={18} />}
onClick={async () => {
await authClient.signIn.social({
provider: "github",
callbackURL: "/profile",
});
}}
>
Continue with GitHub
</Button>
</Paper>
</Container>
);
}

102
src/routes/signup.tsx Normal file
View File

@@ -0,0 +1,102 @@
import {
Anchor,
Button,
Container,
Paper,
PasswordInput,
Text,
TextInput,
Title,
} from "@mantine/core";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useState } from "react";
import { signUp } from "../utils/auth-client";
export const Route = createFileRoute("/signup")({
component: SignupComponent,
});
function SignupComponent() {
const navigate = useNavigate();
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError("");
try {
const { error } = await signUp.email({
name,
email,
password,
});
if (error) {
setError(error.message || "Failed to sign up");
} else {
navigate({ to: "/dashboard" });
}
} catch {
setError("An unexpected error occurred");
} finally {
setLoading(false);
}
};
return (
<Container size={420} my={40}>
<Title ta="center">Create an account</Title>
<Text c="dimmed" size="sm" ta="center" mt={5}>
Already have an account?{" "}
<Anchor
size="sm"
component="button"
onClick={() => navigate({ to: "/signin" })}
>
Sign in
</Anchor>
</Text>
<Paper withBorder shadow="md" p={30} mt={30} radius="md">
<form onSubmit={handleSubmit}>
<TextInput
label="Name"
placeholder="Your name"
required
value={name}
onChange={(e) => setName(e.target.value)}
/>
<TextInput
label="Email"
placeholder="your@email.com"
required
mt="md"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<PasswordInput
label="Password"
placeholder="Your password"
required
mt="md"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
{error && (
<Text c="red" size="sm" mt="md">
{error}
</Text>
)}
<Button fullWidth mt="xl" type="submit" loading={loading}>
Create account
</Button>
</form>
</Paper>
</Container>
);
}

82
src/routes/users/$id.tsx Normal file
View File

@@ -0,0 +1,82 @@
import { createFileRoute, Link, useParams } from "@tanstack/react-router";
import { useEffect, useState } from "react";
export const Route = createFileRoute("/users/$id")({
component: UserDetailPage,
});
interface User {
id: number;
name: string;
email: string;
}
function UserDetailPage() {
const { id } = useParams({ from: "/users/$id" }) as { id: string };
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Simulate fetching user by ID
fetch("/api/users")
.then((res) => res.json())
.then((data) => {
const foundUser = data.users.find((u: User) => u.id === Number(id));
setUser(foundUser || null);
setLoading(false);
})
.catch(() => {
setLoading(false);
});
}, [id]);
if (loading) {
return (
<div className="page-container">
<h1>User Details</h1>
<p className="loading">Loading user...</p>
</div>
);
}
if (!user) {
return (
<div className="page-container">
<h1>User Not Found</h1>
<p>User with ID {id} does not exist.</p>
<Link to="/users" className="back-link">
Back to Users
</Link>
</div>
);
}
return (
<div className="page-container">
<Link to="/users" className="back-link">
Back to Users
</Link>
<div className="user-detail-card">
<div className="user-detail-avatar">{user.name.charAt(0)}</div>
<h1>{user.name}</h1>
<p className="user-email">{user.email}</p>
<div className="user-meta">
<span className="meta-item">
<strong>ID:</strong> {user.id}
</span>
</div>
</div>
<div className="route-info-box">
<strong>🛣 Dynamic Route:</strong> This page uses the route pattern{" "}
<code>/users/$id</code>
<br />
Current parameter: <code>id = {id}</code>
<br />
<br />
Try changing the ID in the URL to see different users!
</div>
</div>
);
}

View File

@@ -0,0 +1,51 @@
import { createFileRoute, Link } from "@tanstack/react-router";
import { useState } from "react";
export const Route = createFileRoute("/users/")({
component: UsersPage,
});
interface User {
id: number;
name: string;
email: string;
}
function UsersPage() {
const [users, _] = useState<User[]>([]);
return (
<div className="page-container">
<h1>Users</h1>
<p className="page-description">
This page demonstrates fetching data from the API and using dynamic
routes.
</p>
<div className="users-grid">
{users.map((user) => (
<Link
key={user.id}
to="/users/$id"
params={{ id: String(user.id) }}
className="user-card"
>
<div className="user-avatar">{user.name.charAt(0)}</div>
<div className="user-info">
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
<div className="user-arrow"></div>
</Link>
))}
</div>
<div className="route-info-box">
<strong>💡 Tip:</strong> Click on a user to see dynamic routing in
action!
<br />
Route pattern: <code>/users/$id</code>
</div>
</div>
);
}