mirror of
https://github.com/LukeHagar/Sveltey.git
synced 2025-12-06 04:21:38 +00:00
Refactor authentication components and remove unused files; update header links and styles, adjusted blog loading and perf
This commit is contained in:
@@ -39,5 +39,4 @@ div.prose a {
|
||||
|
||||
a.disabled {
|
||||
@apply pointer-events-none opacity-50;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
134
src/lib/blog.ts
Normal 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();
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
@@ -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();
|
||||
};
|
||||
0
src/lib/server/supabase-admin.ts
Normal file
0
src/lib/server/supabase-admin.ts
Normal 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
|
||||
};
|
||||
};
|
||||
342
src/routes/(app)/auth/login/+page.svelte
Normal file
342
src/routes/(app)/auth/login/+page.svelte
Normal 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>
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
14
src/routes/(app)/auth/register/+page.server.ts
Normal file
14
src/routes/(app)/auth/register/+page.server.ts
Normal 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
|
||||
};
|
||||
};
|
||||
@@ -1,27 +1,20 @@
|
||||
<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');
|
||||
@@ -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}
|
||||
<Star class="size-8 text-primary-500" />
|
||||
<h1 class="h1">Get <span class="text-primary-500">Started</span></h1>
|
||||
</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}
|
||||
Create your account to start building with our comprehensive SaaS template.
|
||||
</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}
|
||||
<p class="text-xs opacity-50">Must be at least 6 characters long</p>
|
||||
</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}
|
||||
<UserPlus class="size-4" />
|
||||
Create Account (Demo Disabled)
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Terms Notice for Signup -->
|
||||
{#if activeTab === 'signup'}
|
||||
<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}
|
||||
<!-- 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>
|
||||
|
||||
<!-- 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"
|
||||
class="text-primary-500 hover:text-primary-600 transition-colors font-medium"
|
||||
onclick={() => switchTab('login')}
|
||||
>
|
||||
Sign in here
|
||||
</button>
|
||||
</p>
|
||||
{/if}
|
||||
<p class="text-sm opacity-75">
|
||||
Already have an account?
|
||||
<a
|
||||
href="/auth/login"
|
||||
class="text-primary-500 hover:text-primary-600 transition-colors font-medium"
|
||||
>
|
||||
Sign in 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>
|
||||
@@ -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>
|
||||
@@ -1,5 +0,0 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
export function load() {
|
||||
throw redirect(302, '/auth?mode=signup');
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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()
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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) {
|
||||
@@ -1,13 +1,9 @@
|
||||
<script lang="ts">
|
||||
// Icons
|
||||
import { Check, X, Star, Zap, Crown, Mail } from '@lucide/svelte';
|
||||
|
||||
interface Props {
|
||||
// Assuming session might be useful here, though not directly used in this basic structure
|
||||
data: any;
|
||||
}
|
||||
import type { PageProps } from './$types';
|
||||
|
||||
let { data }: Props = $props();
|
||||
let { data }: PageProps = $props();
|
||||
|
||||
const plans = [
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
Reference in New Issue
Block a user