feat(*): Add basic site functionality including sign up, sign in and sign out

This commit is contained in:
Josh Creek
2024-04-29 20:18:18 +01:00
parent 6794580c01
commit 079ccad6ba
13 changed files with 644 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
PUBLIC_SUPABASE_URL=""
PUBLIC_SUPABASE_ANON_KEY=""
+71
View File
@@ -0,0 +1,71 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
function emitSignedInEvent() {
dispatch('signedIn', {});
}
let email = '';
let password = '';
// Access the supabase client from the layout data
export let supabase: any;
async function signInWithEmail() {
// TODO use the data and error from the response
const { data, error } = await supabase.auth
.signInWithPassword({
email: email,
password: password
})
.then(() => {
emitSignedInEvent();
});
}
</script>
<div class="card shrink-0 w-full max-w-sm shadow-2xl bg-base-100">
<form class="card-body">
<div class="form-control">
<label class="input input-bordered flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4 opacity-70"
><path
d="M2.5 3A1.5 1.5 0 0 0 1 4.5v.793c.026.009.051.02.076.032L7.674 8.51c.206.1.446.1.652 0l6.598-3.185A.755.755 0 0 1 15 5.293V4.5A1.5 1.5 0 0 0 13.5 3h-11Z"
/><path
d="M15 6.954 8.978 9.86a2.25 2.25 0 0 1-1.956 0L1 6.954V11.5A1.5 1.5 0 0 0 2.5 13h11a1.5 1.5 0 0 0 1.5-1.5V6.954Z"
/>
</svg>
<input type="text" class="grow" placeholder="Email" required bind:value={email} />
</label>
</div>
<div class="form-control">
<label class="input input-bordered flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4 opacity-70"
><path
fill-rule="evenodd"
d="M14 6a4 4 0 0 1-4.899 3.899l-1.955 1.955a.5.5 0 0 1-.353.146H5v1.5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-2.293a.5.5 0 0 1 .146-.353l3.955-3.955A4 4 0 1 1 14 6Zm-4-2a.75.75 0 0 0 0 1.5.5.5 0 0 1 .5.5.75.75 0 0 0 1.5 0 2 2 0 0 0-2-2Z"
clip-rule="evenodd"
/>
</svg>
<input type="password" class="grow" placeholder="Password" required bind:value={password} />
</label>
</div>
<div class="form-control">
<a href="/" id="forgot-password" class="label-text-alt link link-hover">
Forgot your password?
</a>
</div>
<div class="form-control mt-2">
<button class="btn btn-primary" on:click={signInWithEmail}>Sign In</button>
</div>
</form>
</div>
+20
View File
@@ -0,0 +1,20 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
function emitSignedOutEvent() {
dispatch('signedOut', {});
}
// Access the supabase client from the layout data
export let supabase: any;
async function signOut() {
// TODO use the error from the response
const { error } = await supabase.auth.signOut().then(() => {
emitSignedOutEvent();
});
}
</script>
<button on:click={signOut}>Sign Out</button>
+67
View File
@@ -0,0 +1,67 @@
<script lang="ts">
let email = '';
let password = '';
// Access the supabase client from the layout data
export let supabase: any;
async function signUpNewUser() {
try {
const { data, error } = await supabase.auth.signUp({
email: email,
password: password,
options: {
// Redirect URL after successful sign-up
redirectTo: '/welcome'
}
});
if (error) {
throw error;
}
// Handle success (optional)
} catch (error: any) {
console.error('Sign up error:', error.message);
// Handle error
}
}
</script>
<form class="card-body">
<div class="form-control">
<label class="input input-bordered flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4 opacity-70"
><path
d="M2.5 3A1.5 1.5 0 0 0 1 4.5v.793c.026.009.051.02.076.032L7.674 8.51c.206.1.446.1.652 0l6.598-3.185A.755.755 0 0 1 15 5.293V4.5A1.5 1.5 0 0 0 13.5 3h-11Z"
/><path
d="M15 6.954 8.978 9.86a2.25 2.25 0 0 1-1.956 0L1 6.954V11.5A1.5 1.5 0 0 0 2.5 13h11a1.5 1.5 0 0 0 1.5-1.5V6.954Z"
/>
</svg>
<input type="text" class="grow" placeholder="Email" required bind:value={email} />
</label>
</div>
<div class="form-control">
<label class="input input-bordered flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4 opacity-70"
><path
fill-rule="evenodd"
d="M14 6a4 4 0 0 1-4.899 3.899l-1.955 1.955a.5.5 0 0 1-.353.146H5v1.5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-2.293a.5.5 0 0 1 .146-.353l3.955-3.955A4 4 0 1 1 14 6Zm-4-2a.75.75 0 0 0 0 1.5.5.5 0 0 1 .5.5.75.75 0 0 0 1.5 0 2 2 0 0 0-2-2Z"
clip-rule="evenodd"
/>
</svg>
<input type="password" class="grow" placeholder="Password" required bind:value={password} />
</label>
</div>
<div class="form-control mt-6">
<button class="btn btn-primary" on:click={signUpNewUser}>Sign Up</button>
</div>
</form>
+1
View File
@@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.
+5
View File
@@ -0,0 +1,5 @@
import { writable } from 'svelte/store';
import { type User } from '@supabase/auth-js';
// Create a user store with null as the initial value
export const user = writable<User | null>(null);
+10
View File
@@ -0,0 +1,10 @@
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async ({ locals: { safeGetSession } }) => {
const { session, user } = await safeGetSession();
return {
session,
user
};
};
+230
View File
@@ -0,0 +1,230 @@
<script lang="ts">
import 'tailwindcss/tailwind.css';
import { onDestroy, onMount } from 'svelte';
import { user } from '$lib/stores/user.js';
import { type User } from '@supabase/auth-js';
import SignIn from '$lib/components/SignIn.svelte';
import SignOut from '$lib/components/SignOut.svelte';
export let data;
let { supabase } = data;
$: ({ supabase } = data);
let localUser = null as User | null;
const unsubscribe = user.subscribe((value) => {
localUser = value;
});
onDestroy(unsubscribe);
onMount(async () => {
await getUser();
});
async function getUser() {
const {
data: { session }
} = await supabase.auth.getSession();
if (session) {
localUser = session.user;
} else {
localUser = null;
}
// Update the store so that user is available to all components
user.set(localUser);
}
</script>
<svelte:head>
<meta name="description" content="" />
<meta property="og:title" content="" />
<meta property="og:url" content="" />
<meta property="og:description" content="" />
<link rel="canonical" href="" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
</svelte:head>
<header>
<div class="navbar bg-primary text-primary-content">
<div class="navbar-start">
<div class="dropdown">
<div tabindex="0" role="button" class="btn btn-ghost lg:hidden">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h8m-8 6h16"
/></svg
>
</div>
<ul
class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 text-base-content rounded-box w-52"
>
<li><a href="/">Item 1</a></li>
<li>
<!-- svelte-ignore a11y-missing-attribute -->
<a>Parent</a>
<ul class="p-2">
<li><a href="/">Submenu 1</a></li>
<li><a href="/">Submenu 2</a></li>
</ul>
</li>
<li><a href="/">Item 3</a></li>
</ul>
</div>
<a href="/" class="btn btn-ghost text-xl">daisyUI</a>
</div>
<div class="navbar-center hidden lg:flex">
<ul class="menu menu-horizontal px-1">
<li><a href="/">Item 1</a></li>
<li>
<details>
<summary>Parent</summary>
<ul class="p-2 text-base-content">
<li><a href="/">Submenu 1</a></li>
<li><a href="/">Submenu 2</a></li>
</ul>
</details>
</li>
<li><a href="/">Item 3</a></li>
</ul>
</div>
<div class="navbar-end">
<button class="btn btn-ghost btn-circle">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/></svg
>
</button>
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost btn-circle avatar">
<div class="w-10 rounded-full">
{#if localUser}
<div class="user-circle text-primary-content border-primary-content">
{localUser.email[0].toUpperCase()}
</div>
{:else}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path
fill-rule="evenodd"
d="M18.685 19.097A9.723 9.723 0 0 0 21.75 12c0-5.385-4.365-9.75-9.75-9.75S2.25 6.615 2.25 12a9.723 9.723 0 0 0 3.065 7.097A9.716 9.716 0 0 0 12 21.75a9.716 9.716 0 0 0 6.685-2.653Zm-12.54-1.285A7.486 7.486 0 0 1 12 15a7.486 7.486 0 0 1 5.855 2.812A8.224 8.224 0 0 1 12 20.25a8.224 8.224 0 0 1-5.855-2.438ZM15.75 9a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z"
clip-rule="evenodd"
/>
</svg>
{/if}
</div>
</div>
{#if localUser}
<ul
tabindex="-1"
class="mt-3 z-[1] p-2 shadow menu menu-sm dropdown-content bg-base-100 text-base-content rounded-box min-w-[13rem] w-auto"
>
<li>
<a href="/profile"> Profile </a>
</li>
<li><a href="/settings">Settings</a></li>
<li><SignOut {supabase} on:signedOut={getUser} /></li>
</ul>
{:else}
<div
class="mt-3 z-[1] p-2 shadow menu menu-sm dropdown-content bg-base-100 text-base-content rounded-box min-w-[13rem] w-auto"
>
<SignIn {supabase} on:signedIn={getUser} />
</div>
{/if}
</div>
</div>
</div>
</header>
<main>
<slot />
</main>
<footer class="footer items-center p-4 bg-neutral text-neutral-content">
<aside class="items-center grid-flow-col">
<svg
width="36"
height="36"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
fill-rule="evenodd"
clip-rule="evenodd"
class="fill-current"
>
<path
d="M22.672 15.226l-2.432.811.841 2.515c.33 1.019-.209 2.127-1.23 2.456-1.15.325-2.148-.321-2.463-1.226l-.84-2.518-5.013 1.677.84 2.517c.391 1.203-.434 2.542-1.831 2.542-.88 0-1.601-.564-1.86-1.314l-.842-2.516-2.431.809c-1.135.328-2.145-.317-2.463-1.229-.329-1.018.211-2.127 1.231-2.456l2.432-.809-1.621-4.823-2.432.808c-1.355.384-2.558-.59-2.558-1.839 0-.817.509-1.582 1.327-1.846l2.433-.809-.842-2.515c-.33-1.02.211-2.129 1.232-2.458 1.02-.329 2.13.209 2.461 1.229l.842 2.515 5.011-1.677-.839-2.517c-.403-1.238.484-2.553 1.843-2.553.819 0 1.585.509 1.85 1.326l.841 2.517 2.431-.81c1.02-.33 2.131.211 2.461 1.229.332 1.018-.21 2.126-1.23 2.456l-2.433.809 1.622 4.823 2.433-.809c1.242-.401 2.557.484 2.557 1.838 0 .819-.51 1.583-1.328 1.847m-8.992-6.428l-5.01 1.675 1.619 4.828 5.011-1.674-1.62-4.829z"
></path>
</svg>
<p>Copyright © 2024 - All right reserved</p>
</aside>
<nav class="grid-flow-col gap-4 md:place-self-center md:justify-self-end">
<a href="/">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
class="fill-current"
><path
d="M24 4.557c-.883.392-1.832.656-2.828.775 1.017-.609 1.798-1.574 2.165-2.724-.951.564-2.005.974-3.127 1.195-.897-.957-2.178-1.555-3.594-1.555-3.179 0-5.515 2.966-4.797 6.045-4.091-.205-7.719-2.165-10.148-5.144-1.29 2.213-.669 5.108 1.523 6.574-.806-.026-1.566-.247-2.229-.616-.054 2.281 1.581 4.415 3.949 4.89-.693.188-1.452.232-2.224.084.626 1.956 2.444 3.379 4.6 3.419-2.07 1.623-4.678 2.348-7.29 2.04 2.179 1.397 4.768 2.212 7.548 2.212 9.142 0 14.307-7.721 13.995-14.646.962-.695 1.797-1.562 2.457-2.549z"
></path>
</svg>
</a>
<a href="/"
><svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
class="fill-current"
><path
d="M19.615 3.184c-3.604-.246-11.631-.245-15.23 0-3.897.266-4.356 2.62-4.385 8.816.029 6.185.484 8.549 4.385 8.816 3.6.245 11.626.246 15.23 0 3.897-.266 4.356-2.62 4.385-8.816-.029-6.185-.484-8.549-4.385-8.816zm-10.615 12.816v-8l8 3.993-8 4.007z"
></path>
</svg>
</a>
<a href="/">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
class="fill-current"
><path
d="M9 8h-3v4h3v12h5v-12h3.642l.358-4h-4v-1.667c0-.955.192-1.333 1.115-1.333h2.885v-5h-3.808c-3.596 0-5.192 1.583-5.192 4.615v3.385z"
></path>
</svg>
</a>
</nav>
</footer>
<style scoped>
.user-circle {
width: 40px;
height: 40px;
border-radius: 50%;
border: 2px solid;
/* background-color: red; */
display: flex;
justify-content: center;
align-items: center;
font-size: 20px; /* Adjust font size as needed */
}
</style>
+34
View File
@@ -0,0 +1,34 @@
import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public';
import type { LayoutLoad } from './$types';
import { createBrowserClient, isBrowser, parse } from '@supabase/ssr';
export const load: LayoutLoad = async ({ fetch, data, depends }) => {
depends('supabase:auth');
const supabase = createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
global: {
fetch
},
cookies: {
get(key) {
if (!isBrowser()) {
return JSON.stringify(data.session);
}
const cookie = parse(document.cookie);
return cookie[key];
}
}
});
/**
* It's fine to use `getSession` here, because on the client, `getSession` is
* safe, and on the server, it reads `session` from the `LayoutData`, which
* safely checked the session using `safeGetSession`.
*/
const {
data: { session }
} = await supabase.auth.getSession();
return { supabase, session };
};
+150
View File
@@ -0,0 +1,150 @@
<script lang="ts">
import { onDestroy } from 'svelte';
import { user } from '$lib/stores/user.js';
import { type User } from '@supabase/auth-js';
import SignUp from '$lib/components/SignUp.svelte';
export let data;
let { supabase } = data;
$: ({ supabase } = data);
let localUser: User | null;
const unsubscribe = user.subscribe((value) => {
localUser = value;
});
onDestroy(unsubscribe);
</script>
<div class="hero bg-base-100 my-36">
<div class="hero-content flex-col lg:flex-row-reverse">
<div class="text-center lg:text-left">
<h1 class="text-5xl font-bold">Sign up for this amazing SaaS right now - don't miss out!</h1>
</div>
<div class="card shrink-0 w-full max-w-sm shadow-2xl">
<SignUp {supabase} />
</div>
</div>
</div>
<div
class="md:container md:mx-auto justify-self-center flex justify-center items-center h-full flex-col mb-10"
>
<div class="stats stats-vertical lg:stats-horizontal shadow text-center">
<div class="stat">
<div class="stat-title">Conversions</div>
<div class="stat-value">310K</div>
<div class="stat-desc">Since January 2024</div>
</div>
<div class="stat">
<div class="stat-title">New Users</div>
<div class="stat-value">4,200</div>
<div class="stat-desc">↗︎ 400 (22%)</div>
</div>
<div class="stat">
<div class="stat-title">Contracts Completed</div>
<div class="stat-value">1,200</div>
<div class="stat-desc">↘︎ 90 (14%)</div>
</div>
</div>
<div class="features mt-8">
<h1 class="flex justify-center text-4xl font-bold mb-5">Features</h1>
<ul class="space-y-6">
<li class="p-6 rounded-lg shadow-md">
<div class="flex items-start space-x-4">
<svg
class="w-8 h-8 text-green-500 flex-shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"
></path>
</svg>
<div>
<h2 class="text-lg font-semibold">Blah</h2>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc ut ex blandit, hendrerit
dolor in, egestas erat. In hac habitasse platea dictumst. Vestibulum consequat arcu
ultrices, dignissim quam consectetur, mattis turpis. Quisque ligula est, consectetur a
nunc eget, efficitur euismod sapien.
</p>
</div>
</div>
</li>
<li class="p-6 rounded-lg shadow-md">
<div class="flex items-start space-x-4">
<svg
class="w-8 h-8 text-green-500 flex-shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"
></path>
</svg>
<div>
<h2 class="text-lg font-semibold">Blah</h2>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc ut ex blandit, hendrerit
dolor in, egestas erat. In hac habitasse platea dictumst. Vestibulum consequat arcu
ultrices, dignissim quam consectetur, mattis turpis. Quisque ligula est, consectetur a
nunc eget, efficitur euismod sapien.
</p>
</div>
</div>
</li>
<li class="p-6 rounded-lg shadow-md">
<div class="flex items-start space-x-4">
<svg
class="w-8 h-8 text-green-500 flex-shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"
></path>
</svg>
<div>
<h2 class="text-lg font-semibold">Blah</h2>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc ut ex blandit, hendrerit
dolor in, egestas erat. In hac habitasse platea dictumst. Vestibulum consequat arcu
ultrices, dignissim quam consectetur, mattis turpis. Quisque ligula est, consectetur a
nunc eget, efficitur euismod sapien.
</p>
</div>
</div>
</li>
<li class="p-6 rounded-lg shadow-md">
<div class="flex items-start space-x-4">
<svg
class="w-8 h-8 text-green-500 flex-shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"
></path>
</svg>
<div>
<h2 class="text-lg font-semibold">Blah</h2>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc ut ex blandit, hendrerit
dolor in, egestas erat. In hac habitasse platea dictumst. Vestibulum consequat arcu
ultrices, dignissim quam consectetur, mattis turpis. Quisque ligula est, consectetur a
nunc eget, efficitur euismod sapien.
</p>
</div>
</div>
</li>
</ul>
</div>
</div>
+9
View File
@@ -0,0 +1,9 @@
import { dev } from '$app/environment';
// we don't need any JS on this page, though we'll load
// it in dev so that we get hot module replacement...
export const csr = dev;
// since there's no dynamic data here, we can prerender
// it so that it gets served as a static asset in prod
export const prerender = true;
+22
View File
@@ -0,0 +1,22 @@
import { redirect } from '@sveltejs/kit';
import { type EmailOtpType } from '@supabase/supabase-js';
export const GET = async (event) => {
const {
url,
locals: { supabase }
} = event;
const token_hash = url.searchParams.get('token_hash') as string;
const type = url.searchParams.get('type') as EmailOtpType | null;
const next = url.searchParams.get('next') ?? '/';
if (token_hash && type) {
const { error } = await supabase.auth.verifyOtp({ token_hash, type });
if (!error) {
throw redirect(303, `/${next.slice(1)}`);
}
}
// return the user to an error page with some instructions
throw redirect(303, '/auth/auth-code-error');
};
+23
View File
@@ -0,0 +1,23 @@
<script lang="ts">
import { onDestroy } from 'svelte';
import { user } from '$lib/stores/user.js';
import { type User } from '@supabase/auth-js';
export let data;
let { supabase } = data;
$: ({ supabase } = data);
let localUser: User | null;
const unsubscribe = user.subscribe((value) => {
localUser = value;
});
onDestroy(unsubscribe);
</script>
<div class="h-screen">
{#if localUser}
{localUser?.email}
{:else}
<p>Please log in to view this page</p>
{/if}
</div>