Update getting started guide for SvelteKit
This commit is contained in:
@@ -26,12 +26,12 @@ We can use the [SvelteKit Skeleton Project](https://kit.svelte.dev/docs) to init
|
||||
an app called `supabase-sveltekit` (for this tutorial we will be using TypeScript):
|
||||
|
||||
```bash
|
||||
npm init svelte@next supabase-sveltekit
|
||||
npm create svelte@latest supabase-sveltekit
|
||||
cd supabase-sveltekit
|
||||
npm install
|
||||
```
|
||||
|
||||
Then let's install the only additional dependency: [supabase-js](https://github.com/supabase/supabase-js)
|
||||
Then install the Supabase client library: [supabase-js](https://github.com/supabase/supabase-js)
|
||||
|
||||
```bash
|
||||
npm install @supabase/supabase-js
|
||||
@@ -45,7 +45,7 @@ PUBLIC_SUPABASE_URL="YOUR_SUPABASE_URL"
|
||||
PUBLIC_SUPABASE_ANON_KEY="YOUR_SUPABASE_KEY"
|
||||
```
|
||||
|
||||
Now that we have the API credentials in place, create a `src/lib/supabaseClient.ts` helper file to initialize the Supabase client.
|
||||
{/* Now that we have the API credentials in place, create a `src/lib/supabaseClient.ts` helper file to initialize the Supabase client.
|
||||
These variables will be exposed on the browser, and that's completely fine since we have [Row Level Security](/docs/guides/auth#row-level-security) enabled on our Database.
|
||||
|
||||
```js title=src/lib/supabaseClient.ts
|
||||
@@ -53,9 +53,9 @@ import { createClient } from '@supabase/auth-helpers-sveltekit'
|
||||
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'
|
||||
|
||||
export const supabase = createClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY)
|
||||
```
|
||||
``` */}
|
||||
|
||||
Optionally, update `src/routes/styles.css` with the [CSS from the example](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/svelte-user-management/src/app.css).
|
||||
Optionally, add `src/styles.css` with the [CSS from the example](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/svelte-user-management/src/app.css).
|
||||
|
||||
### Supabase Auth Helpers
|
||||
|
||||
@@ -69,249 +69,380 @@ Install the auth helpers for SvelteKit:
|
||||
npm install @supabase/auth-helpers-sveltekit
|
||||
```
|
||||
|
||||
Add the code below to your `src/hooks.server.ts` to initialize the client on the server:
|
||||
|
||||
```ts title=src/hooks.server.ts
|
||||
// src/hooks.server.ts
|
||||
import {
|
||||
PUBLIC_SUPABASE_URL,
|
||||
PUBLIC_SUPABASE_ANON_KEY
|
||||
} from '$env/static/public';
|
||||
import { createSupabaseServerClient } from '@supabase/auth-helpers-sveltekit';
|
||||
import type { Handle } from '@sveltejs/kit';
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
event.locals.supabase = createSupabaseServerClient({
|
||||
supabaseUrl: PUBLIC_SUPABASE_URL,
|
||||
supabaseKey: PUBLIC_SUPABASE_ANON_KEY,
|
||||
event
|
||||
});
|
||||
|
||||
/**
|
||||
* a little helper that is written for convenience so that instead
|
||||
* of calling `const { data: { session } } = await supabase.auth.getSession()`
|
||||
* you just call this `await getSession()`
|
||||
*/
|
||||
event.locals.getSession = async () => {
|
||||
const {
|
||||
data: { session }
|
||||
} = await event.locals.supabase.auth.getSession();
|
||||
return session;
|
||||
};
|
||||
|
||||
return resolve(event, {
|
||||
/**
|
||||
* There´s an issue with `filterSerializedResponseHeaders` not working when using `sequence`
|
||||
*
|
||||
* https://github.com/sveltejs/kit/issues/8061
|
||||
*/
|
||||
filterSerializedResponseHeaders(name) {
|
||||
return name === 'content-range';
|
||||
}
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
If you are using TypeScript the compiler might complain about `event.locals.supabase` and `event.locals.getSession`, this can be fixed by updating your `src/app.d.ts` with the content below:
|
||||
|
||||
```ts title=src/app.d.ts
|
||||
// src/app.d.ts
|
||||
|
||||
import { SupabaseClient, Session } from '@supabase/supabase-js';
|
||||
import { Database } from './DatabaseDefinitions';
|
||||
|
||||
declare global {
|
||||
namespace App {
|
||||
interface Locals {
|
||||
supabase: SupabaseClient<Database>;
|
||||
getSession(): Promise<Session | null>;
|
||||
}
|
||||
interface PageData {
|
||||
session: Session | null;
|
||||
}
|
||||
// interface Error {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Update your `src/routes/+layout.svelte`:
|
||||
|
||||
```html title=src/routes/+layout.svelte
|
||||
```svelte title=src/routes/+layout.svelte
|
||||
<!-- src/routes/+layout.svelte -->
|
||||
<script lang="ts">
|
||||
import { supabase } from '$lib/supabaseClient'
|
||||
import { invalidate } from '$app/navigation'
|
||||
import { onMount } from 'svelte'
|
||||
import './styles.css'
|
||||
|
||||
onMount(() => {
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange(() => {
|
||||
invalidate('supabase:auth')
|
||||
})
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe()
|
||||
}
|
||||
})
|
||||
import '../styles.css';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>User Management</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="container" style="padding: 50px 0 100px 0">
|
||||
<slot />
|
||||
<slot />
|
||||
</div>
|
||||
```
|
||||
|
||||
Create a new `src/routes/+layout.ts` file to handle the session on the client-side.
|
||||
Create a new `src/routes/+layout.ts` file to handle the session and the supabase object on the client-side.
|
||||
|
||||
```ts title=src/routes/+layout.ts
|
||||
import type { LayoutLoad } from './$types'
|
||||
import { getSupabase } from '@supabase/auth-helpers-sveltekit'
|
||||
// src/routes/+layout.ts
|
||||
import { invalidate } from '$app/navigation';
|
||||
import {
|
||||
PUBLIC_SUPABASE_ANON_KEY,
|
||||
PUBLIC_SUPABASE_URL
|
||||
} from '$env/static/public';
|
||||
import { createSupabaseLoadClient } from '@supabase/auth-helpers-sveltekit';
|
||||
import type { LayoutLoad } from './$types';
|
||||
import type { Database } from '../DatabaseDefinitions';
|
||||
|
||||
export const load: LayoutLoad = async (event) => {
|
||||
const { session } = await getSupabase(event)
|
||||
return { session }
|
||||
}
|
||||
export const load: LayoutLoad = async ({ fetch, data, depends }) => {
|
||||
depends('supabase:auth');
|
||||
|
||||
const supabase = createSupabaseLoadClient<Database>({
|
||||
supabaseUrl: PUBLIC_SUPABASE_URL,
|
||||
supabaseKey: PUBLIC_SUPABASE_ANON_KEY,
|
||||
event: { fetch },
|
||||
serverSession: data.session,
|
||||
onAuthStateChange() {
|
||||
invalidate('supabase:auth');
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
data: { session }
|
||||
} = await supabase.auth.getSession();
|
||||
|
||||
return { supabase, session };
|
||||
};
|
||||
```
|
||||
|
||||
Create a new `src/routes/+layout.server.ts` file to handle the session on the server-side.
|
||||
|
||||
```ts title=src/routes/+layout.server.ts
|
||||
import type { LayoutServerLoad } from './$types'
|
||||
import { getServerSession } from '@supabase/auth-helpers-sveltekit'
|
||||
// src/routes/+layout.server.ts
|
||||
import type { LayoutServerLoad } from './$types';
|
||||
|
||||
export const load: LayoutServerLoad = async (event) => {
|
||||
export const load: LayoutServerLoad = async ({ locals: { getSession } }) => {
|
||||
return {
|
||||
session: await getServerSession(event),
|
||||
}
|
||||
}
|
||||
session: getSession()
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
Be sure to create `src/hooks.client.ts` and `src/hooks.server.ts` in order to get the auth helper started on the client and server-side.
|
||||
### Set up a Login page
|
||||
|
||||
```ts title=src/hooks.client.ts
|
||||
import '$lib/supabaseClient'
|
||||
#### Supabase Auth UI
|
||||
|
||||
We can use the [Supabase Auth UI](/docs/guides/auth/auth-helpers/auth-ui) a pre-built Svelte component for authenticating users via OAuth, email, and magic links.
|
||||
|
||||
Install the Supabase Auth UI for Svelte
|
||||
|
||||
```bash
|
||||
npm install @supabase/auth-ui-svelte @supabase/auth-ui-shared
|
||||
```
|
||||
|
||||
```ts title=src/hooks.server.ts
|
||||
import '$lib/supabaseClient'
|
||||
```
|
||||
Add the `Auth` component to your home page
|
||||
|
||||
### Set up a Login component
|
||||
|
||||
Let's set up a Svelte component to manage logins and sign ups. We'll use Magic Links, so users can sign in with their email without using passwords.
|
||||
Create a new `Auth.svelte` component in the `src/routes` directory to handle this functionality.
|
||||
|
||||
```html title=src/routes/Auth.svelte
|
||||
```svelte title=src/routes/+page.svelte
|
||||
<!-- src/routes/+page.svelte -->
|
||||
<script lang="ts">
|
||||
import { supabase } from '$lib/supabaseClient'
|
||||
import { Auth } from '@supabase/auth-ui-svelte';
|
||||
import { ThemeSupa } from '@supabase/auth-ui-shared';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
let loading = false
|
||||
let email: string
|
||||
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
loading = true
|
||||
const { error } = await supabase.auth.signInWithOtp({ email })
|
||||
if (error) throw error
|
||||
alert('Check your email for the login link!')
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message)
|
||||
}
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
export let data: PageData;
|
||||
</script>
|
||||
|
||||
<form class="row flex-center flex" on:submit|preventDefault="{handleLogin}">
|
||||
<div class="col-6 form-widget">
|
||||
<h1 class="header">Supabase + SvelteKit</h1>
|
||||
<p class="description">Sign in via magic link with your email below</p>
|
||||
<div>
|
||||
<input class="inputField" type="email" placeholder="Your email" bind:value="{email}" />
|
||||
</div>
|
||||
<div>
|
||||
<input type="submit" class="button block" value={loading ? 'Loading' : 'Send magic link'}
|
||||
disabled={loading} />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<svelte:head>
|
||||
<title>User Management</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="row flex-center flex">
|
||||
<div class="col-6 form-widget">
|
||||
<Auth
|
||||
supabaseClient={data.supabase}
|
||||
view="magic_link"
|
||||
redirectTo={`${data.url}/logging-in?redirect=/`}
|
||||
showLinks={false}
|
||||
appearance={{ theme: ThemeSupa, style: { input: 'color: #fff' } }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Account component
|
||||
Create a `src/routes/+page.server.ts` file that will return our website url to be used in our `redirectTo` above.
|
||||
|
||||
> This is necessary because the current supabase auth flow uses implicit grant which returns the tokens as part of the url fragment (#),
|
||||
the redirect to a non server protected page will make sure that the client captures the url fragment as we cannot read these on the server
|
||||
and send it over to the server. This all happens behind the scenes and isn't something you will need to do manually.
|
||||
|
||||
```ts
|
||||
// src/routes/+page.server.ts
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ parent, url }) => {
|
||||
const { session } = await parent();
|
||||
|
||||
// if the user is already logged in return them to the account page
|
||||
if (session) {
|
||||
throw redirect(303, '/account');
|
||||
}
|
||||
|
||||
return { url: url.origin };
|
||||
};
|
||||
```
|
||||
|
||||
### Loading page
|
||||
|
||||
Create a `src/routes/logging-in/+page.svelte` file with the code below.
|
||||
|
||||
This page is used for waiting while your client-side code inside of `src/routes/+layout.ts` sends the tokens over to your server-side code to store it in a cookie.
|
||||
|
||||
```svelte
|
||||
<!-- src/routes/logging-in/+page.svelte -->
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/environment';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
$: {
|
||||
const redirectTo = $page.url.searchParams.get('redirect');
|
||||
|
||||
// check if user has been set in session store then redirect
|
||||
if (browser && data.session) {
|
||||
goto(redirectTo ?? '/account');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<section>
|
||||
"Because as we know, there are known knowns; there are things we know we know. We also know there
|
||||
are known unknowns; that is to say we know there are some things we do not know. But there are
|
||||
also unknown unknowns—the ones we don't know we don't know" - Donald Rumsfeld
|
||||
</section>
|
||||
```
|
||||
|
||||
### Account page
|
||||
|
||||
After a user is signed in, they need to be able to edit their profile details and manage their account.
|
||||
Create a new `Account.svelte` component in the `src/routes` directory to handle this functionality.
|
||||
Create a new `src/routes/account/+page.svelte` file with the content below.
|
||||
|
||||
```html title=src/routes/Account.svelte
|
||||
```svelte title=src/routes/account/+page.svelte
|
||||
<!-- src/routes/account/+page.svelte -->
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import type { AuthSession } from '@supabase/supabase-js'
|
||||
import { supabase } from '$lib/supabaseClient'
|
||||
import { enhance } from '$app/forms';
|
||||
import type { ActionData, PageData } from './$types';
|
||||
|
||||
export let session: AuthSession
|
||||
export let data: PageData;
|
||||
export let form: ActionData;
|
||||
|
||||
let loading = false
|
||||
let username: string | null = null
|
||||
let website: string | null = null
|
||||
let avatarUrl: string | null = null
|
||||
let { session, profile } = data;
|
||||
|
||||
onMount(() => {
|
||||
getProfile()
|
||||
})
|
||||
let profileForm: any;
|
||||
let loading = false;
|
||||
let fullName: string | null = profile?.full_name;
|
||||
let username: string | null = profile?.username;
|
||||
let website: string | null = profile?.website;
|
||||
let avatarUrl: string | null = profile?.avatar_url;
|
||||
|
||||
const getProfile = async () => {
|
||||
try {
|
||||
loading = true
|
||||
const { user } = session
|
||||
|
||||
const { data, error, status } = await supabase
|
||||
.from('profiles')
|
||||
.select(`username, website, avatar_url`)
|
||||
.eq('id', user.id)
|
||||
.single()
|
||||
|
||||
if (data) {
|
||||
username = data.username
|
||||
website = data.website
|
||||
avatarUrl = data.avatar_url
|
||||
}
|
||||
|
||||
if (error && status !== 406) throw error
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message)
|
||||
}
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
async function updateProfile() {
|
||||
try {
|
||||
loading = true
|
||||
const { user } = session
|
||||
|
||||
const updates = {
|
||||
id: user.id,
|
||||
username,
|
||||
website,
|
||||
avatar_url: avatarUrl,
|
||||
updated_at: new Date(),
|
||||
}
|
||||
|
||||
let { error } = await supabase.from('profiles').upsert(updates)
|
||||
|
||||
if (error) throw error
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message)
|
||||
}
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
async function signOut() {
|
||||
try {
|
||||
loading = true
|
||||
let { error } = await supabase.auth.signOut()
|
||||
if (error) throw error
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message)
|
||||
}
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
function handleSubmit() {
|
||||
loading = true;
|
||||
return async () => {
|
||||
loading = false;
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<form class="form-widget" on:submit|preventDefault="{updateProfile}">
|
||||
<div>
|
||||
<label for="email">Email</label>
|
||||
<input id="email" type="text" value="{session.user.email}" disabled />
|
||||
</div>
|
||||
<div>
|
||||
<label for="username">Name</label>
|
||||
<input id="username" type="text" bind:value="{username}" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="website">Website</label>
|
||||
<input id="website" type="website" bind:value="{website}" />
|
||||
</div>
|
||||
<div class="form-widget">
|
||||
<form
|
||||
class="form-widget"
|
||||
method="post"
|
||||
action="?/update"
|
||||
use:enhance={handleSubmit}
|
||||
bind:this={profileForm}
|
||||
>
|
||||
<div>
|
||||
<label for="email">Email</label>
|
||||
<input id="email" type="text" value={session.user.email} disabled />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input type="submit" class="button block primary" value={loading ? 'Loading...' : 'Update'}
|
||||
disabled={loading} />
|
||||
</div>
|
||||
<div>
|
||||
<label for="fullName">Full Name</label>
|
||||
<input id="fullName" name="fullName" type="text" value={form?.fullName ?? fullName} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="button block" on:click="{signOut}" disabled="{loading}">Sign Out</button>
|
||||
</div>
|
||||
</form>
|
||||
<div>
|
||||
<label for="username">Username</label>
|
||||
<input id="username" name="username" type="text" value={form?.username ?? username} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="website">Website</label>
|
||||
<input id="website" name="website" type="website" value={form?.website ?? website} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input
|
||||
type="submit"
|
||||
class="button block primary"
|
||||
value={loading ? 'Loading...' : 'Update'}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form method="post" action="?/signout" use:enhance={handleSubmit}>
|
||||
<div>
|
||||
<button class="button block" disabled={loading}>Sign Out</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
```
|
||||
|
||||
Now create the associated `src/routes/account/+page.server.ts` file that will handle loading our data from the server through the `load` function
|
||||
and handle all our form actions through the `actions` object.
|
||||
|
||||
```ts
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
|
||||
export const load = (async ({ locals: { supabase, getSession } }) => {
|
||||
const session = await getSession();
|
||||
|
||||
if (!session) {
|
||||
throw redirect(303, '/');
|
||||
}
|
||||
|
||||
const { data: profile } = await supabase
|
||||
.from('profiles')
|
||||
.select(`username, full_name, website, avatar_url`)
|
||||
.eq('id', session.user.id)
|
||||
.single();
|
||||
|
||||
return { session, profile };
|
||||
}) satisfies PageServerLoad;
|
||||
|
||||
export const actions = {
|
||||
update: async ({ request, locals: { supabase, getSession } }) => {
|
||||
const formData = await request.formData();
|
||||
const fullName = formData.get('fullName') as string;
|
||||
const username = formData.get('username') as string;
|
||||
const website = formData.get('website') as string;
|
||||
const avatarUrl = formData.get('avatarUrl') as string;
|
||||
|
||||
const session = await getSession();
|
||||
|
||||
const { error } = await supabase.from('profiles').upsert({
|
||||
id: session?.user.id,
|
||||
full_name: fullName,
|
||||
username,
|
||||
website,
|
||||
avatar_url: avatarUrl,
|
||||
updated_at: new Date()
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return fail(500, {
|
||||
fullName,
|
||||
username,
|
||||
website,
|
||||
avatarUrl
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
fullName,
|
||||
username,
|
||||
website,
|
||||
avatarUrl
|
||||
};
|
||||
},
|
||||
signout: async ({ locals: { supabase, getSession } }) => {
|
||||
const session = await getSession();
|
||||
if (session) {
|
||||
await supabase.auth.signOut();
|
||||
throw redirect(303, '/');
|
||||
}
|
||||
}
|
||||
} satisfies Actions;
|
||||
```
|
||||
|
||||
### Launch!
|
||||
|
||||
Now that we have all the components in place, let's update `src/routes/+page.svelte`:
|
||||
|
||||
```html title=src/routes/+page.svelte
|
||||
<script>
|
||||
import { page } from '$app/stores'
|
||||
import Account from './Account.svelte'
|
||||
import Auth from './Auth.svelte'
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Supabase + SvelteKit</title>
|
||||
<meta name="description" content="SvelteKit using supabase-js v2" />
|
||||
</svelte:head>
|
||||
|
||||
{#if !$page.data.session}
|
||||
<Auth />
|
||||
{:else}
|
||||
<Account session="{$page.data.session}" />
|
||||
{/if}
|
||||
```
|
||||
|
||||
Once that's done, run this in a terminal window:
|
||||
Now that we have all the pages in place, run this in a terminal window:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
@@ -327,91 +458,99 @@ Every Supabase project is configured with [Storage](/docs/guides/storage) for ma
|
||||
|
||||
### Create an upload widget
|
||||
|
||||
Let's create an avatar for the user so that they can upload a profile photo. We can start by creating a new component called `Avatar.svelte` in the `src/routes` directory:
|
||||
Let's create an avatar for the user so that they can upload a profile photo. We can start by creating a new component called `Avatar.svelte` in the `src/routes/account` directory:
|
||||
|
||||
```html title=src/routes/Avatar.svelte
|
||||
```svelte title=src/routes/account/Avatar.svelte
|
||||
<!-- src/routes/account/Avatar.svelte -->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { supabase } from '$lib/supabaseClient'
|
||||
import type { SupabaseClient } from '@supabase/supabase-js';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let size = 10
|
||||
export let url: string
|
||||
export let size = 10;
|
||||
export let url: string;
|
||||
export let supabase: SupabaseClient;
|
||||
|
||||
let avatarUrl: string | null = null
|
||||
let uploading = false
|
||||
let files: FileList
|
||||
let avatarUrl: string | null = null;
|
||||
let uploading = false;
|
||||
let files: FileList;
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const downloadImage = async (path: string) => {
|
||||
try {
|
||||
const { data, error } = await supabase.storage.from('avatars').download(path)
|
||||
const downloadImage = async (path: string) => {
|
||||
try {
|
||||
const { data, error } = await supabase.storage.from('avatars').download(path);
|
||||
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const url = URL.createObjectURL(data)
|
||||
avatarUrl = url
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.log('Error downloading image: ', error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
const url = URL.createObjectURL(data);
|
||||
avatarUrl = url;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.log('Error downloading image: ', error.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const uploadAvatar = async () => {
|
||||
try {
|
||||
uploading = true
|
||||
const uploadAvatar = async () => {
|
||||
try {
|
||||
uploading = true;
|
||||
|
||||
if (!files || files.length === 0) {
|
||||
throw new Error('You must select an image to upload.')
|
||||
}
|
||||
if (!files || files.length === 0) {
|
||||
throw new Error('You must select an image to upload.');
|
||||
}
|
||||
|
||||
const file = files[0]
|
||||
const fileExt = file.name.split('.').pop()
|
||||
const filePath = `${Math.random()}.${fileExt}`
|
||||
const file = files[0];
|
||||
const fileExt = file.name.split('.').pop();
|
||||
url = `${Math.random()}.${fileExt}`;
|
||||
|
||||
let { error } = await supabase.storage.from('avatars').upload(filePath, file)
|
||||
let { error } = await supabase.storage.from('avatars').upload(url, file);
|
||||
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
url = filePath
|
||||
dispatch('upload')
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message)
|
||||
}
|
||||
} finally {
|
||||
uploading = false
|
||||
}
|
||||
}
|
||||
dispatch('upload');
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message);
|
||||
}
|
||||
} finally {
|
||||
uploading = false;
|
||||
}
|
||||
};
|
||||
|
||||
$: if (url) downloadImage(url)
|
||||
$: if (url) downloadImage(url);
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if avatarUrl} <img src={avatarUrl} alt={avatarUrl ? 'Avatar' : 'No image'} class="avatar image"
|
||||
style="height: {size}em; width: {size}em;" /> {:else}
|
||||
<div class="avatar no-image" style="height: {size}em; width: {size}em;" />
|
||||
{/if}
|
||||
{#if avatarUrl}
|
||||
<img
|
||||
src={avatarUrl}
|
||||
alt={avatarUrl ? 'Avatar' : 'No image'}
|
||||
class="avatar image"
|
||||
style="height: {size}em; width: {size}em;"
|
||||
/>
|
||||
{:else}
|
||||
<div class="avatar no-image" style="height: {size}em; width: {size}em;" />
|
||||
{/if}
|
||||
<input type="hidden" name="avatarUrl" value={url} />
|
||||
|
||||
<div style="width: {size}em;">
|
||||
<label class="button primary block" for="single">
|
||||
{uploading ? 'Uploading ...' : 'Upload'}
|
||||
</label>
|
||||
<input
|
||||
style="visibility: hidden; position:absolute;"
|
||||
type="file"
|
||||
id="single"
|
||||
accept="image/*"
|
||||
bind:files
|
||||
on:change="{uploadAvatar}"
|
||||
disabled="{uploading}"
|
||||
/>
|
||||
</div>
|
||||
<div style="width: {size}em;">
|
||||
<label class="button primary block" for="single">
|
||||
{uploading ? 'Uploading ...' : 'Upload'}
|
||||
</label>
|
||||
<input
|
||||
style="visibility: hidden; position:absolute;"
|
||||
type="file"
|
||||
id="single"
|
||||
accept="image/*"
|
||||
bind:files
|
||||
on:change={uploadAvatar}
|
||||
disabled={uploading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
@@ -419,18 +558,34 @@ Let's create an avatar for the user so that they can upload a profile photo. We
|
||||
|
||||
And then we can add the widget to the Account page:
|
||||
|
||||
```html title=src/routes/Account.svelte
|
||||
```svelte title=src/routes/account/+page.svelte
|
||||
<!-- src/routes/account/+page.svelte -->
|
||||
<script>
|
||||
// Import the new component
|
||||
import Avatar from './Avatar.svelte'
|
||||
</script>
|
||||
|
||||
<form use:getProfile class="form-widget" on:submit|preventDefault="{updateProfile}">
|
||||
<!-- Add to body -->
|
||||
<Avatar bind:url="{avatarUrl}" size="{10}" on:upload="{updateProfile}" />
|
||||
<div class="form-widget">
|
||||
<form
|
||||
class="form-widget"
|
||||
method="post"
|
||||
action="?/update"
|
||||
use:enhance={handleSubmit}
|
||||
bind:this={profileForm}
|
||||
>
|
||||
<!-- Add to body -->
|
||||
<Avatar
|
||||
{supabase}
|
||||
bind:url={avatarUrl}
|
||||
size={10}
|
||||
on:upload={() => {
|
||||
profileForm.requestSubmit();
|
||||
}}
|
||||
/>
|
||||
|
||||
<!-- Other form elements -->
|
||||
</form>
|
||||
<!-- Other form elements -->
|
||||
</form>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Storage management
|
||||
|
||||
1
package-lock.json
generated
1
package-lock.json
generated
@@ -5,6 +5,7 @@
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "supabase",
|
||||
"version": "0.0.0",
|
||||
"license": "Apache-2.0",
|
||||
"workspaces": [
|
||||
|
||||
Reference in New Issue
Block a user