Auth pages #6
13 changed files with 239 additions and 16 deletions
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
|
|
@ -61,6 +61,7 @@
|
||||||
"graphql-yoga": "^5.10.4",
|
"graphql-yoga": "^5.10.4",
|
||||||
"pino": "^9.5.0",
|
"pino": "^9.5.0",
|
||||||
"pino-pretty": "^13.0.0",
|
"pino-pretty": "^13.0.0",
|
||||||
|
"tailwind-merge": "^2.5.5",
|
||||||
"zod": "^3.24.0"
|
"zod": "^3.24.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
16
src/app.css
16
src/app.css
|
|
@ -1,3 +1,19 @@
|
||||||
@import 'tailwindcss/base';
|
@import 'tailwindcss/base';
|
||||||
@import 'tailwindcss/components';
|
@import 'tailwindcss/components';
|
||||||
@import 'tailwindcss/utilities';
|
@import 'tailwindcss/utilities';
|
||||||
|
|
||||||
|
:root {
|
||||||
|
@apply text-slate-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
@apply font-display text-4xl;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
@apply font-display text-3xl;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
@apply font-display text-2xl;
|
||||||
|
}
|
||||||
|
|
@ -9,12 +9,12 @@
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
primary = false
|
primary = false
|
||||||
}: {
|
}: {
|
||||||
type: HTMLButtonAttributes['type'];
|
type?: HTMLButtonAttributes['type'];
|
||||||
onClick: () => void;
|
onClick?: () => void;
|
||||||
label: string;
|
label: string;
|
||||||
size: 'small' | 'normal' | 'large';
|
size?: 'small' | 'normal' | 'large';
|
||||||
backgroundColor: string;
|
backgroundColor?: string;
|
||||||
primary: boolean;
|
primary?: boolean;
|
||||||
} = $props();
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.button {
|
.button {
|
||||||
@apply inline-block cursor-pointer rounded-full border-0 font-semibold leading-none transition-colors hover:opacity-80 hover:shadow-lg;
|
@apply inline-block cursor-pointer rounded-lg border-0 font-semibold leading-none transition-colors hover:opacity-80 hover:shadow-lg;
|
||||||
}
|
}
|
||||||
.button--small {
|
.button--small {
|
||||||
@apply px-2 py-0.5 text-sm;
|
@apply px-2 py-0.5 text-sm;
|
||||||
|
|
@ -40,7 +40,7 @@
|
||||||
@apply px-2 py-1 text-base;
|
@apply px-2 py-1 text-base;
|
||||||
}
|
}
|
||||||
.button--large {
|
.button--large {
|
||||||
@apply px-2.5 py-1 text-xl;
|
@apply px-3 py-1.5 text-lg;
|
||||||
}
|
}
|
||||||
.button--primary {
|
.button--primary {
|
||||||
@apply bg-red-600 text-white;
|
@apply bg-red-600 text-white;
|
||||||
|
|
|
||||||
43
src/lib/components/Input.stories.svelte
Normal file
43
src/lib/components/Input.stories.svelte
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
<script module lang="ts">
|
||||||
|
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||||
|
import Input from './Input.svelte';
|
||||||
|
|
||||||
|
const { Story } = defineMeta({
|
||||||
|
title: 'Input',
|
||||||
|
component: Input,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
type: {
|
||||||
|
control: 'select',
|
||||||
|
options: [
|
||||||
|
'number',
|
||||||
|
'button',
|
||||||
|
'checkbox',
|
||||||
|
'color',
|
||||||
|
'date',
|
||||||
|
'datetime-local',
|
||||||
|
'email',
|
||||||
|
'file',
|
||||||
|
'hidden',
|
||||||
|
'image',
|
||||||
|
'month',
|
||||||
|
'password',
|
||||||
|
'radio',
|
||||||
|
'range',
|
||||||
|
'reset',
|
||||||
|
'search',
|
||||||
|
'submit',
|
||||||
|
'tel',
|
||||||
|
'text',
|
||||||
|
'time',
|
||||||
|
'url',
|
||||||
|
'week'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Story name="Text" args={{ label: 'Text', name: 'text', type: 'text' }} />
|
||||||
|
<Story name="Password" args={{ label: 'Password', name: 'pass', type: 'password' }} />
|
||||||
|
<Story name="Email" args={{ label: 'Email', name: 'email', type: 'email' }} />
|
||||||
36
src/lib/components/Input.svelte
Normal file
36
src/lib/components/Input.svelte
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes, HTMLInputTypeAttribute } from 'svelte/elements';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
type InputProps = {
|
||||||
|
label?: string;
|
||||||
|
name: string;
|
||||||
|
type?: HTMLInputTypeAttribute;
|
||||||
|
} & HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
|
let { label, name, type = 'text', ...props }: InputProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div {...props} class={twMerge('hestia-input', props.class)}>
|
||||||
|
{#if label}
|
||||||
|
<label for={name}>{label}</label>
|
||||||
|
<div class="line"></div>
|
||||||
|
{/if}
|
||||||
|
<input {name} id={name} {type} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.line {
|
||||||
|
@apply h-8 border border-l-slate-200;
|
||||||
|
}
|
||||||
|
.hestia-input {
|
||||||
|
@apply flex w-fit items-center gap-2 rounded-lg bg-slate-100 p-2 text-slate-700 outline-blue-400 focus-within:outline;
|
||||||
|
}
|
||||||
|
.hestia-input > label {
|
||||||
|
@apply font-display text-lg;
|
||||||
|
}
|
||||||
|
.hestia-input > input {
|
||||||
|
all: unset;
|
||||||
|
@apply text-lg;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.navbar {
|
.navbar {
|
||||||
@apply flex items-center justify-between border-slate-200 bg-slate-100 px-6 py-2 font-display drop-shadow;
|
@apply flex items-center justify-between border-b border-slate-200 bg-slate-50 px-6 py-2 font-display drop-shadow;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar h1 {
|
.navbar h1 {
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,12 @@
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<div class="layout">
|
||||||
{@render children()}
|
{@render children()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.layout {
|
||||||
|
@apply h-screen w-screen animate-fade bg-slate-100;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
18
src/routes/+page.server.ts
Normal file
18
src/routes/+page.server.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { prisma } from '$lib/server/prisma';
|
||||||
|
|
||||||
|
export async function load(event) {
|
||||||
|
const userId = event.cookies.get('user');
|
||||||
|
if (!userId && isNaN(Number(userId))) {
|
||||||
|
return {
|
||||||
|
authenticated: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: {
|
||||||
|
id: Number(userId)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
authenticated: !!user
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -2,10 +2,10 @@
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import Loader from '$lib/components/Loader.svelte';
|
import Loader from '$lib/components/Loader.svelte';
|
||||||
|
|
||||||
|
let { data } = $props();
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const id = setTimeout(() => {
|
const id = setTimeout(() => (data.authenticated ? goto('/app') : goto('/login')), 1500);
|
||||||
goto('/app');
|
|
||||||
}, 1500);
|
|
||||||
return () => {
|
return () => {
|
||||||
clearTimeout(id);
|
clearTimeout(id);
|
||||||
};
|
};
|
||||||
|
|
@ -19,9 +19,6 @@
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.site-loader {
|
.site-loader {
|
||||||
@apply flex h-screen w-screen flex-col items-center justify-center gap-6 bg-slate-100;
|
@apply flex h-screen w-screen flex-col items-center justify-center gap-6;
|
||||||
}
|
|
||||||
.site-loader h1 {
|
|
||||||
@apply font-display text-4xl;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
46
src/routes/login/+page.server.ts
Normal file
46
src/routes/login/+page.server.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { logger } from '$lib/server/logger';
|
||||||
|
import { prisma } from '$lib/server/prisma';
|
||||||
|
import { error, redirect, type Actions } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
login: async (event) => {
|
||||||
|
const form = await event.request.formData();
|
||||||
|
if (!form.has('email')) {
|
||||||
|
return error(400, 'Email is a required form field!');
|
||||||
|
}
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: {
|
||||||
|
email: form.get('email') as string
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!user) {
|
||||||
|
logger.error('User not found! ${user}');
|
||||||
|
return error(401);
|
||||||
|
}
|
||||||
|
event.cookies.set('user', String(user.id), {
|
||||||
|
path: '/',
|
||||||
|
maxAge: 120
|
||||||
|
});
|
||||||
|
redirect(302, '/');
|
||||||
|
},
|
||||||
|
register: async (event) => {
|
||||||
|
const form = await event.request.formData();
|
||||||
|
if (!form.has('email') || !form.has('name')) {
|
||||||
|
return error(400);
|
||||||
|
}
|
||||||
|
const user = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
email: form.get('email') as string,
|
||||||
|
name: form.get('name') as string
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!user) {
|
||||||
|
return error(500);
|
||||||
|
}
|
||||||
|
event.cookies.set('user', String(user.id), {
|
||||||
|
path: '/',
|
||||||
|
maxAge: 120
|
||||||
|
});
|
||||||
|
redirect(302, '/');
|
||||||
|
}
|
||||||
|
} satisfies Actions;
|
||||||
49
src/routes/login/+page.svelte
Normal file
49
src/routes/login/+page.svelte
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import Button from '$lib/components/Button.svelte';
|
||||||
|
import Input from '$lib/components/Input.svelte';
|
||||||
|
import { fade, scale } from 'svelte/transition';
|
||||||
|
|
||||||
|
let mode: 'register' | 'login' = $state('login');
|
||||||
|
let action = $derived(mode === 'login' ? '?/login' : '?/register');
|
||||||
|
|
||||||
|
function onViewToggle() {
|
||||||
|
mode = mode === 'login' ? 'register' : 'login';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="page">
|
||||||
|
<h1 class="underline">Hestia</h1>
|
||||||
|
<div class="login">
|
||||||
|
<form method="POST" {action} transition:scale>
|
||||||
|
<h2 transition:fade>{mode === 'login' ? 'Login' : 'Register'}</h2>
|
||||||
|
{#if mode === 'register'}
|
||||||
|
<div transition:fade>
|
||||||
|
<Input label="Name" name="name" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<Input label="Email" name="email" type="email" />
|
||||||
|
<Input label="Password" name="password" type="password" />
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={onViewToggle}
|
||||||
|
label={mode === 'login' ? 'Register' : 'Login'}
|
||||||
|
size="large"
|
||||||
|
primary
|
||||||
|
/>
|
||||||
|
<Button type="submit" label="Submit" size="large" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.page {
|
||||||
|
@apply flex flex-col items-center justify-around gap-24 py-[10%];
|
||||||
|
}
|
||||||
|
.login {
|
||||||
|
@apply w-fit max-w-lg animate-fade rounded-lg bg-white p-8;
|
||||||
|
}
|
||||||
|
.login > form {
|
||||||
|
@apply flex w-full flex-col items-center gap-8 rounded-lg;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -8,6 +8,15 @@ export default {
|
||||||
extend: {
|
extend: {
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
display: ['Baskervville SC']
|
display: ['Baskervville SC']
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
fade: 'fadeIn .5s ease-in-out'
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
fadeIn: {
|
||||||
|
from: { opacity: '0' },
|
||||||
|
to: { opacity: '1' }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue