feat: docs

This commit is contained in:
2025-03-14 05:19:43 -04:00
parent 6ab00206df
commit d382485a7c
21 changed files with 4451 additions and 79 deletions

3947
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -18,11 +18,14 @@
"@connectrpc/connect-web": "^2.0.2",
"@eslint/compat": "^1.2.5",
"@eslint/js": "^9.18.0",
"@lucide/svelte": "^0.479.0",
"@scalar/api-reference": "^1.28.1",
"@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/kit": "^2.16.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tailwindcss/vite": "^4.0.13",
"bits-ui": "^1.3.11",
"clsx": "^2.1.1",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-svelte": "^3.0.0",
@ -32,7 +35,10 @@
"prettier-plugin-tailwindcss": "^0.6.11",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"svelte-sonner": "^0.3.28",
"tailwind-merge": "^3.0.2",
"tailwindcss": "^4.0.13",
"tw-animate-css": "^1.2.0",
"typescript": "^5.0.0",
"typescript-eslint": "^8.20.0",
"vite": "^6.0.0"

View File

@ -1 +1,22 @@
@import "tailwindcss";
@import "tailwindcss";
@import "tw-animate-css";
@theme {
--color-crust: #11111b;
--color-mantle: #181825;
--color-base: #1e1e2e;
--color-surface-0: #313244;
--color-surface-1: #45475a;
--color-surface-2: #585b70;
--color-overlay-0: #6c7086;
--color-overlay-1: #7f849c;
--color-overlay-2: #9399b2;
--color-subtext-0: #a6adc8;
--color-subtext-1: #bac2de;
--color-text: #cdd6f4;
--color-sky: #89dceb;
}

View File

