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_URL="YOUR_SUPABASE_URL"
PUBLIC_SUPABASE_ANON_KEY="YOUR_SUPABASE_ANON_KEY" 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-abbr": "^1.4.2",
"remark-toc": "^9.0.0", "remark-toc": "^9.0.0",
"remark-unwrap-images": "^4.0.1", "remark-unwrap-images": "^4.0.1",
"resend": "^4.5.1",
"shiki": "^3.4.2", "shiki": "^3.4.2",
"svelte": "^5.25.0", "svelte": "^5.25.0",
"svelte-check": "^4.0.0", "svelte-check": "^4.0.0",
@@ -876,6 +877,25 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/@rollup/plugin-inject": {
"version": "5.0.5", "version": "5.0.5",
"resolved": "https://registry.npmjs.org/@rollup/plugin-inject/-/plugin-inject-5.0.5.tgz", "resolved": "https://registry.npmjs.org/@rollup/plugin-inject/-/plugin-inject-5.0.5.tgz",
@@ -1202,6 +1222,20 @@
"win32" "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": { "node_modules/@shikijs/core": {
"version": "3.4.2", "version": "3.4.2",
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.4.2.tgz", "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.4.2.tgz",
@@ -3548,6 +3582,21 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/domain-browser": {
"version": "4.22.0", "version": "4.22.0",
"resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-4.22.0.tgz", "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-4.22.0.tgz",
@@ -3561,6 +3610,50 @@
"url": "https://bevry.me/fund" "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": { "node_modules/dunder-proto": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -3620,6 +3713,19 @@
"node": ">=10.13.0" "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": { "node_modules/es-define-property": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "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" "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": { "node_modules/html-void-elements": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", "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" "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": { "node_modules/https-browserify": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz",
@@ -4774,6 +4917,16 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/levn": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -5792,6 +5945,20 @@
"node": ">= 0.10" "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": { "node_modules/path-browserify": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
@@ -5843,6 +6010,16 @@
"node": ">=0.12" "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": { "node_modules/picocolors": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -6304,6 +6481,48 @@
"safe-buffer": "^5.1.0" "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": { "node_modules/readable-stream": {
"version": "3.6.2", "version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
@@ -6495,6 +6714,19 @@
"url": "https://opencollective.com/unified" "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": { "node_modules/resolve": {
"version": "1.22.10", "version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
@@ -6664,6 +6896,14 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/schema-dts": {
"version": "1.1.5", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/schema-dts/-/schema-dts-1.1.5.tgz", "resolved": "https://registry.npmjs.org/schema-dts/-/schema-dts-1.1.5.tgz",
@@ -6671,6 +6911,19 @@
"dev": true, "dev": true,
"license": "Apache-2.0" "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": { "node_modules/semver": {
"version": "7.7.2", "version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",

View File

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

View File

@@ -37,3 +37,8 @@
div.prose a { div.prose a {
@apply anchor @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"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import { Palette } from '@lucide/svelte'; import { Check, ChevronDown, Moon, Palette, Sun } from '@lucide/svelte';
// Available color themes // Available color themes
const colorThemes = [ const colorThemes = [
@@ -29,7 +29,7 @@
{ value: 'wintry', label: 'Wintry' } { value: 'wintry', label: 'Wintry' }
]; ];
let currentColorTheme = $state('skeleton'); let currentColorTheme = $state('legacy');
let isDarkMode = $state(true); let isDarkMode = $state(true);
let mounted = $state(false); let mounted = $state(false);
let showDropdown = $state(false); let showDropdown = $state(false);
@@ -98,71 +98,34 @@
} }
} }
}); });
// 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> </script>
<div class="flex items-center space-x-3">
<!-- Light/Dark Mode Toggle --> <!-- Light/Dark Mode Toggle -->
<button <button
onclick={toggleDarkMode} onclick={toggleDarkMode}
class="btn btn-sm preset-outlined-surface-500 flex items-center h-8" class="btn btn-sm items-center"
title="Toggle light/dark mode" title="Toggle light/dark mode"
aria-label="Toggle light/dark mode" aria-label="Toggle light/dark mode"
> >
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{#if !isDarkMode} {#if !isDarkMode}
<!-- Sun icon for dark mode (click to go light) --> <Sun class="size-4" aria-hidden="true" />
<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} {:else}
<!-- Moon icon for light mode (click to go dark) --> <Moon class="size-4" aria-hidden="true" />
<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} {/if}
</svg>
</button> </button>
<!-- Color Theme Selector --> <!-- Color Theme Selector -->
<div class="theme-dropdown relative"> <div class="relative">
<button <button
onclick={() => (showDropdown = !showDropdown)} onclick={() => (showDropdown = !showDropdown)}
class="btn btn-sm preset-outlined-surface-500 h-8 flex items-center" class="btn btn-sm flex h-8 items-center"
title="Select color theme" title="Select color theme"
aria-label="Select color theme" aria-label="Select color theme"
aria-expanded={showDropdown} aria-expanded={showDropdown}
> >
<Palette class="size-4" /> <Palette class="size-4" aria-hidden="true" />
<span class="text-sm capitalize">{currentColorTheme}</span> <span class="capitalize">{currentColorTheme}</span>
<svg <ChevronDown class={`size-4 transition-transform ${showDropdown === true ? 'rotate-180' : ''}`} aria-hidden="true" />
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> </button>
{#if showDropdown} {#if showDropdown}
@@ -172,21 +135,15 @@
{#each colorThemes as theme} {#each colorThemes as theme}
<button <button
onclick={() => selectColorTheme(theme.value)} 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" 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> <span>{theme.label}</span>
{#if currentColorTheme === theme.value} {#if currentColorTheme === theme.value}
<svg class="h-4 w-4" fill="currentColor" viewBox="0 0 20 20"> <Check class="size-4" aria-hidden="true" />
<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} {/if}
</button> </button>
{/each} {/each}
</div> </div>
{/if} {/if}
</div> </div>
</div>

View File

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

View File

@@ -369,6 +369,8 @@
type="button" type="button"
class="absolute right-3 top-1/2 -translate-y-1/2 text-surface-500" class="absolute right-3 top-1/2 -translate-y-1/2 text-surface-500"
disabled={true} disabled={true}
aria-label={showPassword ? 'Hide password' : 'Show password'}
title={showPassword ? 'Hide password' : 'Show password'}
> >
{#if showPassword} {#if showPassword}
<EyeOff class="size-4" /> <EyeOff class="size-4" />
@@ -432,12 +434,14 @@
class="btn w-full flex items-center justify-center gap-3 {provider.color}" class="btn w-full flex items-center justify-center gap-3 {provider.color}"
onclick={() => handleOAuth(provider.provider)} onclick={() => handleOAuth(provider.provider)}
disabled={!provider.enabled || loading || oauthLoading !== ''} 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} {#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... Connecting...
{:else} {:else}
<provider.icon class="size-4" /> <provider.icon class="size-4" aria-hidden="true" />
{provider.description} {provider.description}
{/if} {/if}
</button> </button>

View File

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

View File

@@ -38,12 +38,8 @@
</div> </div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8"> <div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
{#each featuredPosts as post} {#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"> <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"> <div class="flex items-center gap-1 text-sm opacity-75">
<Calendar class="size-4" /> <Calendar class="size-4" />
{formatDate(post.publishedAt)} {formatDate(post.publishedAt)}
@@ -87,7 +83,7 @@
<h2 class="h2 text-center">Recent Posts</h2> <h2 class="h2 text-center">Recent Posts</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{#each regularPosts as post} {#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"> <div class="flex items-center gap-1 text-sm opacity-75">
<Calendar class="size-4" /> <Calendar class="size-4" />
{formatDate(post.publishedAt)} {formatDate(post.publishedAt)}

View File

@@ -72,8 +72,9 @@
rel="noopener noreferrer" rel="noopener noreferrer"
class="btn btn-sm preset-outlined-secondary-500" class="btn btn-sm preset-outlined-secondary-500"
title="Share on Twitter" title="Share on Twitter"
aria-label="Share this article on Twitter"
> >
<Twitter class="size-4" /> <Twitter class="size-4" aria-hidden="true" />
</a> </a>
<a <a
href="https://www.linkedin.com/sharing/share-offsite/?url={encodeURIComponent(typeof window !== 'undefined' ? window.location.href : '')}" href="https://www.linkedin.com/sharing/share-offsite/?url={encodeURIComponent(typeof window !== 'undefined' ? window.location.href : '')}"
@@ -81,8 +82,9 @@
rel="noopener noreferrer" rel="noopener noreferrer"
class="btn btn-sm preset-outlined-secondary-500" class="btn btn-sm preset-outlined-secondary-500"
title="Share on LinkedIn" title="Share on LinkedIn"
aria-label="Share this article on LinkedIn"
> >
<Linkedin class="size-4" /> <Linkedin class="size-4" aria-hidden="true" />
</a> </a>
</div> </div>
</div> </div>
@@ -129,8 +131,9 @@
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="btn btn-sm preset-outlined-surface-200-800" 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> <span>Twitter</span>
</a> </a>
<a <a
@@ -138,8 +141,9 @@
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="btn btn-sm preset-outlined-surface-200-800" 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> <span>LinkedIn</span>
</a> </a>
</div> </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> <h1 class="h1">Get in <span class="text-primary-500">Touch</span></h1>
</div> </div>
<p class="text-xl opacity-75 max-w-2xl mx-auto"> <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> </p>
</header> </header>
@@ -61,7 +61,7 @@
<div class="space-y-8"> <div class="space-y-8">
<!-- Contact Methods --> <!-- Contact Methods -->
<div class="space-y-6"> <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="flex items-center gap-3">
<div class="w-10 h-10 bg-primary-500 rounded-lg flex items-center justify-center"> <div class="w-10 h-10 bg-primary-500 rounded-lg flex items-center justify-center">
<Mail class="size-5 text-white" /> <Mail class="size-5 text-white" />
@@ -73,7 +73,7 @@
</div> </div>
</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="flex items-center gap-3">
<div class="w-10 h-10 bg-secondary-500 rounded-lg flex items-center justify-center"> <div class="w-10 h-10 bg-secondary-500 rounded-lg flex items-center justify-center">
<Phone class="size-5 text-white" /> <Phone class="size-5 text-white" />
@@ -85,7 +85,7 @@
</div> </div>
</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="flex items-center gap-3">
<div class="w-10 h-10 bg-tertiary-500 rounded-lg flex items-center justify-center"> <div class="w-10 h-10 bg-tertiary-500 rounded-lg flex items-center justify-center">
<MapPin class="size-5 text-white" /> <MapPin class="size-5 text-white" />
@@ -99,7 +99,7 @@
</div> </div>
<!-- Response Time --> <!-- 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" /> <Clock class="size-8 mx-auto mb-3" />
<h3 class="h4 mb-2">Quick Response</h3> <h3 class="h4 mb-2">Quick Response</h3>
<p class="text-sm opacity-90"> <p class="text-sm opacity-90">
@@ -110,7 +110,7 @@
<!-- Contact Form --> <!-- Contact Form -->
<div class="lg:col-span-2"> <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 class="space-y-6">
<div> <div>
<h2 class="h3 mb-2">Send us a message</h2> <h2 class="h3 mb-2">Send us a message</h2>
@@ -124,7 +124,7 @@
Name * Name *
</label> </label>
<input <input
class="input preset-outlined-surface-200-800" class="input preset-outlined-surface-500"
type="text" type="text"
id="name" id="name"
bind:value={formData.name} bind:value={formData.name}
@@ -139,7 +139,7 @@
Email * Email *
</label> </label>
<input <input
class="input preset-outlined-surface-200-800" class="input preset-outlined-surface-500"
type="email" type="email"
id="email" id="email"
bind:value={formData.email} bind:value={formData.email}
@@ -155,7 +155,7 @@
Subject * Subject *
</label> </label>
<input <input
class="input preset-outlined-surface-200-800" class="input preset-outlined-surface-500"
type="text" type="text"
id="subject" id="subject"
bind:value={formData.subject} bind:value={formData.subject}
@@ -170,7 +170,7 @@
Message * Message *
</label> </label>
<textarea <textarea
class="textarea preset-outlined-surface-200-800" class="textarea preset-outlined-surface-500"
id="message" id="message"
bind:value={formData.message} bind:value={formData.message}
placeholder="Tell us more about your question or feedback..." placeholder="Tell us more about your question or feedback..."
@@ -184,12 +184,13 @@
type="submit" type="submit"
class="btn preset-filled-primary-500 w-full flex items-center justify-center gap-2" class="btn preset-filled-primary-500 w-full flex items-center justify-center gap-2"
disabled={loading} disabled={loading}
aria-label={loading ? 'Sending message...' : 'Send message'}
> >
{#if loading} {#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... Sending...
{:else} {:else}
<Send class="size-4" /> <Send class="size-4" aria-hidden="true" />
Send Message Send Message
{/if} {/if}
</button> </button>

View File

@@ -121,9 +121,9 @@
{#each plan.features as feature} {#each plan.features as feature}
<li class="flex items-center gap-3"> <li class="flex items-center gap-3">
{#if feature.included} {#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} {: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} {/if}
<span class="{feature.included ? '' : 'opacity-50'}">{feature.name}</span> <span class="{feature.included ? '' : 'opacity-50'}">{feature.name}</span>
</li> </li>
@@ -131,7 +131,10 @@
</ul> </ul>
<!-- CTA Button --> <!-- 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} {plan.buttonText}
</button> </button>
</div> </div>

View File

@@ -74,21 +74,30 @@
<button <button
onclick={() => window.history.back()} onclick={() => window.history.back()}
class="btn preset-outlined-surface-200-800 flex items-center gap-2" 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> <span>Go Back</span>
</button> </button>
<button <button
onclick={() => window.location.reload()} onclick={() => window.location.reload()}
class="btn preset-outlined-surface-200-800 flex items-center gap-2" 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> <span>Try Again</span>
</button> </button>
<a href="/" class="btn preset-filled-primary-500 flex items-center gap-2"> <a
<Home class="size-4" /> 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> <span>Go Home</span>
</a> </a>
</div> </div>

View File

@@ -2,14 +2,12 @@
import { invalidate } from '$app/navigation'; import { invalidate } from '$app/navigation';
import { page } from '$app/state'; import { page } from '$app/state';
import { toaster } from '$lib'; import { toaster } from '$lib';
import ThemeSwitch from '$lib/components/ThemeSwitch.svelte'; import { Modal, Toaster } from '@skeletonlabs/skeleton-svelte';
import { Avatar, Modal, Toaster } from '@skeletonlabs/skeleton-svelte';
import { BookOpen, DollarSign, Home, LayoutDashboard, LogOut, User } from '@lucide/svelte';
import 'prism-themes/themes/prism-vsc-dark-plus.css'; import 'prism-themes/themes/prism-vsc-dark-plus.css';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { MetaTags, deepMerge } from 'svelte-meta-tags'; import { MetaTags, deepMerge } from 'svelte-meta-tags';
import '../app.css'; import '../app.css';
import SvelteyLogoLetter from '$lib/assets/Sveltey-logo-letter.svelte'; import Header from '$lib/components/Header.svelte';
let { data, children } = $props(); let { data, children } = $props();
let { session, supabase } = $derived(data); let { session, supabase } = $derived(data);
@@ -66,185 +64,7 @@
<Toaster {toaster}></Toaster> <Toaster {toaster}></Toaster>
<Modal /> <Modal />
<!-- Navigation Header --> <Header {data} />
<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>
<!-- Main Content --> <!-- Main Content -->
<main class="min-h-screen"> <main class="min-h-screen">

View File

@@ -16,16 +16,28 @@
</p> </p>
<div class="flex flex-col sm:flex-row gap-4"> <div class="flex flex-col sm:flex-row gap-4">
{#if data.session} {#if data.session}
<a href="/app/dashboard" class="btn btn-lg preset-filled-primary-500"> <a
<Rocket class="size-5" /> 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> <span>Go to Dashboard</span>
</a> </a>
{:else} {:else}
<a href="/auth/signup" class="btn btn-lg preset-filled-primary-500"> <a
<Star class="size-5" /> 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> <span>Get Started Free</span>
</a> </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> <span>Sign In</span>
</a> </a>
{/if} {/if}
@@ -33,8 +45,8 @@
</div> </div>
<!-- Hero Image/Graphic --> <!-- Hero Image/Graphic -->
<div class="order-1 lg:order-2 flex justify-center"> <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"> <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" /> <Rocket class="size-32 lg:size-64 text-primary-500" aria-hidden="true" />
</div> </div>
</div> </div>
</header> </header>
@@ -68,7 +80,7 @@
<!-- Features Section --> <!-- Features Section -->
<section class="grid grid-cols-1 md:grid-cols-3 gap-4 lg:gap-8"> <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"> <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> <h3 class="h3">Supabase Ready</h3>
<p class="opacity-75"> <p class="opacity-75">
Authentication, real-time database, and file storage configured out of the box. Authentication, real-time database, and file storage configured out of the box.
@@ -76,7 +88,7 @@
</p> </p>
</div> </div>
<div class="card preset-outlined-surface-200-800 p-4 md:p-8 space-y-4"> <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> <h3 class="h3">Lightning Fast</h3>
<p class="opacity-75"> <p class="opacity-75">
Built with SvelteKit for maximum performance. Server-side rendering, Built with SvelteKit for maximum performance. Server-side rendering,
@@ -84,7 +96,7 @@
</p> </p>
</div> </div>
<div class="card preset-outlined-surface-200-800 p-4 md:p-8 space-y-4"> <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> <h3 class="h3">Developer Experience</h3>
<p class="opacity-75"> <p class="opacity-75">
TypeScript, ESLint, Prettier, and comprehensive documentation. 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> <p class="opacity-75">Join thousands of developers who trust our template to kickstart their SaaS projects.</p>
</div> </div>
{#if !data.session} {#if !data.session}
<a href="/auth/signup" class="btn md:btn-lg preset-filled-primary-500"> <a
<Users class="size-5" /> 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> <span>Start Building Today</span>
</a> </a>
{:else} {:else}
<a href="/pricing" class="btn md:btn-lg preset-filled-secondary-500"> <a
<Star class="size-5" /> 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> <span>View Pricing</span>
</a> </a>
{/if} {/if}