docs: Update User management quickstart to Svelte 5 (#37646)
* General updates and switch to Svelte 5 * Update code example to Svelte 5 * Update some examples to use codesamples component * Add Svelte to codesample component * Final code updates * Add more CodeSample components
This commit is contained in:
@@ -17,11 +17,11 @@ If you get stuck while working through this guide, refer to the [full example on
|
||||
|
||||
## Building the app
|
||||
|
||||
Let's start building the Svelte app from scratch.
|
||||
Start building the Svelte app from scratch.
|
||||
|
||||
### Initialize a Svelte app
|
||||
|
||||
We can use the Vite Svelte TypeScript Template to initialize an app called `supabase-svelte`:
|
||||
You can use the Vite Svelte TypeScript Template to initialize an app called `supabase-svelte`:
|
||||
|
||||
```bash
|
||||
npm create vite@latest supabase-svelte -- --template svelte-ts
|
||||
@@ -29,14 +29,14 @@ cd supabase-svelte
|
||||
npm install
|
||||
```
|
||||
|
||||
Then let's install the only additional dependency: [supabase-js](https://github.com/supabase/supabase-js)
|
||||
Install the only additional dependency: [supabase-js](https://github.com/supabase/supabase-js)
|
||||
|
||||
```bash
|
||||
npm install @supabase/supabase-js
|
||||
```
|
||||
|
||||
And finally we want to save the environment variables in a `.env`.
|
||||
All we need are the API URL and the `anon` key that you copied [earlier](#get-the-api-keys).
|
||||
Finally, save the environment variables in a `.env`.
|
||||
All you need are the API URL and the `anon` key that you copied [earlier](#get-the-api-keys).
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
@@ -47,221 +47,46 @@ VITE_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
Now that we have the API credentials in place, let's create a 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.
|
||||
Now you have the API credentials in place, create a helper file to initialize the Supabase client. These variables will be exposed on the browser, and that's fine since you have [Row Level Security](/docs/guides/auth#row-level-security) enabled on the Database.
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```js name=src/supabaseClient.ts
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
|
||||
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY
|
||||
|
||||
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
|
||||
```
|
||||
|
||||
</$CodeTabs>
|
||||
<$CodeSample
|
||||
path="user-management/svelte-user-management/src/supabaseClient.ts"
|
||||
meta="name=src/supabaseClient.ts"
|
||||
/>
|
||||
|
||||
### App styling (optional)
|
||||
|
||||
An optional step is to update the CSS file `src/app.css` to make the app look nice.
|
||||
You can find the full contents of this file [here](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/svelte-user-management/src/app.css).
|
||||
Optionally, update the CSS file `src/app.css` to make the app look nice.
|
||||
You can find the full contents of this file [on GitHub](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/svelte-user-management/src/app.css).
|
||||
|
||||
### 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.
|
||||
Set up a Svelte component to manage logins and sign ups. It uses Magic Links, so users can sign in with their email without using passwords.
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```html name=src/lib/Auth.svelte
|
||||
<script lang="ts">
|
||||
import { supabase } from '../supabaseClient'
|
||||
|
||||
let loading = false
|
||||
let email = ''
|
||||
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
loading = true
|
||||
const { error } = await supabase.auth.signInWithOtp({ email })
|
||||
if (error) throw error
|
||||
alert('Check your email for login link!')
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message)
|
||||
}
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="row flex-center flex">
|
||||
<div class="col-6 form-widget" aria-live="polite">
|
||||
<h1 class="header">Supabase + Svelte</h1>
|
||||
<p class="description">Sign in via magic link with your email below</p>
|
||||
<form class="form-widget" on:submit|preventDefault="{handleLogin}">
|
||||
<div>
|
||||
<label for="email">Email</label>
|
||||
<input
|
||||
id="email"
|
||||
class="inputField"
|
||||
type="email"
|
||||
placeholder="Your email"
|
||||
bind:value="{email}"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="button block" aria-live="polite" disabled="{loading}">
|
||||
<span>{loading ? 'Loading' : 'Send magic link'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
</$CodeTabs>
|
||||
<$CodeSample
|
||||
path="user-management/svelte-user-management/src/lib/Auth.svelte"
|
||||
meta="name=src/lib/Auth.svelte"
|
||||
/>
|
||||
|
||||
### Account page
|
||||
|
||||
After a user is signed in we can allow them to edit their profile details and manage their account.
|
||||
Let's create a new component for that called `Account.svelte`.
|
||||
After a user is signed in, allow them to edit their profile details and manage their account.
|
||||
Create a new component for that called `Account.svelte`.
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```tsx name=src/lib/Account.svelte
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import type { AuthSession } from '@supabase/supabase-js'
|
||||
import { supabase } from '../supabaseClient'
|
||||
|
||||
export let session: AuthSession
|
||||
|
||||
let loading = false
|
||||
let username: string | null = null
|
||||
let website: string | null = null
|
||||
let avatarUrl: string | null = null
|
||||
|
||||
onMount(() => {
|
||||
getProfile()
|
||||
})
|
||||
|
||||
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 (error && status !== 406) throw error
|
||||
|
||||
if (data) {
|
||||
username = data.username
|
||||
website = data.website
|
||||
avatarUrl = data.avatar_url
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message)
|
||||
}
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
const updateProfile = async () => {
|
||||
try {
|
||||
loading = true
|
||||
const { user } = session
|
||||
|
||||
const updates = {
|
||||
id: user.id,
|
||||
username,
|
||||
website,
|
||||
avatar_url: avatarUrl,
|
||||
updated_at: new Date().toISOString(),
|
||||
}
|
||||
|
||||
const { error } = await supabase.from('profiles').upsert(updates)
|
||||
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message)
|
||||
}
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<form on:submit|preventDefault="{updateProfile}" class="form-widget">
|
||||
<div>Email: {session.user.email}</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="text" bind:value="{website}" />
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="button primary block" disabled="{loading}">
|
||||
{loading ? 'Saving ...' : 'Update profile'}
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="button block" on:click={() => supabase.auth.signOut()}> Sign Out
|
||||
</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
</$CodeTabs>
|
||||
<$CodeSample
|
||||
path="/user-management/svelte-user-management/src/lib/Account.svelte"
|
||||
lines={[[1, 4], [7, 11], [14, 33], [34,53], [55,73], [75,-1]]}
|
||||
meta="src/lib/Account.svelte"
|
||||
/>
|
||||
|
||||
### Launch!
|
||||
|
||||
Now that we have all the components in place, let's update `App.svelte`:
|
||||
Now that you have all the components in place, update `App.svelte`:
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```html name=src/App.svelte
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import { supabase } from './supabaseClient'
|
||||
import type { AuthSession } from '@supabase/supabase-js'
|
||||
import Account from './lib/Account.svelte'
|
||||
import Auth from './lib/Auth.svelte'
|
||||
|
||||
let session: AuthSession | null
|
||||
|
||||
onMount(() => {
|
||||
supabase.auth.getSession().then(({ data }) => {
|
||||
session = data.session
|
||||
})
|
||||
|
||||
supabase.auth.onAuthStateChange((_event, _session) => {
|
||||
session = _session
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="container" style="padding: 50px 0 100px 0">
|
||||
{#if !session}
|
||||
<Auth />
|
||||
{:else}
|
||||
<Account {session} />
|
||||
{/if}
|
||||
</div>
|
||||
```
|
||||
|
||||
</$CodeTabs>
|
||||
<$CodeSample
|
||||
path="user-management/svelte-user-management/src/App.svelte"
|
||||
meta="name=src/App.svelte"
|
||||
/>
|
||||
|
||||
Once that's done, run this in a terminal window:
|
||||
|
||||
@@ -273,7 +98,7 @@ And then open the browser to [localhost:5173](http://localhost:5173) and you sho
|
||||
|
||||
<Admonition type="tip">
|
||||
|
||||
Svelte uses Vite and the default port is `5173`, Supabase uses `port 3000`. To change the redirection port for Supabase go to: `Authentication > Settings` and change the `Site Url` to `http://localhost:5173/`
|
||||
Svelte uses Vite and the default port is `5173`, Supabase uses `port 3000`. To change the redirection port for Supabase go to: **Authentication > URL Configuration** and change the **Site URL** to `http://localhost:5173/`
|
||||
|
||||
</Admonition>
|
||||
|
||||
@@ -285,118 +110,21 @@ 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:
|
||||
Create an avatar for the user so that they can upload a profile photo. Start by creating a new component:
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```html name=src/lib/Avatar.svelte
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { supabase } from '../supabaseClient'
|
||||
|
||||
export let size: number
|
||||
export let url: string | null = null
|
||||
|
||||
let avatarUrl: string | null = null
|
||||
let uploading = false
|
||||
let files: FileList
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const downloadImage = async (path: string) => {
|
||||
try {
|
||||
const { data, error } = await supabase.storage.from('avatars').download(path)
|
||||
|
||||
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 uploadAvatar = async () => {
|
||||
try {
|
||||
uploading = true
|
||||
|
||||
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 { error } = await supabase.storage.from('avatars').upload(filePath, file)
|
||||
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
|
||||
url = filePath
|
||||
dispatch('upload')
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message)
|
||||
}
|
||||
} finally {
|
||||
uploading = false
|
||||
}
|
||||
}
|
||||
|
||||
$: if (url) downloadImage(url)
|
||||
</script>
|
||||
|
||||
<div style="width: {size}px" aria-live="polite">
|
||||
{#if avatarUrl} <img src={avatarUrl} alt={avatarUrl ? 'Avatar' : 'No image'} class="avatar image"
|
||||
style="height: {size}px, width: {size}px" /> {:else}
|
||||
<div class="avatar no-image" style="height: {size}px, width: {size}px" />
|
||||
{/if}
|
||||
<div style="width: {size}px">
|
||||
<label class="button primary block" for="single">
|
||||
{uploading ? 'Uploading ...' : 'Upload avatar'}
|
||||
</label>
|
||||
<span style="display:none">
|
||||
<input
|
||||
type="file"
|
||||
id="single"
|
||||
accept="image/*"
|
||||
bind:files
|
||||
on:change={uploadAvatar}
|
||||
disabled={uploading}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
</$CodeTabs>
|
||||
<$CodeSample
|
||||
path="user-management/svelte-user-management/src/lib/Avatar.svelte"
|
||||
meta="name=src/lib/Avatar.svelte"
|
||||
/>
|
||||
|
||||
### Add the new widget
|
||||
|
||||
And then we can add the widget to the Account page:
|
||||
And then you can add the widget to the Account page:
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```html name=src/lib/Account.svelte
|
||||
<script lang="ts">
|
||||
// Import the new component
|
||||
import Avatar from './Avatar.svelte'
|
||||
</script>
|
||||
|
||||
<form on:submit|preventDefault="{updateProfile}" class="form-widget">
|
||||
<!-- Add to body -->
|
||||
<Avatar bind:url="{avatarUrl}" size="{150}" on:upload="{updateProfile}" />
|
||||
|
||||
<!-- Other form elements -->
|
||||
</form>
|
||||
```
|
||||
|
||||
</$CodeTabs>
|
||||
<$CodeSample
|
||||
path="/user-management/svelte-user-management/src/lib/Account.svelte"
|
||||
lines={[[1,1], [5,5], [71,73], [74,74], [92,-1]]}
|
||||
meta="src/lib/Account.svelte"
|
||||
/>
|
||||
|
||||
At this stage you have a fully functional application!
|
||||
|
||||
@@ -344,6 +344,8 @@ function matchLang(lang: string) {
|
||||
return 'swift'
|
||||
case 'sql':
|
||||
return 'sql'
|
||||
case 'svelte':
|
||||
return 'svelte'
|
||||
default:
|
||||
return null
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,25 +1,23 @@
|
||||
{
|
||||
"name": "svelte-user-management",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^1.0.1",
|
||||
"@tsconfig/svelte": "^3.0.0",
|
||||
"svelte": "^3.49.0",
|
||||
"svelte-check": "^2.8.0",
|
||||
"svelte-preprocess": "^4.10.7",
|
||||
"tslib": "^2.4.0",
|
||||
"typescript": "^4.6.4",
|
||||
"vite": "^4.3.9"
|
||||
},
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^2.0.4"
|
||||
}
|
||||
"name": "supabase-svelte",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^6.1.0",
|
||||
"@tsconfig/svelte": "^5.0.4",
|
||||
"svelte": "^5.37.3",
|
||||
"svelte-check": "^4.3.1",
|
||||
"typescript": "~5.9.2",
|
||||
"vite": "^7.0.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^2.53.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { supabase } from './supabaseClient';
|
||||
import type { AuthSession } from '@supabase/supabase-js';
|
||||
import Account from './lib/Account.svelte';
|
||||
import { onMount } from 'svelte'
|
||||
import { supabase } from './supabaseClient'
|
||||
import type { AuthSession } from '@supabase/supabase-js'
|
||||
import Account from './lib/Account.svelte'
|
||||
import Auth from './lib/Auth.svelte'
|
||||
|
||||
let session: AuthSession
|
||||
let session = $state<AuthSession | null>(null)
|
||||
|
||||
onMount(() => {
|
||||
supabase.auth.getSession().then(({ data }) => {
|
||||
@@ -20,8 +20,8 @@
|
||||
|
||||
<div class="container" style="padding: 50px 0 100px 0">
|
||||
{#if !session}
|
||||
<Auth />
|
||||
<Auth />
|
||||
{:else}
|
||||
<Account {session} />
|
||||
<Account {session} />
|
||||
{/if}
|
||||
</div>
|
||||
@@ -369,4 +369,4 @@ button.primary,
|
||||
width: 17px;
|
||||
animation: spin 1s linear infinite;
|
||||
filter: invert(1);
|
||||
}
|
||||
}
|
||||
@@ -4,76 +4,80 @@
|
||||
import { supabase } from "../supabaseClient";
|
||||
import Avatar from "./Avatar.svelte";
|
||||
|
||||
export let session: AuthSession;
|
||||
interface Props {
|
||||
session: AuthSession;
|
||||
}
|
||||
|
||||
let loading = false
|
||||
let username: string | null = null
|
||||
let website: string | null = null
|
||||
let avatarUrl: string | null = null
|
||||
let { session }: Props = $props();
|
||||
|
||||
let loading = $state(false);
|
||||
let username = $state<string | null>(null);
|
||||
let website = $state<string | null>(null);
|
||||
let avatarUrl = $state<string | null>(null);
|
||||
|
||||
onMount(() => {
|
||||
getProfile()
|
||||
})
|
||||
getProfile();
|
||||
});
|
||||
|
||||
const getProfile = async () => {
|
||||
try {
|
||||
loading = true
|
||||
const { user } = session
|
||||
|
||||
loading = true;
|
||||
const { user } = session;
|
||||
|
||||
const { data, error, status } = await supabase
|
||||
.from('profiles')
|
||||
.select('username, website, avatar_url')
|
||||
.eq('id', user.id)
|
||||
.single()
|
||||
|
||||
if (error && status !== 406) throw error
|
||||
.from("profiles")
|
||||
.select("username, website, avatar_url")
|
||||
.eq("id", user.id)
|
||||
.single();
|
||||
|
||||
if (error && status !== 406) throw error;
|
||||
|
||||
if (data) {
|
||||
username = data.username
|
||||
website = data.website
|
||||
avatarUrl = data.avatar_url
|
||||
}
|
||||
username = data.username;
|
||||
website = data.website;
|
||||
avatarUrl = data.avatar_url;
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message)
|
||||
alert(error.message);
|
||||
}
|
||||
} finally {
|
||||
loading = false
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const updateProfile = async () => {
|
||||
try {
|
||||
loading = true
|
||||
const { user } = session
|
||||
loading = true;
|
||||
const { user } = session;
|
||||
|
||||
const updates = {
|
||||
id: user.id,
|
||||
username,
|
||||
website,
|
||||
avatar_url: avatarUrl,
|
||||
updated_at: new Date().toISOString(),
|
||||
}
|
||||
const updates = {
|
||||
id: user.id,
|
||||
username,
|
||||
website,
|
||||
avatar_url: avatarUrl,
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const { error } = await supabase.from("profiles").upsert(updates);
|
||||
|
||||
let { error } = await supabase.from('profiles').upsert(updates)
|
||||
|
||||
if (error) {
|
||||
throw error
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message)
|
||||
alert(error.message);
|
||||
}
|
||||
} finally {
|
||||
loading = false
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<form on:submit|preventDefault={updateProfile} class="form-widget">
|
||||
<Avatar bind:url={avatarUrl} size={150} on:upload={updateProfile} />
|
||||
<form onsubmit={(e) => { e.preventDefault(); updateProfile(); }} class="form-widget">
|
||||
<div>Email: {session.user.email}</div>
|
||||
<div>
|
||||
<Avatar bind:url={avatarUrl} size={150} onupload={updateProfile} />
|
||||
<label for="username">Name</label>
|
||||
<input id="username" type="text" bind:value={username} />
|
||||
</div>
|
||||
@@ -83,10 +87,14 @@
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="button primary block" disabled={loading}>
|
||||
{loading ? 'Saving ...' : 'Update profile'}
|
||||
{loading ? "Saving ..." : "Update profile"}
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="button block" on:click={() => supabase.auth.signOut()}>
|
||||
<button
|
||||
type="button"
|
||||
class="button block"
|
||||
onclick={() => supabase.auth.signOut()}
|
||||
>
|
||||
Sign Out
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
<script lang="ts">
|
||||
import { supabase } from "../supabaseClient";
|
||||
|
||||
let loading = false
|
||||
let email = ''
|
||||
let loading = $state(false);
|
||||
let email = $state("");
|
||||
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
loading = true
|
||||
const { error } = await supabase.auth.signInWithOtp({ email })
|
||||
if (error) throw error
|
||||
alert('Check your email for login link!')
|
||||
loading = true;
|
||||
const { error } = await supabase.auth.signInWithOtp({ email });
|
||||
if (error) throw error;
|
||||
alert("Check your email for login link!");
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message)
|
||||
alert(error.message);
|
||||
}
|
||||
} finally {
|
||||
loading = false
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="row flex-center flex">
|
||||
<div class="col-6 form-widget" aria-live="polite">
|
||||
<h1 class="header">Supabase + Svelte</h1>
|
||||
<p class="description">Sign in via magic link with your email below</p>
|
||||
<form class="form-widget" on:submit|preventDefault={handleLogin}>
|
||||
<form class="form-widget" onsubmit={(e) => { e.preventDefault(); handleLogin(); }}>
|
||||
<div>
|
||||
<label for="email">Email</label>
|
||||
<input
|
||||
@@ -36,8 +36,13 @@
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="button block" aria-live="polite" disabled={loading}>
|
||||
<span>{loading ? 'Loading' : 'Send magic link'}</span>
|
||||
<button
|
||||
type="submit"
|
||||
class="button block"
|
||||
aria-live="polite"
|
||||
disabled={loading}
|
||||
>
|
||||
<span>{loading ? "Loading" : "Send magic link"}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,84 +1,87 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { supabase } from "../supabaseClient"
|
||||
import { supabase } from "../supabaseClient";
|
||||
|
||||
export let size: number
|
||||
export let url: string
|
||||
|
||||
let avatarUrl: string = null
|
||||
let uploading = false
|
||||
let files: FileList
|
||||
interface Props {
|
||||
size: number;
|
||||
url?: string | null;
|
||||
onupload?: () => void;
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let { size, url = $bindable(null), onupload }: Props = $props();
|
||||
|
||||
let avatarUrl = $state<string | null>(null);
|
||||
let uploading = $state(false);
|
||||
let files = $state<FileList>();
|
||||
|
||||
const downloadImage = async (path: string) => {
|
||||
try {
|
||||
const { data, error } = await supabase.storage.from('avatars').download(path)
|
||||
const { data, error } = await supabase.storage
|
||||
.from("avatars")
|
||||
.download(path);
|
||||
|
||||
if (error) {
|
||||
throw error
|
||||
throw error;
|
||||
}
|
||||
|
||||
const url = URL.createObjectURL(data)
|
||||
avatarUrl = url
|
||||
const url = URL.createObjectURL(data);
|
||||
avatarUrl = url;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.log('Error downloading image: ', error.message)
|
||||
console.log("Error downloading image: ", error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const uploadAvatar = async () => {
|
||||
try {
|
||||
uploading = true
|
||||
uploading = true;
|
||||
|
||||
if (!files || files.length === 0) {
|
||||
throw new Error('You must select an image to upload.')
|
||||
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();
|
||||
const filePath = `${Math.random()}.${fileExt}`;
|
||||
|
||||
const { error } = await supabase.storage
|
||||
.from("avatars")
|
||||
.upload(filePath, file);
|
||||
|
||||
let { error } = await supabase.storage
|
||||
.from('avatars')
|
||||
.upload(filePath, file)
|
||||
|
||||
if (error) {
|
||||
throw error
|
||||
throw error;
|
||||
}
|
||||
|
||||
url = filePath
|
||||
dispatch('upload')
|
||||
url = filePath;
|
||||
onupload?.();
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message)
|
||||
alert(error.message);
|
||||
}
|
||||
} finally {
|
||||
uploading = false
|
||||
uploading = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$: if (url) downloadImage(url)
|
||||
$effect(() => {
|
||||
if (url) downloadImage(url);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div style="width: {size}px" aria-live="polite">
|
||||
{#if avatarUrl}
|
||||
{#if avatarUrl}
|
||||
<img
|
||||
src={avatarUrl}
|
||||
alt={avatarUrl ? 'Avatar' : 'No image'}
|
||||
alt={avatarUrl ? "Avatar" : "No image"}
|
||||
class="avatar image"
|
||||
style="height: {size}px; width: {size}px"
|
||||
style="height: {size}px, width: {size}px"
|
||||
/>
|
||||
{:else}
|
||||
<div
|
||||
class="avatar no-image"
|
||||
style="height: {size}px; width: {size}px"
|
||||
/>
|
||||
<div class="avatar no-image" style="height: {size}px, width: {size}px"></div>
|
||||
{/if}
|
||||
<div style="width: {size}px">
|
||||
<label class="button primary block" for="single">
|
||||
{uploading ? 'Uploading ...' : 'Upload avatar'}
|
||||
{uploading ? "Uploading ..." : "Upload avatar"}
|
||||
</label>
|
||||
<span style="display:none">
|
||||
<input
|
||||
@@ -86,7 +89,7 @@
|
||||
id="single"
|
||||
accept="image/*"
|
||||
bind:files
|
||||
on:change={uploadAvatar}
|
||||
onchange={uploadAvatar}
|
||||
disabled={uploading}
|
||||
/>
|
||||
</span>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { mount } from 'svelte'
|
||||
import './app.css'
|
||||
import App from './App.svelte'
|
||||
|
||||
const app = new App({
|
||||
target: document.getElementById('app')
|
||||
const app = mount(App, {
|
||||
target: document.getElementById('app')!,
|
||||
})
|
||||
|
||||
export default app
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
export type Json = string | number | boolean | null | { [key: string]: Json } | Json[]
|
||||
|
||||
export interface Database {
|
||||
public: {
|
||||
Tables: {
|
||||
profiles: {
|
||||
Row: {
|
||||
id: string
|
||||
updated_at: string | null
|
||||
username: string | null
|
||||
avatar_url: string | null
|
||||
website: string | null
|
||||
}
|
||||
Insert: {
|
||||
id: string
|
||||
updated_at?: string | null
|
||||
username?: string | null
|
||||
avatar_url?: string | null
|
||||
website?: string | null
|
||||
}
|
||||
Update: {
|
||||
id?: string
|
||||
updated_at?: string | null
|
||||
username?: string | null
|
||||
avatar_url?: string | null
|
||||
website?: string | null
|
||||
}
|
||||
}
|
||||
}
|
||||
Views: {
|
||||
[_ in never]: never
|
||||
}
|
||||
Functions: {
|
||||
[_ in never]: never
|
||||
}
|
||||
Enums: {
|
||||
[_ in never]: never
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import type { Database } from './schema'
|
||||
|
||||
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
|
||||
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY
|
||||
|
||||
export const supabase = createClient<Database>(supabaseUrl, supabaseAnonKey)
|
||||
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
|
||||
@@ -1,7 +1,7 @@
|
||||
import sveltePreprocess from 'svelte-preprocess'
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
|
||||
|
||||
export default {
|
||||
// Consult https://github.com/sveltejs/svelte-preprocess
|
||||
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
|
||||
// for more information about preprocessors
|
||||
preprocess: sveltePreprocess()
|
||||
preprocess: vitePreprocess(),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"resolveJsonModule": true,
|
||||
/**
|
||||
* Typecheck JS in `.svelte` and `.js` files by default.
|
||||
* Disable checkJs if you'd like to use dynamic types in JS.
|
||||
* Note that setting allowJs false does not prevent the use
|
||||
* of JS in `.svelte` files.
|
||||
*/
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force"
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"]
|
||||
}
|
||||
@@ -1,21 +1,7 @@
|
||||
{
|
||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"resolveJsonModule": true,
|
||||
"baseUrl": ".",
|
||||
/**
|
||||
* Typecheck JS in `.svelte` and `.js` files by default.
|
||||
* Disable checkJs if you'd like to use dynamic types in JS.
|
||||
* Note that setting allowJs false does not prevent the use
|
||||
* of JS in `.svelte` files.
|
||||
*/
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"isolatedModules": true
|
||||
},
|
||||
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,8 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node"
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [svelte()]
|
||||
plugins: [svelte()],
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user