Theming, A11y, added resend, formatting

This commit is contained in:
Luke Hagar
2025-06-02 17:46:41 -05:00
parent 0ba9cbba26
commit 9e514d03c1
17 changed files with 582 additions and 330 deletions

View File

@@ -1,2 +1,3 @@
PUBLIC_SUPABASE_URL="YOUR_SUPABASE_URL"
PUBLIC_SUPABASE_ANON_KEY="YOUR_SUPABASE_ANON_KEY"
RESEND_API_KEY="YOUR_RESEND_KEY"

253
package-lock.json generated
View File

@@ -38,6 +38,7 @@
"remark-abbr": "^1.4.2",
"remark-toc": "^9.0.0",
"remark-unwrap-images": "^4.0.1",
"resend": "^4.5.1",
"shiki": "^3.4.2",
"svelte": "^5.25.0",
"svelte-check": "^4.0.0",
@@ -876,6 +877,25 @@
"dev": true,
"license": "MIT"
},
"node_modules/@react-email/render": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.0.6.tgz",
"integrity": "sha512-zNueW5Wn/4jNC1c5LFgXzbUdv5Lhms+FWjOvWAhal7gx5YVf0q6dPJ0dnR70+ifo59gcMLwCZEaTS9EEuUhKvQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"html-to-text": "9.0.5",
"prettier": "3.5.3",
"react-promise-suspense": "0.3.4"
},
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"react": "^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^18.0 || ^19.0 || ^19.0.0-rc"
}
},
"node_modules/@rollup/plugin-inject": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/@rollup/plugin-inject/-/plugin-inject-5.0.5.tgz",
@@ -1202,6 +1222,20 @@
"win32"
]
},
"node_modules/@selderee/plugin-htmlparser2": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz",
"integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"domhandler": "^5.0.3",
"selderee": "^0.11.0"
},
"funding": {
"url": "https://ko-fi.com/killymxi"
}
},
"node_modules/@shikijs/core": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.4.2.tgz",
@@ -3548,6 +3582,21 @@
"dev": true,
"license": "MIT"
},
"node_modules/dom-serializer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
"dev": true,
"license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.2",
"entities": "^4.2.0"
},
"funding": {
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
}
},
"node_modules/domain-browser": {
"version": "4.22.0",
"resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-4.22.0.tgz",
@@ -3561,6 +3610,50 @@
"url": "https://bevry.me/fund"
}
},
"node_modules/domelementtype": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "BSD-2-Clause"
},
"node_modules/domhandler": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"domelementtype": "^2.3.0"
},
"engines": {
"node": ">= 4"
},
"funding": {
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/domutils": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"dom-serializer": "^2.0.0",
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3"
},
"funding": {
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -3620,6 +3713,19 @@
"node": ">=10.13.0"
}
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -4436,6 +4542,23 @@
"minimalistic-crypto-utils": "^1.0.1"
}
},
"node_modules/html-to-text": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz",
"integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@selderee/plugin-htmlparser2": "^0.11.0",
"deepmerge": "^4.3.1",
"dom-serializer": "^2.0.0",
"htmlparser2": "^8.0.2",
"selderee": "^0.11.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/html-void-elements": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
@@ -4447,6 +4570,26 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/htmlparser2": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
"integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
"dev": true,
"funding": [
"https://github.com/fb55/htmlparser2?sponsor=1",
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3",
"domutils": "^3.0.1",
"entities": "^4.4.0"
}
},
"node_modules/https-browserify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz",
@@ -4774,6 +4917,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/leac": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz",
"integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://ko-fi.com/killymxi"
}
},
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -5792,6 +5945,20 @@
"node": ">= 0.10"
}
},
"node_modules/parseley": {
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz",
"integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==",
"dev": true,
"license": "MIT",
"dependencies": {
"leac": "^0.6.0",
"peberminta": "^0.9.0"
},
"funding": {
"url": "https://ko-fi.com/killymxi"
}
},
"node_modules/path-browserify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
@@ -5843,6 +6010,16 @@
"node": ">=0.12"
}
},
"node_modules/peberminta": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz",
"integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://ko-fi.com/killymxi"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -6304,6 +6481,48 @@
"safe-buffer": "^5.1.0"
}
},
"node_modules/react": {
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.26.0"
},
"peerDependencies": {
"react": "^19.1.0"
}
},
"node_modules/react-promise-suspense": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/react-promise-suspense/-/react-promise-suspense-0.3.4.tgz",
"integrity": "sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^2.0.1"
}
},
"node_modules/react-promise-suspense/node_modules/fast-deep-equal": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
"integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==",
"dev": true,
"license": "MIT"
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
@@ -6495,6 +6714,19 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/resend": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/resend/-/resend-4.5.1.tgz",
"integrity": "sha512-ryhHpZqCBmuVyzM19IO8Egtc2hkWI4JOL5lf5F3P7Dydu3rFeX6lHNpGqG0tjWoZ63rw0l731JEmuJZBdDm3og==",
"dev": true,
"license": "MIT",
"dependencies": {
"@react-email/render": "1.0.6"
},
"engines": {
"node": ">=18"
}
},
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
@@ -6664,6 +6896,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/scheduler": {
"version": "0.26.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
"integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/schema-dts": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/schema-dts/-/schema-dts-1.1.5.tgz",
@@ -6671,6 +6911,19 @@
"dev": true,
"license": "Apache-2.0"
},
"node_modules/selderee": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz",
"integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==",
"dev": true,
"license": "MIT",
"dependencies": {
"parseley": "^0.12.0"
},
"funding": {
"url": "https://ko-fi.com/killymxi"
}
},
"node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",

View File

@@ -43,6 +43,7 @@
"remark-abbr": "^1.4.2",
"remark-toc": "^9.0.0",
"remark-unwrap-images": "^4.0.1",
"resend": "^4.5.1",
"shiki": "^3.4.2",
"svelte": "^5.25.0",
"svelte-check": "^4.0.0",

View File

@@ -36,4 +36,9 @@
div.prose a {
@apply anchor
}
a.disabled {
@apply pointer-events-none opacity-50;
}

View File

@@ -0,0 +1,104 @@
<script lang="ts">
import { page } from '$app/state';
import SvelteyLogoLetter from '$lib/assets/Sveltey-logo-letter.svelte';
import ThemeSwitch from '$lib/components/ThemeSwitch.svelte';
import { BookOpen, DollarSign, Home, LayoutDashboard, LogOut, User } from '@lucide/svelte';
import { Avatar } from '@skeletonlabs/skeleton-svelte';
let { data } = $props();
let { session } = $derived(data);
// Helper function to check if a path is active
function isActivePath(path: string): boolean {
return page.url.pathname === path;
}
// Helper function to get navigation link classes
function getNavClasses(path: string): string {
return `btn btn-sm flex items-center gap-2 ${page.url.pathname === path ? 'cursor-default disabled' : ''}`;
}
</script>
<header
class="bg-surface-50-950-token border-surface-200-700-token sticky top-0 z-50 border-b backdrop-blur-2xl"
>
<nav class="container mx-auto px-6 py-4">
<div class="flex items-center justify-between">
<!-- Left side - Brand and Main navigation -->
<div class="flex items-center gap-8">
<!-- Brand -->
<a href="/" class="flex items-center gap-2" aria-label="Sveltey - Go to homepage">
<SvelteyLogoLetter size="size-12" />
<span class="hidden text-xl font-bold sm:block">Sveltey</span>
</a>
<!-- Main Navigation -->
<div class="hidden items-center gap-2 md:flex">
<a href="/" class={getNavClasses('/')} aria-label="Go to homepage">
<Home class="size-4" aria-hidden="true" />
<span>Home</span>
</a>
<a href="/pricing" class={getNavClasses('/pricing')} aria-label="View pricing plans">
<DollarSign class="size-4" aria-hidden="true" />
<span>Pricing</span>
</a>
<a href="/blog" class={getNavClasses('/blog')} aria-label="Read our blog">
<BookOpen class="size-4" aria-hidden="true" />
<span>Blog</span>
</a>
{#if session}
<a href="/app/dashboard" class={getNavClasses('/app/dashboard')} aria-label="Go to dashboard">
<LayoutDashboard class="size-4" aria-hidden="true" />
<span>Dashboard</span>
</a>
{/if}
</div>
</div>
<!-- Right side - User actions and theme switcher -->
<div class="flex gap-3">
<ThemeSwitch />
{#if session}
<!-- User Profile Section -->
<div class="border-surface-300-600 flex items-center gap-3 border-l pl-3">
<Avatar
size="size-8"
src={session.user.user_metadata.avatar_url}
name={session.user.user_metadata.full_name || session.user.email}
/>
<div class="hidden md:block">
<p class="text-sm font-medium">
{session.user.user_metadata.full_name || session.user.email?.split('@')[0]}
</p>
<p class="text-xs opacity-75">{session.user.email}</p>
</div>
<form action="/auth/logout" method="POST" style="display: inline;">
<button
type="submit"
class="btn flex items-center gap-2"
title="Sign Out"
aria-label="Sign out of your account"
>
<LogOut class="size-4" aria-hidden="true" />
<span class="hidden sm:inline">Sign Out</span>
</button>
</form>
</div>
{:else}
<!-- Authentication Buttons -->
<div class="flex items-center gap-3">
<a href="/auth" class={getNavClasses('/auth')} aria-label="Sign in or register">
<User class="size-4" aria-hidden="true" />
<span>Sign In / Register</span>
</a>
</div>
{/if}
</div>
</div>
</nav>
</header>

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { browser } from '$app/environment';
import { Palette } from '@lucide/svelte';
import { Check, ChevronDown, Moon, Palette, Sun } from '@lucide/svelte';
// Available color themes
const colorThemes = [
@@ -29,7 +29,7 @@
{ value: 'wintry', label: 'Wintry' }
];
let currentColorTheme = $state('skeleton');
let currentColorTheme = $state('legacy');
let isDarkMode = $state(true);
let mounted = $state(false);
let showDropdown = $state(false);
@@ -98,95 +98,52 @@
}
}
});
// Close dropdown when clicking outside
function handleClickOutside(event: MouseEvent) {
const target = event.target as HTMLElement;
if (!target.closest('.theme-dropdown')) {
showDropdown = false;
}
}
onMount(() => {
if (browser) {
document.addEventListener('click', handleClickOutside);
return () => document.removeEventListener('click', handleClickOutside);
}
});
</script>
<div class="flex items-center space-x-3">
<!-- Light/Dark Mode Toggle -->
<!-- Light/Dark Mode Toggle -->
<button
onclick={toggleDarkMode}
class="btn btn-sm items-center"
title="Toggle light/dark mode"
aria-label="Toggle light/dark mode"
>
{#if !isDarkMode}
<Sun class="size-4" aria-hidden="true" />
{:else}
<Moon class="size-4" aria-hidden="true" />
{/if}
</button>
<!-- Color Theme Selector -->
<div class="relative">
<button
onclick={toggleDarkMode}
class="btn btn-sm preset-outlined-surface-500 flex items-center h-8"
title="Toggle light/dark mode"
aria-label="Toggle light/dark mode"
onclick={() => (showDropdown = !showDropdown)}
class="btn btn-sm flex h-8 items-center"
title="Select color theme"
aria-label="Select color theme"
aria-expanded={showDropdown}
>
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{#if !isDarkMode}
<!-- Sun icon for dark mode (click to go light) -->
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"
/>
{:else}
<!-- Moon icon for light mode (click to go dark) -->
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
/>
{/if}
</svg>
<Palette class="size-4" aria-hidden="true" />
<span class="capitalize">{currentColorTheme}</span>
<ChevronDown class={`size-4 transition-transform ${showDropdown === true ? 'rotate-180' : ''}`} aria-hidden="true" />
</button>
<!-- Color Theme Selector -->
<div class="theme-dropdown relative">
<button
onclick={() => (showDropdown = !showDropdown)}
class="btn btn-sm preset-outlined-surface-500 h-8 flex items-center"
title="Select color theme"
aria-label="Select color theme"
aria-expanded={showDropdown}
{#if showDropdown}
<div
class="card preset-outlined-primary-500 bg-surface-50-950 absolute left-0 z-50 mt-2 w-48 overflow-hidden rounded-lg shadow-lg"
>
<Palette class="size-4" />
<span class="text-sm capitalize">{currentColorTheme}</span>
<svg
class="ml-1 h-3 w-3 transition-transform {showDropdown ? 'rotate-180' : ''}"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
{#if showDropdown}
<div
class="card preset-outlined-primary-500 bg-surface-50-950 absolute left-0 z-50 mt-2 w-48 overflow-hidden rounded-lg shadow-lg"
>
{#each colorThemes as theme}
<button
onclick={() => selectColorTheme(theme.value)}
class="hover:bg-surface-950-50 hover:text-surface-50-950 flex w-full items-center justify-between px-4 py-2 text-left text-sm transition-colors"
>
<span>{theme.label}</span>
{#if currentColorTheme === theme.value}
<svg class="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clip-rule="evenodd"
/>
</svg>
{/if}
</button>
{/each}
</div>
{/if}
</div>
{#each colorThemes as theme}
<button
onclick={() => selectColorTheme(theme.value)}
class="btn btn-sm flex w-full items-center justify-between px-4 py-2 transition-colors"
aria-label="Select {theme.label} theme"
>
<span>{theme.label}</span>
{#if currentColorTheme === theme.value}
<Check class="size-4" aria-hidden="true" />
{/if}
</button>
{/each}
</div>
{/if}
</div>

View File

@@ -99,8 +99,10 @@
{#each dashboardStats as stat}
<div
class="card preset-outlined-surface-200-800 space-y-4 p-6 text-center transition-all duration-300 hover:scale-105"
role="article"
aria-label="{stat.label}: {stat.value}"
>
<stat.icon class={`mx-auto size-8 ${stat.color}`} />
<stat.icon class={`mx-auto size-8 ${stat.color}`} aria-hidden="true" />
<div class="space-y-1">
<div class="text-3xl font-bold">{stat.value}</div>
<p class="text-sm opacity-75">{stat.label}</p>
@@ -122,16 +124,26 @@
>
<action.icon
class="text-primary-500 size-10 transition-transform group-hover:scale-110"
aria-hidden="true"
/>
<h3 class="h4 group-hover:text-primary-500 transition-colors">{action.title}</h3>
<p class="text-sm opacity-75">{action.description}</p>
{#if action.available}
<a href={action.href} class="btn preset-filled-primary-500 w-full">
<a
href={action.href}
class="btn preset-filled-primary-500 w-full"
aria-label="{action.action} - {action.title}"
>
{action.action}
</a>
{:else}
<button class="btn preset-outlined-surface-200-800 w-full opacity-50" disabled>
<button
class="btn preset-outlined-surface-200-800 w-full opacity-50"
disabled
aria-label="{action.action} - {action.title} (Coming Soon)"
title="This feature is coming soon"
>
{action.action} (Coming Soon)
</button>
{/if}
@@ -146,7 +158,7 @@
<section class="space-y-8">
<div class="card preset-outlined-primary-500 space-y-6 p-8 text-center">
<div class="flex items-center justify-center gap-2">
<Star class="text-primary-500 size-8" />
<Star class="text-primary-500 size-8" aria-hidden="true" />
<h3 class="h3 text-primary-500">Current Plan: Starter</h3>
</div>
<p class="mx-auto max-w-2xl opacity-75">
@@ -154,12 +166,21 @@
limits, and priority support.
</p>
<div class="flex flex-col justify-center gap-4 sm:flex-row">
<a href="/pricing" class="btn preset-filled-primary-500">
<Star class="size-5" />
<a
href="/pricing"
class="btn preset-filled-primary-500"
aria-label="Upgrade your plan to unlock premium features"
>
<Star class="size-5" aria-hidden="true" />
<span>Upgrade Plan</span>
</a>
<button class="btn preset-outlined-surface-200-800" disabled>
<BarChart class="size-5" />
<button
class="btn preset-outlined-surface-200-800"
disabled
aria-label="View usage statistics (Coming Soon)"
title="Usage statistics feature is coming soon"
>
<BarChart class="size-5" aria-hidden="true" />
<span>View Usage</span>
</button>
</div>
@@ -170,17 +191,27 @@
<section class="space-y-8">
<h2 class="h2 text-center">Recent Activity</h2>
<div class="card preset-outlined-surface-200-800 space-y-4 p-8 text-center">
<BarChart class="text-primary-500 mx-auto size-16 opacity-50" />
<BarChart class="text-primary-500 mx-auto size-16 opacity-50" aria-hidden="true" />
<h3 class="h4">No recent activity</h3>
<p class="opacity-75">
Start using our services to see your activity here. Create your first project or make an
API call to get started.
</p>
<div class="flex flex-col justify-center gap-4 sm:flex-row">
<button class="btn preset-filled-primary-500" disabled>
<button
class="btn preset-filled-primary-500"
disabled
aria-label="Create new project (Coming Soon)"
title="Project creation feature is coming soon"
>
Create Project (Coming Soon)
</button>
<button class="btn preset-outlined-surface-200-800" disabled>
<button
class="btn preset-outlined-surface-200-800"
disabled
aria-label="View documentation (Coming Soon)"
title="Documentation feature is coming soon"
>
View Documentation (Coming Soon)
</button>
</div>
@@ -189,17 +220,25 @@
{:else if session === null}
<!-- Access Denied -->
<div class="card preset-outlined-error-500 mx-auto max-w-2xl space-y-6 p-8 text-center md:p-12">
<Shield class="text-error-500 mx-auto size-16" />
<Shield class="text-error-500 mx-auto size-16" aria-hidden="true" />
<h2 class="h3">Access Denied</h2>
<p class="opacity-75">
You need to be logged in to access your dashboard. Please sign in to continue.
</p>
<div class="flex flex-col justify-center gap-4 sm:flex-row">
<a href="/auth/login" class="btn preset-filled-primary-500">
<User class="size-5" />
<a
href="/auth/login"
class="btn preset-filled-primary-500"
aria-label="Sign in to access your dashboard"
>
<User class="size-5" aria-hidden="true" />
<span>Sign In</span>
</a>
<a href="/auth/signup" class="btn preset-outlined-surface-200-800">
<a
href="/auth/signup"
class="btn preset-outlined-surface-200-800"
aria-label="Create new account to get started"
>
<span>Create Account</span>
</a>
</div>
@@ -208,8 +247,13 @@
<!-- Loading State -->
<div
class="card preset-outlined-surface-200-800 mx-auto max-w-2xl space-y-4 p-8 text-center md:p-12"
role="status"
aria-live="polite"
>
<div class="border-primary-500 mx-auto h-12 w-12 animate-spin rounded-full border-b-2"></div>
<div
class="border-primary-500 mx-auto h-12 w-12 animate-spin rounded-full border-b-2"
aria-label="Loading dashboard"
></div>
<h3 class="h4">Loading your dashboard...</h3>
<p class="opacity-75">Please wait while we prepare your personalized experience.</p>
</div>

View File

@@ -369,6 +369,8 @@
type="button"
class="absolute right-3 top-1/2 -translate-y-1/2 text-surface-500"
disabled={true}
aria-label={showPassword ? 'Hide password' : 'Show password'}
title={showPassword ? 'Hide password' : 'Show password'}
>
{#if showPassword}
<EyeOff class="size-4" />
@@ -432,12 +434,14 @@
class="btn w-full flex items-center justify-center gap-3 {provider.color}"
onclick={() => handleOAuth(provider.provider)}
disabled={!provider.enabled || loading || oauthLoading !== ''}
aria-label="{provider.description}"
title={provider.enabled ? provider.description : `${provider.name} login is disabled in demo mode`}
>
{#if oauthLoading === provider.provider}
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-current"></div>
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-current" aria-hidden="true"></div>
Connecting...
{:else}
<provider.icon class="size-4" />
<provider.icon class="size-4" aria-hidden="true" />
{provider.description}
{/if}
</button>

View File

@@ -143,6 +143,8 @@
type="button"
class="absolute right-3 top-1/2 -translate-y-1/2 text-surface-500"
disabled={true}
aria-label={showPassword ? 'Hide password' : 'Show password'}
title={showPassword ? 'Hide password' : 'Show password'}
>
{#if showPassword}
<EyeOff class="size-4" />
@@ -173,6 +175,8 @@
type="button"
class="absolute right-3 top-1/2 -translate-y-1/2 text-surface-500"
disabled={true}
aria-label={showConfirmPassword ? 'Hide confirm password' : 'Show confirm password'}
title={showConfirmPassword ? 'Hide confirm password' : 'Show confirm password'}
>
{#if showConfirmPassword}
<EyeOff class="size-4" />

View File

@@ -38,12 +38,8 @@
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
{#each featuredPosts as post}
<a href="/blog/{post.slug}" class="card preset-outlined-primary-500 p-6 md:p-8 space-y-4 hover:scale-105 transition-all duration-300 group">
<a href="/blog/{post.slug}" class="card preset-outlined-primary-500 p-6 md:p-8 space-y-4 hover:scale-105 hover:shadow-2xl transition-all duration-300 group">
<div class="flex items-center justify-between">
<span class="badge preset-filled-primary-500 flex items-center gap-1">
<Star class="size-3" />
Featured
</span>
<div class="flex items-center gap-1 text-sm opacity-75">
<Calendar class="size-4" />
{formatDate(post.publishedAt)}
@@ -87,7 +83,7 @@
<h2 class="h2 text-center">Recent Posts</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{#each regularPosts as post}
<a href="/blog/{post.slug}" class="card preset-outlined-primary-500 p-4 md:p-6 space-y-4 hover:scale-105 transition-all duration-300 group">
<a href="/blog/{post.slug}" class="card preset-outlined-primary-500 p-4 md:p-6 space-y-4 hover:scale-105 hover:shadow-2xl transition-all duration-300 group">
<div class="flex items-center gap-1 text-sm opacity-75">
<Calendar class="size-4" />
{formatDate(post.publishedAt)}

View File

@@ -72,8 +72,9 @@
rel="noopener noreferrer"
class="btn btn-sm preset-outlined-secondary-500"
title="Share on Twitter"
aria-label="Share this article on Twitter"
>
<Twitter class="size-4" />
<Twitter class="size-4" aria-hidden="true" />
</a>
<a
href="https://www.linkedin.com/sharing/share-offsite/?url={encodeURIComponent(typeof window !== 'undefined' ? window.location.href : '')}"
@@ -81,8 +82,9 @@
rel="noopener noreferrer"
class="btn btn-sm preset-outlined-secondary-500"
title="Share on LinkedIn"
aria-label="Share this article on LinkedIn"
>
<Linkedin class="size-4" />
<Linkedin class="size-4" aria-hidden="true" />
</a>
</div>
</div>
@@ -129,8 +131,9 @@
target="_blank"
rel="noopener noreferrer"
class="btn btn-sm preset-outlined-surface-200-800"
aria-label="Share this article on Twitter"
>
<Twitter class="size-4" />
<Twitter class="size-4" aria-hidden="true" />
<span>Twitter</span>
</a>
<a
@@ -138,8 +141,9 @@
target="_blank"
rel="noopener noreferrer"
class="btn btn-sm preset-outlined-surface-200-800"
aria-label="Share this article on LinkedIn"
>
<Linkedin class="size-4" />
<Linkedin class="size-4" aria-hidden="true" />
<span>LinkedIn</span>
</a>
</div>

View File

@@ -0,0 +1,26 @@
import { Resend } from 'resend';
import { RESEND_API_KEY } from '$env/static/private';
const resend = new Resend(RESEND_API_KEY);
export const actions = {
sendEmail: async ({ request }) => {
const formData = await request.formData();
const email = formData.get('email');
const message = formData.get('message');
try {
await resend.emails.send({
from: 'your-email@example.com',
to: 'recipient@example.com',
subject: 'New Contact Form Submission',
text: `Email: ${email}\nMessage: ${message}`,
});
return { success: true };
} catch (error) {
console.error('Error sending email:', error);
return { success: false, error: 'Failed to send email' };
}
}
};

View File

@@ -52,7 +52,7 @@
<h1 class="h1">Get in <span class="text-primary-500">Touch</span></h1>
</div>
<p class="text-xl opacity-75 max-w-2xl mx-auto">
Have questions? We'd love to hear from you. Send us a message and we'll respond as soon as possible.
Have questions? We'd love to hear from you. <br> Send us a message and we'll respond as soon as possible.
</p>
</header>
@@ -61,7 +61,7 @@
<div class="space-y-8">
<!-- Contact Methods -->
<div class="space-y-6">
<div class="card preset-outlined-surface-200-800 p-6 space-y-4">
<div class="card preset-outlined-surface-500 p-6 space-y-4">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-primary-500 rounded-lg flex items-center justify-center">
<Mail class="size-5 text-white" />
@@ -73,7 +73,7 @@
</div>
</div>
<div class="card preset-outlined-surface-200-800 p-6 space-y-4">
<div class="card preset-outlined-surface-500 p-6 space-y-4">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-secondary-500 rounded-lg flex items-center justify-center">
<Phone class="size-5 text-white" />
@@ -85,7 +85,7 @@
</div>
</div>
<div class="card preset-outlined-surface-200-800 p-6 space-y-4">
<div class="card preset-outlined-surface-500 p-6 space-y-4">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-tertiary-500 rounded-lg flex items-center justify-center">
<MapPin class="size-5 text-white" />
@@ -99,7 +99,7 @@
</div>
<!-- Response Time -->
<div class="card preset-filled-primary-500 p-6 text-center">
<div class="card preset-outlined-primary-500 p-6 text-center">
<Clock class="size-8 mx-auto mb-3" />
<h3 class="h4 mb-2">Quick Response</h3>
<p class="text-sm opacity-90">
@@ -110,7 +110,7 @@
<!-- Contact Form -->
<div class="lg:col-span-2">
<div class="card preset-outlined-surface-200-800 p-8">
<div class="card preset-outlined-surface-500 p-8">
<div class="space-y-6">
<div>
<h2 class="h3 mb-2">Send us a message</h2>
@@ -124,7 +124,7 @@
Name *
</label>
<input
class="input preset-outlined-surface-200-800"
class="input preset-outlined-surface-500"
type="text"
id="name"
bind:value={formData.name}
@@ -139,7 +139,7 @@
Email *
</label>
<input
class="input preset-outlined-surface-200-800"
class="input preset-outlined-surface-500"
type="email"
id="email"
bind:value={formData.email}
@@ -155,7 +155,7 @@
Subject *
</label>
<input
class="input preset-outlined-surface-200-800"
class="input preset-outlined-surface-500"
type="text"
id="subject"
bind:value={formData.subject}
@@ -170,7 +170,7 @@
Message *
</label>
<textarea
class="textarea preset-outlined-surface-200-800"
class="textarea preset-outlined-surface-500"
id="message"
bind:value={formData.message}
placeholder="Tell us more about your question or feedback..."
@@ -184,12 +184,13 @@
type="submit"
class="btn preset-filled-primary-500 w-full flex items-center justify-center gap-2"
disabled={loading}
aria-label={loading ? 'Sending message...' : 'Send message'}
>
{#if loading}
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-white" aria-hidden="true"></div>
Sending...
{:else}
<Send class="size-4" />
<Send class="size-4" aria-hidden="true" />
Send Message
{/if}
</button>

View File

@@ -121,9 +121,9 @@
{#each plan.features as feature}
<li class="flex items-center gap-3">
{#if feature.included}
<Check class="size-5 text-success-500 flex-shrink-0" />
<Check class="size-5 text-success-500 flex-shrink-0" aria-hidden="true" />
{:else}
<X class="size-5 text-error-500 flex-shrink-0" />
<X class="size-5 text-error-500 flex-shrink-0" aria-hidden="true" />
{/if}
<span class="{feature.included ? '' : 'opacity-50'}">{feature.name}</span>
</li>
@@ -131,7 +131,10 @@
</ul>
<!-- CTA Button -->
<button class="btn w-full {plan.buttonClass}">
<button
class="btn w-full {plan.buttonClass}"
aria-label="{plan.buttonText} - {plan.name} plan"
>
{plan.buttonText}
</button>
</div>

View File

@@ -74,21 +74,30 @@
<button
onclick={() => window.history.back()}
class="btn preset-outlined-surface-200-800 flex items-center gap-2"
aria-label="Go back to previous page"
title="Go back to previous page"
>
<ArrowLeft class="size-4" />
<ArrowLeft class="size-4" aria-hidden="true" />
<span>Go Back</span>
</button>
<button
onclick={() => window.location.reload()}
class="btn preset-outlined-surface-200-800 flex items-center gap-2"
aria-label="Reload current page"
title="Reload current page"
>
<RefreshCw class="size-4" />
<RefreshCw class="size-4" aria-hidden="true" />
<span>Try Again</span>
</button>
<a href="/" class="btn preset-filled-primary-500 flex items-center gap-2">
<Home class="size-4" />
<a
href="/"
class="btn preset-filled-primary-500 flex items-center gap-2"
aria-label="Return to homepage"
title="Return to homepage"
>
<Home class="size-4" aria-hidden="true" />
<span>Go Home</span>
</a>
</div>

View File

@@ -2,14 +2,12 @@
import { invalidate } from '$app/navigation';
import { page } from '$app/state';
import { toaster } from '$lib';
import ThemeSwitch from '$lib/components/ThemeSwitch.svelte';
import { Avatar, Modal, Toaster } from '@skeletonlabs/skeleton-svelte';
import { BookOpen, DollarSign, Home, LayoutDashboard, LogOut, User } from '@lucide/svelte';
import { Modal, Toaster } from '@skeletonlabs/skeleton-svelte';
import 'prism-themes/themes/prism-vsc-dark-plus.css';
import { onMount } from 'svelte';
import { MetaTags, deepMerge } from 'svelte-meta-tags';
import '../app.css';
import SvelteyLogoLetter from '$lib/assets/Sveltey-logo-letter.svelte';
import Header from '$lib/components/Header.svelte';
let { data, children } = $props();
let { session, supabase } = $derived(data);
@@ -66,185 +64,7 @@
<Toaster {toaster}></Toaster>
<Modal />
<!-- Navigation Header -->
<header class="bg-surface-50-950-token border-surface-200-700-token border-b sticky top-0 z-50 backdrop-blur-2xl">
<nav class="container mx-auto px-6 py-4">
<div class="flex items-center justify-between">
<!-- Left side - Brand and Main navigation -->
<div class="flex items-center space-x-8">
<!-- Brand -->
<a href="/" class="flex items-center gap-2 transition-opacity hover:opacity-75">
<SvelteyLogoLetter size="size-12" />
<span class="text-xl font-bold">Sveltey</span>
</a>
<!-- Main Navigation -->
<div class="hidden items-center space-x-1 md:flex">
{#if isActivePath('/')}
<span class={getNavLinkClasses('/')} aria-current="page">
<Home class="size-4" />
<span>Home</span>
</span>
{:else}
<a href="/" class={getNavLinkClasses('/')}>
<Home class="size-4" />
<span>Home</span>
</a>
{/if}
{#if isActivePath('/pricing')}
<span class={getNavLinkClasses('/pricing')} aria-current="page">
<DollarSign class="size-4" />
<span>Pricing</span>
</span>
{:else}
<a href="/pricing" class={getNavLinkClasses('/pricing')}>
<DollarSign class="size-4" />
<span>Pricing</span>
</a>
{/if}
{#if isActivePath('/blog')}
<span class={getNavLinkClasses('/blog')} aria-current="page">
<BookOpen class="size-4" />
<span>Blog</span>
</span>
{:else}
<a href="/blog" class={getNavLinkClasses('/blog')}>
<BookOpen class="size-4" />
<span>Blog</span>
</a>
{/if}
{#if session}
{#if isActivePath('/app/dashboard')}
<span class={getNavLinkClasses('/app/dashboard')} aria-current="page">
<LayoutDashboard class="size-4" />
<span>Dashboard</span>
</span>
{:else}
<a href="/app/dashboard" class={getNavLinkClasses('/app/dashboard')}>
<LayoutDashboard class="size-4" />
<span>Dashboard</span>
</a>
{/if}
{/if}
</div>
</div>
<!-- Right side - User actions and theme switcher -->
<div class="flex items-center space-x-3">
<ThemeSwitch />
{#if session}
<!-- User Profile Section -->
<div class="border-surface-300-600-token flex items-center gap-3 border-l pl-3">
<Avatar
size="size-8"
src={session.user.user_metadata.avatar_url}
name={session.user.user_metadata.full_name || session.user.email}
/>
<div class="hidden md:block">
<p class="text-sm font-medium">
{session.user.user_metadata.full_name || session.user.email?.split('@')[0]}
</p>
<p class="text-xs opacity-75">{session.user.email}</p>
</div>
<form action="/auth/logout" method="POST" style="display: inline;">
<button
type="submit"
class="btn preset-outlined-surface-200-800 btn-sm flex items-center gap-2"
title="Sign Out"
>
<LogOut class="size-4" />
<span class="hidden sm:inline">Sign Out</span>
</button>
</form>
</div>
{:else}
<!-- Authentication Buttons -->
<div class="flex items-center gap-2">
{#if isActivePath('/auth')}
<span
class="btn preset-filled-primary-500 flex h-8 cursor-default items-center gap-2"
aria-current="page"
>
<User class="size-4" />
<span>Sign In</span>
</span>
{:else}
<a href="/auth" class="btn preset-outlined-surface-500 flex h-8 items-center gap-2">
<User class="size-4" />
<span>Sign In</span>
</a>
{/if}
<a
href="/auth?mode=signup"
class="btn preset-filled-primary-500 flex items-center gap-2"
>
<span>Get Started</span>
</a>
</div>
{/if}
</div>
</div>
<!-- Mobile Navigation Menu (Hidden by default, would need JS to toggle) -->
<div class="border-surface-300-600-token mt-4 border-t pt-4 md:hidden">
<div class="flex flex-col space-y-2">
{#if isActivePath('/')}
<span class={getMobileNavLinkClasses('/')} aria-current="page">
<Home class="size-4" />
<span>Home</span>
</span>
{:else}
<a href="/" class={getMobileNavLinkClasses('/')}>
<Home class="size-4" />
<span>Home</span>
</a>
{/if}
{#if isActivePath('/pricing')}
<span class={getMobileNavLinkClasses('/pricing')} aria-current="page">
<DollarSign class="size-4" />
<span>Pricing</span>
</span>
{:else}
<a href="/pricing" class={getMobileNavLinkClasses('/pricing')}>
<DollarSign class="size-4" />
<span>Pricing</span>
</a>
{/if}
{#if isActivePath('/blog')}
<span class={getMobileNavLinkClasses('/blog')} aria-current="page">
<BookOpen class="size-4" />
<span>Blog</span>
</span>
{:else}
<a href="/blog" class={getMobileNavLinkClasses('/blog')}>
<BookOpen class="size-4" />
<span>Blog</span>
</a>
{/if}
{#if session}
{#if isActivePath('/app/dashboard')}
<span class={getMobileNavLinkClasses('/app/dashboard')} aria-current="page">
<LayoutDashboard class="size-4" />
<span>Dashboard</span>
</span>
{:else}
<a href="/app/dashboard" class={getMobileNavLinkClasses('/app/dashboard')}>
<LayoutDashboard class="size-4" />
<span>Dashboard</span>
</a>
{/if}
{/if}
</div>
</div>
</nav>
</header>
<Header {data} />
<!-- Main Content -->
<main class="min-h-screen">

View File

@@ -16,16 +16,28 @@
</p>
<div class="flex flex-col sm:flex-row gap-4">
{#if data.session}
<a href="/app/dashboard" class="btn btn-lg preset-filled-primary-500">
<Rocket class="size-5" />
<a
href="/app/dashboard"
class="btn btn-lg preset-filled-primary-500"
aria-label="Go to your dashboard"
>
<Rocket class="size-5" aria-hidden="true" />
<span>Go to Dashboard</span>
</a>
{:else}
<a href="/auth/signup" class="btn btn-lg preset-filled-primary-500">
<Star class="size-5" />
<a
href="/auth/signup"
class="btn btn-lg preset-filled-primary-500"
aria-label="Sign up for free account"
>
<Star class="size-5" aria-hidden="true" />
<span>Get Started Free</span>
</a>
<a href="/auth/login" class="btn btn-lg preset-outlined-primary-500">
<a
href="/auth/login"
class="btn btn-lg preset-outlined-primary-500"
aria-label="Sign in to existing account"
>
<span>Sign In</span>
</a>
{/if}
@@ -33,8 +45,8 @@
</div>
<!-- Hero Image/Graphic -->
<div class="order-1 lg:order-2 flex justify-center">
<div class="size-64 lg:size-96 bg-gradient-to-br from-primary-500/20 to-secondary-500/20 rounded-full flex items-center justify-center shadow-2xl animate-tilt">
<Rocket class="size-32 lg:size-64 text-primary-500" />
<div class="size-64 lg:size-96 bg-gradient-to-br from-primary-500/20 to-secondary-500/20 rounded-full flex items-center justify-center shadow-2xl animate-tilt" role="img" aria-label="SaaS launch illustration">
<Rocket class="size-32 lg:size-64 text-primary-500" aria-hidden="true" />
</div>
</div>
</header>
@@ -68,7 +80,7 @@
<!-- Features Section -->
<section class="grid grid-cols-1 md:grid-cols-3 gap-4 lg:gap-8">
<div class="card preset-outlined-surface-200-800 p-4 md:p-8 space-y-4">
<Database class="stroke-primary-500 size-10" />
<Database class="stroke-primary-500 size-10" aria-hidden="true" />
<h3 class="h3">Supabase Ready</h3>
<p class="opacity-75">
Authentication, real-time database, and file storage configured out of the box.
@@ -76,7 +88,7 @@
</p>
</div>
<div class="card preset-outlined-surface-200-800 p-4 md:p-8 space-y-4">
<Zap class="stroke-primary-500 size-10" />
<Zap class="stroke-primary-500 size-10" aria-hidden="true" />
<h3 class="h3">Lightning Fast</h3>
<p class="opacity-75">
Built with SvelteKit for maximum performance. Server-side rendering,
@@ -84,7 +96,7 @@
</p>
</div>
<div class="card preset-outlined-surface-200-800 p-4 md:p-8 space-y-4">
<GitBranch class="stroke-primary-500 size-10" />
<GitBranch class="stroke-primary-500 size-10" aria-hidden="true" />
<h3 class="h3">Developer Experience</h3>
<p class="opacity-75">
TypeScript, ESLint, Prettier, and comprehensive documentation.
@@ -135,13 +147,21 @@
<p class="opacity-75">Join thousands of developers who trust our template to kickstart their SaaS projects.</p>
</div>
{#if !data.session}
<a href="/auth/signup" class="btn md:btn-lg preset-filled-primary-500">
<Users class="size-5" />
<a
href="/auth/signup"
class="btn md:btn-lg preset-filled-primary-500"
aria-label="Start building your SaaS today"
>
<Users class="size-5" aria-hidden="true" />
<span>Start Building Today</span>
</a>
{:else}
<a href="/pricing" class="btn md:btn-lg preset-filled-secondary-500">
<Star class="size-5" />
<a
href="/pricing"
class="btn md:btn-lg preset-filled-secondary-500"
aria-label="View pricing plans"
>
<Star class="size-5" aria-hidden="true" />
<span>View Pricing</span>
</a>
{/if}