@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<body data-sveltekit-preload-data="tap" class="min-h-screen bg-base text-text">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf";
* Describes the file user/v1/auth.proto.
*/
export const file_user_v1_auth: GenFile = /*@__PURE__*/
fileDesc("ChJ1c2VyL3YxL2F1dGgucHJvdG8SB3VzZXIudjEiMgoMTG9naW5SZXF1ZXN0EhAKCHVzZXJuYW1lGAEgASgJEhAKCHBhc3N3b3JkGAIgASgJIh4KDUxvZ2luUmVzcG9uc2USDQoFdG9rZW4YASABKAkiMwoNU2lnblVwUmVxdWVzdBIQCgh1c2VybmFtZRgBIAEoCRIQCghwYXNzd29yZBgCIAEoCSIQCg5TaWduVXBSZXNwb25zZSIPCg1Mb2dvdXRSZXF1ZXN0IhAKDkxvZ291dFJlc3BvbnNlMsEBCgtBdXRoU2VydmljZRI4CgVMb2dpbhIVLnVzZXIudjEuTG9naW5SZXF1ZXN0GhYudXNlci52MS5Mb2dpblJlc3BvbnNlIgASOwoGU2lnblVwEhYudXNlci52MS5TaWduVXBSZXF1ZXN0GhcudXNlci52MS5TaWduVXBSZXNwb25zZSIAEjsKBkxvZ291dBIWLnVzZXIudjEuTG9nb3V0UmVxdWVzdBoXLnVzZXIudjEuTG9nb3V0UmVzcG9uc2UiAEKdAQoLY29tLnVzZXIudjFCCUF1dGhQcm90b1ABWkZnaXRodWIuY29tL3Nwb3RkZW1vNC90cmV2c3RhY2svc2VydmVyL2ludGVybmFsL3NlcnZpY2VzL3VzZXIvdjE7dXNlcnYxogIDVVhYqgIHVXNlci5WMcoCB1VzZXJcVjHiAhNVc2VyXFYxXEdQQk1ldGFkYXRh6gIIVXNlcjo6VjFiBnByb3RvMw");
fileDesc("ChJ1c2VyL3YxL2F1dGgucHJvdG8SB3VzZXIudjEiMgoMTG9naW5SZXF1ZXN0EhAKCHVzZXJuYW1lGAEgASgJEhAKCHBhc3N3b3JkGAIgASgJIh4KDUxvZ2luUmVzcG9uc2USDQoFdG9rZW4YASABKAkiTQoNU2lnblVwUmVxdWVzdBIQCgh1c2VybmFtZRgBIAEoCRIQCghwYXNzd29yZBgCIAEoCRIYChBjb25maXJtX3Bhc3N3b3JkGAMgASgJIhAKDlNpZ25VcFJlc3BvbnNlIg8KDUxvZ291dFJlcXVlc3QiEAoOTG9nb3V0UmVzcG9uc2UywQEKC0F1dGhTZXJ2aWNlEjgKBUxvZ2luEhUudXNlci52MS5Mb2dpblJlcXVlc3QaFi51c2VyLnYxLkxvZ2luUmVzcG9uc2UiABI7CgZTaWduVXASFi51c2VyLnYxLlNpZ25VcFJlcXVlc3QaFy51c2VyLnYxLlNpZ25VcFJlc3BvbnNlIgASOwoGTG9nb3V0EhYudXNlci52MS5Mb2dvdXRSZXF1ZXN0GhcudXNlci52MS5Mb2dvdXRSZXNwb25zZSIAQp0BCgtjb20udXNlci52MUIJQXV0aFByb3RvUAFaRmdpdGh1Yi5jb20vc3BvdGRlbW80L3RyZXZzdGFjay9zZXJ2ZXIvaW50ZXJuYWwvc2VydmljZXMvdXNlci92MTt1c2VydjGiAgNVWFiqAgdVc2VyLlYxygIHVXNlclxWMeICE1VzZXJcVjFcR1BCTWV0YWRhdGHqAghVc2VyOjpWMWIGcHJvdG8z");
/**
* @generated from message user.v1.LoginRequest
@ -64,6 +64,11 @@ export type SignUpRequest = Message<"user.v1.SignUpRequest"> & {
* @generated from field: string password = 2;
*/
password: string;
/**
* @generated from field: string confirm_password = 3;
*/
confirmPassword: string;
};
/**

6
client/src/lib/utils.ts Normal file
View File

@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View File

@ -0,0 +1,189 @@
<script lang="ts">
import {
LayoutGrid,
Settings,
LogOut,
Menu,
LayoutList,
Book,
House,
type Icon as IconType
} from '@lucide/svelte';
import { NavigationMenu, Popover, Separator, Dialog } from 'bits-ui';
import { fly, slide } from 'svelte/transition';
import { toast } from 'svelte-sonner';
import { goto } from '$app/navigation';
import { AuthClient } from '$lib/transport';
import { page } from '$app/state';
import { cn } from '$lib/utils';
let { children } = $props();
const username = localStorage.getItem('username');
let sidebarOpen = $state(false);
type MenuItem = {
name: string;
href: string;
icon: typeof IconType;
};
const menuItems: MenuItem[] = [
{
name: 'Home',
href: '/',
icon: House
},
{
name: 'Items',
href: '/items',
icon: LayoutList
},
{
name: 'Docs',
href: '/docs/',
icon: Book
}
];
async function logout() {
await AuthClient.logout({});
localStorage.removeItem('username');
await goto('/auth');
toast.success('logged out successfully');
}
</script>
<header
class="border-surface-0 bg-mantle fixed flex h-[50px] w-full items-center justify-between border-b p-2 px-6 drop-shadow-md"
>
<div class="flex items-center gap-4">
<Dialog.Root bind:open={sidebarOpen}>
<Dialog.Trigger class="hover:bg-surface-0 cursor-pointer rounded p-1 px-3 transition-all">
<Menu />
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay class="fixed inset-0 z-50 mt-[50px] bg-black/50" />
<Dialog.Content forceMount>
{#snippet child({ props, open })}
{#if open}
<div
class="bg-mantle border-surface-0 fixed inset-0 z-50 mt-[50px] flex w-60 flex-col justify-between border-r drop-shadow-md"
{...props}
transition:slide={{
axis: 'x'
}}
>
<NavigationMenu.Root orientation="vertical">
<NavigationMenu.List class="flex w-full flex-col gap-2 overflow-y-scroll p-2">
{#each menuItems as item}
{@const Icon = item.icon}
<NavigationMenu.Item>
<NavigationMenu.Link
class={cn(
'hover:bg-surface-0 flex select-none gap-2 whitespace-nowrap rounded-lg p-2 transition-all',
page.url.pathname === item.href && 'bg-surface-0'
)}
href={item.href}
onclick={() => {
if (sidebarOpen) {
sidebarOpen = false;
}
}}
>
<Icon />
<span>{item.name}</span>
</NavigationMenu.Link>
</NavigationMenu.Item>
{/each}
</NavigationMenu.List>
</NavigationMenu.Root>
<div class="border-surface-0 flex flex-col gap-2 border-t p-2">
<a
href="/settings"
class="hover:bg-surface-0 flex select-none items-center gap-2 rounded-lg p-2 transition-all"
>
<Settings />
<span>Settings</span>
</a>
<button
class="hover:bg-surface-0 flex w-full cursor-pointer items-center gap-2 whitespace-nowrap rounded-lg p-2 transition-all"
onclick={logout}
>
<LogOut size="20" />
Log out
</button>
</div>
</div>
{/if}
{/snippet}
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
<a href="/" class="flex select-none items-center gap-2 text-2xl font-bold tracking-wider">
TrevStack
<LayoutGrid />
</a>
</div>
<NavigationMenu.Root class="hidden md:block">
<NavigationMenu.List class="flex gap-2">
{#each menuItems as item}
<NavigationMenu.Item>
<NavigationMenu.Link
class={cn(
'hover:bg-surface-0 flex select-none gap-2 rounded-lg p-1 px-2 transition-all',
page.url.pathname === item.href && 'bg-surface-0'
)}
href={item.href}
>
<span>{item.name}</span>
</NavigationMenu.Link>
</NavigationMenu.Item>
{/each}
</NavigationMenu.List>
<NavigationMenu.Viewport class="absolute" />
</NavigationMenu.Root>
<Popover.Root>
<Popover.Trigger
class="border-surface-2 hover:bg-surface-0 cursor-pointer rounded border p-1 px-4 text-sm transition-all"
>
{username}
</Popover.Trigger>
<Popover.Content forceMount>
{#snippet child({ wrapperProps, props, open })}
{#if open}
<div {...wrapperProps}>
<div
class="bg-mantle border-surface-0 z-50 mt-1 rounded border drop-shadow-md transition-all"
{...props}
transition:fly
>
<a
class="hover:bg-surface-0 flex items-center gap-1 p-3 px-4 text-sm"
href="/settings"
>
<Settings size="20" />
Settings
</a>
<Separator.Root class="bg-surface-0 h-px" />
<button
class="hover:bg-surface-0 flex w-full cursor-pointer items-center gap-1 p-3 px-4 text-sm transition-all"
onclick={logout}
>
<LogOut size="20" />
Log out
</button>
</div>
</div>
{/if}
{/snippet}
</Popover.Content>
</Popover.Root>
</header>
<div class="pt-[50px]">
{@render children()}
</div>

View File

@ -1,2 +1,2 @@
<h1>Welcome to TrevStack</h1>
<p>Visit <a href="https://github.com/spotdemo4/trevstack">github.com/spotdemo4/trevstack</a> to read the documentation</p>
<p>Visit <a href="https://github.com/spotdemo4/trevstack">github.com/spotdemo4/trevstack</a> to read the documentation</p>

View File

@ -0,0 +1,26 @@
<script lang="ts">
import { createApiReference } from '@scalar/api-reference';
import { onDestroy, onMount } from 'svelte';
let ref: ReturnType<typeof createApiReference>;
let htmlref: HTMLDivElement;
onMount(() => {
ref = createApiReference(htmlref, {
url: '/openapi/openapi.yaml',
hideClientButton: true
});
});
onDestroy(() => {
ref.destroy();
});
</script>
<svelte:head>
<style>
@import '@scalar/api-reference/style.css';
</style>
</svelte:head>
<div bind:this={htmlref}></div>

View File

@ -1,6 +1,14 @@
<script>
let { children } = $props();
import '../app.css';
import { Toaster } from 'svelte-sonner';
let { children } = $props();
</script>
<Toaster toastOptions={{
classes: {
toast: '!bg-mantle !text-text !border-surface-0',
}
}} />
{@render children()}

View File

@ -1,2 +1,3 @@
export const prerender = true;
export const ssr = false;
export const ssr = false;
export const trailingSlash = 'always';

View File

@ -1,29 +1,151 @@
<script lang="ts">
import { Tabs } from 'bits-ui';
import { Tabs, Button } from 'bits-ui';
import { cn } from '$lib/utils';
import { AuthClient } from '$lib/transport';
import { goto } from '$app/navigation';
import { ConnectError } from '@connectrpc/connect';
import { toast } from 'svelte-sonner';
let tab = $state('login');
</script>
<div class="pt-6">
<Tabs.Root
value="outbound"
class="rounded-card border-muted bg-background-alt shadow-card w-[390px] border p-3"
>
<div class="flex h-screen flex-col items-center justify-center">
<Tabs.Root bind:value={tab} class="w-[390px] p-3">
<Tabs.List
class="rounded-9px bg-dark-10 shadow-mini-inset dark:bg-background grid w-full grid-cols-2 gap-1 p-1 text-sm font-semibold leading-[0.01em] dark:border dark:border-neutral-600/30"
class="bg-mantle border-surface-0 flex w-full justify-around gap-1 rounded-lg border p-1 drop-shadow-md"
>
<Tabs.Trigger
value="outbound"
class="data-[state=active]:shadow-mini dark:data-[state=active]:bg-muted h-8 rounded-[7px] bg-transparent py-2 data-[state=active]:bg-white"
value="login"
class={cn(
'hover:bg-surface-0 grow cursor-pointer rounded p-2 transition-all',
tab == 'login' && 'bg-surface-0'
)}>Log In</Tabs.Trigger
>
Outbound
</Tabs.Trigger>
<Tabs.Trigger
value="inbound"
class="data-[state=active]:shadow-mini dark:data-[state=active]:bg-muted h-8 rounded-[7px] bg-transparent py-2 data-[state=active]:bg-white"
value="signup"
class={cn(
'hover:bg-surface-0 grow cursor-pointer rounded p-2 transition-all',
tab == 'signup' && 'bg-surface-0'
)}>Sign Up</Tabs.Trigger
>
Inbound
</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="outbound" class="select-none pt-3">Test1</Tabs.Content>
<Tabs.Content value="inbound" class="select-none pt-3">Test2</Tabs.Content>
<Tabs.Content
value="login"
class="bg-mantle border-surface-0 mt-2 rounded-lg border p-6 drop-shadow-md"
>
<form
onsubmit={async (e) => {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
const username = formData.get('login-username')?.toString();
const password = formData.get('login-password')?.toString();
try {
const response = await AuthClient.login({
username: username,
password: password
});
if (response.token && username) {
localStorage.setItem('username', username);
goto('/');
}
} catch (err) {
const error = ConnectError.from(err);
toast.error(error.rawMessage);
}
}}
>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-1">
<label for="login-username" class="text-sm">Username</label>
<input
id="login-username"
name="login-username"
type="text"
class="border-surface-0 rounded border p-2 text-sm"
/>
</div>
<div class="flex flex-col gap-1">
<label for="login-password" class="text-sm">Password</label>
<input
id="login-password"
name="login-password"
type="password"
class="border-surface-0 rounded border p-2 text-sm"
/>
</div>
<Button.Root
type="submit"
class="bg-sky text-crust hover:brightness-120 w-20 cursor-pointer rounded p-2 px-4 text-sm transition-all"
>
Submit
</Button.Root>
</div>
</form>
</Tabs.Content>
<Tabs.Content
value="signup"
class="bg-mantle border-surface-0 mt-2 rounded-lg border p-6 drop-shadow-md"
>
<form
onsubmit={async (e) => {
e.preventDefault();
const form = e.target as HTMLFormElement;
const formData = new FormData(form);
try {
await AuthClient.signUp({
username: formData.get('signup-username')?.toString(),
password: formData.get('signup-password')?.toString(),
confirmPassword: formData.get('signup-confirm-password')?.toString()
});
toast.success('account created successfully, please log in');
form.reset();
tab = 'login';
} catch (err) {
const error = ConnectError.from(err);
toast.error(error.rawMessage);
}
}}
>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-1">
<label for="signup-username" class="text-sm">Username</label>
<input
id="signup-username"
name="signup-username"
type="text"
class="border-surface-0 rounded border p-2 text-sm"
/>
</div>
<div class="flex flex-col gap-1">
<label for="signup-password" class="text-sm">Password</label>
<input
id="signup-password"
name="signup-password"
type="password"
class="border-surface-0 rounded border p-2 text-sm"
/>
</div>
<div class="flex flex-col gap-1">
<label for="signup-confirm-password" class="text-sm">Confirm Password</label>
<input
id="signup-confirm-password"
name="signup-confirm-password"
type="password"
class="border-surface-0 rounded border p-2 text-sm"
/>
</div>
<Button.Root
type="submit"
class="bg-sky text-crust hover:brightness-120 w-20 cursor-pointer rounded p-2 px-4 text-sm transition-all"
>
Submit
</Button.Root>
</div>
</form>
</Tabs.Content>
</Tabs.Root>
</div>

View File

@ -51,6 +51,9 @@ components:
password:
type: string
title: password
confirmPassword:
type: string
title: confirm_password
title: SignUpRequest
additionalProperties: false
user.v1.SignUpResponse: