rsnext/examples/with-redis/pages/index.tsx
JTaylor0196 f7baa56792
update example Deploy button URLs (#48842)
### What?
Updates Deploy button URLs for all listed examples here:
https://github.com/vercel/next.js/tree/canary/examples

### Why?
The Deploy URLs are currently broken and result in a failed clone
attempt on Vercel.

### How?
The URLs have been changed from https://vercel.com/new/git/external?… to
-> https://vercel.com/new/clone?…
(Last updated
[here](8eaabe2fb0)
in 2021)
2023-04-26 13:31:44 -04:00

307 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useRef } from 'react'
import type { NextApiRequest } from 'next'
import type { MouseEvent } from 'react'
import Head from 'next/head'
import clsx from 'clsx'
import useSWR, { mutate } from 'swr'
import toast from 'react-hot-toast'
import redis from '../lib/redis'
type Feature = {
id: string
title: string
score: number
ip: string
}
const fetcher = (url: string) => fetch(url).then((res) => res.json())
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>
)
}
function Item({
isFirst,
isLast,
isReleased,
hasVoted,
feature,
}: {
isFirst: boolean
isLast: boolean
isReleased: boolean
hasVoted: boolean
feature: Feature
}) {
const upvote = async (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault()
const res = await fetch('/api/vote', {
body: JSON.stringify({
id: feature.id,
title: feature.title,
}),
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
})
const { error } = await res.json()
if (error) {
return toast.error(error)
}
mutate('/api/features')
}
return (
<div
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'
)}
disabled={isReleased || hasVoted}
onClick={upvote}
>
{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>
</div>
)
}
export default function Roadmap({
features,
ip,
}: {
features: Feature[]
ip: string
}) {
const [isCreateLoading, setCreateLoading] = useState(false)
const [isEmailLoading, setEmailLoading] = useState(false)
const featureInputRef = useRef<HTMLInputElement>(null)
const subscribeInputRef = useRef<HTMLInputElement>(null)
const { data, error } = useSWR('/api/features', fetcher, {
initialData: { features },
})
if (error) {
toast.error(error)
}
const addFeature = async (e: MouseEvent<HTMLFormElement>) => {
e.preventDefault()
setCreateLoading(true)
const res = await fetch('/api/create', {
body: JSON.stringify({
title: featureInputRef?.current?.value ?? '',
}),
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
})
const { error } = await res.json()
setCreateLoading(false)
if (error) {
toast.error(error)
return
}
mutate('/api/features')
if (featureInputRef.current) {
featureInputRef.current.value = ''
}
}
const subscribe = async (e: MouseEvent<HTMLFormElement>) => {
e.preventDefault()
setEmailLoading(true)
const res = await fetch('/api/subscribe', {
body: JSON.stringify({
email: subscribeInputRef?.current?.value ?? '',
}),
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
})
const { error } = await res.json()
setEmailLoading(false)
if (error) {
return toast.error(error)
}
toast.success('You are now subscribed to feature updates!')
if (subscribeInputRef.current) {
subscribeInputRef.current.value = ''
}
}
return (
<div className="flex flex-col items-center justify-center min-h-screen py-2">
<Head>
<title>Roadmap Next.js and Redis Example</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<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">
<img src="/upstash.svg" alt="Upstash Logo" className="h-8 sm:h-16" />
</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 the next release.
</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">
<div className="mx-8 w-full">
<form className="relative my-8" onSubmit={addFeature}>
<input
ref={featureInputRef}
aria-label="Suggest a feature for our roadmap"
placeholder="I want..."
type="text"
maxLength={150}
required
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"
/>
<button
className="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"
type="submit"
>
{isCreateLoading ? <LoadingSpinner invert /> : 'Request'}
</button>
</form>
</div>
<div className="w-full">
{data.features.map((feature: Feature, index: number) => (
<Item
key={index}
isFirst={index === 0}
isLast={index === data.features.length - 1}
isReleased={false}
hasVoted={feature.ip === ip}
feature={feature}
/>
))}
</div>
<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" onSubmit={subscribe}>
<input
ref={subscribeInputRef}
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"
>
{isEmailLoading ? <LoadingSpinner /> : '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
<img src="/vercel.svg" alt="Vercel Logo" className="h-5 mx-2" />
and
<img
src="/upstash.svg"
alt="Upstash Logo"
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/clone?repository-url=https%3A%2F%2Fgithub.com%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&integration-ids=oac_V3R1GIpkoJorr6fqyiwdhl17"
>
<img
src="https://vercel.com/button"
alt="Vercel Deploy Button"
/>
</a>
</div>
</div>
</div>
</main>
</div>
)
}
export async function getServerSideProps({ req }: { req: NextApiRequest }) {
const ip =
req.headers['x-forwarded-for'] || req.headers['Remote_Addr'] || 'NA'
const features = (await redis.hvals('features'))
.map((entry) => JSON.parse(entry))
.sort((a, b) => {
// Primary sort is score
if (a.score > b.score) return -1
if (a.score < b.score) return 1
// Secondary sort is title
if (a.title > b.title) return 1
if (a.title < b.title) return -1
return 1
})
return { props: { features, ip } }
}