Brijesh's Git Server — catalyst @ ecc323dd9b78e9b2f3973ef2436f175b6d588241

feat: github oauth and some dashboard UI
Brijesh Wawdhane brijesh@wawdhane.com
Sat, 09 Nov 2024 19:04:48 +0530
commit

ecc323dd9b78e9b2f3973ef2436f175b6d588241

parent

140dfab3dc903a978b408323711728d995df5117

M client/package.jsonclient/package.json

@@ -33,6 +33,7 @@ "typescript-eslint": "^8.0.0",

"vite": "^5.0.3" }, "dependencies": { - "@auth/sveltekit": "^1.7.3" + "@auth/sveltekit": "^1.7.3", + "lucide-svelte": "^0.456.0" } }
M client/pnpm-lock.yamlclient/pnpm-lock.yaml

@@ -11,6 +11,9 @@ dependencies:

'@auth/sveltekit': specifier: ^1.7.3 version: 1.7.3(@sveltejs/kit@2.8.0(@sveltejs/vite-plugin-svelte@4.0.0(svelte@5.1.12)(vite@5.4.10))(svelte@5.1.12)(vite@5.4.10))(svelte@5.1.12) + lucide-svelte: + specifier: ^0.456.0 + version: 0.456.0(svelte@5.1.12) devDependencies: '@sveltejs/adapter-auto': specifier: ^3.0.0

@@ -1005,6 +1008,11 @@ resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}

lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lucide-svelte@0.456.0: + resolution: {integrity: sha512-SSdmxJOmL1h42ZypuGr54dYsXAT5o6WkQPKVDFnnrD0liOaXzfNNJuA7JbpkO9FFozBchyVpB1A1mKRmXOD8ug==} + peerDependencies: + svelte: ^3 || ^4 || ^5.0.0-next.42 magic-string@0.30.12: resolution: {integrity: sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==}

@@ -2376,6 +2384,10 @@

lodash.merge@4.6.2: {} lru-cache@10.4.3: {} + + lucide-svelte@0.456.0(svelte@5.1.12): + dependencies: + svelte: 5.1.12 magic-string@0.30.12: dependencies:
M client/src/app.htmlclient/src/app.html

@@ -4,10 +4,24 @@ <head>

<meta charset="utf-8" /> <link rel="icon" href="%sveltekit.assets%/favicon.png" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> - <link rel="stylesheet" href="./app.css" /> + <!-- <link rel="stylesheet" href="./app.css" /> --> + <link rel="preconnect" href="https://fonts.googleapis.com" /> + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> + <link + href="https://fonts.googleapis.com/css2?family=Geist+Mono:wght@100..900&family=Geist:wght@100..900&family=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap" + rel="stylesheet" + /> + <style> + .geist-font { + font-family: 'Geist', sans-serif !important; + } + .geist-mono-font { + font-family: 'Geist Mono', monospace !important; + } + </style> %sveltekit.head% </head> - <body data-sveltekit-preload-data="hover"> + <body data-sveltekit-preload-data="hover" class="geist-font"> <div style="display: contents">%sveltekit.body%</div> </body> </html>
M client/src/auth.tsclient/src/auth.ts

@@ -1,5 +1,6 @@

