Add Convex example (#38129)

Adds an example using [Convex](https://convex.dev/).

## Documentation / Examples

- [x] Make sure the linting passes by running `pnpm lint`
- [x] The examples guidelines are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing.md#adding-examples)


Co-authored-by: Balázs Orbán <18369201+balazsorban44@users.noreply.github.com>
This commit is contained in:
Thomas Ballinger 2022-06-30 03:26:46 -07:00 committed by GitHub
parent 5e2ad29309
commit 8f707d4d74
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 412 additions and 0 deletions

40
examples/convex/.gitignore vendored Normal file
View file

@ -0,0 +1,40 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
# vercel
.vercel
# convex
convex/_generated
# typescript
*.tsbuildinfo

35
examples/convex/README.md Normal file
View file

@ -0,0 +1,35 @@
# Convex
This example demonstrates the Convex global state management framework.
## Deploy your own
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/convex&project-name=convex&repository-name=convex)
## 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), [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/), or [pnpm](https://pnpm.io) to bootstrap the example:
```bash
npx create-next-app --example with-convex with-convex-app
# or
yarn create next-app --example with-convex with-convex-app
# or
pnpm create next-app --example with-convex with-convex-app
```
After creating a [Convex account](https://www.convex.dev) and getting a beta key,
```bash
npx convex init --beta-key <your beta key>
```
Next, push the Convex functions for this project.
```bash
npx convex push
```
Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)).

View file

@ -0,0 +1,45 @@
# Welcome to your functions directory
Write your convex functions in this directory.
A query function (how you read data) looks like this:
```typescript
// getCounter.ts
import { query } from './_generated/server'
export default query(async ({ db }): Promise<number> => {
const counterDoc = await db.table('counter_table').first()
console.log('Got stuff')
if (counterDoc === null) {
return 0
}
return counterDoc.counter
})
```
A mutation function (how you write data) looks like this:
```typescript
// incrementCounter.ts
import { mutation } from './_generated/server'
export default mutation(async ({ db }, increment: number) => {
let counterDoc = await db.table('counter_table').first()
if (counterDoc === null) {
counterDoc = {
counter: increment,
}
db.insert('counter_table', counterDoc)
} else {
counterDoc.counter += increment
db.replace(counterDoc._id, counterDoc)
}
// Like console.log but relays log messages from the server to client.
console.log(`Value of counter is now ${counterDoc.counter}`)
})
```
The convex cli is your friend. See everything it can do by running
`npx convex -h` in your project root directory. To learn more, launch the docs
with `npx convex docs`.

View file

@ -0,0 +1,13 @@
import { query } from './_generated/server'
export default query(async ({ db }, counterName: string): Promise<number> => {
const counterDoc = await db
.table('counter_table')
.filter((q) => q.eq(q.field('name'), counterName))
.first()
console.log('Got stuff')
if (counterDoc === null) {
return 0
}
return counterDoc.counter
})

View file

@ -0,0 +1,22 @@
import { mutation } from './_generated/server'
export default mutation(
async ({ db }, counterName: string, increment: number) => {
let counterDoc = await db
.table('counter_table')
.filter((q) => q.eq(q.field('name'), counterName))
.first()
if (counterDoc === null) {
counterDoc = {
name: counterName,
counter: increment,
}
db.insert('counter_table', counterDoc)
} else {
counterDoc.counter += increment
db.replace(counterDoc._id, counterDoc)
}
// Like console.log but relays log messages from the server to client.
console.log(`Value of counter is now ${counterDoc.counter}`)
}
)

5
examples/convex/next-env.d.ts vendored Normal file
View file

@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View file

@ -0,0 +1,20 @@
{
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "latest",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"convex-dev": "0.1.4"
},
"devDependencies": {
"@types/node": "~16.11.12",
"@types/react": "17.0.45",
"@types/react-dom": "17.0.17",
"typescript": "^4.7.3"
}
}

View file

@ -0,0 +1,16 @@
import '../styles/globals.css'
import type { AppProps } from 'next/app'
import { ConvexProvider, ConvexReactClient } from 'convex-dev/react'
import convexConfig from '../convex.json'
const convex = new ConvexReactClient(convexConfig.origin)
function MyApp({ Component, pageProps }: AppProps) {
return (
<ConvexProvider client={convex}>
<Component {...pageProps} />;
</ConvexProvider>
)
}
export default MyApp

View file

