- Initial dashboard draft
- Login page 
- supabase magic link auth
- env parsing
This commit is contained in:
Alex Selimov 2026-05-30 06:59:24 -04:00
parent 5b05cb4ff8
commit fcc59f0afb
10 changed files with 329 additions and 16 deletions

50
deno.lock generated
View file

@ -3,6 +3,7 @@
"specifiers": { "specifiers": {
"npm:@eslint/compat@^2.0.4": "2.1.0_eslint@10.4.0", "npm:@eslint/compat@^2.0.4": "2.1.0_eslint@10.4.0",
"npm:@eslint/js@^10.0.1": "10.0.1_eslint@10.4.0", "npm:@eslint/js@^10.0.1": "10.0.1_eslint@10.4.0",
"npm:@supabase/supabase-js@^2.106.2": "2.106.2",
"npm:@sveltejs/adapter-auto@^7.0.1": "7.0.1_@sveltejs+kit@2.61.1__@sveltejs+vite-plugin-svelte@7.1.2___svelte@5.55.10___vite@8.0.14____@types+node@24.12.4___@types+node@24.12.4__svelte@5.55.10__typescript@6.0.3__vite@8.0.14___@types+node@24.12.4__@types+node@24.12.4_@sveltejs+vite-plugin-svelte@7.1.2__svelte@5.55.10__vite@8.0.14___@types+node@24.12.4__@types+node@24.12.4_@types+node@24.12.4_svelte@5.55.10_typescript@6.0.3_vite@8.0.14__@types+node@24.12.4", "npm:@sveltejs/adapter-auto@^7.0.1": "7.0.1_@sveltejs+kit@2.61.1__@sveltejs+vite-plugin-svelte@7.1.2___svelte@5.55.10___vite@8.0.14____@types+node@24.12.4___@types+node@24.12.4__svelte@5.55.10__typescript@6.0.3__vite@8.0.14___@types+node@24.12.4__@types+node@24.12.4_@sveltejs+vite-plugin-svelte@7.1.2__svelte@5.55.10__vite@8.0.14___@types+node@24.12.4__@types+node@24.12.4_@types+node@24.12.4_svelte@5.55.10_typescript@6.0.3_vite@8.0.14__@types+node@24.12.4",
"npm:@sveltejs/kit@^2.57.0": "2.61.1_@sveltejs+vite-plugin-svelte@7.1.2__svelte@5.55.10__vite@8.0.14___@types+node@24.12.4__@types+node@24.12.4_svelte@5.55.10_typescript@6.0.3_vite@8.0.14__@types+node@24.12.4_@types+node@24.12.4", "npm:@sveltejs/kit@^2.57.0": "2.61.1_@sveltejs+vite-plugin-svelte@7.1.2__svelte@5.55.10__vite@8.0.14___@types+node@24.12.4__@types+node@24.12.4_svelte@5.55.10_typescript@6.0.3_vite@8.0.14__@types+node@24.12.4_@types+node@24.12.4",
"npm:@sveltejs/vite-plugin-svelte@7": "7.1.2_svelte@5.55.10_vite@8.0.14__@types+node@24.12.4_@types+node@24.12.4", "npm:@sveltejs/vite-plugin-svelte@7": "7.1.2_svelte@5.55.10_vite@8.0.14__@types+node@24.12.4_@types+node@24.12.4",
@ -258,6 +259,51 @@
"@standard-schema/spec@1.1.0": { "@standard-schema/spec@1.1.0": {
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==" "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="
}, },
"@supabase/auth-js@2.106.2": {
"integrity": "sha512-VcAjUErkHkhC5Jaf+g/G1qbkQrFh8edaCdHa7pxJmHUjkWKjT7UnYCtPA89XV0N0GIYRkEqJZw5V62CtOxTmBQ==",
"dependencies": [
"tslib"
]
},
"@supabase/functions-js@2.106.2": {
"integrity": "sha512-oRnr0QrL8H+zTO1YyQ1QjiHZU/957jvubbxSJTUm2XLAgzoGGV9Tahfyd+uvLsBLRVmXLtpU3oyCjdQIvkGMOA==",
"dependencies": [
"tslib"
]
},
"@supabase/phoenix@0.4.2": {
"integrity": "sha512-YSAGnmDAfuleFCVt3CeurQZAhxRfXWeZIIkwp7NhYzQ1UwW6ePSnzsFAiUm/mbCkfoCf70QQHKW/K6RKh52a4A=="
},
"@supabase/postgrest-js@2.106.2": {
"integrity": "sha512-tDOzyPgp9pIRMR2x6C9+uDSJrnXSzxLtt3d7nC+Lrsy3jnJDHYfdQC/xcRyhJE/TOBJ0heSqRKR3UmejDjZxsw==",
"dependencies": [
"tslib"
]
},
"@supabase/realtime-js@2.106.2": {
"integrity": "sha512-LdRGT7DNhyZkPjubUv5bSdAZ0jSEX8wTHvx7htj7+K59TOZRvz4TuQK7tL2RWxyIZVeFMRluL04SzWS61rKnUA==",
"dependencies": [
"@supabase/phoenix",
"tslib"
]
},
"@supabase/storage-js@2.106.2": {
"integrity": "sha512-xgKCSYuev1YarV+iVqr+zlfgSyremnJtn8T0NCT8L4XmMv1CLtESc0Q6kNp8+mKWdX/8ND0nzm7OMKx08kwNAw==",
"dependencies": [
"iceberg-js",
"tslib"
]
},
"@supabase/supabase-js@2.106.2": {
"integrity": "sha512-2/RZ/1fmJx/MRSEDG2Xk8+J4JVk5clM9V0uSI6kUTrcS32KA89DtqI5RUOC9r6mzY3WBC9qexLjssIHjbLyVJA==",
"dependencies": [
"@supabase/auth-js",
"@supabase/functions-js",
"@supabase/postgrest-js",
"@supabase/realtime-js",
"@supabase/storage-js"
]
},
"@sveltejs/acorn-typescript@1.0.10_acorn@8.16.0": { "@sveltejs/acorn-typescript@1.0.10_acorn@8.16.0": {
"integrity": "sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA==", "integrity": "sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA==",
"dependencies": [ "dependencies": [
@ -935,6 +981,9 @@
"graceful-fs@4.2.11": { "graceful-fs@4.2.11": {
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
}, },
"iceberg-js@0.8.1": {
"integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA=="
},
"ignore@5.3.2": { "ignore@5.3.2": {
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==" "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="
}, },
@ -1528,6 +1577,7 @@
"dependencies": [ "dependencies": [
"npm:@eslint/compat@^2.0.4", "npm:@eslint/compat@^2.0.4",
"npm:@eslint/js@^10.0.1", "npm:@eslint/js@^10.0.1",
"npm:@supabase/supabase-js@^2.106.2",
"npm:@sveltejs/adapter-auto@^7.0.1", "npm:@sveltejs/adapter-auto@^7.0.1",
"npm:@sveltejs/kit@^2.57.0", "npm:@sveltejs/kit@^2.57.0",
"npm:@sveltejs/vite-plugin-svelte@7", "npm:@sveltejs/vite-plugin-svelte@7",

16
src/lib/server/env.ts Normal file
View file

@ -0,0 +1,16 @@
import { env } from "$env/dynamic/public";
function required(name: string): string {
const value = env[name];
if (!value) {
throw new Error(`Missing required environment variable ${name}`);
}
return value;
}
export const envConfig = {
PUBLIC_SUPABASE_URL: required("PUBLIC_SUPABASE_URL"),
PUBLIC_SUPABASE_PUBLISHABLE_KEY: required("PUBLIC_SUPABASE_PUBLISHABLE_KEY"),
};

View file

@ -0,0 +1,8 @@
import { createClient } from '@supabase/supabase-js'
import { envConfig } from "./env.ts";
export const supabase = createClient(
envConfig.PUBLIC_SUPABASE_URL,
envConfig.PUBLIC_SUPABASE_PUBLISHABLE_KEY,
);

View file

@ -1,4 +1,21 @@
:root {
--color-bg: #fff;
--color-text: #222;
--color-accent: #1eaedb;
--color-input-bg: #f1f1f1;
color-scheme: light;
}
@media (prefers-color-scheme: dark) {
:root {
--color-bg: #222;
--color-text: #fff;
--color-input-bg: #333;
color-scheme: dark;
}
}
*, *,
*::before, *::before,
*::after { *::after {
@ -31,7 +48,7 @@ h6 {
} }
.site-logo { .site-logo {
color: #222; color: var(--color-text);
flex: 0 0 auto; flex: 0 0 auto;
font-family: 'VT323', monospace; font-family: 'VT323', monospace;
font-size: clamp(32px, 9vw, 40px); font-size: clamp(32px, 9vw, 40px);
@ -40,7 +57,7 @@ h6 {
} }
.site-logo:hover { .site-logo:hover {
color: #1eaedb; color: var(--color-accent);
text-decoration: underline; text-decoration: underline;
} }
@ -56,7 +73,7 @@ h6 {
} }
.site-nav a { .site-nav a {
color: #222; color: var(--color-text);
font-family: 'VT323', monospace; font-family: 'VT323', monospace;
font-size: clamp(16px, 5vw, 24px); font-size: clamp(16px, 5vw, 24px);
font-weight: 400; font-weight: 400;
@ -65,21 +82,21 @@ h6 {
} }
.site-nav a:hover { .site-nav a:hover {
color: #1eaedb; color: var(--color-accent);
text-decoration: underline; text-decoration: underline;
} }
.site-nav .login-link { .site-nav .login-link {
background: transparent; background: transparent;
border: 1px solid #222; border: 1px solid var(--color-text);
color: #222; color: var(--color-text);
padding: clamp(2px, 1vw, 4px) clamp(8px, 2.5vw, 14px); padding: clamp(2px, 1vw, 4px) clamp(8px, 2.5vw, 14px);
} }
.site-nav .login-link:hover { .site-nav .login-link:hover {
background: transparent; background: transparent;
border-color: #1eaedb; border-color: var(--color-accent);
color: #1eaedb; color: var(--color-accent);
text-decoration: underline; text-decoration: underline;
} }
@ -149,6 +166,116 @@ li::before {
} }
body { body {
background: var(--color-bg);
color: var(--color-text);
font-size: 18px; font-size: 18px;
font-family: 'VT323', monospace; font-family: 'VT323', monospace;
} }
textarea#text-input {
background: var(--color-input-bg);
border: 1px solid var(--color-text);
color: var(--color-text);
font-family: 'VT323', monospace;
font-size: 20px;
height: 320px;
line-height: 1.3;
min-height: 320px;
resize: vertical;
width: 100%;
}
textarea#text-input:focus {
border-color: var(--color-accent);
outline: 0;
}
.login-page {
align-items: center;
display: flex;
justify-content: center;
min-height: calc(100vh - 96px);
padding-bottom: 48px;
padding-top: 48px;
}
.login-card {
border: 1px solid var(--color-text);
max-width: 520px;
padding: clamp(24px, 5vw, 48px);
width: 100%;
}
.login-card .display-heading {
margin-bottom: 12px;
}
.login-copy {
font-size: 24px;
line-height: 1.25;
margin-bottom: 28px;
}
.login-form {
display: flex;
flex-direction: column;
gap: 14px;
}
.login-form label {
font-size: 24px;
margin-bottom: 0;
}
.login-form input[type='email'] {
background: var(--color-input-bg);
border: 1px solid var(--color-text);
color: var(--color-text);
font-family: 'VT323', monospace;
font-size: 24px;
height: auto;
margin-bottom: 8px;
padding: 12px 14px;
width: 100%;
}
.login-form input[type='email']:focus {
border-color: var(--color-accent);
outline: 0;
}
.login-form button {
align-self: flex-start;
margin-top: 8px;
}
.dashboard-button-row {
display: flex;
gap: 16px;
margin-bottom: 24px;
}
.dashboard-button-column {
display: flex;
flex: 1;
justify-content: center;
}
.dashboard-button {
width: 220px;
}
button {
background: transparent;
border: 1px solid var(--color-text);
color: var(--color-text);
font-size: 20px;
}
button:active,
button:focus,
.dashboard-button.active {
border-color: var(--color-accent);
color: var(--color-accent);
outline: 0;
}

View file

@ -7,14 +7,6 @@
<svelte:head><link rel="icon" href={favicon} /></svelte:head> <svelte:head><link rel="icon" href={favicon} /></svelte:head>
<header class="site-header">
<a class="site-logo" href="/">gitKeep</a>
<nav class="site-nav" aria-label="Main navigation">
<a href="#how-it-works">How it works</a>
<a class="login-link" href="#get-started">Login</a>
</nav>
</header>
<main> <main>
{@render children()} {@render children()}
</main> </main>

View file

@ -1,3 +1,10 @@
<header class="site-header">
<a class="site-logo" href="/">gitKeep</a>
<nav class="site-nav" aria-label="Main navigation">
<a href="#how-it-works">How it works</a>
<a class="login-link" href="/login">Login</a>
</nav>
</header>
<section class="container hero" id="intro"> <section class="container hero" id="intro">
<div class="row"> <div class="row">
<div class = "six columns"> <div class = "six columns">

View file

@ -0,0 +1,51 @@
<header class="site-header">
<a class="site-logo" href="/">gitKeep</a>
<nav class="site-nav" aria-label="Main navigation">
<a class="login-link" href="#get-started">Logout</a>
</nav>
</header>
<script lang="ts">
import Config from "./Config.svelte";
let activePanel = $state("configuration");
let { data } = $props();;
</script>
<section class="container hero" id="intro">
<h1> Welcome to gitKeep...</h1>
<div class="dashboard-button-row">
<div class="dashboard-button-column">
<button
class="dashboard-button"
class:active={activePanel === "configuration"}
aria-pressed={activePanel === "configuration"}
on:click={() => activePanel = "configuration"}
>Configure message</button>
</div>
<div class="dashboard-button-column">
<button
class="dashboard-button"
class:active={activePanel === "vouchList"}
aria-pressed={activePanel === "vouchList"}
on:click={() => activePanel = "vouchList"}
>Vouch list</button>
</div>
<div class="dashboard-button-column">
<button
class="dashboard-button"
class:active={activePanel === "gitHelp"}
aria-pressed={activePanel === "gitHelp"}
on:click={() => activePanel = "gitHelp"}
>Github setup</button>
</div>
</div>
{#if activePanel === "configuration"}
<Config markdown = {data.markdown} />
{/if}
</section>

View file

@ -0,0 +1,14 @@
<script lang="ts">
let { markdown = '' } = $props();
let text = $state(markdown);
</script>
<label for="text-input">Text</label>
<textarea
id="text-input"
name="Onboarding agreement:"
bind:value={text}
placeholder="Enter text..."
rows="20"
></textarea>
<button type="submit">Submit</button>

View file

@ -0,0 +1,29 @@
import { fail } from '@sveltejs/kit';
import { supabase } from '$lib/server/supabase.ts';
import type { Actions } from './$types';
export const actions: Actions = {
default: async ({request, url}) =>{
const form = await request.formData();
const email = String(form.get("email") ?? "");
const baseUrl = url.origin;
if (!email){
return fail(400, {message: "Missing email"})
}
const { error } = await supabase.auth.signInWithOtp({
email: email,
options: {
shouldCreateUser: true,
emailRedirectTo: `${baseUrl}/dashboard`,
},
})
if (error){
return fail(400, error.message);
}
return {success: true}
}
}

View file

@ -0,0 +1,19 @@
<header class="site-header">
<a class="site-logo" href="/">gitKeep</a>
<nav class="site-nav" aria-label="Main navigation">
<a href="/">Home</a>
</nav>
</header>
<section class="container login-page">
<div class="login-card">
<h1 class="display-heading">Login</h1>
<p class="login-copy">Enter your email and well send you a magic link.</p>
<form class="login-form" method = "POST">
<label for="email">Email</label>
<input id="email" name="email" type="email" autocomplete="email" placeholder="you@example.com" required />
<button type="submit">Send magic link</button>
</form>
</div>
</section>