diff --git a/.env.example b/.env.example
index be36636..b71aa16 100644
--- a/.env.example
+++ b/.env.example
@@ -1,2 +1,3 @@
PUBLIC_SUPABASE_URL="YOUR_SUPABASE_URL"
PUBLIC_SUPABASE_ANON_KEY="YOUR_SUPABASE_ANON_KEY"
+RESEND_API_KEY="YOUR_RESEND_KEY"
diff --git a/package-lock.json b/package-lock.json
index 92fa434..f576836 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index 74d92b5..3b9f7f8 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/app.css b/src/app.css
index 382662f..78311b6 100644
--- a/src/app.css
+++ b/src/app.css
@@ -36,4 +36,9 @@
div.prose a {
@apply anchor
+}
+
+a.disabled {
+ @apply pointer-events-none opacity-50;
+
}
\ No newline at end of file
diff --git a/src/lib/components/Header.svelte b/src/lib/components/Header.svelte
new file mode 100644
index 0000000..8027b44
--- /dev/null
+++ b/src/lib/components/Header.svelte
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {#if session}
+
+
+
+
+
+ {session.user.user_metadata.full_name || session.user.email?.split('@')[0]}
+
+
{session.user.email}
+
+
+
+ {:else}
+
+
+ {/if}
+
+
+
+
diff --git a/src/lib/components/ThemeSwitch.svelte b/src/lib/components/ThemeSwitch.svelte
index 091f854..441084a 100644
--- a/src/lib/components/ThemeSwitch.svelte
+++ b/src/lib/components/ThemeSwitch.svelte
@@ -1,7 +1,7 @@
-
-
+
+
+ {#if !isDarkMode}
+
+ {:else}
+
+ {/if}
+
+
+
+
(showDropdown = !showDropdown)}
+ class="btn btn-sm flex h-8 items-center"
+ title="Select color theme"
+ aria-label="Select color theme"
+ aria-expanded={showDropdown}
>
-
- {#if !isDarkMode}
-
-
- {:else}
-
-
- {/if}
-
+
+ {currentColorTheme}
+
-
-
-
(showDropdown = !showDropdown)}
- class="btn btn-sm preset-outlined-surface-500 h-8 flex items-center"
- title="Select color theme"
- aria-label="Select color theme"
- aria-expanded={showDropdown}
+ {#if showDropdown}
+
-
-
{currentColorTheme}
-
-
-
-
-
- {#if showDropdown}
-
- {#each colorThemes as theme}
-
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"
- >
- {theme.label}
- {#if currentColorTheme === theme.value}
-
-
-
- {/if}
-
- {/each}
-
- {/if}
-
+ {#each colorThemes as theme}
+ selectColorTheme(theme.value)}
+ class="btn btn-sm flex w-full items-center justify-between px-4 py-2 transition-colors"
+ aria-label="Select {theme.label} theme"
+ >
+ {theme.label}
+ {#if currentColorTheme === theme.value}
+
+ {/if}
+
+ {/each}
+
+ {/if}
diff --git a/src/routes/(app)/app/dashboard/+page.svelte b/src/routes/(app)/app/dashboard/+page.svelte
index 2c0bd8d..7373af8 100644
--- a/src/routes/(app)/app/dashboard/+page.svelte
+++ b/src/routes/(app)/app/dashboard/+page.svelte
@@ -99,8 +99,10 @@
{#each dashboardStats as stat}
-
+
{stat.value}
{stat.label}
@@ -122,16 +124,26 @@
>
{action.title}
{action.description}
{#if action.available}
-
+
{action.action}
{:else}
-
+
{action.action} (Coming Soon)
{/if}
@@ -146,7 +158,7 @@
-
+
Current Plan: Starter
@@ -154,12 +166,21 @@
limits, and priority support.
@@ -170,17 +191,27 @@
Recent Activity
-
+
No recent activity
Start using our services to see your activity here. Create your first project or make an
API call to get started.
-
+
Create Project (Coming Soon)
-
+
View Documentation (Coming Soon)
@@ -189,17 +220,25 @@
{:else if session === null}
-
+
Access Denied
You need to be logged in to access your dashboard. Please sign in to continue.
@@ -208,8 +247,13 @@
-
+
Loading your dashboard...
Please wait while we prepare your personalized experience.
diff --git a/src/routes/(app)/auth/+page.svelte b/src/routes/(app)/auth/+page.svelte
index 80e8f7d..7833610 100644
--- a/src/routes/(app)/auth/+page.svelte
+++ b/src/routes/(app)/auth/+page.svelte
@@ -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}
@@ -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}
-
+
Connecting...
{:else}
-
+
{provider.description}
{/if}
diff --git a/src/routes/(app)/auth/update-password/+page.svelte b/src/routes/(app)/auth/update-password/+page.svelte
index c2309dd..886b628 100644
--- a/src/routes/(app)/auth/update-password/+page.svelte
+++ b/src/routes/(app)/auth/update-password/+page.svelte
@@ -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}
@@ -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}
diff --git a/src/routes/(marketing)/blog/+page.svelte b/src/routes/(marketing)/blog/+page.svelte
index f8eb1bc..1b2b7d9 100644
--- a/src/routes/(marketing)/blog/+page.svelte
+++ b/src/routes/(marketing)/blog/+page.svelte
@@ -38,12 +38,8 @@