mirror of
https://github.com/LukeHagar/Sveltey.git
synced 2025-12-06 04:21:38 +00:00
Theming, A11y, added resend, formatting
This commit is contained in:
@@ -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
253
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -37,3 +37,8 @@
|
||||
div.prose a {
|
||||
@apply anchor
|
||||
}
|
||||
|
||||
a.disabled {
|
||||
@apply pointer-events-none opacity-50;
|
||||
|
||||
}
|
||||
104
src/lib/components/Header.svelte
Normal file
104
src/lib/components/Header.svelte
Normal 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>
|
||||
@@ -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,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>
|
||||
|
||||
<div class="flex items-center space-x-3">
|
||||
<!-- Light/Dark Mode Toggle -->
|
||||
<button
|
||||
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"
|
||||
aria-label="Toggle light/dark mode"
|
||||
>
|
||||
<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"
|
||||
/>
|
||||
<Sun class="size-4" aria-hidden="true" />
|
||||
{: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"
|
||||
/>
|
||||
<Moon class="size-4" aria-hidden="true" />
|
||||
{/if}
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Color Theme Selector -->
|
||||
<div class="theme-dropdown relative">
|
||||
<div class="relative">
|
||||
<button
|
||||
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"
|
||||
aria-label="Select color theme"
|
||||
aria-expanded={showDropdown}
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
{#if showDropdown}
|
||||
@@ -172,21 +135,15 @@
|
||||
{#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"
|
||||
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}
|
||||
<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>
|
||||
<Check class="size-4" aria-hidden="true" />
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
26
src/routes/(marketing)/contact/+page.server.ts
Normal file
26
src/routes/(marketing)/contact/+page.server.ts
Normal 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' };
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user