mirror of
https://github.com/supabase/supabase.git
synced 2026-10-09 03:15:06 +03:00
Merge branch 'blog'
This commit is contained in:
commit
2b958992f1
82 files changed
+58164
-15
No files matched your search
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1,2 @@
|
||||
const withSass = require('@zeit/next-sass')
|
||||
module.exports = withSass()
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"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.
@@ -0,0 +1,3 @@
|
||||
import '../styles/style.scss'
|
||||
|
||||
export default () => <div className="example">Hello World!</div>
|
||||
@@ -0,0 +1,4 @@
|
||||
$color: #2ecc71;
|
||||
.example {
|
||||
background-color: $color;
|
||||
}
|
||||
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1,35 @@
|
||||
-- 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);
|
||||
@@ -0,0 +1,30 @@
|
||||
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.
@@ -0,0 +1,7 @@
|
||||
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
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"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.
@@ -0,0 +1,3 @@
|
||||
import '../styles/style.scss'
|
||||
|
||||
export default () => <div className="example">Hello World!</div>
|
||||
@@ -0,0 +1,4 @@
|
||||
$color: #2ecc71;
|
||||
.example {
|
||||
background-color: $color;
|
||||
}
|
||||
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1,35 @@
|
||||
-- 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);
|
||||
@@ -0,0 +1,30 @@
|
||||
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
|
||||
@@ -0,0 +1,53 @@
|
||||
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.
@@ -0,0 +1,7 @@
|
||||
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
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
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
|
||||
@@ -0,0 +1,3 @@
|
||||
import '../styles/style.scss'
|
||||
|
||||
export default () => <div className="example">Hello World!</div>
|
||||
@@ -0,0 +1,4 @@
|
||||
$color: #2ecc71;
|
||||
.example {
|
||||
background-color: $color;
|
||||
}
|
||||
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1,35 @@
|
||||
-- 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);
|
||||
@@ -0,0 +1,30 @@
|
||||
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
|
||||
@@ -0,0 +1,53 @@
|
||||
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.
@@ -0,0 +1,7 @@
|
||||
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
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
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
|
||||
@@ -0,0 +1,3 @@
|
||||
import '../styles/style.scss'
|
||||
|
||||
export default () => <div className="example">Hello World!</div>
|
||||
@@ -0,0 +1,3 @@
|
||||
@import '~tailwindcss/dist/base.css';
|
||||
@import '~tailwindcss/dist/components.css';
|
||||
@import '~tailwindcss/dist/utilities.css';
|
||||
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1,35 @@
|
||||
-- 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);
|
||||
@@ -0,0 +1,30 @@
|
||||
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
|
||||
@@ -0,0 +1,53 @@
|
||||
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.
@@ -0,0 +1,7 @@
|
||||
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
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
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
|
||||
@@ -0,0 +1,3 @@
|
||||
import '../styles/style.scss'
|
||||
|
||||
export default () => <div className="example">Hello World!</div>
|
||||
@@ -0,0 +1,3 @@
|
||||
@import '~tailwindcss/dist/base.css';
|
||||
@import '~tailwindcss/dist/components.css';
|
||||
@import '~tailwindcss/dist/utilities.css';
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState } from 'react'
|
||||
|
||||
const MessageInput = ({onSubmit}) => {
|
||||
const [messageText, setMessageText] = useState('')
|
||||
|
||||
@@ -0,0 +1,452 @@
|
||||
---
|
||||
title: Building a basic Slack clone with Postgres and React Hooks
|
||||
description: Building a basic Slack clone with Postgres and React Hooks
|
||||
author: Paul Copplestone
|
||||
author_title: Supabase
|
||||
author_url: https://github.com/kiwicopple
|
||||
author_image_url: https://avatars2.githubusercontent.com/u/10214025?s=400&u=c6775be2ae667e2acae3ccd347fed62bb3f5b3e7&v=4
|
||||
authorURL: https://github.com/kiwicopple
|
||||
tags: [postgres, react]
|
||||
---
|
||||
|
||||
import Figure from '../src/components/Figure'
|
||||
import Mermaid from '../src/components/Mermaid'
|
||||
import CustomCodeBlock from '../src/components/CustomCodeBlock'
|
||||
import * as Snippets from './basic-slack-clone-react-hooks/snippets'
|
||||
|
||||
<div className="alert alert--warning" role="alert">
|
||||
This guide uses Docker Compose to simplify Postgres setup and data loading.
|
||||
We won't go over how to use Docker, but as long as you have it installed and running it will be simple to follow along.
|
||||
</div>
|
||||
|
||||
[Full code](https://github.com/supabase/supabase/blob/master/examples/slack-clone-basic/README.md) |
|
||||
[HN Discussion](https://new.ycombinator.com) |
|
||||
[Dev Article](https://dev.to)
|
||||
|
||||
<Figure
|
||||
src="https://media.giphy.com/media/J07U8iblJhlKDqZOxV/source.gif"
|
||||
alt="Slack Clone"
|
||||
caption="A simple Next.js app that reacts to changes in your Postgres database."
|
||||
/>
|
||||
|
||||
|
||||
## Introduction
|
||||
|
||||
This is a thorough guide that will step you through four useful technologies which can scale up to production-ready realtime applications:
|
||||
|
||||
- [PostgreSQL](https://www.postgresql.org) - an extremely scalable relational database
|
||||
- [Supabase](https://supabase.io) - a JS wrapper for Postgres for instant realtime and RESTful APIs
|
||||
- [Next.js](https://nextjs.org/) - a JS framework for building server-rendered React apps
|
||||
- [Tailwind](https://tailwindcss.com/) - a CSS framework for quickly styling our frontend app
|
||||
|
||||
<Mermaid
|
||||
caption="Supabase listens to Postgres and sends all changes to Next.js."
|
||||
graph={`graph LR
|
||||
Supabase(Supabase)
|
||||
Postgres(PostgreSQL)
|
||||
NextJs(Next.js)
|
||||
Tailwind(Tailwind)
|
||||
Supabase-->NextJs-->Supabase
|
||||
Postgres-->Supabase-->Postgres
|
||||
NextJs-.-Tailwind
|
||||
subgraph Frontend
|
||||
NextJs
|
||||
Tailwind
|
||||
end
|
||||
subgraph Backend
|
||||
Postgres
|
||||
Supabase
|
||||
end
|
||||
`}
|
||||
/>
|
||||
|
||||
|
||||
## Stage 1: Setup the project
|
||||
|
||||
<div className="alert alert--warning" role="alert">
|
||||
All the code for this section can be found in our main repo:<br />
|
||||
<a href="">slack-clone-basic-tutorial/stage-1-setup</a>
|
||||
</div>
|
||||
|
||||
|
||||
### Create the application
|
||||
|
||||
We will create a new Next.js app called `slack-clone-basic`, using the [Next.js SASS example](https://github.com/zeit/next.js/tree/master/examples/with-next-sass) to get us started:
|
||||
|
||||
|
||||
<CustomCodeBlock
|
||||
header="Initialise the app using Next.js"
|
||||
code={`
|
||||
npm init next-app --example with-next-sass slack-clone-basic --useNpm
|
||||
`}
|
||||
language="bash"
|
||||
/>
|
||||
|
||||
|
||||
This comes with [SASS](https://duckduckgo.com/?q=sass+css&t=ffab&ia=web) built in. You don't need to know anything about SASS, except that it will be useful for Tailwind later on.
|
||||
|
||||
|
||||
### Install dependencies
|
||||
|
||||
Now let's install our dependencies inside the `./slack-clone-basic` folder:
|
||||
|
||||
<CustomCodeBlock
|
||||
header="./slack-clone-basic"
|
||||
code={`
|
||||
npm install --save @supabase/supabase-js tailwindcss
|
||||
`}
|
||||
language="bash"
|
||||
/>
|
||||
|
||||
- `@supabase/supabase-js` - for interacting with the database. This will handle all the APIs and realtime functionality
|
||||
- `tailwindcss` - for making our frontend look great
|
||||
|
||||
|
||||
### Setup all the files
|
||||
|
||||
```bash
|
||||
mkdir components && touch components/MessageInput.js && \
|
||||
mkdir db && touch db/schema.sql && \
|
||||
touch pages/{[channelId].js} && \
|
||||
touch Store.js && \
|
||||
touch docker-compose.yml
|
||||
```
|
||||
|
||||
You will now have the following boilerplate, ready to fill in with code.
|
||||
|
||||
```bash
|
||||
├── components
|
||||
│ └── MessageInput.js # The text input for writing new messages
|
||||
├── db
|
||||
│ └── schema.sql # Basic schema and dummy data
|
||||
├── pages
|
||||
│ ├── [channelId].js # Page for showing all the messages in a channel
|
||||
│ └── index.js # Home page
|
||||
├── styles
|
||||
│ └── style.scss # SASS file for styling our app
|
||||
├── next.config.js # For passing ENV variables to our app
|
||||
├── Store.js # A basic data store using React Hooks
|
||||
└── docker-compose.yml # Docker compose for Postgres and Supabase
|
||||
```
|
||||
|
||||
By now you can get a simle app running
|
||||
|
||||
```
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Visit <a href="http://localhost:3000">http://localhost:3000</a> and you should see a page with a nice green "Hello World!"
|
||||
|
||||
## Stage 2: Setup the backend
|
||||
|
||||
|
||||
<div className="alert alert--warning" role="alert">
|
||||
All the code for this section can be found in stage-2-backend
|
||||
</div>
|
||||
|
||||
### Setting up Postgres
|
||||
|
||||
Let's sketch out the simplest possible database schema we can use.
|
||||
|
||||
<Mermaid
|
||||
caption="Users can create a message, which belongs to a single channel"
|
||||
graph={`classDiagram
|
||||
class users{
|
||||
id int
|
||||
username text
|
||||
}
|
||||
class channels{
|
||||
id int
|
||||
slug text
|
||||
}
|
||||
class messages{
|
||||
id int
|
||||
message text
|
||||
user_id int
|
||||
channel_id int
|
||||
}
|
||||
users .. messages
|
||||
channels .. messages
|
||||
`}
|
||||
/>
|
||||
|
||||
Save the corresponding schema in `./db/schema.sql`:
|
||||
|
||||
<CustomCodeBlock
|
||||
header="~/db/schema.sql"
|
||||
code={Snippets.schema}
|
||||
language="sql"
|
||||
/>
|
||||
|
||||
This schema will be automatically created when we start our Docker Compose.
|
||||
|
||||
### Setting up the APIs
|
||||
|
||||
This is the easiest step of all - you don't have to do anything!
|
||||
|
||||
Supabase will introspect your database and provide you with APIs and websockets once we start Docker Compose.
|
||||
|
||||
|
||||
### Fire up the Backend
|
||||
|
||||
Now lets start Docker Compose. Fill in the docker compose file with the following code:
|
||||
|
||||
<CustomCodeBlock
|
||||
header="~/docker-compose.yml"
|
||||
code={Snippets.dockerfile}
|
||||
language="yml"
|
||||
/>
|
||||
|
||||
Now run `docker-compose up`. If you set everything up correctly, you should be able to visit
|
||||
[http://localhost:8000/rest/v1/messages?apikey=examplekey](http://localhost:8000/rest/v1/messages?apikey=examplekey)
|
||||
where you will see some of the messages that were created in the database.
|
||||
|
||||
From now on you can forget about this route! Supabase will take care of everything when working with the database.
|
||||
|
||||
Let's leave this running for the remainder of the tutorial.
|
||||
|
||||
### Add ENV variables to Next JS
|
||||
|
||||
Let Next.js know how to access Supabase.
|
||||
|
||||
<CustomCodeBlock
|
||||
header="~/next.config.js"
|
||||
code={Snippets.nextJsConfig1}
|
||||
language="js"
|
||||
/>
|
||||
|
||||
|
||||
## Stage 3: Setup the Frontend
|
||||
|
||||
|
||||
<div className="alert alert--warning" role="alert">
|
||||
All the code for this section can be found in our main repo:<br />
|
||||
<a href="">slack-clone-basic-tutorial/stage-3-frontend</a>
|
||||
</div>
|
||||
|
||||
|
||||
### Building a basic store with React hooks
|
||||
|
||||
<CustomCodeBlock
|
||||
header="~/lib/Store.js"
|
||||
code={Snippets.store1}
|
||||
language="js"
|
||||
/>
|
||||
|
||||
|
||||
### Displaying our messages
|
||||
|
||||
|
||||
<CustomCodeBlock
|
||||
header="~/pages/[channelId].js"
|
||||
code={Snippets.channelPage1}
|
||||
language="js"
|
||||
/>
|
||||
|
||||
Visit <a href="http://localhost:3000/2">http://localhost:3000/2</a> and you will see a page with 2 buttons, and the channel ID displayed.
|
||||
|
||||
## Stage 4: Making it look like Slack
|
||||
|
||||
<div className="alert alert--warning" role="alert">
|
||||
All the code for this section can be found ..
|
||||
</div>
|
||||
|
||||
|
||||
Now we get to see the power of Tailwind to quickly prototype a frontend.
|
||||
|
||||
### Import Tailwind
|
||||
|
||||
Add the following code:
|
||||
|
||||
<CustomCodeBlock
|
||||
header="./styles/style.scss"
|
||||
code={`
|
||||
@import '~tailwindcss/dist/base.css';
|
||||
@import '~tailwindcss/dist/components.css';
|
||||
@import '~tailwindcss/dist/utilities.css';
|
||||
`}
|
||||
language="css"
|
||||
/>
|
||||
|
||||
### Updating the channels page
|
||||
|
||||
Add the following changes to `./pages/[channelId].js`.
|
||||
Ignore anything annotated with comments, which is just done to abbreviate the code snippets.
|
||||
You won't delete any code in this section, you just need to add classNames to all JSX!
|
||||
|
||||
#### Import the stylesheet
|
||||
|
||||
```js
|
||||
import '../styles/style.scss'
|
||||
```
|
||||
|
||||
#### Layout the sidebar and main content
|
||||
|
||||
```js {1,2,5}
|
||||
<div className="main flex h-screen w-screen absolute">
|
||||
<nav className="w-1/4 bg-gray-900 text-gray-100 h-screen p-2">
|
||||
{/* Sidebar content */}
|
||||
</nav>
|
||||
<main className="w-3/4 bg-gray-800 h-screen p-2">
|
||||
{/* Message content */}
|
||||
</main>
|
||||
</div>
|
||||
```
|
||||
|
||||
#### Style the messages
|
||||
|
||||
```js {3,6}
|
||||
{messages.map(message => (
|
||||
<div>
|
||||
<p className="text-blue-700 font-bold">
|
||||
{message.author.username}
|
||||
</p>
|
||||
<p className="text-white">
|
||||
{message.message}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
```
|
||||
|
||||
#### "New Channel" button
|
||||
|
||||
```js {1}
|
||||
<button className="bg-blue-900 hover:bg-blue-700 text-white font-bold p-2 rounded w-full">
|
||||
New Channel
|
||||
</button>
|
||||
```
|
||||
|
||||
#### Channel list
|
||||
|
||||
```js {1,7}
|
||||
<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>
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Stage 5: Adding realtime functionality
|
||||
|
||||
Make the following changes to the store.
|
||||
|
||||
|
||||
|
||||
### Handle new messages
|
||||
|
||||
```js
|
||||
const [newMessage, handleNewMessage] = useState(null)
|
||||
|
||||
// New message recieved from Postgres
|
||||
useEffect(() => {
|
||||
if (newMessage) {
|
||||
let author = users.get(newMessage.user_id)
|
||||
setMessages(messages.concat({...newMessage, author }))
|
||||
}
|
||||
}, [newMessage])
|
||||
|
||||
```
|
||||
|
||||
### Lisen to the database
|
||||
|
||||
```js {5-16}
|
||||
useEffect(() => {
|
||||
fetchChannels(setChannels)
|
||||
fetchMessages(setMessages)
|
||||
|
||||
// Listen for new messages
|
||||
const messageListener = supabase
|
||||
.from('messages')
|
||||
.eq('channel_id', channelId)
|
||||
.on('*', payload => handleNewMessage(payload.new))
|
||||
.subscribe()
|
||||
|
||||
|
||||
return () => {
|
||||
// Cleanup on unmount
|
||||
messageListener.unsubscribe()
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Stage 6: Sending new messages
|
||||
|
||||
For now we will assume that we are user 2 (tonystark).
|
||||
|
||||
### Add to store
|
||||
|
||||
|
||||
```js
|
||||
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)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Add message component
|
||||
|
||||
|
||||
Create component
|
||||
|
||||
```js
|
||||
import { useState } from 'react'
|
||||
import { addMessage } from '~/lib/Store'
|
||||
|
||||
const MessageInput = ({ channelId }) => {
|
||||
const [messageText, setMessageText] = useState('')
|
||||
|
||||
const submitOnEnter = event => {
|
||||
// Watch for enter key
|
||||
if (event.keyCode === 13) {
|
||||
addMessage(messageText, channelId, 2)
|
||||
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
|
||||
```
|
||||
|
||||
Add to Channel Page
|
||||
|
||||
```js
|
||||
// @todo
|
||||
<MessageInput channelId={channelId} />
|
||||
```
|
||||
|
||||
## Wrapping up
|
||||
|
||||
- You now have a realtime application. Open on multiple browsers to see realtime.
|
||||
- The combination of Docker + Postgres + Supabase made it quick and easy to get going on our local machines so that we could spend most of the time building the product.
|
||||
- Link to the example with user auth, adding channels
|
||||
|
||||
|
||||
If you liked this tutorial, make sure to sign up for [Supabase](http://supabase.io) as we continue to bring all the functionality of Firebase to Postgres.
|
||||
@@ -0,0 +1,183 @@
|
||||
export const nextJsConfig1 = `
|
||||
const withSass = require('@zeit/next-sass')
|
||||
module.exports = withSass({
|
||||
env: {
|
||||
SUPABASE_URL: 'http://localhost:8000',
|
||||
SUPABASE_KEY: 'examplekey',
|
||||
},
|
||||
})
|
||||
`
|
||||
|
||||
export const store1 = `
|
||||
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([])
|
||||
const [messages, setMessages] = useState([])
|
||||
|
||||
// Fetches messages for the channel and starts the channel listeners
|
||||
const reloadStore = () => {
|
||||
if (channelId) {
|
||||
fetchChannels(setChannels)
|
||||
fetchMessages(channelId, setMessages)
|
||||
}
|
||||
}
|
||||
|
||||
// Update the store when the user changes the "channel"
|
||||
useEffect(reloadStore, [channelId])
|
||||
|
||||
// Export properties to use in our app
|
||||
return { messages, channels, channelId }
|
||||
}
|
||||
|
||||
// Get all the channels
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// Get all the messages and the authors in a single request
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
`
|
||||
|
||||
export const schema = `
|
||||
-- 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);
|
||||
`
|
||||
|
||||
export const dockerfile = `
|
||||
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
|
||||
`
|
||||
|
||||
export const channelPage1 = `
|
||||
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>
|
||||
<nav>
|
||||
<div>
|
||||
<button>New Channel</button>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Channels</h3>
|
||||
</div>
|
||||
<ul>
|
||||
{channels.map(channel => (
|
||||
<li>
|
||||
<Link href="/[channelId]" as={\`/\${channel.id}\`}>
|
||||
<a>{channel.slug}</a>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
<main>
|
||||
<div>
|
||||
<h3>Messages</h3>
|
||||
</div>
|
||||
{messages.map(message => (
|
||||
<div>
|
||||
<p>
|
||||
<strong>{message.author.username}</strong>
|
||||
</p>
|
||||
<p>{message.message}</p>
|
||||
</div>
|
||||
))}
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChannelsPage
|
||||
`
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
title: Fast prototyping with Postgres computed columns
|
||||
author: Paul Copplestone
|
||||
author_title: Co-founder of Supabase
|
||||
author_url: https://github.com/kiwicopple
|
||||
author_image_url: https://avatars2.githubusercontent.com/u/10214025?s=400&u=c6775be2ae667e2acae3ccd347fed62bb3f5b3e7&v=4
|
||||
authorURL: https://github.com/kiwicopple
|
||||
tags: [postgres]
|
||||
---
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
title: Types of databases
|
||||
author: Paul Copplestone
|
||||
author_title: Co-founder of Supabase
|
||||
author_url: https://github.com/kiwicopple
|
||||
author_image_url: https://avatars2.githubusercontent.com/u/10214025?s=400&u=c6775be2ae667e2acae3ccd347fed62bb3f5b3e7&v=4
|
||||
authorURL: https://github.com/kiwicopple
|
||||
tags: [databases, nosql]
|
||||
---
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
title: What is Postgres?
|
||||
author: Paul Copplestone
|
||||
author_title: Co-founder of Supabase
|
||||
author_url: https://github.com/kiwicopple
|
||||
author_image_url: https://avatars2.githubusercontent.com/u/10214025?s=400&u=c6775be2ae667e2acae3ccd347fed62bb3f5b3e7&v=4
|
||||
authorURL: https://github.com/kiwicopple
|
||||
tags: [postgres]
|
||||
---
|
||||
|
||||
|
||||
## Introduction
|
||||
|
||||
- @todo
|
||||
|
||||
## What is a database
|
||||
|
||||
- Every business has data. Sometimes you want to store the data in one central location. That's it.
|
||||
|
||||
## What is Postgres
|
||||
|
||||
- Relational databases are like a spreadsheet
|
||||
- Read: types of databases.
|
||||
|
||||
## When would you choose Postgres?
|
||||
|
||||
-
|
||||
|
||||
## When wouldn't you choose Postgres?
|
||||
|
||||
- In the past you would choose NoSQL for faster prototyping. No longer the case with
|
||||
Generated
+418
@@ -918,6 +918,11 @@
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@braintree/sanitize-url": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-3.1.0.tgz",
|
||||
"integrity": "sha512-GcIY79elgB+azP74j8vqkiXz8xLFfIzbQJdlwOPisgbKT00tviJQuEghOXSMVxJ00HoYJbGswr4kcllUc4xCcg=="
|
||||
},
|
||||
"@csstools/convert-colors": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/convert-colors/-/convert-colors-1.4.0.tgz",
|
||||
@@ -3290,6 +3295,19 @@
|
||||
"randomfill": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"crypto-random-string": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-3.1.0.tgz",
|
||||
"integrity": "sha512-Tip3yGB+bA7B0W8E4K4mNf2rZhu5r2G5Tb89/utEl5tP1QuLjTF/S9a1b8ifDrR4ORc9Utf6tscpSEtBY3YcPQ==",
|
||||
"requires": {
|
||||
"type-fest": "^0.8.1"
|
||||
}
|
||||
},
|
||||
"css-b64-images": {
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/css-b64-images/-/css-b64-images-0.2.5.tgz",
|
||||
"integrity": "sha1-QgBdgyBLK0pdk7axpWRBM7WSegI="
|
||||
},
|
||||
"css-blank-pseudo": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz",
|
||||
@@ -3501,21 +3519,207 @@
|
||||
"resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz",
|
||||
"integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk="
|
||||
},
|
||||
"d3": {
|
||||
"version": "5.15.0",
|
||||
"resolved": "https://registry.npmjs.org/d3/-/d3-5.15.0.tgz",
|
||||
"integrity": "sha512-C+E80SL2nLLtmykZ6klwYj5rPqB5nlfN5LdWEAVdWPppqTD8taoJi2PxLZjPeYT8FFRR2yucXq+kBlOnnvZeLg==",
|
||||
"requires": {
|
||||
"d3-array": "1",
|
||||
"d3-axis": "1",
|
||||
"d3-brush": "1",
|
||||
"d3-chord": "1",
|
||||
"d3-collection": "1",
|
||||
"d3-color": "1",
|
||||
"d3-contour": "1",
|
||||
"d3-dispatch": "1",
|
||||
"d3-drag": "1",
|
||||
"d3-dsv": "1",
|
||||
"d3-ease": "1",
|
||||
"d3-fetch": "1",
|
||||
"d3-force": "1",
|
||||
"d3-format": "1",
|
||||
"d3-geo": "1",
|
||||
"d3-hierarchy": "1",
|
||||
"d3-interpolate": "1",
|
||||
"d3-path": "1",
|
||||
"d3-polygon": "1",
|
||||
"d3-quadtree": "1",
|
||||
"d3-random": "1",
|
||||
"d3-scale": "2",
|
||||
"d3-scale-chromatic": "1",
|
||||
"d3-selection": "1",
|
||||
"d3-shape": "1",
|
||||
"d3-time": "1",
|
||||
"d3-time-format": "2",
|
||||
"d3-timer": "1",
|
||||
"d3-transition": "1",
|
||||
"d3-voronoi": "1",
|
||||
"d3-zoom": "1"
|
||||
},
|
||||
"dependencies": {
|
||||
"d3-array": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz",
|
||||
"integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw=="
|
||||
},
|
||||
"d3-scale": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.2.2.tgz",
|
||||
"integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==",
|
||||
"requires": {
|
||||
"d3-array": "^1.2.0",
|
||||
"d3-collection": "1",
|
||||
"d3-format": "1",
|
||||
"d3-interpolate": "1",
|
||||
"d3-time": "1",
|
||||
"d3-time-format": "2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"d3-array": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.4.0.tgz",
|
||||
"integrity": "sha512-KQ41bAF2BMakf/HdKT865ALd4cgND6VcIztVQZUTt0+BH3RWy6ZYnHghVXf6NFjt2ritLr8H1T8LreAAlfiNcw=="
|
||||
},
|
||||
"d3-axis": {
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-1.0.12.tgz",
|
||||
"integrity": "sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ=="
|
||||
},
|
||||
"d3-brush": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-1.1.5.tgz",
|
||||
"integrity": "sha512-rEaJ5gHlgLxXugWjIkolTA0OyMvw8UWU1imYXy1v642XyyswmI1ybKOv05Ft+ewq+TFmdliD3VuK0pRp1VT/5A==",
|
||||
"requires": {
|
||||
"d3-dispatch": "1",
|
||||
"d3-drag": "1",
|
||||
"d3-interpolate": "1",
|
||||
"d3-selection": "1",
|
||||
"d3-transition": "1"
|
||||
}
|
||||
},
|
||||
"d3-chord": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-1.0.6.tgz",
|
||||
"integrity": "sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA==",
|
||||
"requires": {
|
||||
"d3-array": "1",
|
||||
"d3-path": "1"
|
||||
},
|
||||
"dependencies": {
|
||||
"d3-array": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz",
|
||||
"integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"d3-collection": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz",
|
||||
"integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A=="
|
||||
},
|
||||
"d3-color": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.0.tgz",
|
||||
"integrity": "sha512-TzNPeJy2+iEepfiL92LAAB7fvnp/dV2YwANPVHdDWmYMm23qIJBYww3qT8I8C1wXrmrg4UWs7BKc2tKIgyjzHg=="
|
||||
},
|
||||
"d3-contour": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-1.3.2.tgz",
|
||||
"integrity": "sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg==",
|
||||
"requires": {
|
||||
"d3-array": "^1.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"d3-array": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz",
|
||||
"integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"d3-dispatch": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz",
|
||||
"integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA=="
|
||||
},
|
||||
"d3-drag": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-1.2.5.tgz",
|
||||
"integrity": "sha512-rD1ohlkKQwMZYkQlYVCrSFxsWPzI97+W+PaEIBNTMxRuxz9RF0Hi5nJWHGVJ3Om9d2fRTe1yOBINJyy/ahV95w==",
|
||||
"requires": {
|
||||
"d3-dispatch": "1",
|
||||
"d3-selection": "1"
|
||||
}
|
||||
},
|
||||
"d3-dsv": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-1.2.0.tgz",
|
||||
"integrity": "sha512-9yVlqvZcSOMhCYzniHE7EVUws7Fa1zgw+/EAV2BxJoG3ME19V6BQFBwI855XQDsxyOuG7NibqRMTtiF/Qup46g==",
|
||||
"requires": {
|
||||
"commander": "2",
|
||||
"iconv-lite": "0.4",
|
||||
"rw": "1"
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"d3-ease": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.6.tgz",
|
||||
"integrity": "sha512-SZ/lVU7LRXafqp7XtIcBdxnWl8yyLpgOmzAk0mWBI9gXNzLDx5ybZgnRbH9dN/yY5tzVBqCQ9avltSnqVwessQ=="
|
||||
},
|
||||
"d3-fetch": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-1.1.2.tgz",
|
||||
"integrity": "sha512-S2loaQCV/ZeyTyIF2oP8D1K9Z4QizUzW7cWeAOAS4U88qOt3Ucf6GsmgthuYSdyB2HyEm4CeGvkQxWsmInsIVA==",
|
||||
"requires": {
|
||||
"d3-dsv": "1"
|
||||
}
|
||||
},
|
||||
"d3-force": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.2.1.tgz",
|
||||
"integrity": "sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==",
|
||||
"requires": {
|
||||
"d3-collection": "1",
|
||||
"d3-dispatch": "1",
|
||||
"d3-quadtree": "1",
|
||||
"d3-timer": "1"
|
||||
}
|
||||
},
|
||||
"d3-format": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.3.tgz",
|
||||
"integrity": "sha512-mm/nE2Y9HgGyjP+rKIekeITVgBtX97o1nrvHCWX8F/yBYyevUTvu9vb5pUnKwrcSw7o7GuwMOWjS9gFDs4O+uQ=="
|
||||
},
|
||||
"d3-geo": {
|
||||
"version": "1.11.9",
|
||||
"resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.11.9.tgz",
|
||||
"integrity": "sha512-9edcH6J3s/Aa3KJITWqFJbyB/8q3mMlA9Fi7z6yy+FAYMnRaxmC7jBhUnsINxVWD14GmqX3DK8uk7nV6/Ekt4A==",
|
||||
"requires": {
|
||||
"d3-array": "1"
|
||||
},
|
||||
"dependencies": {
|
||||
"d3-array": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz",
|
||||
"integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"d3-hierarchy": {
|
||||
"version": "1.1.9",
|
||||
"resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz",
|
||||
"integrity": "sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ=="
|
||||
},
|
||||
"d3-interpolate": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz",
|
||||
@@ -3529,6 +3733,21 @@
|
||||
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz",
|
||||
"integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="
|
||||
},
|
||||
"d3-polygon": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-1.0.6.tgz",
|
||||
"integrity": "sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ=="
|
||||
},
|
||||
"d3-quadtree": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.7.tgz",
|
||||
"integrity": "sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA=="
|
||||
},
|
||||
"d3-random": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-random/-/d3-random-1.1.2.tgz",
|
||||
"integrity": "sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ=="
|
||||
},
|
||||
"d3-scale": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.2.1.tgz",
|
||||
@@ -3541,6 +3760,20 @@
|
||||
"d3-time-format": "2"
|
||||
}
|
||||
},
|
||||
"d3-scale-chromatic": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz",
|
||||
"integrity": "sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg==",
|
||||
"requires": {
|
||||
"d3-color": "1",
|
||||
"d3-interpolate": "1"
|
||||
}
|
||||
},
|
||||
"d3-selection": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.1.tgz",
|
||||
"integrity": "sha512-BTIbRjv/m5rcVTfBs4AMBLKs4x8XaaLkwm28KWu9S2vKNqXkXt2AH2Qf0sdPZHjFxcWg/YL53zcqAz+3g4/7PA=="
|
||||
},
|
||||
"d3-shape": {
|
||||
"version": "1.3.7",
|
||||
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz",
|
||||
@@ -3562,6 +3795,61 @@
|
||||
"d3-time": "1"
|
||||
}
|
||||
},
|
||||
"d3-timer": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz",
|
||||
"integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw=="
|
||||
},
|
||||
"d3-transition": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-1.3.2.tgz",
|
||||
"integrity": "sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA==",
|
||||
"requires": {
|
||||
"d3-color": "1",
|
||||
"d3-dispatch": "1",
|
||||
"d3-ease": "1",
|
||||
"d3-interpolate": "1",
|
||||
"d3-selection": "^1.1.0",
|
||||
"d3-timer": "1"
|
||||
}
|
||||
},
|
||||
"d3-voronoi": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.4.tgz",
|
||||
"integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg=="
|
||||
},
|
||||
"d3-zoom": {
|
||||
"version": "1.8.3",
|
||||
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-1.8.3.tgz",
|
||||
"integrity": "sha512-VoLXTK4wvy1a0JpH2Il+F2CiOhVu7VRXWF5M/LroMIh3/zBAC3WAt7QoIvPibOavVo20hN6/37vwAsdBejLyKQ==",
|
||||
"requires": {
|
||||
"d3-dispatch": "1",
|
||||
"d3-drag": "1",
|
||||
"d3-interpolate": "1",
|
||||
"d3-selection": "1",
|
||||
"d3-transition": "1"
|
||||
}
|
||||
},
|
||||
"dagre": {
|
||||
"version": "0.8.5",
|
||||
"resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz",
|
||||
"integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==",
|
||||
"requires": {
|
||||
"graphlib": "^2.1.8",
|
||||
"lodash": "^4.17.15"
|
||||
}
|
||||
},
|
||||
"dagre-d3": {
|
||||
"version": "0.6.4",
|
||||
"resolved": "https://registry.npmjs.org/dagre-d3/-/dagre-d3-0.6.4.tgz",
|
||||
"integrity": "sha512-e/6jXeCP7/ptlAM48clmX4xTZc5Ek6T6kagS7Oz2HrYSdqcLZFLqpAfh7ldbZRFfxCZVyh61NEPR08UQRVxJzQ==",
|
||||
"requires": {
|
||||
"d3": "^5.14",
|
||||
"dagre": "^0.8.5",
|
||||
"graphlib": "^2.1.8",
|
||||
"lodash": "^4.17.15"
|
||||
}
|
||||
},
|
||||
"dashdash": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
|
||||
@@ -4092,6 +4380,11 @@
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
|
||||
},
|
||||
"escaper": {
|
||||
"version": "2.5.3",
|
||||
"resolved": "https://registry.npmjs.org/escaper/-/escaper-2.5.3.tgz",
|
||||
"integrity": "sha512-QGb9sFxBVpbzMggrKTX0ry1oiI4CSDAl9vIL702hzl1jGW8VZs7qfqTRX7WDOjoNDoEVGcEtu1ZOQgReSfT2kQ=="
|
||||
},
|
||||
"eslint-scope": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz",
|
||||
@@ -5582,6 +5875,14 @@
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz",
|
||||
"integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ=="
|
||||
},
|
||||
"graphlib": {
|
||||
"version": "2.1.8",
|
||||
"resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz",
|
||||
"integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==",
|
||||
"requires": {
|
||||
"lodash": "^4.17.15"
|
||||
}
|
||||
},
|
||||
"gray-matter": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.2.tgz",
|
||||
@@ -5912,6 +6213,27 @@
|
||||
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz",
|
||||
"integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8="
|
||||
},
|
||||
"html-minifier": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-4.0.0.tgz",
|
||||
"integrity": "sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==",
|
||||
"requires": {
|
||||
"camel-case": "^3.0.0",
|
||||
"clean-css": "^4.2.1",
|
||||
"commander": "^2.19.0",
|
||||
"he": "^1.2.0",
|
||||
"param-case": "^2.1.1",
|
||||
"relateurl": "^0.2.7",
|
||||
"uglify-js": "^3.5.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"html-minifier-terser": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-5.0.3.tgz",
|
||||
@@ -7195,6 +7517,24 @@
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.3.0.tgz",
|
||||
"integrity": "sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw=="
|
||||
},
|
||||
"mermaid": {
|
||||
"version": "8.4.6",
|
||||
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-8.4.6.tgz",
|
||||
"integrity": "sha512-6YQBkXfvhfjKIzRhtqbCics3pJurGrJAYEeqgyRcDZeTHQ/WCB2Bh/4wdAOho1Uffe0jXB+HjmHT5kEUOxudJw==",
|
||||
"requires": {
|
||||
"@braintree/sanitize-url": "^3.1.0",
|
||||
"crypto-random-string": "^3.0.1",
|
||||
"d3": "^5.7.0",
|
||||
"dagre": "^0.8.4",
|
||||
"dagre-d3": "^0.6.4",
|
||||
"graphlib": "^2.1.7",
|
||||
"he": "^1.2.0",
|
||||
"lodash": "^4.17.11",
|
||||
"minify": "^4.1.1",
|
||||
"moment-mini": "^2.22.1",
|
||||
"scope-css": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"methods": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||
@@ -7287,6 +7627,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"minify": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/minify/-/minify-4.1.3.tgz",
|
||||
"integrity": "sha512-ykuscavxivSmVpcCzsXmsVTukWYLUUtPhHj0w2ILvHDGqC+hsuTCihBn9+PJBd58JNvWTNg9132J9nrrI2anzA==",
|
||||
"requires": {
|
||||
"clean-css": "^4.1.6",
|
||||
"css-b64-images": "~0.2.5",
|
||||
"debug": "^4.1.0",
|
||||
"html-minifier": "^4.0.0",
|
||||
"terser": "^4.0.0",
|
||||
"try-catch": "^2.0.0",
|
||||
"try-to-catch": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"minimalistic-assert": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
||||
@@ -7400,6 +7754,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"moment-mini": {
|
||||
"version": "2.22.1",
|
||||
"resolved": "https://registry.npmjs.org/moment-mini/-/moment-mini-2.22.1.tgz",
|
||||
"integrity": "sha512-OUCkHOz7ehtNMYuZjNciXUfwTuz8vmF1MTbAy59ebf+ZBYZO5/tZKuChVWCX+uDo+4idJBpGltNfV8st+HwsGw=="
|
||||
},
|
||||
"move-concurrently": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
|
||||
@@ -10317,6 +10676,11 @@
|
||||
"aproba": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"rw": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
|
||||
"integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q="
|
||||
},
|
||||
"rx": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz",
|
||||
@@ -10371,6 +10735,16 @@
|
||||
"ajv-keywords": "^3.4.1"
|
||||
}
|
||||
},
|
||||
"scope-css": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/scope-css/-/scope-css-1.2.1.tgz",
|
||||
"integrity": "sha512-UjLRmyEYaDNiOS673xlVkZFlVCtckJR/dKgr434VMm7Lb+AOOqXKdAcY7PpGlJYErjXXJzKN7HWo4uRPiZZG0Q==",
|
||||
"requires": {
|
||||
"escaper": "^2.5.3",
|
||||
"slugify": "^1.3.1",
|
||||
"strip-css-comments": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"section-matter": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
|
||||
@@ -10610,6 +10984,11 @@
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz",
|
||||
"integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU="
|
||||
},
|
||||
"slugify": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/slugify/-/slugify-1.3.6.tgz",
|
||||
"integrity": "sha512-wA9XS475ZmGNlEnYYLPReSfuz/c3VQsEMoU43mi6OnKMCdbnFXd4/Yg7J0lBv8jkPolacMpOrWEaoYxuE1+hoQ=="
|
||||
},
|
||||
"snapdragon": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
|
||||
@@ -11106,6 +11485,14 @@
|
||||
"resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
|
||||
"integrity": "sha1-5SEekiQ2n7uB1jOi8ABE3IztrZI="
|
||||
},
|
||||
"strip-css-comments": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-css-comments/-/strip-css-comments-3.0.0.tgz",
|
||||
"integrity": "sha1-elYl7/iisibPiUehElTaluE9rok=",
|
||||
"requires": {
|
||||
"is-regexp": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"strip-eof": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
|
||||
@@ -11562,6 +11949,16 @@
|
||||
"resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz",
|
||||
"integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA=="
|
||||
},
|
||||
"try-catch": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/try-catch/-/try-catch-2.0.1.tgz",
|
||||
"integrity": "sha512-LsOrmObN/2WdM+y2xG+t16vhYrQsnV8wftXIcIOWZhQcBJvKGYuamJGwnU98A7Jxs2oZNkJztXlphEOoA0DWqg=="
|
||||
},
|
||||
"try-to-catch": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/try-to-catch/-/try-to-catch-1.1.1.tgz",
|
||||
"integrity": "sha512-ikUlS+/BcImLhNYyIgZcEmq4byc31QpC+46/6Jm5ECWkVFhf8SM2Fp/0pMVXPX6vk45SMCwrP4Taxucne8I0VA=="
|
||||
},
|
||||
"tryer": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz",
|
||||
@@ -11609,6 +12006,27 @@
|
||||
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
||||
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
|
||||
},
|
||||
"uglify-js": {
|
||||
"version": "3.7.7",
|
||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.7.7.tgz",
|
||||
"integrity": "sha512-FeSU+hi7ULYy6mn8PKio/tXsdSXN35lm4KgV2asx00kzrLU9Pi3oAslcJT70Jdj7PHX29gGUPOT6+lXGBbemhA==",
|
||||
"requires": {
|
||||
"commander": "~2.20.3",
|
||||
"source-map": "~0.6.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"unherit": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.3.tgz",
|
||||
|
||||
+2
-1
@@ -3,7 +3,7 @@
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "docusaurus start",
|
||||
"start": "docusaurus start --port 3005",
|
||||
"build": "docusaurus build",
|
||||
"swizzle": "docusaurus swizzle",
|
||||
"traction": "node traction.js"
|
||||
@@ -12,6 +12,7 @@
|
||||
"@docusaurus/core": "^2.0.0-alpha.40",
|
||||
"@docusaurus/preset-classic": "^2.0.0-alpha.40",
|
||||
"classnames": "^2.2.6",
|
||||
"mermaid": "^8.4.6",
|
||||
"react": "^16.8.4",
|
||||
"react-dom": "^16.8.4",
|
||||
"react-inlinesvg": "^1.2.0",
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import 'react-perfect-scrollbar/dist/css/styles.css'
|
||||
import React, { useState } from 'react'
|
||||
import CodeBlock from '@theme/CodeBlock'
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar'
|
||||
// import PerfectScrollbar from 'react-perfect-scrollbar'
|
||||
|
||||
export default function CustomCodeBlock({ header, js, jsHighlight, response }) {
|
||||
export default function CustomCodeBlock({
|
||||
header,
|
||||
code,
|
||||
highlight,
|
||||
js,
|
||||
jsHighlight,
|
||||
response,
|
||||
language,
|
||||
}) {
|
||||
const [showResponse, toggleResponse] = useState(false)
|
||||
|
||||
let wrapperClass = ''
|
||||
@@ -14,9 +22,14 @@ export default function CustomCodeBlock({ header, js, jsHighlight, response }) {
|
||||
<>
|
||||
<div className={wrapperClass}>
|
||||
{header && <div className="code-header">{header}</div>}
|
||||
<PerfectScrollbar>
|
||||
<CodeBlock metastring={jsHighlight}>{js}</CodeBlock>
|
||||
</PerfectScrollbar>
|
||||
<>
|
||||
{code && (
|
||||
<CodeBlock className={language || 'bash'} metastring={highlight}>
|
||||
{code}
|
||||
</CodeBlock>
|
||||
)}
|
||||
{js && <CodeBlock metastring={jsHighlight}>{js}</CodeBlock>}
|
||||
</>
|
||||
</div>
|
||||
{response && (
|
||||
<>
|
||||
@@ -42,7 +55,7 @@ const styles = {
|
||||
display: 'block',
|
||||
borderRadius: '0',
|
||||
color: 'var(--custom-primary)',
|
||||
borderTop: '1px solid #444'
|
||||
borderTop: '1px solid #444',
|
||||
},
|
||||
responseHidden: {
|
||||
textAlign: 'right',
|
||||
@@ -50,6 +63,6 @@ const styles = {
|
||||
borderBottom: 'none',
|
||||
borderRadius: '0 0 4px 4px',
|
||||
borderTop: '1px solid #444',
|
||||
color: '#ccc'
|
||||
color: '#ccc',
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from 'react'
|
||||
|
||||
export default function Figure({ src, alt, caption }) {
|
||||
return (
|
||||
<div className="Figure">
|
||||
<figure>
|
||||
<img src={src} alt={alt} />
|
||||
<figcaption>{caption}</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -9,8 +9,8 @@ export default function HowCard({ title, description, featureTitle, features })
|
||||
{description}
|
||||
<h5 style={styles.h5}>{featureTitle}</h5>
|
||||
<div style={styles.features}>
|
||||
{features.map(x => (
|
||||
<div>
|
||||
{features.map((x, i) => (
|
||||
<div key={i}>
|
||||
<span>
|
||||
<img src="/img/check.svg" alt="✔" style={styles.check} />
|
||||
</span>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import mermaid from 'mermaid'
|
||||
|
||||
const theme = `
|
||||
.node rect { fill: rgba(62,207,142, 0.3); stroke: #24b47e; }
|
||||
.cluster rect { fill: var(--custom-background-color-highlight); stroke: #999; }
|
||||
.edgePath path { fill: var(--custom-primary) }
|
||||
.edgePath .path { stroke: var(--custom-primary) }
|
||||
|
||||
g.classGroup rect { fill: rgba(62,207,142, 0.3); stroke: #24b47e; }
|
||||
g.classGroup line { stroke: var(--custom-primary); }
|
||||
g.classGroup text { fill: var(--ifm-color-content); color: var(--ifm-color-content); }
|
||||
.classLabel .label { fill: var(--ifm-color-content); color: var(--ifm-color-content); }
|
||||
classGroup .title { fill: var(--ifm-color-content); color: var(--ifm-color-content); }
|
||||
composition { stroke: var(--custom-primary); }
|
||||
aggregation { stroke: var(--custom-primary); }
|
||||
dependency { stroke: var(--custom-primary); }
|
||||
.relation { stroke: var(--custom-primary); }
|
||||
`
|
||||
|
||||
mermaid.initialize({ themeCSS: theme })
|
||||
|
||||
export default function Mermaid({ graph, classNames, caption }) {
|
||||
let [rendered, setRendered] = useState(<div></div>)
|
||||
|
||||
useEffect(() => {
|
||||
mermaid.render('mermaid-ID', graph.toString(), html => setRendered(html))
|
||||
}, [graph])
|
||||
|
||||
return (
|
||||
<div className={`Mermaid ${classNames || ''}`}>
|
||||
{caption && <figcaption>{caption}</figcaption>}
|
||||
<div className="graph" dangerouslySetInnerHTML={{ __html: rendered }}></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+67
-4
@@ -40,6 +40,14 @@
|
||||
--custom-content-color-emphasis: #000;
|
||||
--custom-border-color: #e0e0e0;
|
||||
--custom-color-subtle: #ccc;
|
||||
--custom-color-orange-100: #FDF0EA;
|
||||
--custom-color-orange-200: #FBE2D5;
|
||||
--custom-color-orange-300: #F9D3C0;
|
||||
--custom-color-orange-400: #F5B796;
|
||||
--custom-color-orange-500: #F08B57;
|
||||
--custom-color-orange-600: #EC6E2D;
|
||||
--custom-color-orange-700: #D75614;
|
||||
--custom-color-orange-800: #A93C04;
|
||||
--custom-shadow-lw: 0 3px 5px 0px rgba(0, 0, 0, 0.1);
|
||||
--custom-shadow-md: 0 3px 5px 0px rgba(0, 0, 0, 0.1);
|
||||
--custom-shadow-tl: 0 12px 28px 0 rgba(0, 0, 0, 0.2), 0 2px 4px 0 rgba(0, 0, 0, 0.1);
|
||||
@@ -56,6 +64,7 @@
|
||||
--ifm-color-content-secondary: var(--custom-content-color-light);
|
||||
--ifm-code-background: var(--custom-background-color-diff);
|
||||
--ifm-code-background-dark: #2a2a2a;
|
||||
--ifm-color-warning: var(--custom-color-orange-600);
|
||||
|
||||
/* Primary */
|
||||
--ifm-color-primary-darkest: var(--custom-primary-darkest);
|
||||
@@ -121,6 +130,9 @@
|
||||
/* Tables */
|
||||
--ifm-table-border-color: var(--custom-border-color);
|
||||
--ifm-table-stripe-background: var(--custom-background-color-diff);
|
||||
|
||||
/* Alerts */
|
||||
--ifm-alert-color: var(--custom-color-orange-600);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] {
|
||||
@@ -252,6 +264,15 @@ li + li {
|
||||
color: var(--custom-content-color-lightest) !important;
|
||||
}
|
||||
|
||||
.alert.alert--warning {
|
||||
color: var(--custom-color-orange-800);
|
||||
border-color: var(--custom-color-orange-800);
|
||||
background-color: var(--custom-color-orange-200);
|
||||
border-color: var(--custom-color-orange-400);
|
||||
margin: 20px 0;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.navbar__brand strong {
|
||||
font-family: var(--ifm-heading-font-family);
|
||||
font-size: 1.5rem;
|
||||
@@ -690,14 +711,12 @@ a.hash-link {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.HowSections .col {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.HowSections img.diagram {
|
||||
margin: 0px auto 30px auto;
|
||||
display: block;
|
||||
margin: 0px auto 30px auto;
|
||||
display: block;
|
||||
}
|
||||
.HowCard {
|
||||
position: relative;
|
||||
@@ -734,3 +753,47 @@ a.hash-link {
|
||||
border-width: 11px;
|
||||
margin-left: -11px;
|
||||
}
|
||||
|
||||
.Blog .code-with-header {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.Figure {
|
||||
text-align: center;
|
||||
margin: 30px 0;
|
||||
}
|
||||
.Figure figure {
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
.Figure img {
|
||||
box-shadow: 0px 0px 5px 5px rgba(0, 0, 0, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.Figure figcaption {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.Mermaid {
|
||||
text-align: center;
|
||||
margin: 80px 0;
|
||||
font-family: var(--custom-font-base);
|
||||
color: var(--ifm-color-content);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.Mermaid .graph {
|
||||
overflow-x: auto;
|
||||
margin: 0px calc(var(--ifm-spacing-horizontal) * -1);
|
||||
padding: var(--ifm-spacing-horizontal);
|
||||
}
|
||||
.Mermaid div,
|
||||
.Mermaid tspan,
|
||||
.Mermaid g.classGroup text {
|
||||
font-family: var(--custom-font-base);
|
||||
color: var(--ifm-color-content);
|
||||
/* font-size: 0.9em; */
|
||||
}
|
||||
.Mermaid figcaption {
|
||||
font-size: 0.8rem;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
Executable
+133
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* Copyright (c) 2017-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import {MDXProvider} from '@mdx-js/react';
|
||||
|
||||
import Link from '@docusaurus/Link';
|
||||
import MDXComponents from '@theme/MDXComponents';
|
||||
|
||||
import styles from './styles.module.css';
|
||||
|
||||
const MONTHS = [
|
||||
'January',
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December',
|
||||
];
|
||||
|
||||
function BlogPostItem(props) {
|
||||
const {
|
||||
children,
|
||||
frontMatter,
|
||||
metadata,
|
||||
truncated,
|
||||
isBlogPostPage = false,
|
||||
} = props;
|
||||
const {date, permalink, tags} = metadata;
|
||||
const {author, title} = frontMatter;
|
||||
|
||||
const authorURL = frontMatter.author_url || frontMatter.authorURL;
|
||||
const authorTitle = frontMatter.author_title || frontMatter.authorTitle;
|
||||
const authorImageURL =
|
||||
frontMatter.author_image_url || frontMatter.authorImageURL;
|
||||
|
||||
const renderPostHeader = () => {
|
||||
const TitleHeading = isBlogPostPage ? 'h1' : 'h2';
|
||||
const match = date.substring(0, 10).split('-');
|
||||
const year = match[0];
|
||||
const month = MONTHS[parseInt(match[1], 10) - 1];
|
||||
const day = parseInt(match[2], 10);
|
||||
|
||||
return (
|
||||
<header>
|
||||
<TitleHeading
|
||||
className={classnames('margin-bottom--sm', styles.blogPostTitle)}>
|
||||
{isBlogPostPage ? title : <Link to={permalink}>{title}</Link>}
|
||||
</TitleHeading>
|
||||
<div className="margin-bottom--sm">
|
||||
<time dateTime={date} className={styles.blogPostDate}>
|
||||
{month} {day}, {year}
|
||||
</time>
|
||||
</div>
|
||||
<div className="avatar margin-bottom--md">
|
||||
{authorImageURL && (
|
||||
<a
|
||||
className="avatar__photo-link"
|
||||
href={authorURL}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener">
|
||||
<img
|
||||
className="avatar__photo"
|
||||
src={authorImageURL}
|
||||
alt={author}
|
||||
/>
|
||||
</a>
|
||||
)}
|
||||
<div className="avatar__intro">
|
||||
{author && (
|
||||
<>
|
||||
<h4 className="avatar__name">
|
||||
<a href={authorURL} target="_blank" rel="noreferrer noopener">
|
||||
{author}
|
||||
</a>
|
||||
</h4>
|
||||
<small className="avatar__subtitle">{authorTitle}</small>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<article className={!isBlogPostPage ? 'margin-bottom--xl' : undefined}>
|
||||
{renderPostHeader()}
|
||||
<section className="markdown">
|
||||
<MDXProvider components={MDXComponents}>{children}</MDXProvider>
|
||||
</section>
|
||||
{(tags.length > 0 || truncated) && (
|
||||
<footer className="row margin-vert--lg">
|
||||
{tags.length > 0 && (
|
||||
<div className="col">
|
||||
<strong>Tags:</strong>
|
||||
{tags.map(({label, permalink: tagPermalink}) => (
|
||||
<Link
|
||||
key={tagPermalink}
|
||||
className="margin-horiz--sm"
|
||||
to={tagPermalink}>
|
||||
{label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{truncated && (
|
||||
<div className="col text--right">
|
||||
<Link
|
||||
to={metadata.permalink}
|
||||
aria-label={`Read more about ${title}`}>
|
||||
<strong>Read More</strong>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</footer>
|
||||
)}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
export default BlogPostItem;
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Copyright (c) 2017-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
.blogPostTitle {
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
.blogPostDate {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
Executable
+83
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Copyright (c) 2017-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import Layout from '@theme/Layout';
|
||||
import BlogPostItem from '@theme/BlogPostItem';
|
||||
import BlogPostPaginator from '@theme/BlogPostPaginator';
|
||||
import useTOCHighlight from '@theme/hooks/useTOCHighlight'
|
||||
import styles from './styles.module.css'
|
||||
|
||||
const LINK_CLASS_NAME = 'contents__link'
|
||||
const ACTIVE_LINK_CLASS_NAME = 'contents__link--active'
|
||||
const TOP_OFFSET = 100
|
||||
|
||||
function DocTOC({ headings }) {
|
||||
useTOCHighlight(LINK_CLASS_NAME, ACTIVE_LINK_CLASS_NAME, TOP_OFFSET)
|
||||
return (
|
||||
<div className={styles.tableOfContents}>
|
||||
<div className="contents contents__left-border">
|
||||
<h4 className="title">
|
||||
Contents
|
||||
</h4>
|
||||
<Headings headings={headings} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/* eslint-disable jsx-a11y/control-has-associated-label */
|
||||
function Headings({ headings, isChild }) {
|
||||
if (!headings.length) return null
|
||||
return (
|
||||
<ul className={isChild ? '' : ''}>
|
||||
{headings.map(heading => (
|
||||
<li key={heading.id}>
|
||||
<a
|
||||
href={`#${heading.id}`}
|
||||
className={LINK_CLASS_NAME}
|
||||
dangerouslySetInnerHTML={{ __html: heading.value }}
|
||||
/>
|
||||
<Headings isChild headings={heading.children} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
function BlogPostPage(props) {
|
||||
const {content: BlogPostContents} = props;
|
||||
const {frontMatter, metadata} = BlogPostContents;
|
||||
|
||||
return (
|
||||
<Layout title={metadata.title} description={metadata.description}>
|
||||
{BlogPostContents && (
|
||||
<div className="Blog container margin-vert--xl">
|
||||
<div className="row">
|
||||
<div className="col col--8 col--offset-1">
|
||||
<BlogPostItem frontMatter={frontMatter} metadata={metadata} isBlogPostPage>
|
||||
<BlogPostContents />
|
||||
</BlogPostItem>
|
||||
{(metadata.nextItem || metadata.prevItem) && (
|
||||
<div className="margin-vert--xl">
|
||||
<BlogPostPaginator nextItem={metadata.nextItem} prevItem={metadata.prevItem} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="col col--3">
|
||||
<DocTOC headings={BlogPostContents.rightToc} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
export default BlogPostPage;
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Copyright (c) 2017-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
.docTitle {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 996px) {
|
||||
.docTitle {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.docItemContainer {
|
||||
margin: 0 auto;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
.tableOfContents {
|
||||
display: inherit;
|
||||
max-height: calc(100vh - (var(--ifm-navbar-height) + 2rem));
|
||||
overflow-y: auto;
|
||||
position: sticky;
|
||||
top: calc(var(--ifm-navbar-height) + 2rem);
|
||||
}
|
||||
|
||||
.tableOfContents::-webkit-scrollbar {
|
||||
width: 7px;
|
||||
}
|
||||
|
||||
.tableOfContents::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.tableOfContents::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.tableOfContents::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 996px) {
|
||||
.tableOfContents {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.docItemContainer {
|
||||
padding: 0 0.3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.docLastUpdatedAt {
|
||||
font-weight: bold;
|
||||
}
|
||||
Reference in new issue
Block a user