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 {
|
a.disabled {
|
||||||
@apply pointer-events-none opacity-50;
|
@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
|
* Utility functions for blog content management
|
||||||
@@ -86,7 +86,7 @@ Wrap up your blog post with a compelling conclusion.
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Validate blog post metadata
|
// Validate blog post metadata
|
||||||
export const validatePostMetadata = (metadata: Partial<BlogPost>): string[] => {
|
export const validatePostMetadata = (metadata: Partial<BlogPost | BlogPostMetadata>): string[] => {
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
|
|
||||||
if (!metadata.title?.trim()) {
|
if (!metadata.title?.trim()) {
|
||||||
@@ -121,8 +121,8 @@ export const validatePostMetadata = (metadata: Partial<BlogPost>): string[] => {
|
|||||||
return errors;
|
return errors;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Search posts by keyword
|
// Search posts by keyword (works with both BlogPost and BlogPostMetadata)
|
||||||
export const searchPosts = (posts: BlogPost[], keyword: string): BlogPost[] => {
|
export const searchPosts = <T extends BlogPost | BlogPostMetadata>(posts: T[], keyword: string): T[] => {
|
||||||
const searchTerm = keyword.toLowerCase();
|
const searchTerm = keyword.toLowerCase();
|
||||||
|
|
||||||
return posts.filter(post =>
|
return posts.filter(post =>
|
||||||
@@ -133,8 +133,8 @@ export const searchPosts = (posts: BlogPost[], keyword: string): BlogPost[] => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Group posts by year
|
// Group posts by year (works with both BlogPost and BlogPostMetadata)
|
||||||
export const groupPostsByYear = (posts: BlogPost[]): Record<string, BlogPost[]> => {
|
export const groupPostsByYear = <T extends BlogPost | BlogPostMetadata>(posts: T[]): Record<string, T[]> => {
|
||||||
return posts.reduce((groups, post) => {
|
return posts.reduce((groups, post) => {
|
||||||
const year = new Date(post.publishedAt).getFullYear().toString();
|
const year = new Date(post.publishedAt).getFullYear().toString();
|
||||||
if (!groups[year]) {
|
if (!groups[year]) {
|
||||||
@@ -142,11 +142,15 @@ export const groupPostsByYear = (posts: BlogPost[]): Record<string, BlogPost[]>
|
|||||||
}
|
}
|
||||||
groups[year].push(post);
|
groups[year].push(post);
|
||||||
return groups;
|
return groups;
|
||||||
}, {} as Record<string, BlogPost[]>);
|
}, {} as Record<string, T[]>);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get related posts based on tags
|
// Get related posts based on tags (works with metadata only for performance)
|
||||||
export const getRelatedPosts = (currentPost: BlogPost, allPosts: BlogPost[], limit: number = 3): BlogPost[] => {
|
export const getRelatedPosts = (
|
||||||
|
currentPost: BlogPost | BlogPostMetadata,
|
||||||
|
allPosts: BlogPostMetadata[],
|
||||||
|
limit: number = 3
|
||||||
|
): BlogPostMetadata[] => {
|
||||||
const related = allPosts
|
const related = allPosts
|
||||||
.filter(post => post.slug !== currentPost.slug)
|
.filter(post => post.slug !== currentPost.slug)
|
||||||
.map(post => {
|
.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 { page } from '$app/state';
|
||||||
import SvelteyLogoLetter from '$lib/assets/Sveltey-logo-letter.svelte';
|
import SvelteyLogoLetter from '$lib/assets/Sveltey-logo-letter.svelte';
|
||||||
import ThemeSwitch from '$lib/components/ThemeSwitch.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';
|
import { Avatar } from '@skeletonlabs/skeleton-svelte';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
@@ -18,7 +26,7 @@
|
|||||||
<header
|
<header
|
||||||
class="bg-surface-50-950-token border-surface-200-700-token sticky top-0 z-50 border-b backdrop-blur-2xl"
|
class="bg-surface-50-950-token border-surface-200-700-token sticky top-0 z-50 border-b backdrop-blur-2xl"
|
||||||
>
|
>
|
||||||
<nav class="container mx-auto px-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">
|
<div class="flex items-center justify-between">
|
||||||
<!-- Left side - Brand and Main navigation -->
|
<!-- Left side - Brand and Main navigation -->
|
||||||
<div class="flex items-center gap-8">
|
<div class="flex items-center gap-8">
|
||||||
@@ -91,9 +99,13 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<!-- Authentication Buttons -->
|
<!-- Authentication Buttons -->
|
||||||
<div class="hidden items-center gap-1 md:flex md:gap-3">
|
<div class="hidden items-center gap-1 md:flex md:gap-3">
|
||||||
<a href="/auth" class={getNavClasses('/auth')} aria-label="Sign in or register">
|
<a href="/auth/login" class={getNavClasses('/auth/login')} aria-label="Login">
|
||||||
<User class="size-4" aria-hidden="true" />
|
<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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -124,9 +136,13 @@
|
|||||||
Dashboard
|
Dashboard
|
||||||
</a>
|
</a>
|
||||||
{:else}
|
{: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" />
|
<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>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</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 = () => {
|
export const load: PageLoad = async ({ parent }) => {
|
||||||
throw redirect(302, '/auth');
|
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">
|
<script lang="ts">
|
||||||
import { supabase } from '$lib/supabaseClient';
|
import { supabase } from '$lib/supabase-client.js';
|
||||||
import { toaster } from '$lib';
|
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';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
const session = $derived(data.session);
|
const session = $derived(data.session);
|
||||||
|
|
||||||
let activeTab = $state('login'); // 'login' or 'signup'
|
|
||||||
let showPassword = $state(false);
|
let showPassword = $state(false);
|
||||||
let authError = $state('');
|
let authError = $state('');
|
||||||
|
|
||||||
onMount(() => {
|
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 urlParams = new URLSearchParams(window.location.search);
|
||||||
const hashParams = new URLSearchParams(window.location.hash.substring(1));
|
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
|
// Check for Supabase auth errors in URL params or hash
|
||||||
const error = urlParams.get('error') || hashParams.get('error');
|
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.'
|
description: 'Email/password authentication is disabled in this demo. Please use GitHub login instead.'
|
||||||
});
|
});
|
||||||
return;
|
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) {
|
async function handleOAuth(provider: string) {
|
||||||
@@ -209,7 +148,7 @@
|
|||||||
const { error } = await supabase.auth.signInWithOAuth({
|
const { error } = await supabase.auth.signInWithOAuth({
|
||||||
provider: provider as any,
|
provider: provider as any,
|
||||||
options: {
|
options: {
|
||||||
redirectTo: `${window.location.origin}/auth`
|
redirectTo: `${window.location.origin}/auth/register`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
@@ -225,22 +164,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchTab(tab: string) {
|
|
||||||
activeTab = tab;
|
|
||||||
message = '';
|
|
||||||
authError = '';
|
|
||||||
formData = { email: '', password: '' };
|
|
||||||
}
|
|
||||||
|
|
||||||
function dismissAuthError() {
|
function dismissAuthError() {
|
||||||
authError = '';
|
authError = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
console.log(session);
|
|
||||||
});
|
|
||||||
</script>
|
</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="container mx-auto py-20">
|
||||||
<div class="max-w-md mx-auto space-y-8">
|
<div class="max-w-md mx-auto space-y-8">
|
||||||
<!-- Demo Notice -->
|
<!-- Demo Notice -->
|
||||||
@@ -280,48 +213,15 @@
|
|||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<header class="text-center space-y-4">
|
<header class="text-center space-y-4">
|
||||||
<div class="flex items-center justify-center gap-2 mb-4">
|
<div class="flex items-center justify-center gap-2 mb-4">
|
||||||
{#if activeTab === 'login'}
|
<Star class="size-8 text-primary-500" />
|
||||||
<LogIn class="size-8 text-primary-500" />
|
<h1 class="h1">Get <span class="text-primary-500">Started</span></h1>
|
||||||
<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>
|
</div>
|
||||||
<p class="text-lg opacity-75">
|
<p class="text-lg opacity-75">
|
||||||
{#if activeTab === 'login'}
|
Create your account to start building with our comprehensive SaaS template.
|
||||||
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>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Tab Switcher -->
|
<!-- Register Form Card -->
|
||||||
<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 -->
|
|
||||||
<div class="card preset-outlined-primary-500 p-8 space-y-6">
|
<div class="card preset-outlined-primary-500 p-8 space-y-6">
|
||||||
<!-- Error Message -->
|
<!-- Error Message -->
|
||||||
{#if message}
|
{#if message}
|
||||||
@@ -360,10 +260,10 @@
|
|||||||
type={showPassword ? 'text' : 'password'}
|
type={showPassword ? 'text' : 'password'}
|
||||||
id="password"
|
id="password"
|
||||||
bind:value={formData.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}
|
disabled={true}
|
||||||
minlength={activeTab === 'signup' ? 6 : undefined}
|
minlength="6"
|
||||||
autocomplete={activeTab === 'login' ? 'current-password' : 'new-password'}
|
autocomplete="new-password"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -379,9 +279,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{#if activeTab === 'signup'}
|
<p class="text-xs opacity-50">Must be at least 6 characters long</p>
|
||||||
<p class="text-xs opacity-50">Must be at least 6 characters long</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</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"
|
class="btn preset-outlined-surface-200-800 w-full flex items-center justify-center gap-2 opacity-50 cursor-not-allowed"
|
||||||
disabled={true}
|
disabled={true}
|
||||||
>
|
>
|
||||||
{#if activeTab === 'login'}
|
<UserPlus class="size-4" />
|
||||||
<LogIn class="size-4" />
|
Create Account (Demo Disabled)
|
||||||
Sign In (Demo Disabled)
|
|
||||||
{:else}
|
|
||||||
<UserPlus class="size-4" />
|
|
||||||
Create Account (Demo Disabled)
|
|
||||||
{/if}
|
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Terms Notice for Signup -->
|
<!-- Terms Notice -->
|
||||||
{#if activeTab === 'signup'}
|
<p class="text-xs opacity-50 text-center">
|
||||||
<p class="text-xs opacity-50 text-center">
|
By creating an account, you agree to our
|
||||||
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>
|
||||||
<a href="/terms" class="text-primary-500 hover:text-primary-600 transition-colors">Terms of Service</a>
|
and
|
||||||
and
|
<a href="/privacy" class="text-primary-500 hover:text-primary-600 transition-colors">Privacy Policy</a>.
|
||||||
<a href="/privacy" class="text-primary-500 hover:text-primary-600 transition-colors">Privacy Policy</a>.
|
</p>
|
||||||
</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 -->
|
<!-- Divider -->
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
@@ -451,29 +333,15 @@
|
|||||||
|
|
||||||
<!-- Footer Links -->
|
<!-- Footer Links -->
|
||||||
<div class="text-center space-y-4">
|
<div class="text-center space-y-4">
|
||||||
{#if activeTab === 'login'}
|
<p class="text-sm opacity-75">
|
||||||
<p class="text-sm opacity-75">
|
Already have an account?
|
||||||
Don't have an account?
|
<a
|
||||||
<button
|
href="/auth/login"
|
||||||
type="button"
|
class="text-primary-500 hover:text-primary-600 transition-colors font-medium"
|
||||||
class="text-primary-500 hover:text-primary-600 transition-colors font-medium"
|
>
|
||||||
onclick={() => switchTab('signup')}
|
Sign in here
|
||||||
>
|
</a>
|
||||||
Create one here
|
</p>
|
||||||
</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}
|
|
||||||
<div class="flex items-center justify-center gap-4 text-sm opacity-50">
|
<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>
|
<a href="/privacy" class="hover:opacity-75 transition-opacity">Privacy Policy</a>
|
||||||
<span>•</span>
|
<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
|
// src/routes/blog/+page.server.ts
|
||||||
import { getAllPosts } from '$lib/server/blog';
|
import { getAllPosts } from '$lib/blog';
|
||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
export const load: PageServerLoad = async () => {
|
export const load: PageServerLoad = async () => {
|
||||||
|
// Get only metadata for listing (cheap operation)
|
||||||
return {
|
return {
|
||||||
posts: await getAllPosts()
|
posts: await getAllPosts()
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
day: 'numeric'
|
day: 'numeric'
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container mx-auto py-20 space-y-12 max-w-4xl">
|
<div class="container mx-auto py-20 space-y-12 max-w-4xl">
|
||||||
@@ -108,7 +109,7 @@
|
|||||||
<!-- Article Content -->
|
<!-- Article Content -->
|
||||||
<article class="prose dark:prose-invert prose-lg max-w-none">
|
<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">
|
<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>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
// src/routes/blog/[slug]/+page.server.ts
|
// src/routes/blog/[slug]/+page.server.ts
|
||||||
import { getPostBySlug } from '$lib/server/blog';
|
import { getPostBySlug } from '$lib/blog';
|
||||||
import { error } from '@sveltejs/kit';
|
import { error } from '@sveltejs/kit';
|
||||||
import type { MetaTagsProps } from 'svelte-meta-tags';
|
import type { MetaTagsProps } from 'svelte-meta-tags';
|
||||||
|
|
||||||
export const load = async ({ params, url }) => {
|
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);
|
const post = await getPostBySlug(params.slug);
|
||||||
|
|
||||||
if (!post) {
|
if (!post) {
|
||||||
@@ -1,13 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
// Icons
|
// Icons
|
||||||
import { Check, X, Star, Zap, Crown, Mail } from '@lucide/svelte';
|
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 = [
|
const plans = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
<Header {data} />
|
<Header {data} />
|
||||||
|
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<main class="min-h-screen p-4">
|
<main class="min-h-screen px-4">
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</main>
|
</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