Skip to content

Commit

Permalink
Convert to App Router and Next.js 14. (#33)
Browse files Browse the repository at this point in the history
  • Loading branch information
leerob authored Oct 28, 2023
1 parent a00cc8a commit 9625be6
Show file tree
Hide file tree
Showing 54 changed files with 1,267 additions and 1,408 deletions.
38 changes: 34 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,37 @@
.next
node_modules
yarn-error.log
# 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
.vscode

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# local env files
.env*.local

# vercel
.vercel
.env

# typescript
*.tsbuildinfo
next-env.d.ts
12 changes: 0 additions & 12 deletions .prettierignore

This file was deleted.

2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2020 Vercel
Copyright (c) 2023 Vercel

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
33 changes: 10 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,50 +1,37 @@
# Next.js 12 React Server Components Notes Demo (Alpha)
# Next.js App Router + Server Components Notes Demo

Try the demo live here: [**next-rsc-notes.vercel.app**](https://next-rsc-notes.vercel.app).
> Try the demo live here: [**next-rsc-notes.vercel.app**](https://next-rsc-notes.vercel.app).
> **Warning**
> This demo is built for showing what features that Server Components provide and what the application structure might look like.
> **It's not ready for production adoption, or performance benchmarking** as the underlying APIs are not stable yet, and might change or be improved in the future.
This demo was originally [built by the React team](https://github.com/reactjs/server-components-demo). This version has been forked and modified for use with the Next.js App Router.

## Introduction

This is a demo app showing Next.js 12's experimental React Server Components support. It's based on the [React Server Components Demo](https://github.com/reactjs/server-components-demo) by the React team. We recommend you taking a look at these links, before trying out the experimental feature:
- [**Introducing Zero-Bundle-Size React Server Components**](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html)
- [**Everything About React Server Components**](https://vercel.com/blog/everything-about-react-server-components)
- [**Docs of React Server Components in Next.js**](https://nextjs.org/docs/advanced-features/react-18#react-server-components)
This is a demo app of a notes application, which shows Next.js 13's App Router with support for React Server Components. [Learn more](https://nextjs.org/docs/getting-started/react-essentials).

## Technical Details

This Next.js application uses React 18 (RC build), with `runtime` set to `'nodejs'` and feature flag `serverComponents` enabled. You can check out [next.config.js](https://github.com/vercel/server-components-notes-demo/blob/main/next.config.js) for more details. It also uses Redis to store the data, and GitHub's OAuth API for authentication. To develop it locally or host it, please follow these instructions:

### Preparation
### Environment Variables

These environment variables are required to start this application (you can create a `.env` file for Next.js to use):

```bash
REDIS_URL='rediss://:<password>@<url>:<port>' # or `redis://` if no TLS support
KV_URL='redis://:<password>@<url>:<port>' # vercel.com/kv
SESSION_KEY='your session key'
OAUTH_CLIENT_KEY='github oauth app id'
OAUTH_CLIENT_SECRET='github oauth app secret'
```

### Running Locally

1. `yarn install`
2. `yarn dev`
1. `pnpm install`
2. `pnpm dev`

Go to `localhost:3000`.

### Deploy

You can quickly deploy the demo to Vercel by clicking this link:

[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fserver-components-notes-demo&env=REDIS_URL,SESSION_KEY,OAUTH_CLIENT_KEY,OAUTH_CLIENT_SECRET&project-name=next-rsc-notes&repo-name=next-rsc-notes&demo-title=React%20Server%20Components%20(Experimental%20Demo)&demo-description=Experimental%20demo%20of%20React%20Server%20Components%20with%20Next.js.%20&demo-url=https%3A%2F%2Fnext-rsc-notes.vercel.app&demo-image=https%3A%2F%2Fnext-rsc-notes.vercel.app%2Fog.png)

## Technical Details

This Next.js application uses React 18 (RC build) and the new [Edge Runtime](https://nextjs.org/docs/api-reference/edge-runtime). It has `runtime` set to `'edge'` and feature flag `serverComponents` enabled. You can check out [next.config.js](https://github.com/vercel/next-server-components/blob/main/next.config.js) for more details.
[![Deploy with Vercel](https://vercel.com/button)](<https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fserver-components-notes-demo&env=REDIS_URL,SESSION_KEY,OAUTH_CLIENT_KEY,OAUTH_CLIENT_SECRET&project-name=next-rsc-notes&repo-name=next-rsc-notes&demo-title=React%20Server%20Components%20(Experimental%20Demo)&demo-description=Experimental%20demo%20of%20React%20Server%20Components%20with%20Next.js.%20&demo-url=https%3A%2F%2Fnext-rsc-notes.vercel.app&demo-image=https%3A%2F%2Fnext-rsc-notes.vercel.app%2Fog.png>)

## License

This demo is MIT licensed.
This demo is MIT licensed. Originally [built by the React team](https://github.com/reactjs/server-components-demo)
39 changes: 39 additions & 0 deletions app/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
'use server'

import { kv } from '@vercel/kv'
import { getUser, userCookieKey } from 'libs/session'
import { cookies } from 'next/headers'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'

export async function saveNote(
noteId: string | null,
title: string,
body: string
) {
const cookieStore = cookies()
const userCookie = cookieStore.get(userCookieKey)
const user = getUser(userCookie?.value)

if (!noteId) {
noteId = Date.now().toString()
}

const payload = {
id: noteId,
title: title.slice(0, 255),
updated_at: Date.now(),
body: body.slice(0, 2048),
created_by: user
}

await kv.hset('notes', { [noteId]: JSON.stringify(payload) })

revalidatePath('/')
redirect(`/note/${noteId}`)
}

export async function deleteNote(noteId: string) {
revalidatePath('/')
redirect('/')
}
66 changes: 66 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import './style.css'

import React from 'react'
import { kv } from '@vercel/kv'
import Sidebar from 'components/sidebar'
import AuthButton from 'components/auth-button'

export const metadata = {
title: 'Next.js 13 + React Server Components Demo',
description: 'Demo of React Server Components in Next.js. Hosted on Vercel.',
openGraph: {
title: 'Next.js 13 + React Server Components Demo',
description:
'Demo of React Server Components in Next.js. Hosted on Vercel.',
images: ['https://next-server-components.vercel.app/og.png']
},
robots: {
index: true,
follow: true
},
metadataBase: new URL('https://next-rsc-notes.vercel.app/')
}

type Note = {
id: string
created_by: string
title: string
body: string
updated_at: number
}

export default async function RootLayout({
children
}: {
children: React.ReactNode
}) {
const notes = await kv.hgetall('notes')
let notesArray: Note[] = notes
? (Object.values(notes) as Note[]).sort(
(a, b) => Number(a.id) - Number(b.id)
)
: []

return (
<html lang="en">
<body>
<div className="container">
<div className="banner">
<a
href="https://nextjs.org/docs/app/building-your-application/rendering/server-components"
target="_blank"
>
Learn more →
</a>
</div>
<div className="main">
<Sidebar notes={notesArray}>
<AuthButton noteId={null}>Add</AuthButton>
</Sidebar>
<section className="col note-viewer">{children}</section>
</div>
</div>
</body>
</html>
)
}
27 changes: 27 additions & 0 deletions app/note/[id]/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export default function NoteSkeleton() {
return (
<div
className="note skeleton-container"
role="progressbar"
aria-busy="true"
>
<div className="note-header">
<div
className="note-title skeleton"
style={{ height: '3rem', width: '65%', marginInline: '12px 1em' }}
/>
<div
className="skeleton skeleton--button"
style={{ width: '8em', height: '2.5em' }}
/>
</div>
<div className="note-preview">
<div className="skeleton v-stack" style={{ height: '1.5em' }} />
<div className="skeleton v-stack" style={{ height: '1.5em' }} />
<div className="skeleton v-stack" style={{ height: '1.5em' }} />
<div className="skeleton v-stack" style={{ height: '1.5em' }} />
<div className="skeleton v-stack" style={{ height: '1.5em' }} />
</div>
</div>
)
}
24 changes: 24 additions & 0 deletions app/note/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { kv } from '@vercel/kv'
import NoteUI from 'components/note-ui'

export const metadata = {
robots: {
index: false
}
}

export default async function Page({ params }: { params: { id: string } }) {
const note = await kv.hget('notes', params.id)

if (note === null) {
return (
<div className="note--empty-state">
<span className="note-text--empty-state">
Click a note on the left to view something! 🥺
</span>
</div>
)
}

return <NoteUI note={note} isEditing={false} />
}
36 changes: 36 additions & 0 deletions app/note/edit/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { kv } from '@vercel/kv'
import { cookies } from 'next/headers'
import { getUser, userCookieKey } from 'libs/session'
import NoteUI from 'components/note-ui'

export const metadata = {
robots: {
index: false
}
}

type Note = {
id: string
created_by: string
}

export default async function EditPage({ params }: { params: { id: string } }) {
const cookieStore = cookies()
const userCookie = cookieStore.get(userCookieKey)
const user = getUser(userCookie?.value)

const note = await kv.hget<Note>('notes', params.id)
const isCreator = note?.created_by === user || true

if (note === null) {
return (
<div className="note--empty-state">
<span className="note-text--empty-state">
Click a note on the left to view something! 🥺
</span>
</div>
)
}

return <NoteUI note={note} isEditing={isCreator} />
}
36 changes: 1 addition & 35 deletions components/NoteSkeleton.js → app/note/edit/loading.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import React from 'react'

function NoteEditorSkeleton() {
export default function EditSkeleton() {
return (
<div
className="note-editor skeleton-container"
Expand Down Expand Up @@ -37,35 +35,3 @@ function NoteEditorSkeleton() {
</div>
)
}

function NotePreviewSkeleton() {
return (
<div
className="note skeleton-container"
role="progressbar"
aria-busy="true"
>
<div className="note-header">
<div
className="note-title skeleton"
style={{ height: '3rem', width: '65%', marginInline: '12px 1em' }}
/>
<div
className="skeleton skeleton--button"
style={{ width: '8em', height: '2.5em' }}
/>
</div>
<div className="note-preview">
<div className="skeleton v-stack" style={{ height: '1.5em' }} />
<div className="skeleton v-stack" style={{ height: '1.5em' }} />
<div className="skeleton v-stack" style={{ height: '1.5em' }} />
<div className="skeleton v-stack" style={{ height: '1.5em' }} />
<div className="skeleton v-stack" style={{ height: '1.5em' }} />
</div>
</div>
)
}

export default function NoteSkeleton({ isEditing }) {
return isEditing ? <NoteEditorSkeleton /> : <NotePreviewSkeleton />
}
16 changes: 16 additions & 0 deletions app/note/edit/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import NoteUI from 'components/note-ui'

export const metadata = {
robots: {
index: false
}
}

export default async function EditPage() {
const defaultNote = {
title: 'Untitled',
body: ''
}

return <NoteUI note={defaultNote} isEditing={true} />
}
9 changes: 9 additions & 0 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export default async function Page() {
return (
<div className="note--empty-state">
<span className="note-text--empty-state">
Click a note on the left to view something! 🥺
</span>
</div>
)
}
Loading

1 comment on commit 9625be6

@vercel
Copy link

@vercel vercel bot commented on 9625be6 Oct 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.