mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
Merge branch 'master' into feature/simple_auth
This commit is contained in:
commit
958a407136
205 files changed
+19460
-72016
No files matched your search
@@ -1,3 +0,0 @@
|
||||
# Example Contributing Guidelines
|
||||
|
||||
This is an example of GitHub's contributing guidelines file. Check out GitHub's [CONTRIBUTING.md help center article](https://help.github.com/articles/setting-guidelines-for-repository-contributors/) for more information.
|
||||
@@ -1,11 +0,0 @@
|
||||
* **I'm submitting a ...**
|
||||
[ ] bug report
|
||||
[ ] feature request
|
||||
[ ] question about the decisions made in the repository
|
||||
[ ] question about how to use this project
|
||||
|
||||
* **Summary**
|
||||
|
||||
|
||||
|
||||
* **Other information** (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. StackOverflow, personal fork, etc.)
|
||||
@@ -1,13 +0,0 @@
|
||||
* **What kind of change does this PR introduce?** (Bug fix, feature, docs update, ...)
|
||||
|
||||
|
||||
|
||||
* **What is the current behavior?** (You can also link to an open issue here)
|
||||
|
||||
|
||||
|
||||
* **What is the new behavior (if this is a feature change)?**
|
||||
|
||||
|
||||
|
||||
* **Other information**:
|
||||
@@ -32,5 +32,12 @@ We are currently in Public Alpha. Watch "releases" of this repo to get notified
|
||||
- [Supabase Realtime](https://github.com/supabase/realtime): Listen to your to PostgreSQL database in realtime via websockets. Built with Elixir.
|
||||
- [Supabase Postgres](https://github.com/supabase/postgres): Unmodified Postgres with some useful plugins.
|
||||
- [@supabase/postgrest-js](https://github.com/supabase/postgrest-js): Isomorphic JavaScript client for PostgREST.
|
||||
- [@supabase/doctest-js](https://github.com/supabase/realtime): Run JSDoc style doc examples as tests within your test suite.
|
||||
- [@supabase/doctest-js](https://github.com/supabase/doctest-js): Run JSDoc style doc examples as tests within your test suite.
|
||||
|
||||
## Sponsors
|
||||
|
||||
We are building the features of Firebase using enterprise-grade, open source products. We support existing communities wherever possible, and if the products don’t exist we build them and open source them ourselves. Thanks to these sponsors who are making the OSS ecosystem better for everyone.
|
||||
|
||||
[](https://www.worklife.vc)
|
||||
[](https://github.com/sponsors/supabase)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Supabase Connectors
|
||||
|
||||
We are considering building connectors that will automatically forward your database changes to varios systems. At this stage we are simply gauging demand.
|
||||
We are considering building connectors that will automatically forward your database changes to various systems. At this stage we are simply gauging demand.
|
||||
|
||||
If you would like us to start building a connector that you need, feel free to show your interest in this [issue on GitHub](https://github.com/supabase/supabase/issues/4).
|
||||
If you would like us to start building a connector that you need, feel free to show your interest in this [issue on GitHub](https://github.com/supabase/supabase/issues/4).
|
||||
@@ -0,0 +1 @@
|
||||
dist/*
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
How to sign up and login using supabase and supabase-js using HTML and Javascript only
|
||||
|
||||
<img width="558" alt="image" src="https://user-images.githubusercontent.com/458736/88377414-b6fb4180-cdd1-11ea-8061-103ec4577b7b.png">
|
||||
|
||||
### running
|
||||
`npm run dev` (requires npx to be installed)
|
||||
|
||||
|
||||
@@ -1,23 +1,16 @@
|
||||
body {
|
||||
/* margin: 0; */
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: rgb(250, 217, 246);
|
||||
}
|
||||
|
||||
/* h1 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 1em;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin: 1em;
|
||||
} */
|
||||
/* margin: 0; */
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',
|
||||
'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: rgb(250, 217, 246);
|
||||
}
|
||||
|
||||
|
||||
label {
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin: 1em;
|
||||
}
|
||||
@@ -9,6 +9,9 @@
|
||||
|
||||
<body>
|
||||
<div class='container'>
|
||||
<div class='section'>
|
||||
<h1>Supabase Auth Example</h1>
|
||||
</div>
|
||||
<div class='section'>
|
||||
<form id='sign-up'>
|
||||
<h3>Sign Up</h3>
|
||||
|
||||
@@ -6,7 +6,7 @@ var SUPABASE_KEY =
|
||||
var supabase = Supabase.createClient(SUPABASE_URL, SUPABASE_KEY)
|
||||
window.userToken = null
|
||||
|
||||
window.onload = function () {
|
||||
document.addEventListener('DOMContentLoaded', function (event) {
|
||||
var signUpForm = document.querySelector('#sign-up')
|
||||
signUpForm.onsubmit = signUpSubmitted.bind(signUpForm)
|
||||
|
||||
@@ -18,7 +18,7 @@ window.onload = function () {
|
||||
|
||||
var logoutButton = document.querySelector('#logout-button')
|
||||
logoutButton.onclick = logoutSubmitted.bind(logoutButton)
|
||||
}
|
||||
})
|
||||
|
||||
const signUpSubmitted = (event) => {
|
||||
event.preventDefault()
|
||||
|
||||
+1
-8760
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,47 @@
|
||||
# Live Tracker Map
|
||||
Build with supabase realtime and leafletjs. [[Demo App]](https://realtime-map.vercel.app/)
|
||||
|
||||
<p align="center">
|
||||
<kbd>
|
||||
<img src="https://media.giphy.com/media/iDU80ngpsSddc0ObGI/giphy.gif" alt="Demo"/>
|
||||
</kbd>
|
||||
</p>
|
||||
|
||||
## How to use
|
||||
#### Clone this folder
|
||||
```bash
|
||||
git clone https://github.com/supabase/supabase
|
||||
cd supabase/examples/live-tracker-map
|
||||
```
|
||||
|
||||
#### Install dependencies
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
#### Start the app
|
||||
```bash
|
||||
# Open a terminal and run:
|
||||
npm run dev
|
||||
```
|
||||
Visit http://localhost:4000 and start testing!
|
||||
|
||||
## Test with your own Supabase Project
|
||||
#### Create locations table
|
||||
Go to [app.supabase.io](https://app.supabase.io/), create a new organisation and project if you haven't had one.
|
||||
Run this sql query to create `locations` table.
|
||||
```sql
|
||||
CREATE TABLE locations (
|
||||
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
latitude numeric NOT NULL,
|
||||
longitude numeric NOT NULL,
|
||||
ref VARCHAR(40)
|
||||
);
|
||||
```
|
||||
|
||||
#### Setup env vars
|
||||
Make a .env.local in this folder with the following, you can get these values from your project dashboard at [app.supabase.io](https://app.supabase.io/):
|
||||
```bash
|
||||
NEXT_PUBLIC_SUPABASE_ENDPOINT=<your-supabase-url>
|
||||
NEXT_PUBLIC_SUPABASE_APIKEY=<your-supabase-key>
|
||||
```
|
||||
@@ -0,0 +1,86 @@
|
||||
import { useEffect, useState, useRef, useMemo, useCallback } from 'react'
|
||||
import { MapContainer, TileLayer, Marker, Popup, Circle } from 'react-leaflet'
|
||||
import { RoundToFixDecimals } from "lib/utils"
|
||||
import TextLog from "components/TextLog"
|
||||
|
||||
function DraggableMarker({ initialPos, addCircle }) {
|
||||
const [position, setPosition] = useState(initialPos)
|
||||
const markerRef = useRef(null)
|
||||
const eventHandlers = useMemo(
|
||||
() => ({
|
||||
dragend() {
|
||||
const marker = markerRef.current
|
||||
const pos = marker.getLatLng()
|
||||
if (addCircle) addCircle(pos)
|
||||
if (marker) setPosition(pos)
|
||||
},
|
||||
}),
|
||||
[addCircle],
|
||||
)
|
||||
|
||||
return (
|
||||
<Marker
|
||||
draggable={true}
|
||||
eventHandlers={eventHandlers}
|
||||
position={position}
|
||||
ref={markerRef}>
|
||||
<Popup minWidth={90}>
|
||||
{`lat: ${position.lat}`}<br />{`long: ${position.lng}`}
|
||||
</Popup>
|
||||
</Marker>
|
||||
)
|
||||
}
|
||||
|
||||
function MapInput({ supabase, clientRef, center, zoom }) {
|
||||
const [log, setLog] = useState(undefined)
|
||||
const [circles, setCircles] = useState([])
|
||||
|
||||
useEffect(() => {
|
||||
let newLog = `Ref: ${clientRef}\nReady to send location...`
|
||||
newLog += circles.map(item => { return `\nsent lat=${RoundToFixDecimals(item.lat)} long=${RoundToFixDecimals(item.lng)}` })
|
||||
setLog(newLog)
|
||||
}, [circles, clientRef])
|
||||
|
||||
const onAddCircle = useCallback(
|
||||
async (pos) => {
|
||||
setCircles([...circles, pos])
|
||||
|
||||
// insert new location
|
||||
await supabase
|
||||
.from('locations')
|
||||
.insert([
|
||||
{ latitude: pos.lat, longitude: pos.lng, ref: clientRef },
|
||||
])
|
||||
},
|
||||
[setCircles, circles, supabase, clientRef]
|
||||
);
|
||||
|
||||
function renderCircles() {
|
||||
return circles.map((item, index) => <Circle key={`${index}`} center={item} pathOptions={{ fillColor: 'blue' }} radius={20} />)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="map-input">
|
||||
<MapContainer center={center} zoom={zoom || 15} scrollWheelZoom={false}>
|
||||
<TileLayer
|
||||
attribution='© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
/>
|
||||
<DraggableMarker initialPos={center} addCircle={onAddCircle} />
|
||||
{renderCircles()}
|
||||
</MapContainer>
|
||||
<TextLog log={log} />
|
||||
|
||||
<style jsx>{`
|
||||
.map-input {
|
||||
}
|
||||
|
||||
.map-input textarea {
|
||||
width: 100%;
|
||||
height: 7rem;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default MapInput
|
||||
@@ -0,0 +1,64 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { MapContainer, TileLayer, Polyline } from 'react-leaflet'
|
||||
import { RoundToFixDecimals } from "lib/utils"
|
||||
import TextLog from "components/TextLog"
|
||||
|
||||
function MapView({ supabase, clientRef, center, zoom }) {
|
||||
const [log, setLog] = useState(undefined)
|
||||
const [positions, setPositions] = useState([])
|
||||
const mySubscription = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
let newLog = `Ref: ${clientRef}\nStart listerning...`
|
||||
newLog += positions.map(item => { return `\nid=${item.id} lat=${RoundToFixDecimals(item.lat)} long=${RoundToFixDecimals(item.lng)}` })
|
||||
setLog(newLog)
|
||||
}, [positions, clientRef])
|
||||
|
||||
useEffect(() => {
|
||||
// Listen to INSERT event on locations table
|
||||
mySubscription.current = supabase
|
||||
.from('locations')
|
||||
.on('INSERT', payload => {
|
||||
const { new: newItem } = payload
|
||||
const { id, ref, latitude, longitude } = newItem
|
||||
if (ref === clientRef) {
|
||||
console.log('Change received!', payload)
|
||||
setPositions([...positions, { id, ref, lat: latitude, lng: longitude }])
|
||||
}
|
||||
})
|
||||
.subscribe()
|
||||
|
||||
return () => {
|
||||
if (mySubscription.current) supabase.removeSubscription(mySubscription.current)
|
||||
}
|
||||
}, [supabase, clientRef, positions, setPositions])
|
||||
|
||||
function drawPolyline() {
|
||||
const temp = positions.map(item => {
|
||||
if (!item) return
|
||||
const { lat, lng } = item
|
||||
return [lat, lng]
|
||||
})
|
||||
const polyline = [center, ...temp]
|
||||
return <Polyline pathOptions={{ color: 'black' }} positions={polyline} />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="map-view">
|
||||
<MapContainer center={center} zoom={zoom || 15} scrollWheelZoom={false}>
|
||||
<TileLayer
|
||||
attribution='© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
/>
|
||||
{drawPolyline()}
|
||||
</MapContainer>
|
||||
<TextLog log={log} />
|
||||
|
||||
<style jsx>{`
|
||||
.map-view {
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default MapView
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
export default function TextLog({ log }) {
|
||||
const textLog = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
textLog.current.scrollTop = textLog.current.scrollHeight;
|
||||
}, [log])
|
||||
|
||||
return (
|
||||
<>
|
||||
<textarea ref={textLog} readOnly value={log} />
|
||||
<style jsx>{`
|
||||
textarea {
|
||||
width: 100%;
|
||||
height: 7rem;
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
)
|
||||
}
|
||||
File renamed without changes.
@@ -0,0 +1,14 @@
|
||||
function RoundToFixDecimals(value, numDecimals = 5) {
|
||||
function addZero(s, size) {
|
||||
while (s.length <= (size || 2)) { s = s + "0"; }
|
||||
return s;
|
||||
}
|
||||
|
||||
const temp = addZero("1", numDecimals)
|
||||
const tempNum = parseInt(temp, 10)
|
||||
return Math.round((value + Number.EPSILON) * tempNum) / tempNum
|
||||
}
|
||||
|
||||
export {
|
||||
RoundToFixDecimals
|
||||
}
|
||||
+719
-1127
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "supabase-realtime-map",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 4000",
|
||||
"build": "next build",
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^0.34.0",
|
||||
"leaflet": "^1.6.0",
|
||||
"nanoid": "^3.1.10",
|
||||
"next": "9.4.4",
|
||||
"react": "16.13.1",
|
||||
"react-dom": "16.13.1",
|
||||
"react-leaflet": "^3.0.0-beta.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import Head from 'next/head'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { nanoid } from 'nanoid'
|
||||
|
||||
const MapInput = dynamic(
|
||||
() => import('components/MapInput'),
|
||||
{ ssr: false }
|
||||
)
|
||||
|
||||
const MapView = dynamic(
|
||||
() => import('components/MapView'),
|
||||
{ ssr: false }
|
||||
)
|
||||
|
||||
export default function Home() {
|
||||
const [clientRef, setClientRef] = useState(null)
|
||||
const apiEndpoint = process.env.NEXT_PUBLIC_SUPABASE_ENDPOINT || "https://BeQiprVORewGZImzsKAS.supabase.co"
|
||||
const apiKey = process.env.NEXT_PUBLIC_SUPABASE_APIKEY || "IHDL7hnmTSlqQ1fm7kYw5SBQPY11Rp"
|
||||
// Create a single supabase client for interacting with your database
|
||||
const supabase = createClient(apiEndpoint, apiKey);
|
||||
const center = {
|
||||
lat: 1.3489728457596013,
|
||||
lng: 103.77043978311998
|
||||
}
|
||||
const zoomLevel = 14
|
||||
|
||||
useEffect(() => {
|
||||
let ref = localStorage.getItem('_client-ref')
|
||||
if (!ref) {
|
||||
ref = nanoid()
|
||||
localStorage.setItem('_client-ref', ref)
|
||||
}
|
||||
setClientRef(ref)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<Head>
|
||||
<title>Live Tracker</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://unpkg.com/leaflet@1.6.0/dist/leaflet.css"
|
||||
integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ=="
|
||||
crossOrigin=""
|
||||
/>
|
||||
<script src="https://unpkg.com/leaflet@1.6.0/dist/leaflet.js"
|
||||
integrity="sha512-gZwIG9x3wUXg2hdXF6+rVkLF/0Vi9U8D2Ntg4Ga5I5BZpVkVxlJWbSQtXPSiUTtC0TjtGOmxa1AJPuV0CPthew=="
|
||||
crossOrigin=""></script>
|
||||
</Head>
|
||||
<main>
|
||||
<h1 className="title">
|
||||
Live Tracker
|
||||
</h1>
|
||||
|
||||
<p className="description">
|
||||
Get started by drag-drop the marker around to simulate location update
|
||||
</p>
|
||||
|
||||
<div className="grid">
|
||||
<div className="card">
|
||||
<MapInput supabase={supabase} clientRef={clientRef} center={center} zoom={zoomLevel} />
|
||||
</div>
|
||||
<div className="card">
|
||||
<MapView supabase={supabase} clientRef={clientRef} center={center} zoom={zoomLevel - 1} />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<a
|
||||
href="https://supabase.io"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Powered by{' '}
|
||||
<img src="/supabase.svg" alt="Supabase Logo" className="logo" />
|
||||
</a>
|
||||
</footer>
|
||||
|
||||
<style jsx>{`
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
padding: 0 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 5rem 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
footer {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
border-top: 1px solid #eaeaea;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
footer img {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
footer a {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
line-height: 1.15;
|
||||
font-size: 4rem;
|
||||
}
|
||||
|
||||
.title,
|
||||
.description {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.description {
|
||||
line-height: 1.5;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin: 1rem;
|
||||
flex-grow: 1;
|
||||
text-align: left;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.grid {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
.card {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
|
||||
<style jsx global>{`
|
||||
html,
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
|
||||
Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue,
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.leaflet-container {
|
||||
min-height: 20rem;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,13 @@
|
||||
<svg width="375" height="375" viewBox="0 0 375 375" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M218.909 28.4831C218.909 21.5889 213.322 16 206.431 16C203.004 16 199.9 17.382 197.645 19.6192L195.912 21.548L99.675 151.658H195.18L218.429 32.0055L218.853 29.6682C218.89 29.2781 218.909 28.8828 218.909 28.4831Z" fill="#3ECF8E"/>
|
||||
<path d="M65 205.336C65 216.366 73.9385 225.308 84.9647 225.308L85.053 225.308H180.87L195.18 151.658H99.675L70.736 190.783L68.8725 193.512C66.4382 196.822 65 200.911 65 205.336Z" fill="#3ECF8E"/>
|
||||
<path d="M310 171.631C310 160.6 301.061 151.658 290.035 151.658H289.245H195.18L180.87 225.308H275.202L305.604 184.205L306.817 182.455C308.831 179.337 310 175.62 310 171.631Z" fill="#3ECF8E"/>
|
||||
<path d="M157.533 346.517C157.533 353.411 163.119 359 170.011 359C173.706 359 177.027 357.393 179.311 354.839L179.803 354.285L275.202 225.308H180.87L157.985 343.089L157.924 343.405C157.668 344.4 157.533 345.443 157.533 346.517Z" fill="#3ECF8E"/>
|
||||
<path d="M157.533 346.517C157.533 353.411 163.119 359 170.011 359C173.706 359 177.027 357.393 179.311 354.839L179.803 354.285L275.202 225.308H180.87L157.985 343.089L157.924 343.405C157.668 344.4 157.533 345.443 157.533 346.517Z" fill="url(#paint0_linear)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear" x1="236" y1="237" x2="167" y2="359" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#23915F"/>
|
||||
<stop offset="1" stop-color="#3ECF8E" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
@@ -1,2 +0,0 @@
|
||||
const withSass = require('@zeit/next-sass')
|
||||
module.exports = withSass()
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"scripts": {
|
||||
"dev": "next",
|
||||
"build": "next build",
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^0.1.18",
|
||||
"@zeit/next-sass": "^1.0.0",
|
||||
"next": "latest",
|
||||
"node-sass": "4.7.2",
|
||||
"react": "^16.7.0",
|
||||
"react-dom": "^16.7.0",
|
||||
"tailwindcss": "^1.2.0"
|
||||
}
|
||||
}
|
||||
Whitespace-only changes.
@@ -1,3 +0,0 @@
|
||||
import '../styles/style.scss'
|
||||
|
||||
export default () => <div className="example">Hello World!</div>
|
||||
@@ -1,4 +0,0 @@
|
||||
$color: #2ecc71;
|
||||
.example {
|
||||
background-color: $color;
|
||||
}
|
||||
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
@@ -1,35 +0,0 @@
|
||||
-- USERS
|
||||
CREATE TABLE public.users (
|
||||
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
username text
|
||||
);
|
||||
-- CHANNELS
|
||||
CREATE TABLE public.channels (
|
||||
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
slug text NOT NULL UNIQUE
|
||||
);
|
||||
-- MESSAGES
|
||||
CREATE TABLE public.messages (
|
||||
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
message text,
|
||||
user_id bigint REFERENCES users NOT NULL,
|
||||
channel_id bigint REFERENCES channels NOT NULL
|
||||
);
|
||||
-- Create the Replication publication for Supabase
|
||||
CREATE PUBLICATION supabase_realtime FOR ALL TABLES;
|
||||
-- SOME DUMMY DATA
|
||||
INSERT INTO
|
||||
public.users (username)
|
||||
VALUES
|
||||
('jarvis'),
|
||||
('tonystark');
|
||||
INSERT INTO
|
||||
public.channels (slug)
|
||||
VALUES
|
||||
('public'),
|
||||
('random');
|
||||
INSERT INTO
|
||||
public.messages (message, channel_id, user_id)
|
||||
VALUES
|
||||
('Hello World 👋', 1, 1),
|
||||
('Goodbye cruel world.', 2, 1);
|
||||
@@ -1,30 +0,0 @@
|
||||
version: '3'
|
||||
services:
|
||||
supabase:
|
||||
image: supabase/supabase-dev:latest
|
||||
ports:
|
||||
- "8000:8000"
|
||||
environment:
|
||||
DB_HOST: db
|
||||
DB_NAME: postgres
|
||||
DB_USER: postgres
|
||||
DB_PASSWORD: postgres
|
||||
DB_PORT: 5432
|
||||
DB_SCHEMA: public
|
||||
depends_on:
|
||||
- db
|
||||
db:
|
||||
image: postgres:12
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- ./db:/docker-entrypoint-initdb.d/
|
||||
command:
|
||||
- "postgres"
|
||||
- "-c"
|
||||
- "wal_level=logical"
|
||||
environment:
|
||||
POSTGRES_DB: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_PORT: 5432
|
||||
Whitespace-only changes.
Whitespace-only changes.
@@ -1,7 +0,0 @@
|
||||
const withSass = require('@zeit/next-sass')
|
||||
module.exports = withSass({
|
||||
env: {
|
||||
SUPABASE_URL: 'http://localhost:8000',
|
||||
SUPABASE_KEY: 'examplekey',
|
||||
},
|
||||
})
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"scripts": {
|
||||
"dev": "next",
|
||||
"build": "next build",
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^0.1.18",
|
||||
"@zeit/next-sass": "^1.0.0",
|
||||
"next": "latest",
|
||||
"node-sass": "4.7.2",
|
||||
"react": "^16.7.0",
|
||||
"react-dom": "^16.7.0",
|
||||
"tailwindcss": "^1.2.0"
|
||||
}
|
||||
}
|
||||
Whitespace-only changes.
@@ -1,3 +0,0 @@
|
||||
import '../styles/style.scss'
|
||||
|
||||
export default () => <div className="example">Hello World!</div>
|
||||
@@ -1,4 +0,0 @@
|
||||
$color: #2ecc71;
|
||||
.example {
|
||||
background-color: $color;
|
||||
}
|
||||
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
@@ -1,35 +0,0 @@
|
||||
-- USERS
|
||||
CREATE TABLE public.users (
|
||||
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
username text
|
||||
);
|
||||
-- CHANNELS
|
||||
CREATE TABLE public.channels (
|
||||
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
slug text NOT NULL UNIQUE
|
||||
);
|
||||
-- MESSAGES
|
||||
CREATE TABLE public.messages (
|
||||
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
message text,
|
||||
user_id bigint REFERENCES users NOT NULL,
|
||||
channel_id bigint REFERENCES channels NOT NULL
|
||||
);
|
||||
-- Create the Replication publication for Supabase
|
||||
CREATE PUBLICATION supabase_realtime FOR ALL TABLES;
|
||||
-- SOME DUMMY DATA
|
||||
INSERT INTO
|
||||
public.users (username)
|
||||
VALUES
|
||||
('jarvis'),
|
||||
('tonystark');
|
||||
INSERT INTO
|
||||
public.channels (slug)
|
||||
VALUES
|
||||
('public'),
|
||||
('random');
|
||||
INSERT INTO
|
||||
public.messages (message, channel_id, user_id)
|
||||
VALUES
|
||||
('Hello World 👋', 1, 1),
|
||||
('Goodbye cruel world.', 2, 1);
|
||||
@@ -1,30 +0,0 @@
|
||||
version: '3'
|
||||
services:
|
||||
supabase:
|
||||
image: supabase/supabase-dev:latest
|
||||
ports:
|
||||
- "8000:8000"
|
||||
environment:
|
||||
DB_HOST: db
|
||||
DB_NAME: postgres
|
||||
DB_USER: postgres
|
||||
DB_PASSWORD: postgres
|
||||
DB_PORT: 5432
|
||||
DB_SCHEMA: public
|
||||
depends_on:
|
||||
- db
|
||||
db:
|
||||
image: postgres:12
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- ./db:/docker-entrypoint-initdb.d/
|
||||
command:
|
||||
- "postgres"
|
||||
- "-c"
|
||||
- "wal_level=logical"
|
||||
environment:
|
||||
POSTGRES_DB: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_PORT: 5432
|
||||
@@ -1,53 +0,0 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY)
|
||||
|
||||
export const useStore = ({ channelId }) => {
|
||||
const [channels, setChannels] = useState(new Array())
|
||||
const [messages, setMessages] = useState(new Array())
|
||||
const [users] = useState(new Map())
|
||||
|
||||
// Fetches messages for the channel and starts the channel listeners
|
||||
const reloadStore = () => {
|
||||
if (channelId) {
|
||||
console.log('channelId', channelId)
|
||||
fetchChannels(setChannels)
|
||||
fetchMessages(channelId, setMessages)
|
||||
}
|
||||
}
|
||||
|
||||
// Update the store when the user changes the "channel"
|
||||
useEffect(reloadStore, [channelId])
|
||||
|
||||
// Export computed properties to use in our app
|
||||
return {
|
||||
channels: channels.sort((a, b) => a.slug.localeCompare(b.slug)),
|
||||
messages,
|
||||
users,
|
||||
}
|
||||
}
|
||||
|
||||
export const fetchChannels = async callback => {
|
||||
try {
|
||||
let { body } = await supabase.from('channels').select('*')
|
||||
if (callback) callback(body)
|
||||
return body
|
||||
} catch (error) {
|
||||
console.log('error', error)
|
||||
}
|
||||
}
|
||||
|
||||
export const fetchMessages = async (channelId, callback) => {
|
||||
try {
|
||||
let { body } = await supabase
|
||||
.from('messages')
|
||||
.eq('channel_id', channelId)
|
||||
.select(`*, author:user_id(*)`)
|
||||
.order('id', true)
|
||||
if (callback) callback(body)
|
||||
return body
|
||||
} catch (error) {
|
||||
console.log('error', error)
|
||||
}
|
||||
}
|
||||
Whitespace-only changes.
@@ -1,7 +0,0 @@
|
||||
const withSass = require('@zeit/next-sass')
|
||||
module.exports = withSass({
|
||||
env: {
|
||||
SUPABASE_URL: 'http://localhost:8000',
|
||||
SUPABASE_KEY: 'examplekey',
|
||||
},
|
||||
})
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"scripts": {
|
||||
"dev": "next",
|
||||
"build": "next build",
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^0.1.18",
|
||||
"@zeit/next-sass": "^1.0.0",
|
||||
"next": "latest",
|
||||
"node-sass": "4.7.2",
|
||||
"react": "^16.7.0",
|
||||
"react-dom": "^16.7.0",
|
||||
"tailwindcss": "^1.2.0"
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useStore } from '../lib/Store'
|
||||
|
||||
const ChannelsPage = props => {
|
||||
const router = useRouter()
|
||||
const { channelId } = router.query
|
||||
const { messages, channels } = useStore({ channelId })
|
||||
|
||||
return (
|
||||
<>
|
||||
<nav>
|
||||
<div>
|
||||
<button>New Channel</button>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Channels</h3>
|
||||
</div>
|
||||
<ul>
|
||||
{channels.map(channel => (
|
||||
<li>
|
||||
<Link href="/[channelId]" as={`/${channel.id}`}>
|
||||
<a style={{ fontWeight: channel.id == channelId ? 'bold' : 'normal' }}>
|
||||
{channel.slug}
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
<main>
|
||||
{messages.map(message => (
|
||||
<div>
|
||||
<p>
|
||||
<strong>{message.author.username}</strong>
|
||||
</p>
|
||||
<p>{message.message}</p>
|
||||
</div>
|
||||
))}
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChannelsPage
|
||||
@@ -1,3 +0,0 @@
|
||||
import '../styles/style.scss'
|
||||
|
||||
export default () => <div className="example">Hello World!</div>
|
||||
@@ -1,4 +0,0 @@
|
||||
$color: #2ecc71;
|
||||
.example {
|
||||
background-color: $color;
|
||||
}
|
||||
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
@@ -1,35 +0,0 @@
|
||||
-- USERS
|
||||
CREATE TABLE public.users (
|
||||
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
username text
|
||||
);
|
||||
-- CHANNELS
|
||||
CREATE TABLE public.channels (
|
||||
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
slug text NOT NULL UNIQUE
|
||||
);
|
||||
-- MESSAGES
|
||||
CREATE TABLE public.messages (
|
||||
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
message text,
|
||||
user_id bigint REFERENCES users NOT NULL,
|
||||
channel_id bigint REFERENCES channels NOT NULL
|
||||
);
|
||||
-- Create the Replication publication for Supabase
|
||||
CREATE PUBLICATION supabase_realtime FOR ALL TABLES;
|
||||
-- SOME DUMMY DATA
|
||||
INSERT INTO
|
||||
public.users (username)
|
||||
VALUES
|
||||
('jarvis'),
|
||||
('tonystark');
|
||||
INSERT INTO
|
||||
public.channels (slug)
|
||||
VALUES
|
||||
('public'),
|
||||
('random');
|
||||
INSERT INTO
|
||||
public.messages (message, channel_id, user_id)
|
||||
VALUES
|
||||
('Hello World 👋', 1, 1),
|
||||
('Goodbye cruel world.', 2, 1);
|
||||
@@ -1,30 +0,0 @@
|
||||
version: '3'
|
||||
services:
|
||||
supabase:
|
||||
image: supabase/supabase-dev:latest
|
||||
ports:
|
||||
- "8000:8000"
|
||||
environment:
|
||||
DB_HOST: db
|
||||
DB_NAME: postgres
|
||||
DB_USER: postgres
|
||||
DB_PASSWORD: postgres
|
||||
DB_PORT: 5432
|
||||
DB_SCHEMA: public
|
||||
depends_on:
|
||||
- db
|
||||
db:
|
||||
image: postgres:12
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- ./db:/docker-entrypoint-initdb.d/
|
||||
command:
|
||||
- "postgres"
|
||||
- "-c"
|
||||
- "wal_level=logical"
|
||||
environment:
|
||||
POSTGRES_DB: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_PORT: 5432
|
||||
@@ -1,53 +0,0 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY)
|
||||
|
||||
export const useStore = ({ channelId }) => {
|
||||
const [channels, setChannels] = useState(new Array())
|
||||
const [messages, setMessages] = useState(new Array())
|
||||
const [users] = useState(new Map())
|
||||
|
||||
// Fetches messages for the channel and starts the channel listeners
|
||||
const reloadStore = () => {
|
||||
if (channelId) {
|
||||
console.log('channelId', channelId)
|
||||
fetchChannels(setChannels)
|
||||
fetchMessages(channelId, setMessages)
|
||||
}
|
||||
}
|
||||
|
||||
// Update the store when the user changes the "channel"
|
||||
useEffect(reloadStore, [channelId])
|
||||
|
||||
// Export computed properties to use in our app
|
||||
return {
|
||||
channels: channels.sort((a, b) => a.slug.localeCompare(b.slug)),
|
||||
messages,
|
||||
users,
|
||||
}
|
||||
}
|
||||
|
||||
export const fetchChannels = async callback => {
|
||||
try {
|
||||
let { body } = await supabase.from('channels').select('*')
|
||||
if (callback) callback(body)
|
||||
return body
|
||||
} catch (error) {
|
||||
console.log('error', error)
|
||||
}
|
||||
}
|
||||
|
||||
export const fetchMessages = async (channelId, callback) => {
|
||||
try {
|
||||
let { body } = await supabase
|
||||
.from('messages')
|
||||
.eq('channel_id', channelId)
|
||||
.select(`*, author:user_id(*)`)
|
||||
.order('id', true)
|
||||
if (callback) callback(body)
|
||||
return body
|
||||
} catch (error) {
|
||||
console.log('error', error)
|
||||
}
|
||||
}
|
||||
Whitespace-only changes.
@@ -1,7 +0,0 @@
|
||||
const withSass = require('@zeit/next-sass')
|
||||
module.exports = withSass({
|
||||
env: {
|
||||
SUPABASE_URL: 'http://localhost:8000',
|
||||
SUPABASE_KEY: 'examplekey',
|
||||
},
|
||||
})
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"scripts": {
|
||||
"dev": "next",
|
||||
"build": "next build",
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^0.1.18",
|
||||
"@zeit/next-sass": "^1.0.0",
|
||||
"next": "latest",
|
||||
"node-sass": "4.7.2",
|
||||
"react": "^16.7.0",
|
||||
"react-dom": "^16.7.0",
|
||||
"tailwindcss": "^1.2.0"
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import '../styles/style.scss'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useStore } from '../lib/Store'
|
||||
|
||||
const ChannelsPage = props => {
|
||||
const router = useRouter()
|
||||
const { channelId } = router.query
|
||||
const { messages, channels } = useStore({ channelId })
|
||||
|
||||
return (
|
||||
<div className="main flex h-screen w-screen absolute">
|
||||
<nav className="w-1/4 bg-gray-900 text-gray-100 h-screen p-2">
|
||||
<button className="bg-blue-900 hover:bg-blue-700 text-white font-bold p-2 rounded w-full">
|
||||
New Channel
|
||||
</button>
|
||||
<h3 className="py-4">Channels</h3>
|
||||
<ul>
|
||||
{channels.map(channel => (
|
||||
<li key={channel.id}>
|
||||
<Link href="/[channelId]" as={`/${channel.id}`}>
|
||||
<a
|
||||
className="hover:text-blue-400"
|
||||
style={{ fontWeight: channel.id == channelId ? 'bold' : 'normal' }}
|
||||
>
|
||||
{channel.slug}
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
<main className="w-3/4 bg-gray-800 h-screen p-2">
|
||||
{messages.map(message => (
|
||||
<div>
|
||||
<p className="text-blue-700 font-bold">{message.author.username}</p>
|
||||
<p className="text-white">{message.message}</p>
|
||||
</div>
|
||||
))}
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChannelsPage
|
||||
@@ -1,3 +0,0 @@
|
||||
import '../styles/style.scss'
|
||||
|
||||
export default () => <div className="example">Hello World!</div>
|
||||
@@ -1,3 +0,0 @@
|
||||
@import '~tailwindcss/dist/base.css';
|
||||
@import '~tailwindcss/dist/components.css';
|
||||
@import '~tailwindcss/dist/utilities.css';
|
||||
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
@@ -1,35 +0,0 @@
|
||||
-- USERS
|
||||
CREATE TABLE public.users (
|
||||
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
username text
|
||||
);
|
||||
-- CHANNELS
|
||||
CREATE TABLE public.channels (
|
||||
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
slug text NOT NULL UNIQUE
|
||||
);
|
||||
-- MESSAGES
|
||||
CREATE TABLE public.messages (
|
||||
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
message text,
|
||||
user_id bigint REFERENCES users NOT NULL,
|
||||
channel_id bigint REFERENCES channels NOT NULL
|
||||
);
|
||||
-- Create the Replication publication for Supabase
|
||||
CREATE PUBLICATION supabase_realtime FOR ALL TABLES;
|
||||
-- SOME DUMMY DATA
|
||||
INSERT INTO
|
||||
public.users (username)
|
||||
VALUES
|
||||
('jarvis'),
|
||||
('tonystark');
|
||||
INSERT INTO
|
||||
public.channels (slug)
|
||||
VALUES
|
||||
('public'),
|
||||
('random');
|
||||
INSERT INTO
|
||||
public.messages (message, channel_id, user_id)
|
||||
VALUES
|
||||
('Hello World 👋', 1, 1),
|
||||
('Goodbye cruel world.', 2, 1);
|
||||
@@ -1,30 +0,0 @@
|
||||
version: '3'
|
||||
services:
|
||||
supabase:
|
||||
image: supabase/supabase-dev:latest
|
||||
ports:
|
||||
- "8000:8000"
|
||||
environment:
|
||||
DB_HOST: db
|
||||
DB_NAME: postgres
|
||||
DB_USER: postgres
|
||||
DB_PASSWORD: postgres
|
||||
DB_PORT: 5432
|
||||
DB_SCHEMA: public
|
||||
depends_on:
|
||||
- db
|
||||
db:
|
||||
image: postgres:12
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- ./db:/docker-entrypoint-initdb.d/
|
||||
command:
|
||||
- "postgres"
|
||||
- "-c"
|
||||
- "wal_level=logical"
|
||||
environment:
|
||||
POSTGRES_DB: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_PORT: 5432
|
||||
@@ -1,53 +0,0 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY)
|
||||
|
||||
export const useStore = ({ channelId }) => {
|
||||
const [channels, setChannels] = useState(new Array())
|
||||
const [messages, setMessages] = useState(new Array())
|
||||
const [users] = useState(new Map())
|
||||
|
||||
// Fetches messages for the channel and starts the channel listeners
|
||||
const reloadStore = () => {
|
||||
if (channelId) {
|
||||
console.log('channelId', channelId)
|
||||
fetchChannels(setChannels)
|
||||
fetchMessages(channelId, setMessages)
|
||||
}
|
||||
}
|
||||
|
||||
// Update the store when the user changes the "channel"
|
||||
useEffect(reloadStore, [channelId])
|
||||
|
||||
// Export computed properties to use in our app
|
||||
return {
|
||||
channels: channels.sort((a, b) => a.slug.localeCompare(b.slug)),
|
||||
messages,
|
||||
users,
|
||||
}
|
||||
}
|
||||
|
||||
export const fetchChannels = async callback => {
|
||||
try {
|
||||
let { body } = await supabase.from('channels').select('*')
|
||||
if (callback) callback(body)
|
||||
return body
|
||||
} catch (error) {
|
||||
console.log('error', error)
|
||||
}
|
||||
}
|
||||
|
||||
export const fetchMessages = async (channelId, callback) => {
|
||||
try {
|
||||
let { body } = await supabase
|
||||
.from('messages')
|
||||
.eq('channel_id', channelId)
|
||||
.select(`*, author:user_id(*)`)
|
||||
.order('id', true)
|
||||
if (callback) callback(body)
|
||||
return body
|
||||
} catch (error) {
|
||||
console.log('error', error)
|
||||
}
|
||||
}
|
||||
Whitespace-only changes.
@@ -1,7 +0,0 @@
|
||||
const withSass = require('@zeit/next-sass')
|
||||
module.exports = withSass({
|
||||
env: {
|
||||
SUPABASE_URL: 'http://localhost:8000',
|
||||
SUPABASE_KEY: 'examplekey',
|
||||
},
|
||||
})
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"scripts": {
|
||||
"dev": "next",
|
||||
"build": "next build",
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^0.1.18",
|
||||
"@zeit/next-sass": "^1.0.0",
|
||||
"next": "latest",
|
||||
"node-sass": "4.7.2",
|
||||
"react": "^16.7.0",
|
||||
"react-dom": "^16.7.0",
|
||||
"tailwindcss": "^1.2.0"
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import '../styles/style.scss'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useStore } from '../lib/Store'
|
||||
|
||||
const ChannelsPage = props => {
|
||||
const router = useRouter()
|
||||
const { channelId } = router.query
|
||||
const { messages, channels } = useStore({ channelId })
|
||||
|
||||
return (
|
||||
<div className="main flex h-screen w-screen absolute">
|
||||
<nav className="w-1/4 bg-gray-900 text-gray-100 h-screen p-2">
|
||||
<button className="bg-blue-900 hover:bg-blue-700 text-white font-bold p-2 rounded w-full">
|
||||
New Channel
|
||||
</button>
|
||||
<h3 className="py-4">Channels</h3>
|
||||
<ul>
|
||||
{channels.map(channel => (
|
||||
<li key={channel.id}>
|
||||
<Link href="/[channelId]" as={`/${channel.id}`}>
|
||||
<a
|
||||
className="hover:text-blue-400"
|
||||
style={{ fontWeight: channel.id == channelId ? 'bold' : 'normal' }}
|
||||
>
|
||||
{channel.slug}
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
<main className="w-3/4 bg-gray-800 h-screen p-2">
|
||||
{messages.map(message => (
|
||||
<div>
|
||||
<p className="text-blue-700 font-bold">{message.author.username}</p>
|
||||
<p className="text-white">{message.message}</p>
|
||||
</div>
|
||||
))}
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChannelsPage
|
||||
@@ -1,3 +0,0 @@
|
||||
import '../styles/style.scss'
|
||||
|
||||
export default () => <div className="example">Hello World!</div>
|
||||
@@ -1,3 +0,0 @@
|
||||
@import '~tailwindcss/dist/base.css';
|
||||
@import '~tailwindcss/dist/components.css';
|
||||
@import '~tailwindcss/dist/utilities.css';
|
||||
@@ -1,49 +0,0 @@
|
||||
# Basic Slack Clone
|
||||
|
||||
Build a simple slack clone using Supabase, Postgres and React hooks.
|
||||
|
||||
<p align="center">
|
||||
<kbd>
|
||||
<img src="https://media.giphy.com/media/J07U8iblJhlKDqZOxV/giphy.gif" alt="Demo"/>
|
||||
</kbd>
|
||||
<br />
|
||||
<a href="https://media.giphy.com/media/J07U8iblJhlKDqZOxV/source.gif">View full image</a>
|
||||
</p>
|
||||
|
||||
|
||||
## How to use
|
||||
|
||||
**Clone the this folder**
|
||||
|
||||
```sh
|
||||
# Copy the repo to your machine
|
||||
git clone --no-checkout https://github.com/supabase/supabase
|
||||
cd supabase
|
||||
|
||||
# Checkout this
|
||||
git sparse-checkout init --cone
|
||||
git sparse-checkout set examples/slack-clone-basic
|
||||
cd examples/slack-clone-basic
|
||||
```
|
||||
|
||||
**Install dependencies**
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
**Start the backend**
|
||||
|
||||
```sh
|
||||
# Open a terminal and run:
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
**Start the frontent**
|
||||
|
||||
```sh
|
||||
# Open a terminal and run:
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Visit http://localhost:3000 and start slacking! Open in two tabs to see everything getting updated in realtime
|
||||
@@ -1,27 +0,0 @@
|
||||
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
|
||||
@@ -1,41 +0,0 @@
|
||||
|
||||
-- Create the Replication publication
|
||||
CREATE PUBLICATION supabase_realtime FOR ALL TABLES;
|
||||
|
||||
-- USERS
|
||||
CREATE TYPE public.user_status AS ENUM ('ONLINE', 'OFFLINE');
|
||||
CREATE TABLE public.users (
|
||||
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
inserted_at timestamp without time zone DEFAULT timezone('utc'::text, now()) NOT NULL,
|
||||
updated_at timestamp without time zone DEFAULT timezone('utc'::text, now()) NOT NULL,
|
||||
data jsonb DEFAULT null,
|
||||
username text,
|
||||
status user_status DEFAULT 'ONLINE'::public.user_status
|
||||
);
|
||||
ALTER TABLE public.users REPLICA IDENTITY FULL; -- Send "previous data" to supabase
|
||||
COMMENT ON COLUMN public.users.data IS 'For unstructured data and prototyping.';
|
||||
|
||||
-- CHANNELS
|
||||
CREATE TABLE public.channels (
|
||||
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
inserted_at timestamp without time zone DEFAULT timezone('utc'::text, now()) NOT NULL,
|
||||
updated_at timestamp without time zone DEFAULT timezone('utc'::text, now()) NOT NULL,
|
||||
data jsonb DEFAULT null,
|
||||
slug text NOT NULL UNIQUE
|
||||
);
|
||||
ALTER TABLE public.users REPLICA IDENTITY FULL; -- Send "previous data" to supabase
|
||||
COMMENT ON COLUMN public.channels.data IS 'For unstructured data and prototyping.';
|
||||
|
||||
-- MESSAGES
|
||||
CREATE TABLE public.messages (
|
||||
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
inserted_at timestamp without time zone DEFAULT timezone('utc'::text, now()) NOT NULL,
|
||||
updated_at timestamp without time zone DEFAULT timezone('utc'::text, now()) NOT NULL,
|
||||
data jsonb DEFAULT null,
|
||||
message text,
|
||||
user_id bigint REFERENCES users NOT NULL,
|
||||
channel_id bigint REFERENCES channels NOT NULL
|
||||
);
|
||||
ALTER TABLE public.messages REPLICA IDENTITY FULL; -- Send "previous data" to supabase
|
||||
COMMENT ON COLUMN public.messages.data IS 'For unstructured data and prototyping.';
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
INSERT INTO
|
||||
public.users (username)
|
||||
VALUES
|
||||
('supabot'),
|
||||
('kiwicopple'),
|
||||
('awailas'),
|
||||
('dragarcia');
|
||||
|
||||
INSERT INTO
|
||||
public.channels (slug)
|
||||
VALUES
|
||||
('public'),
|
||||
('random');
|
||||
|
||||
INSERT INTO
|
||||
public.messages (message, channel_id, user_id)
|
||||
VALUES
|
||||
('Hello World 👋', 1, 1),
|
||||
('Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.', 2, 1);
|
||||
@@ -1,31 +0,0 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
supabase:
|
||||
image: supabase/supabase-dev:0.1.9
|
||||
ports:
|
||||
- "8000:8000"
|
||||
environment:
|
||||
DB_HOST: postgres
|
||||
DB_NAME: postgres
|
||||
DB_USER: postgres
|
||||
DB_PASSWORD: postgres
|
||||
DB_PORT: 5432
|
||||
DB_SCHEMA: public
|
||||
depends_on:
|
||||
- postgres
|
||||
postgres:
|
||||
image: postgres:12
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- ./db:/docker-entrypoint-initdb.d/
|
||||
command:
|
||||
- "postgres"
|
||||
- "-c"
|
||||
- "wal_level=logical"
|
||||
environment:
|
||||
POSTGRES_DB: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_PORT: 5432
|
||||
@@ -1,12 +0,0 @@
|
||||
const path = require('path')
|
||||
|
||||
module.exports = {
|
||||
env: {
|
||||
SUPABASE_URL: process.env.SUPABASE_URL || 'http://localhost:8000',
|
||||
SUPABASE_KEY: process.env.SUPABASE_KEY || 'examplekey',
|
||||
},
|
||||
webpack(config) {
|
||||
config.resolve.alias['~'] = path.join(__dirname, '')
|
||||
return config
|
||||
},
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { useState, useContext } from 'react'
|
||||
import UserContext from '../lib/UserContext'
|
||||
|
||||
const Home = () => {
|
||||
const { signIn } = useContext(UserContext)
|
||||
const [username, setUsername] = useState('')
|
||||
|
||||
const handleLogin = e => {
|
||||
e.preventDefault()
|
||||
if (username != '') signIn(username)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto h-full flex justify-center items-center">
|
||||
<div className="w-1/3 mt-8">
|
||||
<div className="border-teal p-8 border-t-12 bg-white mb-6 rounded-lg shadow-lg">
|
||||
<div className="mb-4">
|
||||
<label className="font-bold text-grey-darker block mb-2">Username</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"
|
||||
onChange={e => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<a
|
||||
onClick={handleLogin}
|
||||
href={'/channels'}
|
||||
className="bg-gray-900 hover:bg-teal text-white font-bold py-2 px-4 rounded"
|
||||
>
|
||||
Login
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
||||
@@ -0,0 +1,2 @@
|
||||
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
|
||||
NEXT_PUBLIC_SUPABASE_KEY=your-anon-key
|
||||
@@ -18,6 +18,7 @@
|
||||
# misc
|
||||
.DS_Store
|
||||
.env*
|
||||
!.env.local.example
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
@@ -0,0 +1,148 @@
|
||||
# Realtime chat example using Supabase
|
||||
|
||||
This is a full-stack Slack clone example using:
|
||||
|
||||
- Frontend:
|
||||
- Next.js.
|
||||
- [Supabase.js](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
|
||||
|
||||
- Live demo: http://supabase-slack-clone.supabase.vercel.app/
|
||||
- CodeSandbox: https://codesandbox.io/s/github/supabase/supabase/tree/master/examples/slack-clone
|
||||
- Video tutorial: https://www.loom.com/share/31eec9b656e44b8d87c88bde8a465676
|
||||
|
||||

|
||||
|
||||
## Deploy your own
|
||||
|
||||
### 1. Create new project
|
||||
|
||||
Sign up to Supabase - [https://app.supabase.io](https://app.supabase.io) and create a new project. Wait for your database to start.
|
||||
|
||||
### 2. Run "Slack Clone" Quickstart
|
||||
|
||||
Once your database has started, run the "Slack Clone" quickstart.
|
||||
|
||||

|
||||
|
||||
### 3. Get the URL and Key
|
||||
|
||||
Go to the 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.
|
||||
|
||||
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.
|
||||
|
||||
### 4. Deploy the Next.js client
|
||||
|
||||
[](https://vercel.com/import/git?s=https%3A%2F%2Fgithub.com%2Fsupabase%2Fsupabase%2Ftree%2Fmaster%2Fexamples%2Fslack-clone&env=NEXT_PUBLIC_SUPABASE_URL,NEXT_PUBLIC_SUPABASE_KEY&envDescription=Find%20the%20Supabase%20URL%20and%20key%20in%20the%20your%20auto-generated%20docs%20at%20app.supabase.io&project-name=supabase-slack-clone&repo-name=supabase-slack-clone)
|
||||
|
||||
You will be asked for a `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_KEY`. Use the API URL and `anon` key from [step 3](#3.-get-the-url-and-key).
|
||||
|
||||
## How to use
|
||||
|
||||
### Using `create-next-app`
|
||||
|
||||
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 locally:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
### Download manually
|
||||
|
||||
Download the example:
|
||||
|
||||
```bash
|
||||
curl https://codeload.github.com/vercel/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-supabase-auth-realtime-db
|
||||
cd with-supabase-auth-realtime-db
|
||||
```
|
||||
|
||||
### Required configuration
|
||||
|
||||
Copy the `.env.local.example` file into a file named `.env.local` in the root directory of the example:
|
||||
|
||||
```bash
|
||||
cp .env.local.example .env.local
|
||||
```
|
||||
|
||||
Set your Supabase details from from [step 3](#3.-get-the-url-and-key) above:
|
||||
|
||||
```bash
|
||||
NEXT_PUBLIC_SUPABASE_URL=<replace-with-your-API-url>
|
||||
NEXT_PUBLIC_SUPABASE_KEY=<replace-with-your-anon-key>
|
||||
```
|
||||
|
||||
### Run the development server
|
||||
|
||||
Now install the dependencies and start the development server.
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
# or
|
||||
yarn
|
||||
yarn dev
|
||||
```
|
||||
|
||||
Visit http://localhost:3000 and start chatting! Open a channel across two browser tabs to see everything getting updated in realtime 🥳
|
||||
|
||||
## 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 );
|
||||
```
|
||||
|
||||
## Authors
|
||||
|
||||
- [Supabase](https://supabase.io)
|
||||
|
||||
Supabase is open source, we'd love for you to follow along and get involved at https://github.com/supabase/supabase
|
||||
+18
-13
@@ -3,16 +3,16 @@ import { useContext } from 'react'
|
||||
import UserContext from '~/lib/UserContext'
|
||||
import { addChannel } from '~/lib/Store'
|
||||
|
||||
export default props => {
|
||||
export default function Layout(props) {
|
||||
const { signOut } = useContext(UserContext)
|
||||
|
||||
const slugify = text => {
|
||||
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(/[^\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
|
||||
}
|
||||
@@ -20,28 +20,33 @@ export default props => {
|
||||
const newChannel = async () => {
|
||||
const slug = prompt('Please enter your name')
|
||||
if (slug) {
|
||||
// let { body } = await
|
||||
addChannel(slugify(slug))
|
||||
// console.log('body', body)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="main flex h-screen w-screen absolute">
|
||||
<main className="main flex h-screen w-screen absolute overflow-hidden">
|
||||
{/* Sidebar */}
|
||||
<nav className="w-1/4 bg-gray-900 text-gray-100 h-screen">
|
||||
<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} />
|
||||
{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-700 text-white font-bold py-2 px-4 rounded w-full"
|
||||
className="bg-blue-900 hover:bg-blue-800 text-white py-2 px-4 rounded w-full transition duration-150"
|
||||
onClick={() => newChannel()}
|
||||
>
|
||||
New Channel
|
||||
@@ -50,7 +55,7 @@ export default props => {
|
||||
<hr className="m-2" />
|
||||
<div className="p-2">
|
||||
<button
|
||||
className="bg-blue-900 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded w-full"
|
||||
className="bg-blue-900 hover:bg-blue-800 text-white py-2 px-4 rounded w-full transition duration-150"
|
||||
onClick={() => signOut()}
|
||||
>
|
||||
Log out
|
||||
@@ -59,7 +64,7 @@ export default props => {
|
||||
</nav>
|
||||
|
||||
{/* Messages */}
|
||||
<div className="w-3/4 bg-gray-800 h-screen">{props.children}</div>
|
||||
<div className="flex-1 bg-gray-800 h-screen">{props.children}</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
File renamed without changes.
@@ -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
|
||||
Loaded 100 of 205 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user