Refactor authentication components and remove unused files; update header links and styles, adjusted blog loading and perf

This commit is contained in:
Luke Hagar
2025-06-04 15:07:44 -05:00
parent dc8c51168c
commit 773ce87d4d
23 changed files with 577 additions and 768 deletions

View File

@@ -40,4 +40,3 @@ div.prose a {
a.disabled {
@apply pointer-events-none opacity-50;
}

View File

@@ -1,4 +1,4 @@
import type { BlogPost } from './blog';
import type { BlogPost, BlogPostMetadata } from './blog';
/**
* Utility functions for blog content management
@@ -86,7 +86,7 @@ Wrap up your blog post with a compelling conclusion.
};
// Validate blog post metadata
export const validatePostMetadata = (metadata: Partial<BlogPost>): string[] => {
export const validatePostMetadata = (metadata: Partial<BlogPost | BlogPostMetadata>): string[] => {
const errors: string[] = [];
if (!metadata.title?.trim()) {
@@ -121,8 +121,8 @@ export const validatePostMetadata = (metadata: Partial<BlogPost>): string[] => {
return errors;
};
// Search posts by keyword
export const searchPosts = (posts: BlogPost[], keyword: string): BlogPost[] => {
// Search posts by keyword (works with both BlogPost and BlogPostMetadata)
export const searchPosts = <T extends BlogPost | BlogPostMetadata>(posts: T[], keyword: string): T[] => {
const searchTerm = keyword.toLowerCase();
return posts.filter(post =>
@@ -133,8 +133,8 @@ export const searchPosts = (posts: BlogPost[], keyword: string): BlogPost[] => {
);
};
// Group posts by year
export const groupPostsByYear = (posts: BlogPost[]): Record<string, BlogPost[]> => {
// Group posts by year (works with both BlogPost and BlogPostMetadata)
export const groupPostsByYear = <T extends BlogPost | BlogPostMetadata>(posts: T[]): Record<string, T[]> => {
return posts.reduce((groups, post) => {
const year = new Date(post.publishedAt).getFullYear().toString();
if (!groups[year]) {
@@ -142,11 +142,15 @@ export const groupPostsByYear = (posts: BlogPost[]): Record<string, BlogPost[]>
}
groups[year].push(post);
return groups;
}, {} as Record<string, BlogPost[]>);
}, {} as Record<string, T[]>);
};
// Get related posts based on tags
export const getRelatedPosts = (currentPost: BlogPost, allPosts: BlogPost[], limit: number = 3): BlogPost[] => {
// Get related posts based on tags (works with metadata only for performance)
export const getRelatedPosts = (
currentPost: BlogPost | BlogPostMetadata,
allPosts: BlogPostMetadata[],
limit: number = 3
): BlogPostMetadata[] => {
const related = allPosts
.filter(post => post.slug !== currentPost.slug)
.map(post => {

134
src/lib/blog.ts Normal file
View File

@@ -0,0 +1,134 @@
import { dev } from '$app/environment';
export interface BlogPost {
title: string;
slug: string;
excerpt: string;
publishedAt: string;
author: string;
tags: string[];
featured: boolean;
content?: any; // Optional since not always needed
}
export interface BlogPostMetadata {
title: string;
slug: string;
excerpt: string;
publishedAt: string;
author: string;
tags: string[];
featured: boolean;
}
// Get all blog post files
const allPostFiles = import.meta.glob('$lib/posts/*.md');
// Parse frontmatter and extract metadata only (no content rendering)
const parsePostMetadataOnly = async (filename: string, module: any): Promise<BlogPostMetadata> => {
const { metadata } = await module();
// Extract slug from filename if not provided in frontmatter
const slug = metadata.slug || filename.split('/').pop()?.replace('.md', '') || '';
return {
title: metadata.title || 'Untitled',
slug,
excerpt: metadata.excerpt || '',
publishedAt: metadata.publishedAt || new Date().toISOString().split('T')[0],
author: metadata.author || 'Anonymous',
tags: metadata.tags || [],
featured: metadata.featured || false
};
};
// Parse frontmatter and extract metadata with content
const parsePostWithContent = async (filename: string, module: any): Promise<BlogPost> => {
const { metadata, default: content } = await module();
// Extract slug from filename if not provided in frontmatter
const slug = metadata.slug || filename.split('/').pop()?.replace('.md', '') || '';
return {
title: metadata.title || 'Untitled',
slug,
excerpt: metadata.excerpt || '',
publishedAt: metadata.publishedAt || new Date().toISOString().split('T')[0],
author: metadata.author || 'Anonymous',
tags: metadata.tags || [],
featured: metadata.featured || false,
content: content
};
};
// Get all posts metadata only (cheap operation for listing)
export const getAllPosts = async (): Promise<BlogPostMetadata[]> => {
const posts: BlogPostMetadata[] = [];
for (const [filename, module] of Object.entries(allPostFiles)) {
try {
const post = await parsePostMetadataOnly(filename, module);
// Only include posts with valid publish dates
if (post.publishedAt && new Date(post.publishedAt) <= new Date()) {
posts.push(post);
}
} catch (error) {
if (dev) {
console.error(`Error loading post metadata ${filename}:`, error);
}
}
}
// Sort by publication date (newest first)
return posts.sort((a, b) => new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime());
};
// Get a single post with full content by slug
export const getPostBySlug = async (slug: string): Promise<BlogPost | null> => {
for (const [filename, module] of Object.entries(allPostFiles)) {
try {
// First check metadata to see if this is the right post
const metadata = await parsePostMetadataOnly(filename, module);
if (metadata.slug === slug) {
// Only if we found the right post, load the full content
const fullPost = await parsePostWithContent(filename, module);
// Check if post should be published
if (fullPost.publishedAt && new Date(fullPost.publishedAt) <= new Date()) {
return fullPost;
}
}
} catch (error) {
if (dev) {
console.error(`Error loading post ${filename}:`, error);
}
}
}
return null;
};
// Get featured posts metadata only (cheap operation)
export const getFeaturedPosts = async (): Promise<BlogPostMetadata[]> => {
const posts = await getAllPosts();
return posts.filter(post => post.featured);
};
// Get posts by tag metadata only (cheap operation)
export const getPostsByTag = async (tag: string): Promise<BlogPostMetadata[]> => {
const posts = await getAllPosts();
return posts.filter(post => post.tags.includes(tag));
};
// Get all unique tags (metadata only)
export const getAllTags = async (): Promise<string[]> => {
const posts = await getAllPosts();
const tagSet = new Set<string>();
posts.forEach(post => {
post.tags.forEach(tag => tagSet.add(tag));
});
return Array.from(tagSet).sort();
};

View File

@@ -2,7 +2,15 @@
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 {
BookOpen,
DollarSign,
FormInputIcon,
Home,
LayoutDashboard,
LogOut,
User
} from '@lucide/svelte';
import { Avatar } from '@skeletonlabs/skeleton-svelte';
let { data } = $props();
@@ -18,7 +26,7 @@
<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-2 py-2 md:py-4 md:px-6">
<nav class="container mx-auto px-2 py-2 md:px-6 md:py-4">
<div class="flex items-center justify-between">
<!-- Left side - Brand and Main navigation -->
<div class="flex items-center gap-8">
@@ -91,9 +99,13 @@
{:else}
<!-- Authentication Buttons -->
<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/login" class={getNavClasses('/auth/login')} aria-label="Login">
<User class="size-4" aria-hidden="true" />
<span class="">Sign In / Register</span>
<span class="">Login</span>
</a>
<a href="/auth/register" class={getNavClasses('/auth/register')} aria-label="Register">
<FormInputIcon class="size-4" aria-hidden="true" />
<span class="">Register</span>
</a>
</div>
{/if}
@@ -124,9 +136,13 @@
Dashboard
</a>
{:else}
<a href="/auth" class={getNavClasses('/auth')} aria-label="Sign in or register">
<a href="/auth/login" class={getNavClasses('/auth/login')} aria-label="Login">
<User class="size-4" aria-hidden="true" />
Sign In / Up
<span class="">Login</span>
</a>
<a href="/auth/register" class={getNavClasses('/auth/register')} aria-label="Register">
<FormInputIcon class="size-4" aria-hidden="true" />
<span class="">Register</span>
</a>
{/if}
</div>

View File

@@ -1,15 +0,0 @@
---
title: "All about Sveltey"
slug: "all-about-sveltey"
excerpt: "The Sveltey project ReadMe included as a blog post with the magic of mdsvex, and relative file paths :D"
publishedAt: "2025-05-30"
featured: true
author: "Luke Hagar"
tags: ["mdsvex", "ftw", "rofl"]
---
<script>
import ReadMe from '../../../README.md';
</script>
<ReadMe />

View File

@@ -1,88 +0,0 @@
import { dev } from '$app/environment';
import { render } from 'svelte/server';
export interface BlogPost {
title: string;
slug: string;
excerpt: string;
publishedAt: string;
author: string;
tags: string[];
featured: boolean;
content: any;
}
// Get all blog post files
const allPostFiles = import.meta.glob('$lib/posts/*.md');
// Parse frontmatter and extract metadata only
const parsePostMetadata = async (filename: string, module: any): Promise<BlogPost> => {
const postModule = await module();
const { metadata, default: content } = postModule;
// Extract slug from filename if not provided in frontmatter
const slug = metadata.slug || filename.split('/').pop()?.replace('.md', '') || '';
return {
title: metadata.title || 'Untitled',
slug,
excerpt: metadata.excerpt || '',
publishedAt: metadata.publishedAt || new Date().toISOString().split('T')[0],
author: metadata.author || 'Anonymous',
tags: metadata.tags || [],
featured: metadata.featured || false,
content: render(content)
};
};
// Get all posts metadata only
export const getAllPosts = async (): Promise<BlogPost[]> => {
const posts: BlogPost[] = [];
for (const [filename, module] of Object.entries(allPostFiles)) {
try {
const post = await parsePostMetadata(filename, module);
// Only include posts with valid publish dates
if (post.publishedAt && new Date(post.publishedAt) <= new Date()) {
posts.push(post);
}
} catch (error) {
if (dev) {
console.error(`Error loading post ${filename}:`, error);
}
}
}
// Sort by publication date (newest first)
return posts.sort((a, b) => new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime());
};
// Get a single post metadata by slug
export const getPostBySlug = async (slug: string): Promise<BlogPost | null> => {
const posts = await getAllPosts();
return posts.find(post => post.slug === slug) || null;
};
// Get featured posts metadata only
export const getFeaturedPosts = async (): Promise<BlogPost[]> => {
const posts = await getAllPosts();
return posts.filter(post => post.featured);
};
// Get posts by tag metadata only
export const getPostsByTag = async (tag: string): Promise<BlogPost[]> => {
const posts = await getAllPosts();
return posts.filter(post => post.tags.includes(tag));
};
// Get all unique tags
export const getAllTags = async (): Promise<string[]> => {
const posts = await getAllPosts();
const tagSet = new Set<string>();
posts.forEach(post => {
post.tags.forEach(tag => tagSet.add(tag));
});
return Array.from(tagSet).sort();
};

View File

View File

@@ -1,17 +0,0 @@
import type { MetaTagsProps } from 'svelte-meta-tags';
export const load = () => {
const pageMetaTags = Object.freeze({
title: 'Sign In',
description: 'Sign in to your account or create a new one to access your dashboard and start building with Sveltey.',
robots: 'noindex,nofollow', // Don't index auth pages
openGraph: {
title: 'Sign In - Sveltey',
description: 'Sign in to your account or create a new one to access your dashboard.'
}
}) satisfies MetaTagsProps;
return {
pageMetaTags
};
};

View File

@@ -0,0 +1,342 @@
<script lang="ts">
import { supabase } from '$lib/supabase-client.js';
import { toaster } from '$lib';
import { Mail, Lock, LogIn, Github, Chrome, MessageCircle, Twitter, Eye, EyeOff, AlertTriangle } from '@lucide/svelte';
import { onMount } from 'svelte';
let { data } = $props();
const session = $derived(data.session);
let showPassword = $state(false);
let authError = $state('');
onMount(() => {
// Check URL parameters to handle errors
const urlParams = new URLSearchParams(window.location.search);
const hashParams = new URLSearchParams(window.location.hash.substring(1));
// Check for Supabase auth errors in URL params or hash
const error = urlParams.get('error') || hashParams.get('error');
const errorCode = urlParams.get('error_code') || hashParams.get('error_code');
const errorDescription = urlParams.get('error_description') || hashParams.get('error_description');
if (error) {
let errorMessage = 'Authentication failed';
// Map common Supabase error codes to user-friendly messages
switch (errorCode) {
case 'unexpected_failure':
if (errorDescription?.includes('user profile from external provider')) {
errorMessage = 'Unable to retrieve your profile from the OAuth provider. This may be due to privacy settings or a temporary issue. Please try again.';
} else {
errorMessage = 'An unexpected error occurred during authentication. Please try again.';
}
break;
case 'oauth_callback_error':
errorMessage = 'OAuth authentication was cancelled or failed. Please try again.';
break;
case 'access_denied':
errorMessage = 'Access was denied by the OAuth provider. Please try again and ensure you grant the necessary permissions.';
break;
case 'server_error':
errorMessage = 'A server error occurred during authentication. Please try again in a moment.';
break;
default:
// Use the error description if available, otherwise use generic message
if (errorDescription) {
errorMessage = decodeURIComponent(errorDescription.replace(/\+/g, ' '));
}
break;
}
authError = errorMessage;
// Show toast notification for the error
toaster.create({
type: 'error',
title: 'Authentication Error',
description: errorMessage
});
// Clean up the URL by removing error parameters
const cleanUrl = new URL(window.location.href);
cleanUrl.searchParams.delete('error');
cleanUrl.searchParams.delete('error_code');
cleanUrl.searchParams.delete('error_description');
cleanUrl.hash = '';
// Use replaceState to avoid adding to browser history
window.history.replaceState({}, '', cleanUrl.toString());
}
});
let formData = $state({
email: '',
password: ''
});
let loading = $state(false);
let oauthLoading = $state('');
let message = $state('');
// OAuth providers configuration - only GitHub enabled for demo
const oauthProviders = [
{
name: 'GitHub',
provider: 'github',
icon: Github,
color: 'bg-[#333] hover:bg-[#555] text-white',
description: 'Continue with GitHub',
enabled: true
},
{
name: 'Google',
provider: 'google',
icon: Chrome,
color: 'bg-gray-300 text-gray-500 cursor-not-allowed',
description: 'Continue with Google (Demo Disabled)',
enabled: false
},
{
name: 'Discord',
provider: 'discord',
icon: MessageCircle,
color: 'bg-gray-300 text-gray-500 cursor-not-allowed',
description: 'Continue with Discord (Demo Disabled)',
enabled: false
},
{
name: 'Twitter',
provider: 'twitter',
icon: Twitter,
color: 'bg-gray-300 text-gray-500 cursor-not-allowed',
description: 'Continue with Twitter (Demo Disabled)',
enabled: false
}
];
async function handleSubmit(e: Event) {
e.preventDefault();
// Disable email/password authentication for demo
toaster.create({
type: 'warning',
title: 'Demo Mode',
description: 'Email/password authentication is disabled in this demo. Please use GitHub login instead.'
});
return;
}
async function handleOAuth(provider: string) {
// Only allow GitHub for demo
if (provider !== 'github') {
toaster.create({
type: 'warning',
title: 'Demo Mode',
description: `${provider.charAt(0).toUpperCase() + provider.slice(1)} login is disabled in this demo. Only GitHub login is available.`
});
return;
}
// Clear any previous auth errors
authError = '';
message = '';
oauthLoading = provider;
try {
const { error } = await supabase.auth.signInWithOAuth({
provider: provider as any,
options: {
redirectTo: `${window.location.origin}/auth/login`
}
});
if (error) throw error;
} catch (error: any) {
toaster.create({
type: 'error',
title: `${provider} authentication failed`,
description: error.message
});
authError = error.message;
} finally {
oauthLoading = '';
}
}
function dismissAuthError() {
authError = '';
}
</script>
<svelte:head>
<title>Sign In - Sveltey</title>
<meta name="description" content="Sign in to your account to access your dashboard and manage your projects." />
</svelte:head>
<div class="container mx-auto py-20">
<div class="max-w-md mx-auto space-y-8">
<!-- Demo Notice -->
<div class="card preset-outlined-warning-500 p-4">
<div class="flex items-start gap-3">
<AlertTriangle class="size-5 text-warning-500 flex-shrink-0 mt-0.5" />
<div class="space-y-2">
<h3 class="font-semibold text-warning-700 dark:text-warning-300">Demo Mode</h3>
<p class="text-sm text-warning-600 dark:text-warning-400">
This is a demo deployment. Only <strong>GitHub login</strong> is enabled.
Email/password authentication and other OAuth providers are disabled for demonstration purposes.
</p>
</div>
</div>
</div>
<!-- Auth Error Display -->
{#if authError}
<div class="card preset-outlined-error-500 p-4">
<div class="flex items-start gap-3">
<AlertTriangle class="size-5 text-error-500 flex-shrink-0 mt-0.5" />
<div class="flex-1 space-y-2">
<h3 class="font-semibold text-error-700 dark:text-error-300">Authentication Failed</h3>
<p class="text-sm text-error-600 dark:text-error-400">{authError}</p>
<button
type="button"
class="text-xs text-error-500 hover:text-error-600 underline"
onclick={dismissAuthError}
>
Dismiss
</button>
</div>
</div>
</div>
{/if}
<!-- Header -->
<header class="text-center space-y-4">
<div class="flex items-center justify-center gap-2 mb-4">
<LogIn class="size-8 text-primary-500" />
<h1 class="h1">Welcome <span class="text-primary-500">Back</span></h1>
</div>
<p class="text-lg opacity-75">
Sign in to your account to access your dashboard and manage your projects.
</p>
</header>
<!-- Login Form Card -->
<div class="card preset-outlined-primary-500 p-8 space-y-6">
<!-- Error Message -->
{#if message}
<div class="card preset-outlined-error-500 p-4 text-center">
<p class="text-error-600 dark:text-error-400 text-sm">{message}</p>
</div>
{/if}
<!-- Email/Password Form (Disabled for demo) -->
<form onsubmit={handleSubmit} class="space-y-6">
<div class="space-y-4 opacity-50">
<div class="space-y-2">
<label class="label font-medium" for="email">
<Mail class="size-4 inline mr-2" />
Email Address
</label>
<input
class="input preset-outlined-surface-200-800"
type="email"
id="email"
bind:value={formData.email}
placeholder="Enter your email (disabled in demo)"
disabled={true}
autocomplete="email"
/>
</div>
<div class="space-y-2">
<label class="label font-medium" for="password">
<Lock class="size-4 inline mr-2" />
Password
</label>
<div class="relative">
<input
class="input preset-outlined-surface-200-800 pr-10"
type={showPassword ? 'text' : 'password'}
id="password"
bind:value={formData.password}
placeholder="Enter your password (disabled)"
disabled={true}
autocomplete="current-password"
/>
<button
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" />
{:else}
<Eye class="size-4" />
{/if}
</button>
</div>
</div>
</div>
<button
type="submit"
class="btn preset-outlined-surface-200-800 w-full flex items-center justify-center gap-2 opacity-50 cursor-not-allowed"
disabled={true}
>
<LogIn class="size-4" />
Sign In (Demo Disabled)
</button>
</form>
<!-- Divider -->
<div class="flex items-center">
<hr class="flex-grow opacity-30" />
<span class="px-4 text-sm opacity-50">or continue with</span>
<hr class="flex-grow opacity-30" />
</div>
<!-- OAuth Providers -->
<div class="space-y-3">
{#each oauthProviders as provider}
<button
type="button"
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" aria-hidden="true"></div>
Connecting...
{:else}
<provider.icon class="size-4" aria-hidden="true" />
{provider.description}
{/if}
</button>
{/each}
</div>
</div>
<!-- Footer Links -->
<div class="text-center space-y-4">
<p class="text-sm opacity-75">
Don't have an account?
<a
href="/auth/register"
class="text-primary-500 hover:text-primary-600 transition-colors font-medium"
>
Create one here
</a>
</p>
<div class="flex items-center justify-center gap-4 text-sm opacity-50">
<a href="/privacy" class="hover:opacity-75 transition-opacity">Privacy Policy</a>
<span></span>
<a href="/terms" class="hover:opacity-75 transition-opacity">Terms of Service</a>
</div>
</div>
</div>
</div>

View File

@@ -1,5 +1,9 @@
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
export const load = () => {
throw redirect(302, '/auth');
export const load: PageLoad = async ({ parent }) => {
const { session } = await parent();
return {
session
};
};

View File

@@ -0,0 +1,14 @@
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ parent }) => {
const { session } = await parent();
if (session) {
throw redirect(302, "/app/dashboard");
}
return {
session
};
};

View File

@@ -1,28 +1,21 @@
<script lang="ts">
import { supabase } from '$lib/supabaseClient';
import { supabase } from '$lib/supabase-client.js';
import { toaster } from '$lib';
import { Mail, Lock, LogIn, UserPlus, Github, Chrome, MessageCircle, Twitter, Star, Eye, EyeOff, AlertTriangle } from '@lucide/svelte';
import { Mail, Lock, UserPlus, Github, Chrome, MessageCircle, Twitter, Star, Eye, EyeOff, AlertTriangle } from '@lucide/svelte';
import { onMount } from 'svelte';
let { data } = $props();
const session = $derived(data.session);
let activeTab = $state('login'); // 'login' or 'signup'
let showPassword = $state(false);
let authError = $state('');
onMount(() => {
// Check URL parameters to set initial tab and handle errors
// Check URL parameters to handle errors
const urlParams = new URLSearchParams(window.location.search);
const hashParams = new URLSearchParams(window.location.hash.substring(1));
// Check for mode parameter
const mode = urlParams.get('mode');
if (mode === 'signup') {
activeTab = 'signup';
}
// Check for Supabase auth errors in URL params or hash
const error = urlParams.get('error') || hashParams.get('error');
const errorCode = urlParams.get('error_code') || hashParams.get('error_code');
@@ -133,60 +126,6 @@
description: 'Email/password authentication is disabled in this demo. Please use GitHub login instead.'
});
return;
// Original code commented out for demo
/*
loading = true;
message = '';
try {
if (activeTab === 'login') {
const { error } = await supabase.auth.signInWithPassword({
email: formData.email,
password: formData.password
});
if (error) throw error;
toaster.create({
type: 'info',
title: 'Welcome back!',
description: 'You have been logged in successfully.'
});
} else {
const { data, error } = await supabase.auth.signUp({
email: formData.email,
password: formData.password
});
if (error) throw error;
if (data.session) {
toaster.create({
type: 'info',
title: 'Welcome aboard!',
description: 'Your account has been created and you are now logged in.'
});
} else if (data.user && !data.session) {
toaster.create({
type: 'info',
title: 'Account created successfully!',
description: 'Please check your email to confirm your account.'
});
return; // Don't redirect if email confirmation is needed
}
}
goto('/dashboard');
} catch (error: any) {
toaster.create({
type: 'error',
title: activeTab === 'login' ? 'Login failed' : 'Signup failed',
description: error.message
});
message = error.message;
} finally {
loading = false;
}
*/
}
async function handleOAuth(provider: string) {
@@ -209,7 +148,7 @@
const { error } = await supabase.auth.signInWithOAuth({
provider: provider as any,
options: {
redirectTo: `${window.location.origin}/auth`
redirectTo: `${window.location.origin}/auth/register`
}
});
if (error) throw error;
@@ -225,22 +164,16 @@
}
}
function switchTab(tab: string) {
activeTab = tab;
message = '';
authError = '';
formData = { email: '', password: '' };
}
function dismissAuthError() {
authError = '';
}
$effect(() => {
console.log(session);
});
</script>
<svelte:head>
<title>Sign Up - Sveltey</title>
<meta name="description" content="Create your account to start building with our comprehensive SaaS template." />
</svelte:head>
<div class="container mx-auto py-20">
<div class="max-w-md mx-auto space-y-8">
<!-- Demo Notice -->
@@ -280,48 +213,15 @@
<!-- Header -->
<header class="text-center space-y-4">
<div class="flex items-center justify-center gap-2 mb-4">
{#if activeTab === 'login'}
<LogIn class="size-8 text-primary-500" />
<h1 class="h1">Welcome <span class="text-primary-500">Back</span></h1>
{:else}
<Star class="size-8 text-primary-500" />
<h1 class="h1">Get <span class="text-primary-500">Started</span></h1>
{/if}
</div>
<p class="text-lg opacity-75">
{#if activeTab === 'login'}
Sign in to your account to access your dashboard and manage your projects.
{:else}
Create your account to start building with our comprehensive SaaS template.
{/if}
</p>
</header>
<!-- Tab Switcher -->
<div class="card preset-outlined-primary-500 p-2">
<div class="flex gap-1">
<button
type="button"
class="btn flex-1 {activeTab === 'login' ? 'preset-filled-primary-500' : 'preset-ghost-primary-500'}"
onclick={() => switchTab('login')}
disabled={loading || oauthLoading !== ''}
>
<LogIn class="size-4" />
Sign In
</button>
<button
type="button"
class="btn flex-1 {activeTab === 'signup' ? 'preset-filled-primary-500' : 'preset-ghost-primary-500'}"
onclick={() => switchTab('signup')}
disabled={loading || oauthLoading !== ''}
>
<UserPlus class="size-4" />
Sign Up
</button>
</div>
</div>
<!-- Auth Form Card -->
<!-- Register Form Card -->
<div class="card preset-outlined-primary-500 p-8 space-y-6">
<!-- Error Message -->
{#if message}
@@ -360,10 +260,10 @@
type={showPassword ? 'text' : 'password'}
id="password"
bind:value={formData.password}
placeholder={activeTab === 'login' ? 'Enter your password (disabled)' : 'Create a strong password (disabled)'}
placeholder="Create a strong password (disabled)"
disabled={true}
minlength={activeTab === 'signup' ? 6 : undefined}
autocomplete={activeTab === 'login' ? 'current-password' : 'new-password'}
minlength="6"
autocomplete="new-password"
/>
<button
type="button"
@@ -379,9 +279,7 @@
{/if}
</button>
</div>
{#if activeTab === 'signup'}
<p class="text-xs opacity-50">Must be at least 6 characters long</p>
{/if}
</div>
</div>
@@ -390,34 +288,18 @@
class="btn preset-outlined-surface-200-800 w-full flex items-center justify-center gap-2 opacity-50 cursor-not-allowed"
disabled={true}
>
{#if activeTab === 'login'}
<LogIn class="size-4" />
Sign In (Demo Disabled)
{:else}
<UserPlus class="size-4" />
Create Account (Demo Disabled)
{/if}
</button>
</form>
<!-- Terms Notice for Signup -->
{#if activeTab === 'signup'}
<!-- Terms Notice -->
<p class="text-xs opacity-50 text-center">
By creating an account, you agree to our
<a href="/terms" class="text-primary-500 hover:text-primary-600 transition-colors">Terms of Service</a>
and
<a href="/privacy" class="text-primary-500 hover:text-primary-600 transition-colors">Privacy Policy</a>.
</p>
{/if}
<!-- Forgot Password for Login -->
{#if activeTab === 'login'}
<div class="text-center">
<span class="text-sm text-surface-500 opacity-50">
Forgot your password? (Demo disabled)
</span>
</div>
{/if}
<!-- Divider -->
<div class="flex items-center">
@@ -451,29 +333,15 @@
<!-- Footer Links -->
<div class="text-center space-y-4">
{#if activeTab === 'login'}
<p class="text-sm opacity-75">
Don't have an account?
<button
type="button"
class="text-primary-500 hover:text-primary-600 transition-colors font-medium"
onclick={() => switchTab('signup')}
>
Create one here
</button>
</p>
{:else}
<p class="text-sm opacity-75">
Already have an account?
<button
type="button"
<a
href="/auth/login"
class="text-primary-500 hover:text-primary-600 transition-colors font-medium"
onclick={() => switchTab('login')}
>
Sign in here
</button>
</a>
</p>
{/if}
<div class="flex items-center justify-center gap-4 text-sm opacity-50">
<a href="/privacy" class="hover:opacity-75 transition-opacity">Privacy Policy</a>
<span></span>

View File

@@ -1,155 +0,0 @@
<script lang="ts">
import { supabase } from '$lib/supabaseClient';
import { toaster } from '$lib';
import { Mail, Send, ArrowLeft, KeyRound, AlertTriangle } from '@lucide/svelte';
let email = $state('');
let loading = $state(false);
let sent = $state(false);
async function handleReset(e: Event) {
e.preventDefault();
// Disable password reset for demo
toaster.create({
type: 'warning',
title: 'Demo Mode',
description: 'Password reset is disabled in this demo. Please use GitHub login instead.'
});
return;
// Original code commented out for demo
/*
loading = true;
try {
const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${window.location.origin}/auth/update-password`
});
if (error) throw error;
sent = true;
toaster.create({
type: 'info',
title: 'Reset email sent!',
description: 'Check your email for password reset instructions.'
});
} catch (error: any) {
toaster.create({
type: 'error',
title: 'Reset failed',
description: error.message
});
} finally {
loading = false;
}
*/
}
</script>
<svelte:head>
<title>Reset Password - Sveltey</title>
<meta name="description" content="Reset your password to regain access to your account." />
</svelte:head>
<div class="container mx-auto py-20">
<div class="max-w-md mx-auto space-y-8">
<!-- Demo Notice -->
<div class="card preset-outlined-warning-500 p-4">
<div class="flex items-start gap-3">
<AlertTriangle class="size-5 text-warning-500 flex-shrink-0 mt-0.5" />
<div class="space-y-2">
<h3 class="font-semibold text-warning-700 dark:text-warning-300">Demo Mode</h3>
<p class="text-sm text-warning-600 dark:text-warning-400">
Password reset is disabled in this demo. Only <strong>GitHub login</strong> is available.
</p>
</div>
</div>
</div>
<!-- Header -->
<header class="text-center space-y-4">
<div class="flex items-center justify-center gap-2 mb-4">
<KeyRound class="size-8 text-primary-500" />
<h1 class="h1">Reset <span class="text-primary-500">Password</span></h1>
</div>
<p class="text-lg opacity-75">
Enter your email address and we'll send you a link to reset your password.
</p>
</header>
<!-- Reset Form Card -->
<div class="card preset-outlined-primary-500 p-8 space-y-6">
{#if !sent}
<form onsubmit={handleReset} class="space-y-6">
<div class="space-y-2 opacity-50">
<label class="label font-medium" for="email">
<Mail class="size-4 inline mr-2" />
Email Address
</label>
<input
class="input preset-outlined-surface-200-800"
type="email"
id="email"
bind:value={email}
placeholder="Enter your email address (disabled in demo)"
disabled={true}
autocomplete="email"
/>
<p class="text-xs opacity-75">
We'll send password reset instructions to this email address.
</p>
</div>
<button
type="submit"
class="btn preset-outlined-surface-200-800 w-full flex items-center justify-center gap-2 opacity-50 cursor-not-allowed"
disabled={true}
>
<Send class="size-4" />
Send Reset Email (Demo Disabled)
</button>
</form>
{:else}
<!-- Success State -->
<div class="text-center space-y-4">
<div class="w-16 h-16 bg-success-500 rounded-full flex items-center justify-center mx-auto mb-4">
<Mail class="size-8 text-white" />
</div>
<h2 class="h3">Check your email</h2>
<p class="opacity-75">
We've sent password reset instructions to <strong>{email}</strong>
</p>
<p class="text-sm opacity-50">
Didn't receive the email? Check your spam folder or try again.
</p>
<button
type="button"
class="btn preset-outlined-surface-200-800 w-full"
onclick={() => { sent = false; email = ''; }}
>
Try Different Email
</button>
</div>
{/if}
</div>
<!-- Back to Login -->
<div class="text-center">
<a href="/auth" class="btn preset-ghost-surface-200-800 flex items-center justify-center gap-2">
<ArrowLeft class="size-4" />
Back to Sign In
</a>
</div>
<!-- Help Section -->
<div class="text-center space-y-4">
<div class="flex items-center justify-center gap-4 text-sm opacity-50">
<a href="/contact" class="hover:opacity-75 transition-opacity">Need Help?</a>
<span></span>
<a href="/privacy" class="hover:opacity-75 transition-opacity">Privacy Policy</a>
</div>
</div>
</div>
</div>

View File

@@ -1,5 +0,0 @@
import { redirect } from '@sveltejs/kit';
export function load() {
throw redirect(302, '/auth?mode=signup');
}

View File

@@ -1,247 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { toaster } from '$lib';
import { supabase } from '$lib/supabaseClient';
import { Eye, EyeOff, KeyRound, Lock, Save, AlertTriangle } from '@lucide/svelte';
import type { Session } from '@supabase/supabase-js';
import { onMount } from 'svelte';
let password = $state('');
let confirmPassword = $state('');
let loading = $state(false);
let showPassword = $state(false);
let showConfirmPassword = $state(false);
let session = $state<Session | null>(null);
onMount(async () => {
// Check if user is authenticated (came from reset link)
const { data: { session: currentSession } } = await supabase.auth.getSession();
session = currentSession;
if (!session) {
toaster.create({
type: 'error',
title: 'Invalid reset link',
description: 'Please request a new password reset link.'
});
goto('/auth/reset-password');
}
});
async function handleUpdatePassword(e: Event) {
e.preventDefault();
// Disable password update for demo
toaster.create({
type: 'warning',
title: 'Demo Mode',
description: 'Password update is disabled in this demo. Please use GitHub login instead.'
});
return;
// Original code commented out for demo
/*
if (password !== confirmPassword) {
toaster.create({
type: 'error',
title: 'Passwords don\'t match',
description: 'Please ensure both passwords are the same.'
});
return;
}
if (password.length < 6) {
toaster.create({
type: 'error',
title: 'Password too short',
description: 'Password must be at least 6 characters long.'
});
return;
}
loading = true;
try {
const { error } = await supabase.auth.updateUser({
password: password
});
if (error) throw error;
toaster.create({
type: 'info',
title: 'Password updated!',
description: 'Your password has been successfully updated.'
});
goto('/dashboard');
} catch (error: any) {
toaster.create({
type: 'error',
title: 'Update failed',
description: error.message
});
} finally {
loading = false;
}
*/
}
</script>
<svelte:head>
<title>Update Password - Sveltey</title>
<meta name="description" content="Set your new password to secure your account." />
</svelte:head>
<div class="container mx-auto py-20">
<div class="max-w-md mx-auto space-y-8">
<!-- Demo Notice -->
<div class="card preset-outlined-warning-500 p-4">
<div class="flex items-start gap-3">
<AlertTriangle class="size-5 text-warning-500 flex-shrink-0 mt-0.5" />
<div class="space-y-2">
<h3 class="font-semibold text-warning-700 dark:text-warning-300">Demo Mode</h3>
<p class="text-sm text-warning-600 dark:text-warning-400">
Password update is disabled in this demo. Only <strong>GitHub login</strong> is available.
</p>
</div>
</div>
</div>
<!-- Header -->
<header class="text-center space-y-4">
<div class="flex items-center justify-center gap-2 mb-4">
<KeyRound class="size-8 text-primary-500" />
<h1 class="h1">Update <span class="text-primary-500">Password</span></h1>
</div>
<p class="text-lg opacity-75">
Choose a strong password to secure your account.
</p>
</header>
<!-- Update Form Card -->
<div class="card preset-outlined-surface-200-800 p-8 space-y-6">
<form onsubmit={handleUpdatePassword} class="space-y-6">
<div class="space-y-4 opacity-50">
<div class="space-y-2">
<label class="label font-medium" for="password">
<Lock class="size-4 inline mr-2" />
New Password
</label>
<div class="relative">
<input
class="input preset-outlined-surface-200-800 pr-10"
type={showPassword ? 'text' : 'password'}
id="password"
bind:value={password}
placeholder="Create a strong password (disabled in demo)"
disabled={true}
minlength="6"
autocomplete="new-password"
/>
<button
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" />
{:else}
<Eye class="size-4" />
{/if}
</button>
</div>
<p class="text-xs opacity-50">Must be at least 6 characters long</p>
</div>
<div class="space-y-2">
<label class="label font-medium" for="confirmPassword">
<Lock class="size-4 inline mr-2" />
Confirm New Password
</label>
<div class="relative">
<input
class="input preset-outlined-surface-200-800 pr-10"
type={showConfirmPassword ? 'text' : 'password'}
id="confirmPassword"
bind:value={confirmPassword}
placeholder="Confirm your password (disabled in demo)"
disabled={true}
autocomplete="new-password"
/>
<button
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" />
{:else}
<Eye class="size-4" />
{/if}
</button>
</div>
</div>
</div>
<!-- Password Strength Indicator -->
{#if password.length > 0}
<div class="space-y-2 opacity-50">
<p class="text-sm font-medium">Password Strength:</p>
<div class="flex gap-1">
<div class="h-2 flex-1 rounded {password.length >= 6 ? 'bg-success-500' : 'bg-surface-300'}"></div>
<div class="h-2 flex-1 rounded {password.length >= 8 ? 'bg-success-500' : 'bg-surface-300'}"></div>
<div class="h-2 flex-1 rounded {password.length >= 10 && /[A-Z]/.test(password) && /[0-9]/.test(password) ? 'bg-success-500' : 'bg-surface-300'}"></div>
</div>
<p class="text-xs opacity-50">
{#if password.length < 6}
Weak - Add more characters
{:else if password.length < 8}
Good - Consider adding more characters
{:else if password.length >= 10 && /[A-Z]/.test(password) && /[0-9]/.test(password)}
Strong - Great password!
{:else}
Good - Consider adding uppercase letters and numbers
{/if}
</p>
</div>
{/if}
<!-- Password Match Indicator -->
{#if confirmPassword.length > 0}
<div class="flex items-center gap-2 text-sm opacity-50">
{#if password === confirmPassword}
<div class="w-2 h-2 bg-success-500 rounded-full"></div>
<span class="text-success-600 dark:text-success-400">Passwords match</span>
{:else}
<div class="w-2 h-2 bg-error-500 rounded-full"></div>
<span class="text-error-600 dark:text-error-400">Passwords don't match</span>
{/if}
</div>
{/if}
<button
type="submit"
class="btn preset-outlined-surface-200-800 w-full flex items-center justify-center gap-2 opacity-50 cursor-not-allowed"
disabled={true}
>
<Save class="size-4" />
Update Password (Demo Disabled)
</button>
</form>
</div>
<!-- Help Section -->
<div class="text-center space-y-4">
<div class="flex items-center justify-center gap-4 text-sm opacity-50">
<a href="/contact" class="hover:opacity-75 transition-opacity">Need Help?</a>
<span></span>
<a href="/privacy" class="hover:opacity-75 transition-opacity">Privacy Policy</a>
</div>
</div>
</div>
</div>

View File

@@ -1,8 +1,9 @@
// src/routes/blog/+page.server.ts
import { getAllPosts } from '$lib/server/blog';
import { getAllPosts } from '$lib/blog';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async () => {
// Get only metadata for listing (cheap operation)
return {
posts: await getAllPosts()
};

View File

@@ -12,6 +12,7 @@
day: 'numeric'
});
};
</script>
<div class="container mx-auto py-20 space-y-12 max-w-4xl">
@@ -108,7 +109,7 @@
<!-- Article Content -->
<article class="prose dark:prose-invert prose-lg max-w-none">
<div class="prose dark:prose-invert prose-lg max-w-none prose-headings:text-primary-500 prose-links:text-primary-500 prose-code:text-primary-500">
{@html post.content.html}
<post.content />
</div>
</article>

View File

@@ -1,10 +1,10 @@
// src/routes/blog/[slug]/+page.server.ts
import { getPostBySlug } from '$lib/server/blog';
import { getPostBySlug } from '$lib/blog';
import { error } from '@sveltejs/kit';
import type { MetaTagsProps } from 'svelte-meta-tags';
export const load = async ({ params, url }) => {
// Get only the metadata for the post
// Get the full post with content
const post = await getPostBySlug(params.slug);
if (!post) {

View File

@@ -1,13 +1,9 @@
<script lang="ts">
// Icons
import { Check, X, Star, Zap, Crown, Mail } from '@lucide/svelte';
import type { PageProps } from './$types';
interface Props {
// Assuming session might be useful here, though not directly used in this basic structure
data: any;
}
let { data }: Props = $props();
let { data }: PageProps = $props();
const plans = [
{

View File

@@ -68,7 +68,7 @@
<Header {data} />
<!-- Main Content -->
<main class="min-h-screen p-4">
<main class="min-h-screen px-4">
{@render children()}
</main>

View File

@@ -1,25 +0,0 @@
// tests/auth.spec.ts
import { test, expect } from '@playwright/test';
test('login page has login form and can attempt interaction', async ({ page }) => {
await page.goto('/auth/login');
await expect(page.getByRole('heading', { name: 'Login' })).toBeVisible();
await page.getByLabel('Email').fill('test@example.com');
await page.getByLabel('Password').fill('password123');
// We don't expect a successful login without a backend or if Supabase URL isn't configured.
// This test mainly checks if the form fields are present and submittable.
// Further assertions would depend on actual behavior (e.g., error messages).
await page.getByRole('button', { name: 'Login' }).click();
// Add a small wait to see if any client-side error message appears or URL changes
// This is a very basic check. In a real scenario, you'd mock Supabase or check for specific outcomes.
await page.waitForTimeout(1000);
// Example: Check if it stays on the login page (e.g. due to error)
// await expect(page.getByRole('heading', { name: 'Login' })).toBeVisible();
// Or, if it redirects (less likely here without backend):
// await expect(page).toHaveURL('/dashboard');
});

View File

@@ -1,18 +0,0 @@
// tests/basic-navigation.spec.ts
import { test, expect } from '@playwright/test';
test('homepage has expected title and loads', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveTitle(/SvelteKit SaaS Template/); // Assuming a title like this, or adjust
await expect(page.getByRole('heading', { name: 'Your Next SaaS Adventure Starts Here' })).toBeVisible();
});
test('pricing page loads', async ({ page }) => {
await page.goto('/pricing');
await expect(page.getByRole('heading', { name: "Find the Plan That's Right For You" })).toBeVisible();
});
test('blog page loads', async ({ page }) => {
await page.goto('/blog');
await expect(page.getByRole('heading', { name: 'Our Blog' })).toBeVisible();
});