Skip to content

Commit

Permalink
examples: update Redis to App Router (#59311)
Browse files Browse the repository at this point in the history
  • Loading branch information
leerob authored Dec 24, 2023
1 parent d4b7c86 commit 0fc1d9e
Show file tree
Hide file tree
Showing 24 changed files with 1,396 additions and 482 deletions.
4 changes: 4 additions & 0 deletions examples/with-redis/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
KV_URL=
KV_REST_API_URL=
KV_REST_API_TOKEN=
KV_REST_API_READ_ONLY_TOKEN=
1 change: 0 additions & 1 deletion examples/with-redis/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz

# testing
/coverage
Expand Down
42 changes: 42 additions & 0 deletions examples/with-redis/app/actions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use server'

import { kv } from '@vercel/kv'
import { revalidatePath } from 'next/cache'
import { Feature } from './types'

export async function saveFeature(feature: Feature) {
await kv.hset(`item:${feature.id}`, feature)
await kv.zadd('items_by_score', {
score: Number(feature.score),
member: feature.id,
})

revalidatePath('/')
}

export async function saveEmail(formData: FormData) {
const email = formData.get('email')

function validateEmail(email: FormDataEntryValue) {
const re =
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
return re.test(String(email).toLowerCase())
}

if (email && validateEmail(email)) {
await kv.sadd('emails', email)
revalidatePath('/')
}
}

export async function upvote(feature: Feature) {
const newScore = Number(feature.score) + 1
await kv.hset(`item:${feature.id}`, {
...feature,
score: newScore,
})

await kv.zadd('items_by_score', { score: newScore, member: feature.id })

revalidatePath('/')
}
File renamed without changes.
163 changes: 163 additions & 0 deletions examples/with-redis/app/form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
'use client'

import clsx from 'clsx'
import { useOptimistic, useRef } from 'react'
import { saveFeature, upvote } from './actions'
import { v4 as uuidv4 } from 'uuid'
import { Feature } from './types'

function Item({
isFirst,
isLast,
isReleased,
hasVoted,
feature,
pending,
mutate,
}: {
isFirst: boolean
isLast: boolean
isReleased: boolean
hasVoted: boolean
feature: Feature
pending: boolean
mutate: any
}) {
return (
<form
action={async () => {
mutate({
updatedFeature: {
...feature,
score: (Number(feature.score) + 1).toString(),
},
pending: true,
})
await upvote(feature)
}}
className={clsx(
'p-6 mx-8 flex items-center border-t border-l border-r',
isFirst && 'rounded-t-md',
isLast && 'border-b rounded-b-md'
)}
>
<button
className={clsx(
'ring-1 ring-gray-200 rounded-full w-8 min-w-[2rem] h-8 mr-4 focus:outline-none focus:ring focus:ring-blue-300',
(isReleased || hasVoted) &&
'bg-green-100 cursor-not-allowed ring-green-300',
pending && 'bg-gray-100 cursor-not-allowed'
)}
disabled={isReleased || hasVoted || pending}
type="submit"
>
{isReleased ? '✅' : '👍'}
</button>
<h3 className="text font-semibold w-full text-left">{feature.title}</h3>
<div className="bg-gray-200 text-gray-700 text-sm rounded-xl px-2 ml-2">
{feature.score}
</div>
</form>
)
}

type FeatureState = {
newFeature: Feature
updatedFeature?: Feature
pending: boolean
}

export default function FeatureForm({ features }: { features: Feature[] }) {
const formRef = useRef<HTMLFormElement>(null)
const [state, mutate] = useOptimistic(
{ features, pending: false },
function createReducer(state, newState: FeatureState) {
if (newState.newFeature) {
return {
features: [...state.features, newState.newFeature],
pending: newState.pending,
}
} else {
return {
features: [
...state.features.filter(
(f) => f.id !== newState.updatedFeature!.id
),
newState.updatedFeature,
] as Feature[],
pending: newState.pending,
}
}
}
)

let sortedFeatures = state.features.sort((a, b) => {
// First, compare by score in descending order
if (Number(a.score) > Number(b.score)) return -1
if (Number(a.score) < Number(b.score)) return 1

// If scores are equal, then sort by created_at in ascending order
return new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
})

return (
<>
<div className="mx-8 w-full">
<form
className="relative my-8"
ref={formRef}
action={async (formData) => {
const newFeature = {
id: uuidv4(),
title: `${formData.get('feature')}`,
created_at: new Date().toISOString(),
score: '1',
}

mutate({
newFeature,
pending: true,
})
formRef.current?.reset()
await saveFeature(newFeature)
}}
>
<input
aria-label="Suggest a feature for our roadmap"
className="pl-3 pr-28 py-3 mt-1 text-lg block w-full border border-gray-200 rounded-md text-gray-900 placeholder-gray-400 focus:outline-none focus:ring focus:ring-blue-300"
maxLength={150}
placeholder="I want..."
required
type="text"
name="feature"
disabled={state.pending}
/>
<button
className={clsx(
'flex items-center justify-center absolute right-2 top-2 px-4 h-10 text-lg border bg-black text-white rounded-md w-24 focus:outline-none focus:ring focus:ring-blue-300 focus:bg-gray-800',
state.pending && 'bg-gray-700 cursor-not-allowed'
)}
type="submit"
disabled={state.pending}
>
Request
</button>
</form>
</div>
<div className="w-full">
{sortedFeatures.map((feature: any, index: number) => (
<Item
key={feature.id}
isFirst={index === 0}
isLast={index === sortedFeatures.length - 1}
isReleased={false}
hasVoted={false}
feature={feature}
pending={state.pending}
mutate={mutate}
/>
))}
</div>
</>
)
}
File renamed without changes.
14 changes: 14 additions & 0 deletions examples/with-redis/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import './globals.css'
import { GeistSans } from 'geist/font/sans'

export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" className={GeistSans.variable}>
<body>{children}</body>
</html>
)
}
119 changes: 119 additions & 0 deletions examples/with-redis/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { kv } from '@vercel/kv'
import { saveEmail } from './actions'
import FeatureForm from './form'
import { Feature } from './types'

