Refactor login page to support user registration. Enhanced form handling by adding name and password confirmation fields, improved error messaging, and updated button labels for better user experience. Adjusted internal server options to reflect changes in form data structure.
This commit is contained in:
@@ -24,7 +24,7 @@ export const options = {
|
||||
app: ({ head, body, assets, nonce, env }) => "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <link rel=\"icon\" href=\"" + assets + "/favicon.png\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=5, viewport-fit=cover\" />\n <meta name=\"theme-color\" content=\"#0f172a\" />\n " + head + "\n </head>\n <body data-sveltekit-preload-data=\"hover\">\n <div style=\"display: contents\">" + body + "</div>\n </body>\n</html>\n",
|
||||
error: ({ status, message }) => "<!doctype html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\" />\n\t\t<title>" + message + "</title>\n\n\t\t<style>\n\t\t\tbody {\n\t\t\t\t--bg: white;\n\t\t\t\t--fg: #222;\n\t\t\t\t--divider: #ccc;\n\t\t\t\tbackground: var(--bg);\n\t\t\t\tcolor: var(--fg);\n\t\t\t\tfont-family:\n\t\t\t\t\tsystem-ui,\n\t\t\t\t\t-apple-system,\n\t\t\t\t\tBlinkMacSystemFont,\n\t\t\t\t\t'Segoe UI',\n\t\t\t\t\tRoboto,\n\t\t\t\t\tOxygen,\n\t\t\t\t\tUbuntu,\n\t\t\t\t\tCantarell,\n\t\t\t\t\t'Open Sans',\n\t\t\t\t\t'Helvetica Neue',\n\t\t\t\t\tsans-serif;\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t\tjustify-content: center;\n\t\t\t\theight: 100vh;\n\t\t\t\tmargin: 0;\n\t\t\t}\n\n\t\t\t.error {\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t\tmax-width: 32rem;\n\t\t\t\tmargin: 0 1rem;\n\t\t\t}\n\n\t\t\t.status {\n\t\t\t\tfont-weight: 200;\n\t\t\t\tfont-size: 3rem;\n\t\t\t\tline-height: 1;\n\t\t\t\tposition: relative;\n\t\t\t\ttop: -0.05rem;\n\t\t\t}\n\n\t\t\t.message {\n\t\t\t\tborder-left: 1px solid var(--divider);\n\t\t\t\tpadding: 0 0 0 1rem;\n\t\t\t\tmargin: 0 0 0 1rem;\n\t\t\t\tmin-height: 2.5rem;\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t}\n\n\t\t\t.message h1 {\n\t\t\t\tfont-weight: 400;\n\t\t\t\tfont-size: 1em;\n\t\t\t\tmargin: 0;\n\t\t\t}\n\n\t\t\t@media (prefers-color-scheme: dark) {\n\t\t\t\tbody {\n\t\t\t\t\t--bg: #222;\n\t\t\t\t\t--fg: #ddd;\n\t\t\t\t\t--divider: #666;\n\t\t\t\t}\n\t\t\t}\n\t\t</style>\n\t</head>\n\t<body>\n\t\t<div class=\"error\">\n\t\t\t<span class=\"status\">" + status + "</span>\n\t\t\t<div class=\"message\">\n\t\t\t\t<h1>" + message + "</h1>\n\t\t\t</div>\n\t\t</div>\n\t</body>\n</html>\n"
|
||||
},
|
||||
version_hash: "1pywl2x"
|
||||
version_hash: "3lzvrw"
|
||||
};
|
||||
|
||||
export async function get_hooks() {
|
||||
|
||||
@@ -8,37 +8,97 @@ export const load = async ({ url, locals }: Parameters<PageServerLoad>[0]) => {
|
||||
}
|
||||
return {
|
||||
redirectTo: url.searchParams.get('redirectTo') ?? '',
|
||||
error: null
|
||||
error: null,
|
||||
form: null as { error?: string; redirectTo?: string } | null
|
||||
};
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
default: async ({ request, locals, url }: import('./$types').RequestEvent) => {
|
||||
const formData = await request.formData();
|
||||
const email = String(formData.get('email') ?? '').trim();
|
||||
const password = String(formData.get('password') ?? '');
|
||||
const redirectTo = String(formData.get('redirectTo') ?? '').trim() || '/';
|
||||
const loginAction = async ({
|
||||
request,
|
||||
locals
|
||||
}: {
|
||||
request: Request;
|
||||
locals: App.Locals;
|
||||
}) => {
|
||||
const formData = await request.formData();
|
||||
const intent = String(formData.get('intent') ?? 'login').trim();
|
||||
const name = String(formData.get('name') ?? '').trim();
|
||||
const email = String(formData.get('email') ?? '').trim();
|
||||
const password = String(formData.get('password') ?? '');
|
||||
const passwordConfirm = String(formData.get('passwordConfirm') ?? '');
|
||||
const redirectTo = String(formData.get('redirectTo') ?? '').trim() || '/';
|
||||
|
||||
if (intent === 'register') {
|
||||
if (!name) {
|
||||
return {
|
||||
redirectTo,
|
||||
form: { error: 'Name is required.', redirectTo }
|
||||
};
|
||||
}
|
||||
if (!email || !password) {
|
||||
return {
|
||||
redirectTo,
|
||||
error: 'Email and password are required.'
|
||||
form: { error: 'Email and password are required.', redirectTo }
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
await locals.pb.collection('users').authWithPassword(email, password);
|
||||
} catch (err) {
|
||||
const message = err && typeof err === 'object' && 'message' in err
|
||||
? String((err as { message: string }).message)
|
||||
: 'Invalid email or password.';
|
||||
if (password.length < 8) {
|
||||
return {
|
||||
redirectTo,
|
||||
error: message
|
||||
form: { error: 'Password must be at least 8 characters.', redirectTo }
|
||||
};
|
||||
}
|
||||
if (password !== passwordConfirm) {
|
||||
return {
|
||||
redirectTo,
|
||||
form: { error: 'Passwords do not match.', redirectTo }
|
||||
};
|
||||
}
|
||||
try {
|
||||
await locals.pb.collection('users').create({
|
||||
name,
|
||||
email,
|
||||
password,
|
||||
passwordConfirm: password,
|
||||
Stackq: true
|
||||
});
|
||||
await locals.pb.collection('users').authWithPassword(email, password);
|
||||
} catch (err) {
|
||||
const message =
|
||||
err && typeof err === 'object' && 'message' in err
|
||||
? String((err as { message: string }).message)
|
||||
: 'Could not create account. Email may already be in use.';
|
||||
return {
|
||||
redirectTo,
|
||||
form: { error: message, redirectTo }
|
||||
};
|
||||
}
|
||||
|
||||
throw redirect(303, redirectTo);
|
||||
}
|
||||
|
||||
if (!email || !password) {
|
||||
return {
|
||||
redirectTo,
|
||||
form: { error: 'Email and password are required.', redirectTo }
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
await locals.pb.collection('users').authWithPassword(email, password);
|
||||
} catch (err) {
|
||||
const message =
|
||||
err && typeof err === 'object' && 'message' in err
|
||||
? String((err as { message: string }).message)
|
||||
: 'Invalid email or password.';
|
||||
return {
|
||||
redirectTo,
|
||||
form: { error: message, redirectTo }
|
||||
};
|
||||
}
|
||||
|
||||
throw redirect(303, redirectTo);
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
default: loginAction
|
||||
};
|
||||
;null as any as Actions;
|
||||
@@ -7,36 +7,96 @@ export const load: PageServerLoad = async ({ url, locals }) => {
|
||||
}
|
||||
return {
|
||||
redirectTo: url.searchParams.get('redirectTo') ?? '',
|
||||
error: null
|
||||
error: null,
|
||||
form: null as { error?: string; redirectTo?: string } | null
|
||||
};
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async ({ request, locals, url }) => {
|
||||
const formData = await request.formData();
|
||||
const email = String(formData.get('email') ?? '').trim();
|
||||
const password = String(formData.get('password') ?? '');
|
||||
const redirectTo = String(formData.get('redirectTo') ?? '').trim() || '/';
|
||||
const loginAction = async ({
|
||||
request,
|
||||
locals
|
||||
}: {
|
||||
request: Request;
|
||||
locals: App.Locals;
|
||||
}) => {
|
||||
const formData = await request.formData();
|
||||
const intent = String(formData.get('intent') ?? 'login').trim();
|
||||
const name = String(formData.get('name') ?? '').trim();
|
||||
const email = String(formData.get('email') ?? '').trim();
|
||||
const password = String(formData.get('password') ?? '');
|
||||
const passwordConfirm = String(formData.get('passwordConfirm') ?? '');
|
||||
const redirectTo = String(formData.get('redirectTo') ?? '').trim() || '/';
|
||||
|
||||
if (intent === 'register') {
|
||||
if (!name) {
|
||||
return {
|
||||
redirectTo,
|
||||
form: { error: 'Name is required.', redirectTo }
|
||||
};
|
||||
}
|
||||
if (!email || !password) {
|
||||
return {
|
||||
redirectTo,
|
||||
error: 'Email and password are required.'
|
||||
form: { error: 'Email and password are required.', redirectTo }
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
await locals.pb.collection('users').authWithPassword(email, password);
|
||||
} catch (err) {
|
||||
const message = err && typeof err === 'object' && 'message' in err
|
||||
? String((err as { message: string }).message)
|
||||
: 'Invalid email or password.';
|
||||
if (password.length < 8) {
|
||||
return {
|
||||
redirectTo,
|
||||
error: message
|
||||
form: { error: 'Password must be at least 8 characters.', redirectTo }
|
||||
};
|
||||
}
|
||||
if (password !== passwordConfirm) {
|
||||
return {
|
||||
redirectTo,
|
||||
form: { error: 'Passwords do not match.', redirectTo }
|
||||
};
|
||||
}
|
||||
try {
|
||||
await locals.pb.collection('users').create({
|
||||
name,
|
||||
email,
|
||||
password,
|
||||
passwordConfirm: password,
|
||||
Stackq: true
|
||||
});
|
||||
await locals.pb.collection('users').authWithPassword(email, password);
|
||||
} catch (err) {
|
||||
const message =
|
||||
err && typeof err === 'object' && 'message' in err
|
||||
? String((err as { message: string }).message)
|
||||
: 'Could not create account. Email may already be in use.';
|
||||
return {
|
||||
redirectTo,
|
||||
form: { error: message, redirectTo }
|
||||
};
|
||||
}
|
||||
|
||||
throw redirect(303, redirectTo);
|
||||
}
|
||||
|
||||
if (!email || !password) {
|
||||
return {
|
||||
redirectTo,
|
||||
form: { error: 'Email and password are required.', redirectTo }
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
await locals.pb.collection('users').authWithPassword(email, password);
|
||||
} catch (err) {
|
||||
const message =
|
||||
err && typeof err === 'object' && 'message' in err
|
||||
? String((err as { message: string }).message)
|
||||
: 'Invalid email or password.';
|
||||
return {
|
||||
redirectTo,
|
||||
form: { error: message, redirectTo }
|
||||
};
|
||||
}
|
||||
|
||||
throw redirect(303, redirectTo);
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
default: loginAction
|
||||
};
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
formAction: string;
|
||||
} = $props();
|
||||
|
||||
let isRegister = $state(false);
|
||||
|
||||
const redirectTo = $derived(data.form?.redirectTo ?? data.redirectTo ?? '');
|
||||
const error = $derived(data.form?.error);
|
||||
</script>
|
||||
@@ -17,17 +19,35 @@
|
||||
<div class="flex min-h-0 flex-1 flex-col items-center justify-center p-4">
|
||||
<Card.Root class="w-full max-w-sm">
|
||||
<Card.Header>
|
||||
<Card.Title>Sign in</Card.Title>
|
||||
<Card.Description>Enter your email and password.</Card.Description>
|
||||
<Card.Title>{isRegister ? 'Create account' : 'Sign in'}</Card.Title>
|
||||
<Card.Description>
|
||||
{isRegister
|
||||
? 'Enter your email and a password (min 8 characters).'
|
||||
: 'Enter your email and password.'}
|
||||
</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<Card.Content class="flex flex-col gap-4">
|
||||
<form
|
||||
method="POST"
|
||||
action={formAction}
|
||||
use:enhance
|
||||
class="flex flex-col gap-4"
|
||||
>
|
||||
<input type="hidden" name="intent" value={isRegister ? 'register' : 'login'} />
|
||||
<input type="hidden" name="redirectTo" value={redirectTo} />
|
||||
{#if isRegister}
|
||||
<div class="grid gap-2">
|
||||
<Label for="name">Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="Your name"
|
||||
required
|
||||
autocomplete="name"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="grid gap-2">
|
||||
<Label for="email">Email</Label>
|
||||
<Input
|
||||
@@ -36,7 +56,7 @@
|
||||
type="email"
|
||||
placeholder="you@example.com"
|
||||
required
|
||||
autocomplete="email"
|
||||
autocomplete={isRegister ? 'email' : 'email'}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
@@ -46,14 +66,37 @@
|
||||
name="password"
|
||||
type="password"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
minlength={isRegister ? 8 : undefined}
|
||||
autocomplete={isRegister ? 'new-password' : 'current-password'}
|
||||
/>
|
||||
</div>
|
||||
{#if isRegister}
|
||||
<div class="grid gap-2">
|
||||
<Label for="passwordConfirm">Confirm password</Label>
|
||||
<Input
|
||||
id="passwordConfirm"
|
||||
name="passwordConfirm"
|
||||
type="password"
|
||||
required
|
||||
minlength={8}
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if error}
|
||||
<p class="text-sm text-destructive">{error}</p>
|
||||
{/if}
|
||||
<Button type="submit" class="w-full">Sign in</Button>
|
||||
<Button type="submit" class="w-full">
|
||||
{isRegister ? 'Create account' : 'Sign in'}
|
||||
</Button>
|
||||
</form>
|
||||
<button
|
||||
type="button"
|
||||
class="text-muted-foreground hover:text-foreground text-center text-sm underline"
|
||||
onclick={() => (isRegister = !isRegister)}
|
||||
>
|
||||
{isRegister ? 'Already have an account? Sign in' : 'Create an account'}
|
||||
</button>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user