import { SvelteKitAuth } from '@auth/sveltekit'; +import GitHub from '@auth/sveltekit/providers/github'; -export const { handle } = SvelteKitAuth({ - providers: [] +export const { handle, signIn, signOut } = SvelteKitAuth({ + providers: [GitHub] });
A client/src/components/Sidebar.svelte

@@ -0,0 +1,100 @@

+<script> + import { + ChevronDown, + Database, + Terminal, + Package, + Cloud, + Shield, + Monitor, + Plug, + Lock, + Telescope, + ChartNoAxesColumn, + Box, + Asterisk + } from 'lucide-svelte'; + import { signOut } from '@auth/sveltekit/client'; + import { page } from '$app/stores'; + + const handleSignOut = () => { + signOut(); + }; + + // Sections data + const mainItems = [ + { icon: Database, label: 'Database' }, + { icon: Terminal, label: 'Shell scipts' }, + { icon: Lock, label: 'Authentication' }, + { icon: Telescope, label: 'Observability' }, + { icon: ChartNoAxesColumn, label: 'Analytics' }, + { icon: Monitor, label: 'Uptime Monitoring' }, + { icon: Box, label: 'CDN' } + ]; + + const listItems = [ + { icon: Package, label: 'Packages' }, + { icon: Cloud, label: 'Cloud Services' }, + { icon: Shield, label: 'Security' }, + { icon: Monitor, label: 'Monitoring' }, + { icon: Plug, label: 'Integrations' } + ]; + + let isListsExpanded = true; +</script> + +<div class="sidebar flex h-screen w-72 flex-col border-r border-neutral-600 bg-zinc-700 font-sans"> + <header class="header flex h-[50px] items-center border-b border-neutral-600 px-3"> + <div class="logo-section flex cursor-pointer items-center gap-0.5"> + <Asterisk class="h-6 w-6 text-white" /> + <span class="company-name geist-mono-font text mt-0.5 font-medium text-white">CATALYST</span> + </div> + </header> + + <nav class="navigation flex-1 overflow-y-auto p-2"> + {#each mainItems as item} + <a + href="#{item.label.toLowerCase()}" + class="nav-item flex items-center gap-3 rounded px-1.5 py-1 text-sm text-neutral-300 no-underline transition-colors duration-200 hover:bg-neutral-600" + > + <span class="icon rounded border border-neutral-500 bg-neutral-600 p-1 text-neutral-300" + ><item.icon size={16} /></span + > + <span class="label">{item.label}</span> + </a> + {/each} + + <div class="section my-2"> + <button + class="section-header flex w-full cursor-pointer items-center border-none bg-none p-2 text-xs text-neutral-400" + on:click={() => (isListsExpanded = !isListsExpanded)} + > + <span>Lists</span> + <ChevronDown class="chevron" /> + <div class="add-button ml-auto h-5 w-5 cursor-pointer rounded bg-none opacity-0">+</div> + </button> + {#if isListsExpanded} + {#each listItems as item} + <a + href="#{item.label.toLowerCase()}" + class="nav-item flex items-center gap-3 rounded px-1.5 py-1 text-sm text-neutral-300 no-underline transition-colors duration-200 hover:bg-neutral-600" + > + <span class="icon rounded border border-neutral-500 bg-purple-500 p-1 text-white" + ><item.icon size={16} /></span + > + <span class="label">{item.label}</span> + </a> + {/each} + {/if} + </div> + </nav> + + <footer class="footer border-t border-neutral-600 p-4"> + <div class="user-section flex items-center justify-between gap-2 p-2 text-sm text-neutral-400"> + <span class="user-name text-white">{$page.data.session?.user?.name}</span> + <button on:click={handleSignOut} aria-label="Sign Out" class="text-neutral-400" + >Sign Out</button + > + </div> + </footer> +</div>
M client/src/routes/+layout.server.tsclient/src/routes/+layout.server.ts

@@ -1,7 +1,14 @@

+import { redirect } from '@sveltejs/kit'; import type { LayoutServerLoad } from './$types'; export const load: LayoutServerLoad = async (event) => { const session = await event.locals.auth(); + + if (!session?.user?.name) { + if (event.url.pathname !== '/login') { + throw redirect(303, `/login`); + } + } return { session
M client/src/routes/+layout.svelteclient/src/routes/+layout.svelte

@@ -1,6 +1,29 @@

<script lang="ts"> import '../app.css'; + import Sidebar from '../components/Sidebar.svelte'; + import { onMount } from 'svelte'; + import { page } from '$app/stores'; + import { writable } from 'svelte/store'; + let { children } = $props(); + const showSidebar = writable<boolean>(false); + const nonDashboardRoutes = ['/login']; + + onMount(() => { + const unsubscribe = page.subscribe(($page) => { + showSidebar.set(!nonDashboardRoutes.some((route) => $page.url.pathname.startsWith(route))); + }); + return () => unsubscribe(); + }); </script> -{@render children()} +{#if $showSidebar} + <main class="flex"> + <Sidebar /> + <div class="flex-1"> + {@render children()} + </div> + </main> +{:else} + {@render children()} +{/if}
A client/src/routes/+layout.ts

@@ -0,0 +1,11 @@

+import type { LayoutLoad } from './$types'; + +const nonDashboardRoutes = ['/login']; + +export const load: LayoutLoad = ({ url }) => { + const showSidebar = !nonDashboardRoutes.some((route) => url.pathname.startsWith(route)); + + return { + showSidebar + }; +};
A client/src/routes/+page.server.ts

@@ -0,0 +1,16 @@

+import { redirect } from '@sveltejs/kit'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async (event) => { + const session = await event.locals.auth(); + + if (!session?.user?.name) { + if (event.url.pathname !== '/login') { + throw redirect(303, `/login`); + } + } + + return { + session + }; +};
M client/src/routes/+page.svelteclient/src/routes/+page.svelte

@@ -1,2 +1,4 @@

-<h1>Welcome to SvelteKit</h1> -<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p> +<div class="flex min-h-screen flex-col items-center justify-center"> + <p>Catalyst</p> + <a class="text-blue-500 hover:text-neutral-700" href="/login">Sign In</a> +</div>
A client/src/routes/app/+page.server.ts

@@ -0,0 +1,9 @@

+import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async (events) => { + const session = await events.locals.auth(); + + return { + session + }; +};
A client/src/routes/app/+page.svelte

@@ -0,0 +1,142 @@

+<script lang="ts"> + import { page } from '$app/stores'; + + let databases = [ + { + name: 'Database 1', + lastTransaction: '2023-09-01', + created: '2023-01-15', + status: 'active', + storageUsed: 383 // bytes + }, + { + name: 'Database 2', + lastTransaction: '2023-09-05', + created: '2023-02-20', + status: 'warning', + storageUsed: 823548 // bytes + }, + { + name: 'Database 3', + lastTransaction: '2023-09-10', + created: '2023-03-25', + status: 'critical', + storageUsed: 1373741824 // bytes + }, + { + name: 'Database 1', + lastTransaction: '2023-09-01', + created: '2023-01-15', + status: 'active', + storageUsed: 383 // bytes + }, + { + name: 'Database 2', + lastTransaction: '2023-09-05', + created: '2023-02-20', + status: 'warning', + storageUsed: 823548 // bytes + }, + { + name: 'Database 3', + lastTransaction: '2023-09-10', + created: '2023-03-25', + status: 'critical', + storageUsed: 1373741824 // bytes + }, + { + name: 'Database 1', + lastTransaction: '2023-09-01', + created: '2023-01-15', + status: 'active', + storageUsed: 383 // bytes + }, + { + name: 'Database 2', + lastTransaction: '2023-09-05', + created: '2023-02-20', + status: 'warning', + storageUsed: 823548 // bytes + }, + { + name: 'Database 3', + lastTransaction: '2023-09-10', + created: '2023-03-25', + status: 'critical', + storageUsed: 1373741824 // bytes + } + ]; + + function formatDate(dateString: string): string { + const options: Intl.DateTimeFormatOptions = { day: 'numeric', month: 'long', year: 'numeric' }; + return new Date(dateString).toLocaleDateString(undefined, options); + } + + function getStatusColor(status: string): string { + switch (status) { + case 'active': + return 'bg-green-500'; + case 'warning': + return 'bg-yellow-500'; + case 'critical': + return 'bg-red-500'; + default: + return 'bg-gray-500'; + } + } + + function formatStorage(bytes: number): string { + const units = ['bytes', 'KB', 'MB', 'GB', 'TB']; + let unitIndex = 0; + let value = bytes; + + while (value >= 1024 && unitIndex < units.length - 1) { + value /= 1024; + unitIndex++; + } + + const formattedValue = value.toFixed(2); + return `${parseFloat(formattedValue)} ${units[unitIndex]}`; + } +</script> + +<div> + <div class="flex h-[50px] items-center justify-between border-b border-zinc-300 px-3"> + <p>Database Manager</p> + <div class="flex items-center gap-2"> + <button class="rounded-lg bg-zinc-700 px-2 py-1 font-medium text-white">New Database</button> + <button class="rounded-lg border border-zinc-400 bg-zinc-100 px-2 py-1 font-medium text-black" + >New Server</button + > + </div> + </div> + <div + class="m-3 grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5" + > + {#each databases as db} + <div + class="card cursor-pointer rounded-lg border border-zinc-200 bg-zinc-50 p-3 hover:bg-zinc-100" + > + <div class="mb-2 flex items-center justify-between"> + <p class="mr-2">{db.name}</p> + <span class="-mt-2 h-2 w-2 rounded-full {getStatusColor(db.status)}"></span> + </div> + <p class="mb-2 text-sm text-zinc-500"> + Storage Used: {formatStorage(db.storageUsed)} + </p> + <p class="text-sm text-zinc-500"> + Last Transaction: {formatDate(db.lastTransaction)} + </p> + <p class="text-sm text-zinc-500"> + Created: {formatDate(db.created)} + </p> + </div> + {/each} + </div> + <details class="m-3 hidden rounded-lg border border-zinc-200 bg-zinc-100 p-3"> + <summary>User Details</summary> + <pre> + {JSON.stringify(JSON.parse(JSON.stringify($page.data.session?.user)), null, 2)} + </pre> + </details> +</div>
A client/src/routes/login/+page.server.ts

@@ -0,0 +1,14 @@

+import type { PageServerLoad } from './$types'; +import { redirect } from '@sveltejs/kit'; + +export const load: PageServerLoad = async (events) => { + const session = await events.locals.auth(); + + if (session?.user?.name) { + throw redirect(303, `/app`); + } + + return { + session + }; +};
A client/src/routes/login/+page.svelte

@@ -0,0 +1,52 @@

+<script lang="ts"> + import { signIn, signOut } from '@auth/sveltekit/client'; + import { page } from '$app/stores'; + import { goto } from '$app/navigation'; + import { onMount } from 'svelte'; + + const handleSignIn = () => { + signIn('github'); + }; + + const handleSignOut = () => { + signOut(); + }; + + const redirectToApp = () => { + goto('/app'); + }; + + onMount(() => { + if ($page.data.session?.user) { + redirectToApp(); + } + }); +</script> + +<div class="flex min-h-screen flex-col items-center justify-center"> + {#if $page.data.session} + <p class="mb-3 text-2xl">Welcome, {$page.data.session.user?.name}</p> + <p class="mb-6 max-w-lg text-center text-neutral-700"> + You are now signed in. Enjoy using Catalyst to solve your development challenges. + </p> + <button + on:click={redirectToApp} + aria-label="Go to App" + class="mb-3 rounded bg-orange-600 px-3 py-2 text-white">Go to App</button + > + <button on:click={handleSignOut} aria-label="Sign Out" class="text-neutral-500">Sign Out</button + > + {:else} + <p class="mb-3 text-2xl">Welcome to Catalyst</p> + <p class="mb-6 max-w-lg text-center text-neutral-700"> + Catalyst is a developer toolkit designed to solve real problems that existing tools overlook. + It’s focused on delivering practical solutions for everyday development challenges, helping + you avoid frustration and focus on what you do best: writing code. + </p> + <button + on:click={handleSignIn} + aria-label="Sign In" + class="rounded bg-gray-800 px-3 py-2 text-white">Sign In</button + > + {/if} +</div>
D client/src/routes/test/+page.svelte

@@ -1,25 +0,0 @@

-<script> - let count = 0; - - function incrementCount() { - count += 1; - } - - function decrementCount() { - count -= 1; - } -</script> - -<div class="m-5 space-y-2"> - <h1>Count: {count}</h1> - <div class="space-x-2"> - <button - class="rounded-md bg-neutral-200 px-2 py-1 text-sm font-medium text-neutral-800" - on:click={incrementCount}>Increment</button - > - <button - class="rounded-md bg-neutral-200 px-2 py-1 text-sm font-medium text-neutral-800" - on:click={decrementCount}>Decrement</button - > - </div> -</div>