Merge branch 'blog'

This commit is contained in:
Paul Copplestone committed 2020-02-27 21:46:22 +13:00
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.
@@ -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.
@@ -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.
@@ -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.
@@ -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.
@@ -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('')
+452
View File
@@ -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]
---
+10
View File
@@ -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]
---
+31
View File
@@ -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
+418
View File
@@ -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
View File
@@ -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",
+20 -7
View File
@@ -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',
},
}
+12
View File
@@ -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>
)
}
+2 -2
View File
@@ -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>
+36
View File
@@ -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
View File
@@ -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;
}
+133
View File
@@ -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;
+14
View File
@@ -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;
}
+83
View File
@@ -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;
}