Skip to content

Commit

Permalink
Merge pull request #136 from supabase/feat/storage-api
Browse files Browse the repository at this point in the history
feat: storage api
  • Loading branch information
kiwicopple authored Mar 23, 2021
2 parents b22189b + f6fd8de commit 98d85cf
Show file tree
Hide file tree
Showing 31 changed files with 3,024 additions and 10 deletions.
34 changes: 34 additions & 0 deletions example/next-storage/.gitignore
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
34 changes: 34 additions & 0 deletions example/next-storage/README.md
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;
```
171 changes: 171 additions & 0 deletions example/next-storage/components/Account.tsx
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>
)
}
54 changes: 54 additions & 0 deletions example/next-storage/components/Auth.tsx
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>
)
}
29 changes: 29 additions & 0 deletions example/next-storage/components/Avatar.tsx
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 }} />
)
}
21 changes: 21 additions & 0 deletions example/next-storage/components/ProfileCard.tsx
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>
)
}
Loading

0 comments on commit 98d85cf

Please sign in to comment.