-
-
Notifications
You must be signed in to change notification settings - Fork 269
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #136 from supabase/feat/storage-api
feat: storage api
- Loading branch information
Showing
31 changed files
with
3,024 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | ||
|
||
# dependencies | ||
/node_modules | ||
/.pnp | ||
.pnp.js | ||
|
||
# testing | ||
/coverage | ||
|
||
# next.js | ||
/.next/ | ||
/out/ | ||
|
||
# production | ||
/build | ||
|
||
# misc | ||
.DS_Store | ||
*.pem | ||
|
||
# debug | ||
npm-debug.log* | ||
yarn-debug.log* | ||
yarn-error.log* | ||
|
||
# local env files | ||
.env.local | ||
.env.development.local | ||
.env.test.local | ||
.env.production.local | ||
|
||
# vercel | ||
.vercel |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
# Supabase storage example | ||
|
||
- Create a file `.env.local` | ||
- Add a `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_KEY` | ||
- Run `npm run dev` | ||
|
||
## Database schema | ||
|
||
```sql | ||
create table profiles ( | ||
id uuid references auth.users not null, | ||
updated_at timestamp with time zone, | ||
username text unique, | ||
avatar_url text, | ||
website text, | ||
|
||
primary key (id), | ||
unique(username), | ||
constraint username_length check (char_length(username) >= 3) | ||
); | ||
alter table profiles enable row level security; | ||
create policy "Public profiles are viewable by everyone." on profiles for select using (true); | ||
create policy "Users can insert their own profile." on profiles for insert with check (auth.uid() = id); | ||
create policy "Users can update own profile." on profiles for update using (auth.uid() = id); | ||
|
||
|
||
-- Set up Realtime! | ||
begin; | ||
drop publication if exists supabase_realtime; | ||
create publication supabase_realtime; | ||
commit; | ||
|
||
alter publication supabase_realtime add table profiles; | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
import { useState, useEffect, ChangeEvent } from 'react' | ||
import { supabase } from '../lib/api' | ||
import UploadButton from '../components/UploadButton' | ||
import Avatar from './Avatar' | ||
import { AuthSession } from '../../../dist/main' | ||
import { DEFAULT_AVATARS_BUCKET, Profile } from '../lib/constants' | ||
|
||
export default function Account({ session }: { session: AuthSession }) { | ||
const [loading, setLoading] = useState<boolean>(true) | ||
const [uploading, setUploading] = useState<boolean>(false) | ||
const [avatar, setAvatar] = useState<string | null>(null) | ||
const [username, setUsername] = useState<string | null>(null) | ||
const [website, setWebsite] = useState<string | null>(null) | ||
|
||
useEffect(() => { | ||
getProfile() | ||
}, [session]) | ||
|
||
async function signOut() { | ||
const { error } = await supabase.auth.signOut() | ||
if (error) console.log('Error logging out:', error.message) | ||
} | ||
|
||
async function uploadAvatar(event: ChangeEvent<HTMLInputElement>) { | ||
try { | ||
setUploading(true) | ||
|
||
if (!event.target.files || event.target.files.length == 0) { | ||
throw 'You must select an image to upload.' | ||
} | ||
|
||
const user = supabase.auth.user() | ||
const file = event.target.files[0] | ||
const fileExt = file.name.split('.').pop() | ||
const fileName = `${session?.user.id}${Math.random()}.${fileExt}` | ||
const filePath = `${DEFAULT_AVATARS_BUCKET}/${fileName}` | ||
|
||
let { error: uploadError } = await supabase.storage.uploadFile(filePath, file) | ||
|
||
if (uploadError) { | ||
throw uploadError | ||
} | ||
|
||
let { error: updateError } = await supabase.from('profiles').upsert({ | ||
id: user.id, | ||
avatar_url: filePath, | ||
}) | ||
|
||
if (updateError) { | ||
throw updateError | ||
} | ||
|
||
setAvatar(null) | ||
setAvatar(filePath) | ||
} catch (error) { | ||
alert(error.message) | ||
} finally { | ||
setUploading(false) | ||
} | ||
} | ||
|
||
function setProfile(profile: Profile) { | ||
setAvatar(profile.avatar_url) | ||
setUsername(profile.username) | ||
setWebsite(profile.website) | ||
} | ||
|
||
async function getProfile() { | ||
try { | ||
setLoading(true) | ||
const user = supabase.auth.user() | ||
|
||
let { data, error } = await supabase | ||
.from('profiles') | ||
.select(`username, website, avatar_url`) | ||
.eq('id', user.id) | ||
.single() | ||
|
||
if (error) { | ||
throw error | ||
} | ||
|
||
setProfile(data) | ||
} catch (error) { | ||
console.log('error', error.message) | ||
} finally { | ||
setLoading(false) | ||
} | ||
} | ||
|
||
async function updateProfile() { | ||
try { | ||
setLoading(true) | ||
const user = supabase.auth.user() | ||
|
||
const updates = { | ||
id: user.id, | ||
username, | ||
website, | ||
updated_at: new Date(), | ||
} | ||
|
||
let { error } = await supabase.from('profiles').upsert(updates, { | ||
returning: 'minimal', // Don't return the value after inserting | ||
}) | ||
|
||
if (error) { | ||
throw error | ||
} | ||
} catch (error) { | ||
alert(error.message) | ||
} finally { | ||
setLoading(false) | ||
} | ||
} | ||
|
||
return ( | ||
<div | ||
style={{ | ||
minWidth: 250, | ||
maxWidth: 600, | ||
margin: 'auto', | ||
display: 'flex', | ||
flexDirection: 'column', | ||
gap: 20, | ||
}} | ||
> | ||
<div className="card"> | ||
<div> | ||
<Avatar url={avatar} size={270} /> | ||
</div> | ||
<UploadButton onUpload={uploadAvatar} loading={uploading} /> | ||
</div> | ||
|
||
<div> | ||
<label htmlFor="email">Email</label> | ||
<input id="email" type="text" value={session.user.email} disabled /> | ||
</div> | ||
<div> | ||
<label htmlFor="username">Username</label> | ||
<input | ||
id="username" | ||
type="text" | ||
value={username || ''} | ||
onChange={(e) => setUsername(e.target.value)} | ||
/> | ||
</div> | ||
<div> | ||
<label htmlFor="website">Website</label> | ||
<input | ||
id="website" | ||
type="website" | ||
value={website || ''} | ||
onChange={(e) => setWebsite(e.target.value)} | ||
/> | ||
</div> | ||
|
||
<div> | ||
<button className="button block primary" onClick={() => updateProfile()} disabled={loading}> | ||
{loading ? 'Loading ...' : 'Update'} | ||
</button> | ||
</div> | ||
|
||
<div> | ||
<button className="button block" onClick={() => signOut()}> | ||
Sign Out | ||
</button> | ||
</div> | ||
</div> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import { useState } from 'react' | ||
import { supabase } from '../lib/api' | ||
// import styles from '../styles/Auth.module.css' | ||
|
||
export default function Auth({}) { | ||
const [loading, setLoading] = useState(false) | ||
const [email, setEmail] = useState('') | ||
|
||
const handleLogin = async (email: string) => { | ||
try { | ||
setLoading(true) | ||
const { error, user } = await supabase.auth.signIn({ email }) | ||
|
||
if (error) { | ||
throw error | ||
} | ||
|
||
console.log('user', user) | ||
alert('Check your email for the login link!') | ||
} catch (error) { | ||
console.log('Error thrown:', error.message) | ||
alert(error.error_description || error.message) | ||
} finally { | ||
setLoading(false) | ||
} | ||
} | ||
|
||
return ( | ||
<div style={{ display: 'flex', gap: 20, flexDirection: 'column' }}> | ||
<div> | ||
<label>Email</label> | ||
<input | ||
type="email" | ||
placeholder="Your email" | ||
value={email} | ||
onChange={(e) => setEmail(e.target.value)} | ||
/> | ||
</div> | ||
|
||
<div> | ||
<button | ||
onClick={(e) => { | ||
e.preventDefault() | ||
handleLogin(email) | ||
}} | ||
className={'button block'} | ||
disabled={loading} | ||
> | ||
{loading ? 'Loading ..' : 'Send magic link'} | ||
</button> | ||
</div> | ||
</div> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { useEffect, useState } from 'react' | ||
import { supabase } from '../lib/api' | ||
|
||
export default function Avatar({ url, size }: { url: string | null; size: number }) { | ||
const [avatarUrl, setAvatarUrl] = useState<string | null>(null) | ||
|
||
useEffect(() => { | ||
if (url) downloadImage(url) | ||
}, [url]) | ||
|
||
async function downloadImage(path: string) { | ||
try { | ||
const { data, error } = await supabase.storage.downloadFile(path) | ||
if (error) { | ||
throw error | ||
} | ||
const url = URL.createObjectURL(data) | ||
setAvatarUrl(url) | ||
} catch (error) { | ||
console.log('Error downloading image: ', error.message) | ||
} | ||
} | ||
|
||
return avatarUrl ? ( | ||
<img src={avatarUrl} className="avatar image" style={{ height: size, width: size }} /> | ||
) : ( | ||
<div className="avatar no-image" style={{ height: size, width: size }} /> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { Profile } from '../lib/constants' | ||
import Avatar from './Avatar' | ||
|
||
export default function ProfileCard({ profile }: { profile: Profile }) { | ||
const lastUpdated = profile.updated_at ? new Date(profile.updated_at) : null | ||
return ( | ||
<div className="card"> | ||
<Avatar url={profile.avatar_url} size={50} /> | ||
<p>Username: {profile.username}</p> | ||
<p>Website: {profile.website}</p> | ||
<p> | ||
<small> | ||
Last updated{' '} | ||
{lastUpdated | ||
? `${lastUpdated.toLocaleDateString()} ${lastUpdated.toLocaleTimeString()}` | ||
: 'Never'} | ||
</small> | ||
</p> | ||
</div> | ||
) | ||
} |
Oops, something went wrong.