Styling and breaking out the components, also readme updates

This commit is contained in:
Luke Hagar
2025-06-03 11:38:24 -05:00
parent 3a486d32b4
commit 05ee2b62b7
8 changed files with 222 additions and 135 deletions

View File

@@ -25,6 +25,10 @@ A modern, production-ready SaaS template built with [SvelteKit 2](https://kit.sv
- [🔧 Configuration](#-configuration) - [🔧 Configuration](#-configuration)
- [Environment Variables](#environment-variables) - [Environment Variables](#environment-variables)
- [Supabase Setup](#supabase-setup) - [Supabase Setup](#supabase-setup)
- [Email Setup (Resend)](#email-setup-resend)
- [Email Features](#email-features)
- [Example Usage](#example-usage)
- [Email Template example](#email-template-example)
- [Stripe Setup](#stripe-setup) - [Stripe Setup](#stripe-setup)
- [Analytics Setup (Plausible)](#analytics-setup-plausible) - [Analytics Setup (Plausible)](#analytics-setup-plausible)
- [Current Configuration](#current-configuration) - [Current Configuration](#current-configuration)
@@ -119,6 +123,7 @@ Visit `http://localhost:5173` and start building your SaaS!
- **UI Components**: Skeleton UI - **UI Components**: Skeleton UI
- **Styling**: Tailwind CSS - **Styling**: Tailwind CSS
- **Analytics**: Plausible Analytics - **Analytics**: Plausible Analytics
- **Email**: Resend
<!-- - **Payments**: Stripe --> <!-- - **Payments**: Stripe -->
- **Deployment**: Vercel/Netlify ready - **Deployment**: Vercel/Netlify ready
- **Language**: TypeScript - **Language**: TypeScript
@@ -159,6 +164,9 @@ Create a `.env` file in the root directory:
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 (for email)
RESEND_API_KEY=your_resend_api_key
# Stripe // coming soon # Stripe // coming soon
PUBLIC_STRIPE_PUBLISHABLE_KEY=your_stripe_publishable_key PUBLIC_STRIPE_PUBLISHABLE_KEY=your_stripe_publishable_key
STRIPE_SECRET_KEY=your_stripe_secret_key STRIPE_SECRET_KEY=your_stripe_secret_key
@@ -173,6 +181,79 @@ STRIPE_WEBHOOK_SECRET=your_webhook_secret
3. Run the included SQL migrations 3. Run the included SQL migrations
4. Set up your authentication providers 4. Set up your authentication providers
### Email Setup (Resend)
Sveltey uses [Resend](https://resend.com/) for reliable email delivery with excellent developer experience.
1. **Create a Resend account** at [resend.com](https://resend.com/)
2. **Get your API key** from the Resend dashboard
3. **Add to environment variables**:
```env
RESEND_API_KEY=re_your_api_key_here
```
4. **Verify your domain** (optional but recommended for production):
- Add your domain in the Resend dashboard
- Configure DNS records as instructed
- This removes the "via resend.com" branding and improves deliverability
#### Email Features
- **Transactional Emails**: Password resets, welcome emails, notifications
- **Template Support**: Beautiful HTML email templates
- **Delivery Tracking**: Monitor email delivery and engagement
- **High Deliverability**: Excellent inbox placement rates
- **Simple API**: Easy integration with SvelteKit API routes
#### Example Usage
```typescript
// src/routes/api/send-email/+server.ts
import { RESEND_API_KEY } from '$env/static/private';
import { Resend } from 'resend';
const resend = new Resend(RESEND_API_KEY);
export async function POST({ request }) {
const { to, subject, html } = await request.json();
try {
const data = await resend.emails.send({
from: 'noreply@yourdomain.com',
to,
subject,
html
});
return new Response(JSON.stringify({ success: true, data }), {
headers: { 'Content-Type': 'application/json' }
});
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
}
```
#### Email Template example
Create reusable email templates in `src/lib/emails/`:
```typescript
// src/lib/emails/welcome.ts
export const welcomeEmail = (userName: string) => `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h1 style="color: #333;">Welcome to Sveltey, ${userName}!</h1>
<p>Thank you for joining our platform. We're excited to have you on board.</p>
<a href="https://yourdomain.com/dashboard"
style="background: #007bff; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px;">
Get Started
</a>
</div>
`;
```
### Stripe Setup ### Stripe Setup
1. Create a Stripe account 1. Create a Stripe account

View File

@@ -0,0 +1,90 @@
<script>
</script>
<footer class="bg-surface-100-850-token border-surface-500 mt-20 border-t">
<div class="container mx-auto px-4 py-12 md:px-6">
<div class="grid grid-cols-1 gap-8 md:grid-cols-4">
<!-- Brand Column -->
<div class="space-y-4">
<div class="flex items-center gap-2">
<div class="bg-primary-500 flex h-8 w-8 items-center justify-center rounded-lg">
<span class="text-lg font-bold text-white">S</span>
</div>
<span class="text-xl font-bold">Sveltey</span>
</div>
<p class="text-sm opacity-75">
The complete SvelteKit & Supabase SaaS template. Launch your next project in minutes, not
months.
</p>
</div>
<!-- Product Links -->
<div class="space-y-4">
<h3 class="font-semibold">Product</h3>
<div class="space-y-2 text-sm">
<a href="/pricing" class="block opacity-75 transition-opacity hover:opacity-100"
>Pricing</a
>
<a href="/blog" class="block opacity-75 transition-opacity hover:opacity-100">Blog</a>
<a href="/dashboard" class="block opacity-75 transition-opacity hover:opacity-100"
>Dashboard</a
>
</div>
</div>
<!-- Support Links -->
<div class="space-y-4">
<h3 class="font-semibold">Support</h3>
<div class="space-y-2 text-sm">
<a
href="https://github.com/LukeHagar/sveltey"
class="block opacity-75 transition-opacity hover:opacity-100"
>
GitHub
</a>
<a href="/contact" class="block opacity-75 transition-opacity hover:opacity-100">
Contact
</a>
<a href="/help" class="block opacity-75 transition-opacity hover:opacity-100">
Help Center
</a>
</div>
</div>
<!-- Legal Links -->
<div class="space-y-4">
<h3 class="font-semibold">Legal</h3>
<div class="space-y-2 text-sm">
<a href="/privacy" class="block opacity-75 transition-opacity hover:opacity-100">
Privacy Policy
</a>
<a href="/terms" class="block opacity-75 transition-opacity hover:opacity-100">
Terms of Service
</a>
<a href="/cookies" class="block opacity-75 transition-opacity hover:opacity-100">
Cookie Policy
</a>
</div>
</div>
</div>
<!-- Footer Bottom -->
<div
class="border-surface-300-600-token mt-8 flex flex-col items-center justify-between border-t pt-8 md:flex-row"
>
<p class="text-sm opacity-50">© 2025 Sveltey. All rights reserved.</p>
<div class="mt-4 flex flex-wrap items-center justify-center gap-4 md:mt-0">
<span class="text-sm opacity-50">Built with</span>
<div class="flex items-center gap-2 text-sm opacity-75">
<a class="anchor" href="https://kit.svelte.dev/" target="_blank">SvelteKit</a>
<span></span>
<a class="anchor" href="https://supabase.com/" target="_blank">Supabase</a>
<span></span>
<a class="anchor" href="https://skeleton.dev/" target="_blank">Skeleton UI</a>
</div>
</div>
</div>
</div>
</footer>

View File

@@ -9,11 +9,6 @@
let { session } = $derived(data); 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 // Helper function to get navigation link classes
function getNavClasses(path: string): string { function getNavClasses(path: string): string {
return `btn btn-sm flex items-center gap-2 ${page.url.pathname === path ? 'cursor-default disabled' : ''}`; return `btn btn-sm flex items-center gap-2 ${page.url.pathname === path ? 'cursor-default disabled' : ''}`;
@@ -23,7 +18,7 @@
<header <header
class="bg-surface-50-950-token border-surface-200-700-token sticky top-0 z-50 border-b backdrop-blur-2xl" 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"> <nav class="container mx-auto px-2 py-2 md:py-4 md:px-6">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<!-- Left side - Brand and Main navigation --> <!-- Left side - Brand and Main navigation -->
<div class="flex items-center gap-8"> <div class="flex items-center gap-8">
@@ -95,7 +90,7 @@
</div> </div>
{:else} {:else}
<!-- Authentication Buttons --> <!-- Authentication Buttons -->
<div class="hidden md:flex items-center gap-1 md:gap-3 "> <div class="hidden items-center gap-1 md:flex md:gap-3">
<a href="/auth" class={getNavClasses('/auth')} aria-label="Sign in or register"> <a href="/auth" class={getNavClasses('/auth')} aria-label="Sign in or register">
<User class="size-4" aria-hidden="true" /> <User class="size-4" aria-hidden="true" />
<span class="">Sign In / Register</span> <span class="">Sign In / Register</span>
@@ -104,12 +99,12 @@
{/if} {/if}
</div> </div>
</div> </div>
<div class="block pt-4 md:hidden"> <div class="block pt-2 md:hidden">
<nav class="flex flex-wrap items-center justify-center gap-4"> <nav class="flex flex-wrap items-center justify-center gap-1">
<a href="/" class={getNavClasses('/')} aria-label="Go to homepage"> <a href="/" class={getNavClasses('/')} aria-label="Go to homepage">
<Home class="size-4" aria-hidden="true" /> <Home class="size-4" aria-hidden="true" />
Home Home
</a> </a>
<a href="/pricing" class={getNavClasses('/pricing')} aria-label="View pricing plans"> <a href="/pricing" class={getNavClasses('/pricing')} aria-label="View pricing plans">
<DollarSign class="size-4" aria-hidden="true" /> <DollarSign class="size-4" aria-hidden="true" />
Pricing Pricing
@@ -131,7 +126,7 @@
{:else} {:else}
<a href="/auth" class={getNavClasses('/auth')} aria-label="Sign in or register"> <a href="/auth" class={getNavClasses('/auth')} aria-label="Sign in or register">
<User class="size-4" aria-hidden="true" /> <User class="size-4" aria-hidden="true" />
Sign In / Register Sign In / Up
</a> </a>
{/if} {/if}
</div> </div>

View File

@@ -38,7 +38,7 @@
</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 hover:shadow-2xl 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 flex flex-col">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<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" />
@@ -52,7 +52,7 @@
</div> </div>
</h3> </h3>
<p class="opacity-75">{post.excerpt}</p> <p class="opacity-75 grow">{post.excerpt}</p>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
@@ -61,7 +61,7 @@
</div> </div>
<div class="flex flex-wrap gap-1"> <div class="flex flex-wrap gap-1">
{#each post.tags.slice(0, 2) as tag} {#each post.tags.slice(0, 2) as tag}
<span class="badge preset-outlined-surface-200-800 text-xs flex items-center gap-1"> <span class="badge preset-outlined-surface-500 text-xs flex items-center gap-1">
<Tag class="size-3" /> <Tag class="size-3" />
{tag} {tag}
</span> </span>
@@ -105,7 +105,7 @@
{#if post.tags.length > 0} {#if post.tags.length > 0}
<div class="flex flex-wrap gap-1"> <div class="flex flex-wrap gap-1">
{#each post.tags.slice(0, 3) as tag} {#each post.tags.slice(0, 3) as tag}
<span class="badge preset-outlined-surface-200-800 text-xs flex items-center gap-1"> <span class="badge preset-outlined-surface-500 text-xs flex items-center gap-1">
<Tag class="size-3" /> <Tag class="size-3" />
{tag} {tag}
</span> </span>
@@ -119,7 +119,7 @@
</section> </section>
{/if} {/if}
{:else} {:else}
<div class="card preset-outlined-surface-200-800 p-8 md:p-12 text-center max-w-2xl mx-auto space-y-4"> <div class="card preset-outlined-surface-500 p-8 md:p-12 text-center max-w-2xl mx-auto space-y-4">
<BookOpen class="size-16 mx-auto text-primary-500 opacity-50" /> <BookOpen class="size-16 mx-auto text-primary-500 opacity-50" />
<h2 class="h3">No blog posts yet</h2> <h2 class="h3">No blog posts yet</h2>
<p class="opacity-75"> <p class="opacity-75">

View File

@@ -188,7 +188,7 @@
Enterprise teams with specific requirements can get in touch for custom pricing and features. Enterprise teams with specific requirements can get in touch for custom pricing and features.
</p> </p>
</div> </div>
<a href="mailto:sales@example.com" class="btn btn-lg preset-filled-secondary-500 text-surface-50-950"> <a href="mailto:sales@example.com" class="btn btn-lg preset-filled-secondary-500">
<Mail class="size-5" /> <Mail class="size-5" />
<span>Contact Sales</span> <span>Contact Sales</span>
</a> </a>

View File

@@ -60,7 +60,7 @@
</div> </div>
<!-- Error Description --> <!-- Error Description -->
<div class="card preset-outlined-surface-200-800 p-8 space-y-4"> <div class="card preset-outlined-surface-500 p-8 space-y-4">
<p class="text-lg opacity-75">{errorInfo.description}</p> <p class="text-lg opacity-75">{errorInfo.description}</p>
{#if message} {#if message}
<div class="card preset-outlined-error-500 p-4"> <div class="card preset-outlined-error-500 p-4">
@@ -73,7 +73,7 @@
<div class="flex flex-col sm:flex-row gap-4 justify-center"> <div class="flex flex-col sm:flex-row gap-4 justify-center">
<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-500 flex items-center gap-2"
aria-label="Go back to previous page" aria-label="Go back to previous page"
title="Go back to previous page" title="Go back to previous page"
> >
@@ -83,7 +83,7 @@
<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-500 flex items-center gap-2"
aria-label="Reload current page" aria-label="Reload current page"
title="Reload current page" title="Reload current page"
> >
@@ -103,14 +103,14 @@
</div> </div>
<!-- Help Section --> <!-- Help Section -->
<div class="text-sm opacity-50 space-y-2"> <div class="text-sm space-y-2">
<p>Still having trouble? Here are some helpful links:</p> <p>Still having trouble? Here are some helpful links:</p>
<div class="flex items-center justify-center gap-4"> <div class="flex items-center justify-center gap-4">
<a href="/blog" class="hover:opacity-75 transition-opacity">Blog</a> <a href="/blog" class="anchor">Blog</a>
<span></span> <span></span>
<a href="/contact" class="hover:opacity-75 transition-opacity">Contact Support</a> <a href="/contact" class="anchor">Contact Support</a>
<span></span> <span></span>
<a href="/help" class="hover:opacity-75 transition-opacity">Help Center</a> <a href="/help" class="anchor">Help Center</a>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,16 +1,23 @@
<script lang="ts"> <script lang="ts">
import { injectSpeedInsights } from '@vercel/speed-insights/sveltekit';
import { Modal, Toaster } from '@skeletonlabs/skeleton-svelte';
import { MetaTags, deepMerge } from 'svelte-meta-tags';
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 Header from '$lib/components/Header.svelte';
import { Modal, Toaster } from '@skeletonlabs/skeleton-svelte';
import { injectSpeedInsights } from '@vercel/speed-insights/sveltekit';
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 { toaster } from '$lib';
import Footer from '$lib/components/Footer.svelte';
import Header from '$lib/components/Header.svelte';
import 'prism-themes/themes/prism-vsc-dark-plus.css';
import '../app.css'; import '../app.css';
injectSpeedInsights(); try {
injectSpeedInsights();
} catch (error) {
console.error(error);
}
let { data, children } = $props(); let { data, children } = $props();
let { session, supabase } = $derived(data); let { session, supabase } = $derived(data);
@@ -37,16 +44,6 @@
return `${baseClasses} preset-ghost-surface-200-800`; return `${baseClasses} preset-ghost-surface-200-800`;
} }
// Helper function to get mobile navigation link classes
function getMobileNavLinkClasses(path: string): string {
const isActive = isActivePath(path);
const baseClasses = 'btn flex items-center justify-start gap-2';
if (isActive) {
return `${baseClasses} preset-filled-primary-500 text-on-primary-500 cursor-default`;
}
return `${baseClasses} preset-ghost-surface-200-800`;
}
onMount(() => { onMount(() => {
// Sync client-side session with server-side on mount // Sync client-side session with server-side on mount
@@ -67,97 +64,13 @@
<Toaster {toaster}></Toaster> <Toaster {toaster}></Toaster>
<Modal /> <Modal />
<!-- Header -->
<Header {data} /> <Header {data} />
<!-- Main Content --> <!-- Main Content -->
<main class="min-h-screen p-4"> <main class="min-h-screen p-4">
{@render children()} {@render children()}
<!-- Pass session to child pages -->
</main> </main>
<!-- Footer --> <!-- Footer -->
<footer class="bg-surface-100-850-token border-surface-200-700-token mt-20 border-t"> <Footer />
<div class="container mx-auto px-6 py-12">
<div class="grid grid-cols-1 gap-8 md:grid-cols-4">
<!-- Brand Column -->
<div class="space-y-4">
<div class="flex items-center gap-2">
<div class="bg-primary-500 flex h-8 w-8 items-center justify-center rounded-lg">
<span class="text-lg font-bold text-white">S</span>
</div>
<span class="text-xl font-bold">Sveltey</span>
</div>
<p class="text-sm opacity-75">
The complete SvelteKit & Supabase SaaS template. Launch your next project in minutes, not
months.
</p>
</div>
<!-- Product Links -->
<div class="space-y-4">
<h3 class="font-semibold">Product</h3>
<div class="space-y-2 text-sm">
<a href="/pricing" class="block opacity-75 transition-opacity hover:opacity-100"
>Pricing</a
>
<a href="/blog" class="block opacity-75 transition-opacity hover:opacity-100">Blog</a>
<a href="/dashboard" class="block opacity-75 transition-opacity hover:opacity-100"
>Dashboard</a
>
</div>
</div>
<!-- Support Links -->
<div class="space-y-4">
<h3 class="font-semibold">Support</h3>
<div class="space-y-2 text-sm">
<a
href="https://github.com/LukeHagar/sveltey"
class="block opacity-75 transition-opacity hover:opacity-100"
>
GitHub
</a>
<a href="/contact" class="block opacity-75 transition-opacity hover:opacity-100">
Contact
</a>
<a href="/help" class="block opacity-75 transition-opacity hover:opacity-100">
Help Center
</a>
</div>
</div>
<!-- Legal Links -->
<div class="space-y-4">
<h3 class="font-semibold">Legal</h3>
<div class="space-y-2 text-sm">
<a href="/privacy" class="block opacity-75 transition-opacity hover:opacity-100">
Privacy Policy
</a>
<a href="/terms" class="block opacity-75 transition-opacity hover:opacity-100">
Terms of Service
</a>
<a href="/cookies" class="block opacity-75 transition-opacity hover:opacity-100">
Cookie Policy
</a>
</div>
</div>
</div>
<!-- Footer Bottom -->
<div
class="border-surface-300-600-token mt-8 flex flex-col items-center justify-between border-t pt-8 md:flex-row"
>
<p class="text-sm opacity-50">© 2025 Sveltey. All rights reserved.</p>
<div class="mt-4 flex flex-wrap items-center justify-center gap-4 md:mt-0">
<span class="text-sm opacity-50">Built with</span>
<div class="flex items-center gap-2 text-sm opacity-75">
<a class="anchor" href="https://kit.svelte.dev/" target="_blank">SvelteKit</a>
<span></span>
<a class="anchor" href="https://supabase.com/" target="_blank">Supabase</a>
<span></span>
<a class="anchor" href="https://skeleton.dev/" target="_blank">Skeleton UI</a>
</div>
</div>
</div>
</div>
</footer>

View File

@@ -57,11 +57,13 @@
</div> </div>
</header> </header>
<hr class="hr max-w-48" /> <div class="flex justify-center">
<hr class="hr border-surface-500 max-w-64" />
</div>
<!-- Stats Section --> <!-- Stats Section -->
<section> <section>
<div class="flex gap-20 text-center flex-wrap font-bold justify-between justify-center"> <div class="flex gap-20 text-center flex-wrap font-bold justify-center">
<div class="space-y-1"> <div class="space-y-1">
<span class="text-7xl">99%</span> <span class="text-7xl">99%</span>
<p class="text-primary-500">Faster Development</p> <p class="text-primary-500">Faster Development</p>
@@ -81,7 +83,9 @@
</div> </div>
</section> </section>
<hr class="hr max-w-48" /> <div class="flex justify-center">
<hr class="hr border-surface-500 max-w-64" />
</div>
<!-- Features Section --> <!-- Features Section -->
<section class="flex flex-wrap gap-4"> <section class="flex flex-wrap gap-4">
@@ -111,7 +115,9 @@
</div> </div>
</section> </section>
<hr class="hr max-w-48" /> <div class="flex justify-center">
<hr class="hr border-surface-500 max-w-64" />
</div>
<!-- Technologies Section --> <!-- Technologies Section -->
<section class="space-y-8 text-center"> <section class="space-y-8 text-center">
@@ -144,7 +150,9 @@
</div> </div>
</section> </section>
<hr class="hr max-w-48" /> <div class="flex justify-center">
<hr class="hr border-surface-500 max-w-64" />
</div>
<!-- Call to Action Section --> <!-- Call to Action Section -->
<section class="grid grid-cols-1 items-center gap-4 md:grid-cols-[1fr_auto]"> <section class="grid grid-cols-1 items-center gap-4 md:grid-cols-[1fr_auto]">