Skip to content

Commit 98d85cf

Browse files
authored
Merge pull request #136 from supabase/feat/storage-api
feat: storage api
2 parents b22189b + f6fd8de commit 98d85cf

31 files changed

+3024
-10
lines changed

example/next-storage/.gitignore

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
8+
# testing
9+
/coverage
10+
11+
# next.js
12+
/.next/
13+
/out/
14+
15+
# production
16+
/build
17+
18+
# misc
19+
.DS_Store
20+
*.pem
21+
22+
# debug
23+
npm-debug.log*
24+
yarn-debug.log*
25+
yarn-error.log*
26+
27+
# local env files
28+
.env.local
29+
.env.development.local
30+
.env.test.local
31+
.env.production.local
32+
33+
# vercel
34+
.vercel

example/next-storage/README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Supabase storage example
2+
3+
- Create a file `.env.local`
4+
- Add a `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_KEY`
5+
- Run `npm run dev`
6+
7+
## Database schema
8+
9+
```sql
10+
create table profiles (
11+
id uuid references auth.users not null,
12+
updated_at timestamp with time zone,
13+
username text unique,
14+
avatar_url text,
15+
website text,
16+
17+
primary key (id),
18+
unique(username),
19+
constraint username_length check (char_length(username) >= 3)
20+
);
21+
alter table profiles enable row level security;
22+
create policy "Public profiles are viewable by everyone." on profiles for select using (true);
23+
create policy "Users can insert their own profile." on profiles for insert with check (auth.uid() = id);
24+
create policy "Users can update own profile." on profiles for update using (auth.uid() = id);
25+
26+
27+
-- Set up Realtime!
28+
begin;
29+
drop publication if exists supabase_realtime;
30+
create publication supabase_realtime;
31+
commit;
32+
33+
alter publication supabase_realtime add table profiles;
34+
```
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import { useState, useEffect, ChangeEvent } from 'react'
2+
import { supabase } from '../lib/api'
3+
import UploadButton from '../components/UploadButton'
4+
import Avatar from './Avatar'
5+
import { AuthSession } from '../../../dist/main'
6+
import { DEFAULT_AVATARS_BUCKET, Profile } from '../lib/constants'
7+
8+
export default function Account({ session }: { session: AuthSession }) {
9+
const [loading, setLoading] = useState<boolean>(true)
10+
const [uploading, setUploading] = useState<boolean>(false)
11+
const [avatar, setAvatar] = useState<string | null>(null)
12+
const [username, setUsername] = useState<string | null>(null)
13+
const [website, setWebsite] = useState<string | null>(null)
14+
15+
useEffect(() => {
16+
getProfile()
17+
}, [session])
18+
19+
async function signOut() {
20+
const { error } = await supabase.auth.signOut()
21+
if (error) console.log('Error logging out:', error.message)
22+
}
23+
24+
async function uploadAvatar(event: ChangeEvent<HTMLInputElement>) {
25+
try {
26+
setUploading(true)
27+
28+
if (!event.target.files || event.target.files.length == 0) {
29+
throw 'You must select an image to upload.'
30+
}
31+
32+
const user = supabase.auth.user()
33+
const file = event.target.files[0]
34+
const fileExt = file.name.split('.').pop()
35+
const fileName = `${session?.user.id}${Math.random()}.${fileExt}`
36+
const filePath = `${DEFAULT_AVATARS_BUCKET}/${fileName}`
37+
38+
let { error: uploadError } = await supabase.storage.uploadFile(filePath, file)
39+
40+
if (uploadError) {
41+
throw uploadError
42+
}
43+
44+
let { error: updateError } = await supabase.from('profiles').upsert({
45+
id: user.id,
46+
avatar_url: filePath,
47+
})
48+
49+
if (updateError) {
50+
throw updateError
51+
}
52+
53+
setAvatar(null)
54+
setAvatar(filePath)
55+
} catch (error) {
56+
alert(error.message)
57+
} finally {
58+
setUploading(false)
59+
}
60+
}
61+
62+
function setProfile(profile: Profile) {
63+
setAvatar(profile.avatar_url)
64+
setUsername(profile.username)
65+
setWebsite(profile.website)
66+
}
67+
68+
async function getProfile() {
69+
try {
70+
setLoading(true)
71+
const user = supabase.auth.user()
72+
73+
let { data, error } = await supabase
74+
.from('profiles')
75+
.select(`username, website, avatar_url`)
76+
.eq('id', user.id)
77+
.single()
78+
79+
if (error) {
80+
throw error
81+
}
82+
83+
setProfile(data)
84+
} catch (error) {
85+
console.log('error', error.message)
86+
} finally {
87+
setLoading(false)
88+
}
89+
}
90+
91+
async function updateProfile() {
92+
try {
93+
setLoading(true)
94+
const user = supabase.auth.user()
95+
96+
const updates = {
97+
id: user.id,
98+
username,
99+
website,
100+
updated_at: new Date(),
101+
}
102+
103+
let { error } = await supabase.from('profiles').upsert(updates, {
104+
returning: 'minimal', // Don't return the value after inserting
105+
})
106+
107+
if (error) {
108+
throw error
109+
}
110+
} catch (error) {
111+
alert(error.message)
112+
} finally {
113+
setLoading(false)
114+
}
115+
}
116+
117+
return (
118+
<div
119+
style={{
120+
minWidth: 250,
121+
maxWidth: 600,
122+
margin: 'auto',
123+
display: 'flex',
124+
flexDirection: 'column',
125+
gap: 20,
126+
}}
127+
>
128+
<div className="card">
129+
<div>
130+
<Avatar url={avatar} size={270} />
131+
</div>
132+
<UploadButton onUpload={uploadAvatar} loading={uploading} />
133+
</div>
134+
135+
<div>
136+
<label htmlFor="email">Email</label>
137+
<input id="email" type="text" value={session.user.email} disabled />
138+
</div>
139+
<div>
140+
<label htmlFor="username">Username</label>
141+
<input
142+
id="username"
143+
type="text"
144+
value={username || ''}
145+
onChange={(e) => setUsername(e.target.value)}
146+
/>
147+
</div>
148+
<div>
149+
<label htmlFor="website">Website</label>
150+
<input
151+
id="website"
152+
type="website"
153+
value={website || ''}
154+
onChange={(e) => setWebsite(e.target.value)}
155+
/>
156+
</div>
157+
158+
<div>
159+
<button className="button block primary" onClick={() => updateProfile()} disabled={loading}>
160+
{loading ? 'Loading ...' : 'Update'}
161+
</button>
162+
</div>
163+
164+
<div>
165+
<button className="button block" onClick={() => signOut()}>
166+
Sign Out
167+
</button>
168+
</div>
169+
</div>
170+
)
171+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { useState } from 'react'
2+
import { supabase } from '../lib/api'
3+
// import styles from '../styles/Auth.module.css'
4+
5+
export default function Auth({}) {
6+
const [loading, setLoading] = useState(false)
7+
const [email, setEmail] = useState('')
8+
9+
const handleLogin = async (email: string) => {
10+
try {
11+
setLoading(true)
12+
const { error, user } = await supabase.auth.signIn({ email })
13+
14+
if (error) {
15+
throw error
16+
}
17+
18+
console.log('user', user)
19+
alert('Check your email for the login link!')
20+
} catch (error) {
21+
console.log('Error thrown:', error.message)
22+
alert(error.error_description || error.message)
23+
} finally {
24+
setLoading(false)
25+
}
26+
}
27+
28+
return (
29+
<div style={{ display: 'flex', gap: 20, flexDirection: 'column' }}>
30+
<div>
31+
<label>Email</label>
32+
<input
33+
type="email"
34+
placeholder="Your email"
35+
value={email}
36+
onChange={(e) => setEmail(e.target.value)}
37+
/>
38+
</div>
39+
40+
<div>
41+
<button
42+
onClick={(e) => {
43+
e.preventDefault()
44+
handleLogin(email)
45+
}}
46+
className={'button block'}
47+
disabled={loading}
48+
>
49+
{loading ? 'Loading ..' : 'Send magic link'}
50+
</button>
51+
</div>
52+
</div>
53+
)
54+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { useEffect, useState } from 'react'
2+
import { supabase } from '../lib/api'
3+
4+
export default function Avatar({ url, size }: { url: string | null; size: number }) {
5+
const [avatarUrl, setAvatarUrl] = useState<string | null>(null)
6+
7+
useEffect(() => {
8+
if (url) downloadImage(url)
9+
}, [url])
10+
11+
async function downloadImage(path: string) {
12+
try {
13+
const { data, error } = await supabase.storage.downloadFile(path)
14+
if (error) {
15+
throw error
16+
}
17+
const url = URL.createObjectURL(data)
18+
setAvatarUrl(url)
19+
} catch (error) {
20+
console.log('Error downloading image: ', error.message)
21+
}
22+
}
23+
24+
return avatarUrl ? (
25+
<img src={avatarUrl} className="avatar image" style={{ height: size, width: size }} />
26+
) : (
27+
<div className="avatar no-image" style={{ height: size, width: size }} />
28+
)
29+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Profile } from '../lib/constants'
2+
import Avatar from './Avatar'
3+
4+
export default function ProfileCard({ profile }: { profile: Profile }) {
5+
const lastUpdated = profile.updated_at ? new Date(profile.updated_at) : null
6+
return (
7+
<div className="card">
8+
<Avatar url={profile.avatar_url} size={50} />
9+
<p>Username: {profile.username}</p>
10+
<p>Website: {profile.website}</p>
11+
<p>
12+
<small>
13+
Last updated{' '}
14+
{lastUpdated
15+
? `${lastUpdated.toLocaleDateString()} ${lastUpdated.toLocaleTimeString()}`
16+
: 'Never'}
17+
</small>
18+
</p>
19+
</div>
20+
)
21+
}

0 commit comments

Comments
 (0)