Example: Deta Base (#19061)

Add an example for using Next.js with Deta Base on Vercel.
This commit is contained in:
xeust 2020-11-14 00:00:30 +01:00 committed by GitHub
parent ba791d0d84
commit cd1e2e1d60
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 582 additions and 0 deletions

View file

@ -0,0 +1 @@
DETA_PROJECT_KEY=

View file

@ -0,0 +1,73 @@
# Deta Base Example
An example using [Deta Base](https://docs.deta.sh/docs/base/about) in a Next.js project.
## Deploy your own
Once you have access to [the environment variables you'll need](#step-2-setting-up-environment-variables), deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example):
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/with-deta-base&env=DETA_PROJECT_KEY&envDescription=The%20Deta%20Project%20Key%2C%20found%20in%20the%20Deta%20dashboard&envLink=https://github.com/vercel/next.js/tree/canary/examples/with-deta-base%23configuration)
## How to use
Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example:
```bash
npx create-next-app --example with-deta-base with-deta-base-app
# or
yarn create next-app --example with-deta-base with-deta-base-app
```
## Configuration
### Step 1. Create a Deta Account
Create an account on [Deta](https://www.deta.sh/?ref=next.js). Save the default _Project Key_ which will be auto-generated on account creation.
### Step 2. Setting Up Environment Variables
Copy the `.env.local.example` file from this directory to `.env.local` (which will be ignored by Git):
```bash
cp .env.local.example .env.local
```
Then set each variable on `.env.local`:
- `DETA_PROJECT_KEY` should be the default _Project Key_ that you saved from step 1.
The resulting `env.local` file shoule look like this:
```bash
DETA_PROEJECT_KEY=...
```
### Step 3. Run Next.js in development mode
```bash
npm install
npm run dev
# or
yarn install
yarn dev
```
Your todo app should be up and running on [http://localhost:3000](http://localhost:3000)! If it doesn't work, post on [GitHub discussions](https://github.com/vercel/next.js/discussions).
### Step 4. Deploy on Vercel
You can deploy this app to the cloud with [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)).
#### Deploy Your Local Project
To deploy your local project to Vercel, push it to GitHub/GitLab/Bitbucket and [import to Vercel](https://vercel.com/import/git?utm_source=github&utm_medium=readme&utm_campaign=next-example).
**Important**: When you import your project on Vercel, make sure to click on **Environment Variables** and set them to match your `.env.local` file.
#### Deploy from Our Template
Alternatively, you can deploy using our template by clicking on the Deploy button below.
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/with-deta-base&env=DETA_PROJECT_KEY&envDescription=The%20Deta%20Project%20Key%2C%20found%20in%20the%20Deta%20dashboard&envLink=https://github.com/vercel/next.js/tree/canary/examples/with-deta-base%23configuration)

View file

@ -0,0 +1,16 @@
{
"name": "with-deta-base",
"version": "1.0.0",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"deta": "^0.0.8",
"next": "latest",
"react": "17.0.1",
"react-dom": "17.0.1"
},
"license": "MIT"
}

View file

@ -0,0 +1,7 @@
import '../styles/globals.css'
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />
}
export default MyApp

View file

@ -0,0 +1,27 @@
import { Deta } from 'deta'
const deta = Deta(process.env.DETA_PROJECT_KEY)
const base = deta.Base('todos')
const handler = async (req, res) => {
let {
body,
method,
query: { tid },
} = req
let respBody = {}
if (method === 'PUT') {
body = JSON.parse(body)
respBody = await base.put(body)
res.statusCode = 200
} else if (method === 'DELETE') {
respBody = await base.delete(tid)
res.statusCode = 200
}
res.json(respBody)
}
export default handler

View file

@ -0,0 +1,25 @@
import { Deta } from 'deta'
const deta = Deta(process.env.DETA_PROJECT_KEY)
const base = deta.Base('todos')
const handler = async (req, res) => {
let { body, method } = req
let respBody = {}
if (method === 'GET') {
const { value: items } = await base.fetch([]).next()
respBody = items
res.statusCode = 200
} else if (method === 'POST') {
body = JSON.parse(body)
body.isCompleted = false
respBody = await base.put(body)
res.statusCode = 201
}
res.json(respBody)
}
export default handler

View file

@ -0,0 +1,147 @@
import { useState, useEffect } from 'react'
import Head from 'next/head'
import styles from '../styles/Home.module.css'
const ToDo = ({ content, isCompleted, onChange, onDelete }) => {
const cards = ['card', 'card2', 'card3', 'card4', 'card5']
return (
<div className={styles[cards[Math.floor(Math.random() * cards.length)]]}>
<div
className={styles.text}
style={{ textDecoration: isCompleted ? 'line-through' : '' }}
>
{content}
</div>
<div className={styles.reverseWrapper}>
<input
type="checkbox"
className={styles.check}
checked={isCompleted}
onChange={onChange}
/>
<button className={styles.delBtn} onClick={onDelete}>
&#10005;
</button>
</div>
</div>
)
}
export default function Home() {
const [newContent, setNewContent] = useState('')
const [toDos, setToDos] = useState([])
const getToDos = async () => {
const resp = await fetch('api/todos')
const toDos = await resp.json()
setToDos(toDos)
}
const createToDo = async () => {
await fetch('api/todos', {
method: 'post',
body: JSON.stringify({ content: newContent }),
})
await getToDos()
}
const updateToDo = async (todo) => {
let newBody = {
...todo,
isCompleted: !todo.isCompleted,
}
await fetch(`api/todos/${todo.key}`, {
method: 'put',
body: JSON.stringify(newBody),
})
await getToDos()
}
const deleteToDo = async (tid) => {
await fetch(`api/todos/${tid}`, { method: 'delete' })
setTimeout(getToDos, 300)
}
useEffect(() => {
getToDos()
}, [])
const completed = toDos.filter((todo) => todo.isCompleted)
const notCompleted = toDos.filter((todo) => !todo.isCompleted)
return (
<div className={styles.container}>
<Head>
<title>deta + next.js</title>
<link rel="icon" href="/favicon.ico" />
<link
href="https://fonts.googleapis.com/css2?family=Source+Code+Pro&display=swap"
rel="stylesheet"
/>
</Head>
<header className={styles.header}>
<h2>
<a href="https://www.deta.sh">deta base</a> +{' '}
<a href="https://nextjs.org">next.js</a> to dos
</h2>
</header>
<main className={styles.main}>
<div className={styles.incomplete}>
<div className={styles.firstRow}>
<div className={styles.title}>to dos</div>
<div className={styles.reverseWrapper}>
<input
className={styles.inpt}
onChange={(e) => setNewContent(e.target.value)}
></input>
<button className={styles.addBtn} onClick={createToDo}>
&#10011;
</button>
</div>
</div>
<div className={styles.scrolly}>
{notCompleted.map((todo, index) => (
<ToDo
key={todo.key}
content={`${index + 1}. ${todo.content}`}
isCompleted={todo.isCompleted}
onChange={() => updateToDo(todo)}
onDelete={() => deleteToDo(todo.key)}
/>
))}
</div>
</div>
<div className={styles.complete}>
<div className={styles.firstRow}>
<div className={styles.title}>done</div>
</div>
<div className={styles.scrolly}>
{completed.map((todo, index) => (
<ToDo
key={todo.key}
content={`${index + 1}. ${todo.content}`}
isCompleted={todo.isCompleted}
onChange={() => updateToDo(todo)}
onDelete={() => deleteToDo(todo.key)}
/>
))}
</div>
</div>
</main>
<footer className={styles.footer}>
<a
href="https://deta.sh?ref=create-next-deta-app"
target="_blank"
rel="noopener noreferrer"
>
powered by
<img src="/deta.svg" alt="Deta Logo" className={styles.logo} />
&nbsp;deta & next.js
</a>
</footer>
</div>
)
}

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="340.16" height="340.16" xmlns="http://www.w3.org/2000/svg">
<circle cx="170.08" cy="170.08" r="141.73" fill="#ef39a8" vector-effect="non-scaling-stroke"/>
<circle cx="170.08" cy="170.08" r="113.39" fill="#bd399c" vector-effect="non-scaling-stroke"/>
<circle cx="170.08" cy="170.08" r="85.039" fill="#93388e" vector-effect="non-scaling-stroke"/>
<circle cx="170.08" cy="170.08" r="56.693" fill="#563379" vector-effect="non-scaling-stroke"/>
</svg>

After

Width:  |  Height:  |  Size: 499 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,4 @@
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,249 @@
.container {
min-height: 100vh;
padding: 0 0.5rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.main {
padding: 1rem 0;
width: 100%;
flex: 1;
height: calc(100vh - 100px);
display: flex;
flex-direction: row;
justify-content: center;
}
.complete,
.incomplete {
display: flex;
flex-direction: column;
padding-left: 1.5rem;
padding-right: 1.5rem;
width: 50%;
}
.scrolly {
display: flex;
flex-direction: column;
height: calc(100vh - 132px);
overflow: scroll;
}
.firstRow {
display: flex;
align-items: center;
height: 52px;
}
.reverseWrapper {
display: flex;
margin-left: auto;
height: 100%;
align-items: center;
}
.header {
width: 100%;
height: 50px;
border-bottom: 1px solid #f1f1f1;
display: flex;
justify-content: flex-start;
align-items: center;
}
.header h2 {
padding-left: 24px;
}
.inpt {
border: 1px solid #c5c4c7;
border-radius: 2px;
padding-left: 5px;
padding-right: 5px;
font-size: 16px;
height: 32px;
margin-right: 10px;
}
.inpt:focus,
.inpt:active {
border: 1px solid #2b66ff;
}
.addBtn,
.delBtn {
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
background-color: transparent;
border: 1px solid transparent;
color: #c5c4c7;
border-radius: 2px;
height: 32px;
width: 32px;
}
.addBtn:hover,
.addBtn:focus,
.addBtn:active {
color: #1cec73;
}
.delBtn:hover,
.delBtn:focus,
.delBtn:active {
color: #ee6262;
}
.check {
background-color: transparent;
border: 1px solid #c5c4c7;
border-radius: 2px;
height: 20px;
width: 20px;
margin-right: 10px;
}
.footer {
width: 100%;
height: 50px;
border-top: 1px solid #f1f1f1;
display: flex;
justify-content: center;
align-items: center;
}
.footer img {
margin-left: 0.5rem;
}
.footer a {
display: flex;
justify-content: center;
align-items: center;
}
.title a {
color: #2b66ff;
text-decoration: none;
}
.title a:hover,
.title a:focus,
.title a:active {
text-decoration: underline;
}
.title {
margin: 0;
line-height: 1.15;
font-size: 2rem;
padding-top: 10px;
padding-bottom: 10px;
}
.title,
.description {
text-align: flex-start;
}
.description {
line-height: 1.5;
font-size: 1.5rem;
}
.code {
background: #c5c4c7;
border-radius: 5px;
padding: 0.75rem;
font-size: 1.1rem;
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
Bitstream Vera Sans Mono, Courier New, monospace;
}
.card,
.card2,
.card3,
.card4,
.card5 {
display: flex;
color: #c5c4c7;
font-size: 18px;
font-weight: bold;
margin-top: 1rem !important;
margin-bottom: 1rem;
height: 50px;
align-items: center;
text-align: left;
text-decoration: none;
}
.card:hover,
.card:focus,
.card:active {
color: #0070f3;
}
.card2:hover,
.card2:focus,
.card2:active {
color: #ae00f3;
}
.card3:hover,
.card3:focus,
.card3:active {
color: #f3003d;
}
.card4:hover,
.card4:focus,
.card4:active {
color: #f39200;
}
.card4:hover,
.card4:focus,
.card4:active {
color: #29f300;
}
.card5:hover,
.card5:focus,
.card5:active {
color: #00f3cb;
}
.card,
.card2,
.card3,
.card4,
.card5 h3 {
margin: 0 0 1rem 0;
font-size: 1.5rem;
}
.text {
display: flex;
align-items: center;
text-transform: lowercase;
margin: 0;
font-size: 1.5rem;
line-height: 1.5;
}
.logo {
height: 1em;
}
@media (max-width: 600px) {
.main {
width: 100%;
flex-direction: column;
}
}

View file

@ -0,0 +1,26 @@
html,
body {
padding: 0;
margin: 0;
font-family: 'Source Code Pro', Courier New, monospace;
}
a {
color: #0070f3;
text-decoration: none;
}
input,
button {
font-family: 'Source Code Pro', Arial, sans-serif;
}
a:hover,
a:focus,
a:active {
text-decoration: underline;
}
* {
box-sizing: border-box;
}