@ -0,0 +1,51 @@
import type { NextPage } from 'next'
import Head from 'next/head'
import Image from 'next/image'
import styles from '../styles/Home.module.css'
import { useQuery, useMutation } from '../convex/_generated/react'
import { useCallback } from 'react'
const Home: NextPage = () => {
const counter = useQuery('getCounter', 'clicks') ?? 0
const increment = useMutation('incrementCounter')
const incrementByOne = useCallback(() => increment('clicks', 1), [increment])
return (
<div className={styles.container}>
<Head>
<title>Next.js with Convex</title>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={styles.main}>
<h1 className={styles.title}>
Welcome to <a href="https://nextjs.org">Next.js</a> with{' '}
<a href="https://convex.dev">Convex</a>
</h1>
<p className={styles.description}>
{"Here's the counter:"} {counter}
</p>
<button className={styles.button} onClick={incrementByOne}>
Add One!
</button>
</main>
<footer className={styles.footer}>
<a
href="https://www.convex.dev/"
target="_blank"
rel="noopener noreferrer"
>
Powered by{' '}
<span className={styles.logo}>
<Image src="/convex.svg" alt="Convex Logo" width={90} height={18} />
</span>
</a>
</footer>
</div>
)
}
export default Home

View file

@ -0,0 +1,19 @@
<svg width="155" height="28" viewBox="0 0 155 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path d="M140.529 14.0394L133.866 5.10791H141.59L155.22 23.278H147.421L144.391 19.2135L141.361 23.278H133.599L140.529 14.0394Z" fill="currentColor"/>
<path d="M147.25 5.10791H154.937L149.037 13.0636L145.133 7.96551L147.25 5.10791Z" fill="currentColor"/>
<path d="M34.6421 21.2656C32.7863 19.6696 31.8584 17.3106 31.8584 14.1946C31.8584 11.0786 32.8043 8.71955 34.6992 7.12355C36.5911 5.52755 39.1796 4.72803 42.4619 4.72803C43.8252 4.72803 45.0294 4.82227 46.0774 5.01683C47.1255 5.20835 48.1284 5.53363 49.0864 5.99571V11.0512C47.5969 10.3368 45.9063 9.97811 44.0144 9.97811C42.3477 9.97811 41.1165 10.2973 40.3238 10.9357C39.528 11.5741 39.1316 12.6594 39.1316 14.1946C39.1316 15.6781 39.522 16.7512 40.3057 17.4139C41.0865 18.0797 42.3237 18.4111 44.0174 18.4111C45.8102 18.4111 47.5128 17.9885 49.1284 17.1464V22.436C47.3357 23.2538 45.1015 23.6611 42.4258 23.6611C39.0895 23.6611 36.498 22.8616 34.6421 21.2656Z" fill="currentColor"/>
<path d="M50.7441 14.1914C50.7441 11.0997 51.615 8.74981 53.3567 7.13861C55.0984 5.52741 57.723 4.72485 61.2335 4.72485C64.768 4.72485 67.4106 5.53045 69.1673 7.13861C70.921 8.74677 71.7979 11.0997 71.7979 14.1914C71.7979 20.5024 68.2754 23.658 61.2335 23.658C54.2396 23.661 50.7441 20.5055 50.7441 14.1914ZM63.753 17.4138C64.2695 16.7481 64.5277 15.6749 64.5277 14.1945C64.5277 12.7383 64.2695 11.6713 63.753 10.9933C63.2365 10.3154 62.3956 9.97797 61.2335 9.97797C60.0984 9.97797 59.2756 10.3185 58.7711 10.9933C58.2666 11.6713 58.0143 12.7383 58.0143 14.1945C58.0143 15.678 58.2666 16.7511 58.7711 17.4138C59.2756 18.0796 60.0954 18.4109 61.2335 18.4109C62.3956 18.4109 63.2335 18.0765 63.753 17.4138Z" fill="currentColor"/>
<path d="M73.4551 5.10789H80.1187L80.3078 6.48805C81.0406 5.97733 81.9745 5.55477 83.1096 5.22341C84.2447 4.89205 85.4189 4.72485 86.6321 4.72485C88.8783 4.72485 90.5179 5.28725 91.5539 6.41205C92.59 7.53685 93.1065 9.27269 93.1065 11.6257V23.278H85.9895V12.3522C85.9895 11.5345 85.8063 10.9477 85.4399 10.589C85.0735 10.2303 84.4609 10.054 83.6021 10.054C83.0736 10.054 82.53 10.1756 81.9745 10.4188C81.4189 10.662 80.9535 10.9751 80.5721 11.3581V23.278H73.4551V5.10789Z" fill="currentColor"/>
<path d="M93.1279 5.10791H100.548L103.957 15.7631L107.365 5.10791H114.785L107.704 23.278H100.206L93.1279 5.10791Z" fill="currentColor"/>
<path d="M117.025 21.7154C114.887 20.0921 113.887 17.274 113.887 14.2309C113.887 11.2669 114.682 8.82581 116.499 7.13861C118.316 5.45141 121.085 4.72485 124.58 4.72485C127.796 4.72485 130.325 5.47877 132.172 6.98661C134.016 8.49445 134.94 10.5525 134.94 13.1578V16.3407H121.406C121.742 17.2861 122.169 17.9701 123.217 18.3927C124.265 18.8153 125.727 19.025 127.598 19.025C128.715 19.025 129.856 18.9369 131.016 18.7575C131.424 18.6937 132.097 18.5933 132.451 18.5173V22.9345C130.682 23.4209 128.325 23.6641 125.667 23.6641C122.091 23.661 119.163 23.3388 117.025 21.7154ZM127.463 12.4313C127.463 11.5314 126.439 9.59493 124.382 9.59493C122.526 9.59493 121.301 11.501 121.301 12.4313H127.463Z" fill="currentColor"/>
<path d="M16.7204 21.7216C20.6543 21.2868 24.3629 19.2014 26.405 15.7206C25.438 24.3299 15.9757 29.7715 8.25206 26.4305C7.54035 26.1235 6.92775 25.6128 6.50734 24.9561C4.77163 22.2444 4.20106 18.794 5.02087 15.6628C7.36318 19.6848 12.1259 22.1502 16.7204 21.7216Z" fill="#F3B01C"/>
<path d="M4.87728 13.1365C3.2827 16.8027 3.21364 21.0952 5.16856 24.6277C-1.71122 19.4779 -1.63615 8.45792 5.08448 3.35984C5.70609 2.88864 6.44482 2.60896 7.21959 2.5664C10.4057 2.3992 13.6429 3.62432 15.9132 5.90736C11.3006 5.95296 6.80818 8.89264 4.87728 13.1365Z" fill="#8D2676"/>
<path d="M18.1376 7.03551C15.8103 3.80703 12.1677 1.60911 8.17676 1.54223C15.8914 -1.94161 25.3807 3.70671 26.4137 12.0576C26.5098 12.8328 26.3837 13.6232 26.0384 14.3224C24.597 17.2347 21.9243 19.4934 18.8012 20.3294C21.0895 16.1069 20.8072 10.948 18.1376 7.03551Z" fill="#EE342F"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="155" height="28" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View file

