examples: Add with-supabase-auth-realtime-db example. (#16016)
r? @lfades @timothyis cc @kiwicopple @awalias Adding a realtime chat example showing how to implement authentication and realtime data syncing with supabase.io :)
This commit is contained in:
parent
604ca6c61d
commit
bdc465092b
15 changed files with 732 additions and 0 deletions
|
@ -0,0 +1,2 @@
|
|||
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
|
||||
NEXT_PUBLIC_SUPABASE_KEY=your-anon-key
|
34
examples/with-supabase-auth-realtime-db/.gitignore
vendored
Normal file
34
examples/with-supabase-auth-realtime-db/.gitignore
vendored
Normal file
|
@ -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
|
139
examples/with-supabase-auth-realtime-db/README.md
Normal file
139
examples/with-supabase-auth-realtime-db/README.md
Normal file
|
@ -0,0 +1,139 @@
|
|||
# Realtime chat example using Supabase
|
||||
|
||||
This is a full-stack Slack clone example using:
|
||||
|
||||
- Frontend:
|
||||
- Next.js.
|
||||
- [Supabase](https://supabase.io/docs/library/getting-started) for user management and realtime data syncing.
|
||||
- Backend:
|
||||
- [app.supabase.io](https://app.supabase.io/): hosted Postgres database with restful API for usage with Supabase.js.
|
||||
|
||||
![Demo animation gif](./docs/slack-clone-demo.gif)
|
||||
|
||||
This example is a clone of the [Slack Clone example](https://github.com/supabase/supabase/tree/master/examples/slack-clone) in the supabase repo, feel free to check it out!
|
||||
|
||||
## Deploy your own
|
||||
|
||||
Once you have access to [the environment variables you'll need](#step-3-set-up-environment-variables), deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example):
|
||||
|
||||
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/import/git?c=1&s=https://github.com/vercel/next.js/tree/canary/examples/with-supabase-auth-realtime-db&env=NEXT_PUBLIC_SUPABASE_URL,NEXT_PUBLIC_SUPABASE_KEY&envDescription=Required%20to%20connect%20the%20app%to%Supabase&envLink=https://github.com/vercel/next.js/tree/canary/examples/with-supabase-auth-realtime-db%23step-3-set-up-environment-variables&project-name=supabase-slack-clone&repo-name=supabase-slack-clone)
|
||||
|
||||
## How to use
|
||||
|
||||
Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example:
|
||||
|
||||
```bash
|
||||
npx create-next-app --example with-supabase-auth-realtime-db realtime-chat-app
|
||||
# or
|
||||
yarn create next-app --example with-supabase-auth-realtime-db realtime-chat-app
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Step 1. Create a new Supabase project
|
||||
|
||||
Sign up to Supabase - [https://app.supabase.io](https://app.supabase.io) and create a new project. Wait for your database to start.
|
||||
|
||||
### Step 2. Run the "Slack Clone" Quickstart
|
||||
|
||||
Once your database has started, run the "Slack Clone" quickstart.
|
||||
|
||||
![Slack Clone Quick Start](https://user-images.githubusercontent.com/10214025/88916135-1b1d7a00-d298-11ea-82e7-e2c18314e805.png)
|
||||
|
||||
### Step 3. Set up environment variables
|
||||
|
||||
In your Supabase project, go to Project Settings (the cog icon), open the API tab, and find your **API URL** and **anon** key, you'll need these in the next step.
|
||||
|
||||
![image](https://user-images.githubusercontent.com/10214025/88916245-528c2680-d298-11ea-8a71-708f93e1ce4f.png)
|
||||
|
||||
Next, copy the `.env.local.example` file in this directory to `.env.local` (which will be ignored by Git):
|
||||
|
||||
```bash
|
||||
cp .env.local.example .env.local
|
||||
```
|
||||
|
||||
Then set each variable on `.env.local`:
|
||||
|
||||
- `NEXT_PUBLIC_SUPABASE_URL` should be the **API URL**
|
||||
- `NEXT_PUBLIC_SUPABASE_KEY` should be the **anon** key
|
||||
|
||||
The **anon** key is your client-side API key. It allows "anonymous access" to your database, until the user has logged in. Once they have logged in, the keys will switch to the user's own login token. This enables row level security for your data. Read more about this [below](#postgres-row-level-security).
|
||||
|
||||
> **_NOTE_**: The `service_role` key has full access to your data, bypassing any security policies. These keys have to be kept secret and are meant to be used in server environments and never on a client or browser.
|
||||
|
||||
### Step 4. Run Next.js in development mode
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
|
||||
# or
|
||||
|
||||
yarn install
|
||||
yarn dev
|
||||
```
|
||||
|
||||
Visit [http://localhost:3000](http://localhost:3000) and start chatting! Open a channel across two browser tabs to see everything getting updated in realtime 🥳. If it doesn't work, post on [GitHub discussions](https://github.com/vercel/next.js/discussions).
|
||||
|
||||
### Step 5. Deploy on Vercel
|
||||
|
||||
You can deploy this app to the cloud with [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)).
|
||||
|
||||
#### Deploy Your Local Project
|
||||
|
||||
To deploy your local project to Vercel, push it to GitHub/GitLab/Bitbucket and [import to Vercel](https://vercel.com/import/git?utm_source=github&utm_medium=readme&utm_campaign=next-example).
|
||||
|
||||
**Important**: When you import your project on Vercel, make sure to click on **Environment Variables** and set them to match your `.env.local` file.
|
||||
|
||||
#### Deploy from Our Template
|
||||
|
||||
Alternatively, you can deploy using our template by clicking on the Deploy button below.
|
||||
|
||||
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/import/git?c=1&s=https://github.com/vercel/next.js/tree/canary/examples/with-supabase-auth-realtime-db&env=NEXT_PUBLIC_SUPABASE_URL,NEXT_PUBLIC_SUPABASE_KEY&envDescription=Required%20to%20connect%20the%20app%to%Supabase&envLink=https://github.com/vercel/next.js/tree/canary/examples/with-supabase-auth-realtime-db%23step-3-set-up-environment-variables&project-name=supabase-slack-clone&repo-name=supabase-slack-clone)
|
||||
|
||||
## Supabase details
|
||||
|
||||
### Postgres Row level security
|
||||
|
||||
This project uses very high-level Authorization using Postgres' Role Level Security.
|
||||
When you start a Postgres database on Supabase, we populate it with an `auth` schema, and some helper functions.
|
||||
When a user logs in, they are issued a JWT with the role `authenticated` and thier UUID.
|
||||
We can use these details to provide fine-grained control over what each user can and cannot do.
|
||||
|
||||
This is a trimmed-down schema, with the policies:
|
||||
|
||||
```sql
|
||||
-- USER PROFILES
|
||||
CREATE TYPE public.user_status AS ENUM ('ONLINE', 'OFFLINE');
|
||||
CREATE TABLE public.users (
|
||||
id uuid NOT NULL PRIMARY KEY, -- UUID from auth.users (Supabase)
|
||||
username text,
|
||||
status user_status DEFAULT 'OFFLINE'::public.user_status
|
||||
);
|
||||
ALTER TABLE public.users ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY "Allow logged-in read access" on public.users FOR SELECT USING ( auth.role() = 'authenticated' );
|
||||
CREATE POLICY "Allow individual insert access" on public.users FOR INSERT WITH CHECK ( auth.uid() = id );
|
||||
CREATE POLICY "Allow individual update access" on public.users FOR UPDATE USING ( auth.uid() = id );
|
||||
|
||||
-- CHANNELS
|
||||
CREATE TABLE public.channels (
|
||||
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
inserted_at timestamp with time zone DEFAULT timezone('utc'::text, now()) NOT NULL,
|
||||
slug text NOT NULL UNIQUE
|
||||
);
|
||||
ALTER TABLE public.channels ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY "Allow logged-in full access" on public.channels FOR ALL USING ( auth.role() = 'authenticated' );
|
||||
|
||||
-- MESSAGES
|
||||
CREATE TABLE public.messages (
|
||||
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
inserted_at timestamp with time zone DEFAULT timezone('utc'::text, now()) NOT NULL,
|
||||
message text,
|
||||
user_id uuid REFERENCES public.users NOT NULL,
|
||||
channel_id bigint REFERENCES public.channels NOT NULL
|
||||
);
|
||||
ALTER TABLE public.messages ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY "Allow logged-in read access" on public.messages USING ( auth.role() = 'authenticated' );
|
||||
CREATE POLICY "Allow individual insert access" on public.messages FOR INSERT WITH CHECK ( auth.uid() = user_id );
|
||||
CREATE POLICY "Allow individual update access" on public.messages FOR UPDATE USING ( auth.uid() = user_id );
|
||||
```
|
80
examples/with-supabase-auth-realtime-db/components/Layout.js
Normal file
80
examples/with-supabase-auth-realtime-db/components/Layout.js
Normal file
|
@ -0,0 +1,80 @@
|
|||
import Link from 'next/link'
|
||||
import { useContext } from 'react'
|
||||
import UserContext from '~/lib/UserContext'
|
||||
import { addChannel } from '~/lib/Store'
|
||||
|
||||
export default function Layout(props) {
|
||||
const { signOut } = useContext(UserContext)
|
||||
|
||||
const slugify = (text) => {
|
||||
return text
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, '-') // Replace spaces with -
|
||||
.replace(/[^\w-]+/g, '') // Remove all non-word chars
|
||||
.replace(/--+/g, '-') // Replace multiple - with single -
|
||||
.replace(/^-+/, '') // Trim - from start of text
|
||||
.replace(/-+$/, '') // Trim - from end of text
|
||||
}
|
||||
|
||||
const newChannel = async () => {
|
||||
const slug = prompt('Please enter your name')
|
||||
if (slug) {
|
||||
addChannel(slugify(slug))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="main flex h-screen w-screen absolute overflow-hidden">
|
||||
{/* Sidebar */}
|
||||
<nav
|
||||
className="w-64 bg-gray-900 text-gray-100 h-screen"
|
||||
style={{ maxWidth: '20%', minWidth: 150 }}
|
||||
>
|
||||
<div className="p-2">
|
||||
<h4 className="font-bold">Channels</h4>
|
||||
<ul className="channel-list">
|
||||
{props.channels.map((x) => (
|
||||
<SidebarItem
|
||||
channel={x}
|
||||
key={x.id}
|
||||
isActiveChannel={x.id === props.activeChannelId}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<hr className="m-2" />
|
||||
<div className="p-2">
|
||||
<button
|
||||
className="bg-blue-900 hover:bg-blue-800 text-white py-2 px-4 rounded w-full transition duration-150"
|
||||
onClick={() => newChannel()}
|
||||
>
|
||||
New Channel
|
||||
</button>
|
||||
</div>
|
||||
<hr className="m-2" />
|
||||
<div className="p-2">
|
||||
<button
|
||||
className="bg-blue-900 hover:bg-blue-800 text-white py-2 px-4 rounded w-full transition duration-150"
|
||||
onClick={() => signOut()}
|
||||
>
|
||||
Log out
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Messages */}
|
||||
<div className="flex-1 bg-gray-800 h-screen">{props.children}</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
const SidebarItem = ({ channel, isActiveChannel }) => (
|
||||
<>
|
||||
<li>
|
||||
<Link href="/channels/[id]" as={`/channels/${channel.id}`}>
|
||||
<a className={isActiveChannel ? 'font-bold' : ''}>{channel.slug}</a>
|
||||
</Link>
|
||||
</li>
|
||||
</>
|
||||
)
|
|
@ -0,0 +1,10 @@
|
|||
const Message = ({ message }) => (
|
||||
<>
|
||||
<div className="py-1">
|
||||
<p className="text-blue-700 font-bold">{message.author.username}</p>
|
||||
<p className="text-white">{message.message}</p>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
export default Message
|
|
@ -0,0 +1,28 @@
|
|||
import { useState } from 'react'
|
||||
|
||||
const MessageInput = ({ onSubmit }) => {
|
||||
const [messageText, setMessageText] = useState('')
|
||||
|
||||
const submitOnEnter = (event) => {
|
||||
// Watch for enter key
|
||||
if (event.keyCode === 13) {
|
||||
onSubmit(messageText)
|
||||
setMessageText('')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
||||
type="text"
|
||||
placeholder="Send a message"
|
||||
value={messageText}
|
||||
onChange={(e) => setMessageText(e.target.value)}
|
||||
onKeyDown={(e) => submitOnEnter(e)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default MessageInput
|
Binary file not shown.
After Width: | Height: | Size: 1.5 MiB |
8
examples/with-supabase-auth-realtime-db/jsconfig.json
Normal file
8
examples/with-supabase-auth-realtime-db/jsconfig.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./*"]
|
||||
}
|
||||
}
|
||||
}
|
169
examples/with-supabase-auth-realtime-db/lib/Store.js
Normal file
169
examples/with-supabase-auth-realtime-db/lib/Store.js
Normal file
|
@ -0,0 +1,169 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
const supabase = createClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_KEY
|
||||
)
|
||||
|
||||
/**
|
||||
* @param {number} channelId the currently selected Channel
|
||||
*/
|
||||
export const useStore = (props) => {
|
||||
const [channels, setChannels] = useState([])
|
||||
const [messages, setMessages] = useState([])
|
||||
const [users] = useState(new Map())
|
||||
const [newMessage, handleNewMessage] = useState(null)
|
||||
const [newChannel, handleNewChannel] = useState(null)
|
||||
const [newOrUpdatedUser, handleNewOrUpdatedUser] = useState(null)
|
||||
|
||||
// Load initial data and set up listeners
|
||||
useEffect(() => {
|
||||
// Get Channels
|
||||
fetchChannels(setChannels)
|
||||
// Listen for new messages
|
||||
const messageListener = supabase
|
||||
.from('messages')
|
||||
.on('INSERT', (payload) => handleNewMessage(payload.new))
|
||||
.subscribe()
|
||||
// Listen for changes to our users
|
||||
const userListener = supabase
|
||||
.from('users')
|
||||
.on('*', (payload) => handleNewOrUpdatedUser(payload.new))
|
||||
.subscribe()
|
||||
// Listen for new channels
|
||||
const channelListener = supabase
|
||||
.from('channels')
|
||||
.on('INSERT', (payload) => handleNewChannel(payload.new))
|
||||
.subscribe()
|
||||
// Cleanup on unmount
|
||||
return () => {
|
||||
messageListener.unsubscribe()
|
||||
userListener.unsubscribe()
|
||||
channelListener.unsubscribe()
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Update when the route changes
|
||||
useEffect(() => {
|
||||
if (props?.channelId > 0) {
|
||||
fetchMessages(props.channelId, (messages) => {
|
||||
messages.forEach((x) => users.set(x.user_id, x.author))
|
||||
setMessages(messages)
|
||||
})
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [props.channelId])
|
||||
|
||||
// New message recieved from Postgres
|
||||
useEffect(() => {
|
||||
if (newMessage && newMessage.channel_id === Number(props.channelId)) {
|
||||
const handleAsync = async () => {
|
||||
let authorId = newMessage.user_id
|
||||
if (!users.get(authorId))
|
||||
await fetchUser(authorId, (user) => handleNewOrUpdatedUser(user))
|
||||
setMessages(messages.concat(newMessage))
|
||||
}
|
||||
handleAsync()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [newMessage])
|
||||
|
||||
// New channel recieved from Postgres
|
||||
useEffect(() => {
|
||||
if (newChannel) setChannels(channels.concat(newChannel))
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [newChannel])
|
||||
|
||||
// New or updated user recieved from Postgres
|
||||
useEffect(() => {
|
||||
if (newOrUpdatedUser) users.set(newOrUpdatedUser.id, newOrUpdatedUser)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [newOrUpdatedUser])
|
||||
|
||||
return {
|
||||
// We can export computed values here to map the authors to each message
|
||||
messages: messages.map((x) => ({ ...x, author: users.get(x.user_id) })),
|
||||
channels: channels.sort((a, b) => a.slug.localeCompare(b.slug)),
|
||||
users,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all channels
|
||||
* @param {function} setState Optionally pass in a hook or callback to set the state
|
||||
*/
|
||||
export const fetchChannels = async (setState) => {
|
||||
try {
|
||||
let { body } = await supabase.from('channels').select('*')
|
||||
if (setState) setState(body)
|
||||
return body
|
||||
} catch (error) {
|
||||
console.log('error', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a single user
|
||||
* @param {number} userId
|
||||
* @param {function} setState Optionally pass in a hook or callback to set the state
|
||||
*/
|
||||
export const fetchUser = async (userId, setState) => {
|
||||
try {
|
||||
let { body } = await supabase.from('users').eq('id', userId).select(`*`)
|
||||
let user = body[0]
|
||||
if (setState) setState(user)
|
||||
return user
|
||||
} catch (error) {
|
||||
console.log('error', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all messages and their authors
|
||||
* @param {number} channelId
|
||||
* @param {function} setState Optionally pass in a hook or callback to set the state
|
||||
*/
|
||||
export const fetchMessages = async (channelId, setState) => {
|
||||
try {
|
||||
let { body } = await supabase
|
||||
.from('messages')
|
||||
.eq('channel_id', channelId)
|
||||
.select(`*, author:user_id(*)`)
|
||||
.order('inserted_at', true)
|
||||
if (setState) setState(body)
|
||||
return body
|
||||
} catch (error) {
|
||||
console.log('error', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a new channel into the DB
|
||||
* @param {string} slug The channel name
|
||||
*/
|
||||
export const addChannel = async (slug) => {
|
||||
try {
|
||||
let { body } = await supabase.from('channels').insert([{ slug }])
|
||||
return body
|
||||
} catch (error) {
|
||||
console.log('error', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a new message into the DB
|
||||
* @param {string} message The message text
|
||||
* @param {number} channel_id
|
||||
* @param {number} user_id The author
|
||||
*/
|
||||
export const addMessage = async (message, channel_id, user_id) => {
|
||||
try {
|
||||
let { body } = await supabase
|
||||
.from('messages')
|
||||
.insert([{ message, channel_id, user_id }])
|
||||
return body
|
||||
} catch (error) {
|
||||
console.log('error', error)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import { createContext } from 'react'
|
||||
|
||||
const UserContext = createContext()
|
||||
|
||||
export default UserContext
|
18
examples/with-supabase-auth-realtime-db/package.json
Normal file
18
examples/with-supabase-auth-realtime-db/package.json
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "realtime-chat-app",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "next",
|
||||
"build": "next build",
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^0.35.9",
|
||||
"next": "latest",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"sass": "^1.26.2",
|
||||
"tailwindcss": "^1.1.4"
|
||||
}
|
||||
}
|
71
examples/with-supabase-auth-realtime-db/pages/_app.js
Normal file
71
examples/with-supabase-auth-realtime-db/pages/_app.js
Normal file
|
@ -0,0 +1,71 @@
|
|||
import '~/styles/style.scss'
|
||||
import React from 'react'
|
||||
import App from 'next/app'
|
||||
import Router from 'next/router'
|
||||
import UserContext from 'lib/UserContext'
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
const supabase = createClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_KEY
|
||||
)
|
||||
|
||||
export default class SupabaseSlackClone extends App {
|
||||
state = {
|
||||
authLoaded: false,
|
||||
user: null,
|
||||
}
|
||||
|
||||
componentDidMount = () => {
|
||||
const user = localStorage.getItem('supabase-slack-clone')
|
||||
if (user) this.setState({ user, authLoaded: true })
|
||||
else Router.push('/')
|
||||
}
|
||||
|
||||
signIn = async (id, username) => {
|
||||
try {
|
||||
let { body } = await supabase
|
||||
.from('users')
|
||||
.match({ username })
|
||||
.select('id, username')
|
||||
const existing = body[0]
|
||||
const { body: user } = existing?.id
|
||||
? await supabase
|
||||
.from('users')
|
||||
.update({ id, username })
|
||||
.match({ id })
|
||||
.single()
|
||||
: await supabase.from('users').insert([{ id, username }]).single()
|
||||
|
||||
localStorage.setItem('supabase-slack-clone', user.id)
|
||||
this.setState({ user: user.id }, () => {
|
||||
Router.push('/channels/[id]', '/channels/1')
|
||||
})
|
||||
} catch (error) {
|
||||
console.log('error', error)
|
||||
}
|
||||
}
|
||||
|
||||
signOut = () => {
|
||||
supabase.auth.logout()
|
||||
localStorage.removeItem('supabase-slack-clone')
|
||||
this.setState({ user: null })
|
||||
Router.push('/')
|
||||
}
|
||||
|
||||
render() {
|
||||
const { Component, pageProps } = this.props
|
||||
return (
|
||||
<UserContext.Provider
|
||||
value={{
|
||||
authLoaded: this.state.authLoaded,
|
||||
user: this.state.user,
|
||||
signIn: this.signIn,
|
||||
signOut: this.signOut,
|
||||
}}
|
||||
>
|
||||
<Component {...pageProps} />
|
||||
</UserContext.Provider>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
import Layout from '~/components/Layout'
|
||||
import Message from '~/components/Message'
|
||||
import MessageInput from '~/components/MessageInput'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useStore, addMessage } from '~/lib/Store'
|
||||
import { useContext, useEffect, useRef } from 'react'
|
||||
import UserContext from '~/lib/UserContext'
|
||||
|
||||
const ChannelsPage = (props) => {
|
||||
const router = useRouter()
|
||||
const { user, authLoaded, signOut } = useContext(UserContext)
|
||||
const messagesEndRef = useRef(null)
|
||||
|
||||
// Redirect if not signed in.
|
||||
useEffect(() => {
|
||||
if (authLoaded && !user) signOut()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [user, router])
|
||||
|
||||
// Else load up the page
|
||||
const { id: channelId } = router.query
|
||||
const { messages, channels } = useStore({ channelId })
|
||||
|
||||
useEffect(() => {
|
||||
messagesEndRef.current.scrollIntoView({
|
||||
block: 'start',
|
||||
behavior: 'smooth',
|
||||
})
|
||||
}, [messages])
|
||||
|
||||
// Render the channels and messages
|
||||
return (
|
||||
<Layout channels={channels} activeChannelId={channelId}>
|
||||
<div className="relative h-screen">
|
||||
<div className="Messages h-full pb-16">
|
||||
<div className="p-2 overflow-y-auto">
|
||||
{messages.map((x) => (
|
||||
<Message key={x.id} message={x} />
|
||||
))}
|
||||
<div ref={messagesEndRef} style={{ height: 0 }} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-2 absolute bottom-0 left-0 w-full">
|
||||
<MessageInput
|
||||
onSubmit={async (text) => addMessage(text, channelId, user)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChannelsPage
|
87
examples/with-supabase-auth-realtime-db/pages/index.js
Normal file
87
examples/with-supabase-auth-realtime-db/pages/index.js
Normal file
|
@ -0,0 +1,87 @@
|
|||
import { useState, useContext } from 'react'
|
||||
import UserContext from 'lib/UserContext'
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
const supabase = createClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_KEY
|
||||
)
|
||||
|
||||
const Home = () => {
|
||||
const { signIn } = useContext(UserContext)
|
||||
const [username, setUsername] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
|
||||
const handleLogin = async (type, username, password) => {
|
||||
try {
|
||||
const {
|
||||
body: { user },
|
||||
} =
|
||||
type === 'LOGIN'
|
||||
? await supabase.auth.login(username, password)
|
||||
: await supabase.auth.signup(username, password)
|
||||
if (!!user) signIn(user.id, user.email)
|
||||
} catch (error) {
|
||||
console.log('error', error)
|
||||
alert(error.error_description || error)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex justify-center items-center p-4 bg-gray-300">
|
||||
<div className="w-full sm:w-1/2 xl:w-1/3">
|
||||
<div className="border-teal p-8 border-t-12 bg-white mb-6 rounded-lg shadow-lg bg-white">
|
||||
<div className="mb-4">
|
||||
<label className="font-bold text-grey-darker block mb-2">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="block appearance-none w-full bg-white border border-grey-light hover:border-grey px-2 py-2 rounded shadow"
|
||||
placeholder="Your Username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="font-bold text-grey-darker block mb-2">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
className="block appearance-none w-full bg-white border border-grey-light hover:border-grey px-2 py-2 rounded shadow"
|
||||
placeholder="Your password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<a
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
handleLogin('SIGNUP', username, password)
|
||||
}}
|
||||
href={'/channels'}
|
||||
className="bg-indigo-700 hover:bg-teal text-white py-2 px-4 rounded text-center transition duration-150 hover:bg-indigo-600 hover:text-white"
|
||||
>
|
||||
Sign up
|
||||
</a>
|
||||
<a
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
handleLogin('LOGIN', username, password)
|
||||
}}
|
||||
href={'/channels'}
|
||||
className="border border-indigo-700 text-indigo-700 py-2 px-4 rounded w-full text-center transition duration-150 hover:bg-indigo-700 hover:text-white"
|
||||
>
|
||||
Login
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
28
examples/with-supabase-auth-realtime-db/styles/style.scss
Normal file
28
examples/with-supabase-auth-realtime-db/styles/style.scss
Normal file
|
@ -0,0 +1,28 @@
|
|||
@import '~tailwindcss/dist/base.css';
|
||||
@import '~tailwindcss/dist/components.css';
|
||||
@import '~tailwindcss/dist/utilities.css';
|
||||
|
||||
html,
|
||||
body,
|
||||
#__next,
|
||||
.main {
|
||||
max-height: 100vh;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.channel-list {
|
||||
li a:before {
|
||||
content: '# ';
|
||||
opacity: 0.5;
|
||||
}
|
||||
li a:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
.Messages {
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
}
|
Loading…
Reference in a new issue