export let metadata = {
title: 'Next.js and Redis Example',
description: 'Feature roadmap example with Next.js with Redis.',
}

function VercelLogo(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
aria-label="Vercel Logo"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 19"
{...props}
>
<path
clipRule="evenodd"
d="M12.04 2L2.082 18H22L12.04 2z"
fill="#000"
fillRule="evenodd"
stroke="#000"
strokeWidth="1.5"
/>
</svg>
)
}

async function getFeatures() {
let itemIds = await kv.zrange('items_by_score', 0, 100, {
rev: true,
})

if (!itemIds.length) {
return []
}

let multi = kv.multi()
itemIds.forEach((id) => {
multi.hgetall(`item:${id}`)
})

let items: Feature[] = await multi.exec()
return items.map((item) => {
return {
...item,
score: item.score,
created_at: item.created_at,
}
})
}

export default async function Page() {
let features = await getFeatures()

return (
<div className="flex flex-col items-center justify-center min-h-screen py-2">
<main className="flex flex-col items-center justify-center flex-1 px-4 sm:px-20 text-center">
<div className="flex justify-center items-center bg-black rounded-full w-16 sm:w-24 h-16 sm:h-24 my-8">
<VercelLogo className="h-8 sm:h-16 invert p-3 mb-1" />
</div>
<h1 className="text-lg sm:text-2xl font-bold mb-2">
Help us prioritize our roadmap
</h1>
<h2 className="text-md sm:text-xl mx-4">
Create or vote up features you want to see in our product.
</h2>
<div className="flex flex-wrap items-center justify-around max-w-4xl my-8 sm:w-full bg-white rounded-md shadow-xl h-full border border-gray-100">
<FeatureForm features={features} />
<hr className="border-1 border-gray-200 my-8 mx-8 w-full" />
<div className="mx-8 w-full">
<p className="flex text-gray-500">
Leave your email address here to be notified when feature requests
are released.
</p>
<form className="relative my-4" action={saveEmail}>
<input
name="email"
aria-label="Email for updates"
placeholder="Email Address"
type="email"
autoComplete="email"
maxLength={60}
required
className="px-3 py-3 mt-1 text-lg block w-full border border-gray-200 rounded-md text-gray-900 placeholder-gray-400 focus:outline-none focus:ring focus:ring-blue-300"
/>
<button
className="flex items-center justify-center absolute right-2 top-2 px-4 h-10 border border-gray-200 text-gray-900 rounded-md w-14 focus:outline-none focus:ring focus:ring-blue-300 focus:bg-gray-100"
type="submit"
>
OK
</button>
</form>
<div className="flex flex-col sm:flex-row justify-between items-center">
<p className="flex items-center my-8 w-full justify-center sm:justify-start">
Powered by
<VercelLogo className="h-5 mx-2" />
</p>
<a
target="_blank"
rel="noopener noreferrer"
className="flex rounded focus:outline-none focus:ring focus:ring-blue-300 mb-4 sm:mb-0 min-w-max"
href={`https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgit.luolix.top%2Fvercel%2Fnext.js%2Ftree%2Fcanary%2Fexamples%2Fwith-redis&project-name=redis-roadmap&repository-name=redis-roadmap&demo-title=Redis%20Roadmap&demo-description=Create%20and%20upvote%20features%20for%20your%20product.&demo-url=https%3A%2F%2Froadmap-redis.vercel.app%2F&stores=%5B%7B"type"%3A"kv"%7D%5D&`}
>
<img
src="https://vercel.com/button"
alt="Vercel Deploy Button"
/>
</a>
</div>
</div>
</div>
</main>
</div>
)
}
29 changes: 29 additions & 0 deletions examples/with-redis/app/spinner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import clsx from 'clsx'

export default function LoadingSpinner({ invert }: { invert?: boolean }) {
return (
<svg
className={clsx(
'animate-spin h-5 w-5 text-gray-900 dark:text-gray-100',
invert && 'text-gray-100 dark:text-gray-900'
)}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
)
}
6 changes: 6 additions & 0 deletions examples/with-redis/app/types.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type Feature = {
id: string
title: string
score: string
created_at: string
}
5 changes: 0 additions & 5 deletions examples/with-redis/lib/redis.ts

This file was deleted.

Loading

0 comments on commit 0fc1d9e

Please sign in to comment.