@ -0,0 +1,110 @@
.container {
padding: 0 2rem;
}
.main {
min-height: 100vh;
padding: 4rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.button {
font-size: 1rem;
font-weight: 800;
cursor: pointer;
margin: 0.5rem;
padding: 0.5rem;
text-align: left;
text-decoration: none;
border: 1px solid #eaeaea;
border-radius: 10px;
transition: color 0.15s ease, border-color 0.15s ease;
text-align: center;
width: 200px;
}
.button:hover,
.button:focus,
.button:active {
color: #0070f3;
border-color: #0070f3;
}
.footer {
display: flex;
flex: 1;
padding: 2rem 0;
border-top: 1px solid #eaeaea;
justify-content: center;
align-items: center;
}
.footer a {
display: flex;
justify-content: center;
align-items: center;
flex-grow: 1;
}
.title a {
color: #0070f3;
text-decoration: none;
}
.title a:hover,
.title a:focus,
.title a:active {
text-decoration: underline;
}
.title {
margin: 0;
line-height: 1.15;
font-size: 3rem;
}
.title,
.description {
text-align: center;
}
.description {
margin: 2rem 0;
line-height: 1.5;
font-size: 1.5rem;
}
.logo {
height: 1em;
margin-left: 0.5rem;
}
.loadingLayout {
display: flex;
height: 100vh;
}
.loading,
.loading:after {
border-radius: 50%;
width: 5em;
height: 5em;
}
.loading {
margin: auto auto;
border: 0.8em solid #f4e9f1;
border-left: 0.8em solid #8d2676;
animation: load8 1.1s infinite linear;
}
@keyframes load8 {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

View file

@ -0,0 +1,16 @@
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
a {
color: inherit;
text-decoration: none;
}
* {
box-sizing: border-box;
}

View file

@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}