Skip to content

Commit 5dd4999

Browse files
authored
Convert many examples to TypeScript (vercel#41825)
Strategized with @balazsorban44 to open one larger PR, with changes to individual examples as separate commits. For each example, I researched how multiple realworld codebases use the featured technology with TypeScript, to thoughtfully convert them by hand - nothing automated whatsoever. ## 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/examples/adding-examples.md)
1 parent a1072c6 commit 5dd4999

File tree

191 files changed

+1543
-758
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

191 files changed

+1543
-758
lines changed

examples/blog-with-comment/.prettierrc

Lines changed: 0 additions & 4 deletions
This file was deleted.

examples/blog-with-comment/components/comment/form.js renamed to examples/blog-with-comment/components/comment/form.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
import { useAuth0 } from '@auth0/auth0-react'
22

3-
function CommentForm({ text, setText, onSubmit }) {
3+
type CommentFormProps = {
4+
text: string
5+
setText: Function
6+
onSubmit: (e: React.FormEvent) => Promise<void>
7+
}
8+
9+
export default function CommentForm({
10+
text,
11+
setText,
12+
onSubmit,
13+
}: CommentFormProps) {
414
const { isAuthenticated, logout, loginWithPopup } = useAuth0()
515

616
return (
717
<form onSubmit={onSubmit}>
818
<textarea
919
className="flex w-full max-h-40 p-3 rounded resize-y bg-gray-200 text-gray-900 placeholder-gray-500"
10-
rows="2"
20+
rows={2}
1121
placeholder={
1222
isAuthenticated
1323
? `What are your thoughts?`
@@ -41,5 +51,3 @@ function CommentForm({ text, setText, onSubmit }) {
4151
</form>
4252
)
4353
}
44-
45-
export default CommentForm

examples/blog-with-comment/components/comment/index.js renamed to examples/blog-with-comment/components/comment/index.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import CommentForm from './form'
22
import CommentList from './list'
33
import useComments from '../../hooks/useComment'
44

5-
function Comment() {
5+
export default function Comment() {
66
const { text, setText, comments, onSubmit, onDelete } = useComments()
77

88
return (
@@ -12,5 +12,3 @@ function Comment() {
1212
</div>
1313
)
1414
}
15-
16-
export default Comment

examples/blog-with-comment/components/comment/list.js

Lines changed: 0 additions & 52 deletions
This file was deleted.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import type { Comment } from '../../interfaces'
2+
import distanceToNow from '../../lib/dateRelative'
3+
import { useAuth0 } from '@auth0/auth0-react'
4+
5+
type CommentListProps = {
6+
comments?: Comment[]
7+
onDelete: (comment: Comment) => Promise<void>
8+
}
9+
10+
export default function CommentList({ comments, onDelete }: CommentListProps) {
11+
const { user } = useAuth0()
12+
13+
return (
14+
<div className="space-y-6 mt-10">
15+
{comments &&
16+
comments.map((comment) => {
17+
const isAuthor = user && user.sub === comment.user.sub
18+
const isAdmin =
19+
user && user.email === process.env.NEXT_PUBLIC_AUTH0_ADMIN_EMAIL
20+
21+
return (
22+
<div key={comment.created_at} className="flex space-x-4">
23+
<div className="flex-shrink-0">
24+
<img
25+
src={comment.user.picture}
26+
alt={comment.user.name}
27+
width={40}
28+
height={40}
29+
className="rounded-full"
30+
/>
31+
</div>
32+
33+
<div className="flex-grow">
34+
<div className="flex space-x-2">
35+
<b>{comment.user.name}</b>
36+
<time className="text-gray-400">
37+
{distanceToNow(comment.created_at)}
38+
</time>
39+
{(isAdmin || isAuthor) && (
40+
<button
41+
className="text-gray-400 hover:text-red-500"
42+
onClick={() => onDelete(comment)}
43+
aria-label="Close"
44+
>
45+
x
46+
</button>
47+
)}
48+
</div>
49+
50+
<div>{comment.text}</div>
51+
</div>
52+
</div>
53+
)
54+
})}
55+
</div>
56+
)
57+
}

examples/blog-with-comment/components/container.js

Lines changed: 0 additions & 5 deletions
This file was deleted.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
type ContainerProps = {
2+
children: React.ReactNode
3+
}
4+
5+
export default function Container({ children }: ContainerProps) {
6+
return <div className="container max-w-2xl m-auto px-4">{children}</div>
7+
}
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,15 @@
11
import Link from 'next/link'
22
import Container from '../components/container'
33

4-
function Header() {
4+
export default function Header() {
55
return (
66
<header className="py-6">
77
<Container>
88
<nav className="flex space-x-4">
9-
<Link href="/">
10-
<a>About</a>
11-
</Link>
12-
<Link href="/posts">
13-
<a>Posts</a>
14-
</Link>
9+
<Link href="/">About</Link>
10+
<Link href="/posts">Posts</Link>
1511
</nav>
1612
</Container>
1713
</header>
1814
)
1915
}
20-
21-
export default Header
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
declare namespace NodeJS {
2+
export interface ProcessEnv {
3+
readonly NODE_ENV: 'development' | 'production' | 'test'
4+
readonly NEXT_PUBLIC_AUTH0_DOMAIN: string
5+
readonly NEXT_PUBLIC_AUTH0_CLIENT_ID: string
6+
readonly NEXT_PUBLIC_BASE_URL: string
7+
}
8+
}

examples/blog-with-comment/hooks/useComment.js renamed to examples/blog-with-comment/hooks/useComment.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,32 @@
1-
import { useState, useEffect } from 'react'
1+
import type { Comment } from '../interfaces'
2+
import React, { useState, useEffect } from 'react'
23
import useSWR from 'swr'
34
import { useAuth0 } from '@auth0/auth0-react'
45

6+
async function fetcher(url: string) {
7+
const query = new URLSearchParams({ url })
8+
const queryUrl = `${url}?${query.toString()}`
9+
10+
return fetch(queryUrl).then((res) => res.json())
11+
}
12+
513
export default function useComments() {
614
const { getAccessTokenSilently } = useAuth0()
715
const [text, setText] = useState('')
8-
const [url, setUrl] = useState(null)
16+
const [url, setUrl] = useState<string | null>(null)
917

10-
const { data: comments, mutate } = useSWR(
11-
() => {
12-
const query = new URLSearchParams({ url })
13-
return `/api/comment?${query.toString()}`
14-
},
15-
{
16-
initialData: [],
17-
}
18+
const { data: comments, mutate } = useSWR<Comment[]>(
19+
'/api/comment',
20+
fetcher,
21+
{ fallbackData: [] }
1822
)
1923

2024
useEffect(() => {
2125
const url = window.location.origin + window.location.pathname
2226
setUrl(url)
2327
}, [])
2428

25-
const onSubmit = async (e) => {
29+
const onSubmit = async (e: React.FormEvent) => {
2630
e.preventDefault()
2731
const token = await getAccessTokenSilently()
2832

@@ -42,7 +46,7 @@ export default function useComments() {
4246
}
4347
}
4448

45-
const onDelete = async (comment) => {
49+
const onDelete = async (comment: Comment) => {
4650
const token = await getAccessTokenSilently()
4751

4852
try {
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
export type User = {
2+
name: string
3+
picture: string
4+
sub: string
5+
email?: string
6+
}
7+
8+
export type Comment = {
9+
id: string
10+
created_at: number
11+
url: string
12+
text: string
13+
user: User
14+
}
15+
16+
export type Post = {
17+
slug?: string
18+
title?: string
19+
author?: string
20+
date?: Date
21+
content?: string
22+
excerpt?: string
23+
[key: string]: any
24+
}

examples/blog-with-comment/lib/createComment.js renamed to examples/blog-with-comment/lib/createComment.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,34 @@
1+
import type { NextApiRequest, NextApiResponse } from 'next'
2+
import type { Comment } from '../interfaces'
13
import redis from './redis'
24
import { nanoid } from 'nanoid'
35
import getUser from './getUser'
46

5-
export default async function createComments(req, res) {
7+
export default async function createComments(
8+
req: NextApiRequest,
9+
res: NextApiResponse
10+
) {
611
const { url, text } = req.body
712
const { authorization } = req.headers
813

914
if (!url || !text || !authorization) {
1015
return res.status(400).json({ message: 'Missing parameter.' })
1116
}
1217

18+
if (!redis) {
19+
return res
20+
.status(400)
21+
.json({ message: 'Failed to connect to redis client.' })
22+
}
23+
1324
try {
1425
// verify user token
1526
const user = await getUser(authorization)
1627
if (!user) return res.status(400).json({ message: 'Need authorization.' })
1728

1829
const { name, picture, sub, email } = user
1930

20-
const comment = {
31+
const comment: Comment = {
2132
id: nanoid(),
2233
created_at: Date.now(),
2334
url,

examples/blog-with-comment/lib/dateRelative.js renamed to examples/blog-with-comment/lib/dateRelative.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import formatDistanceToNowStrict from 'date-fns/formatDistanceToNowStrict'
22

3-
export default function distanceToNow(dateTime) {
3+
export default function distanceToNow(dateTime: number | Date) {
44
return formatDistanceToNowStrict(dateTime, {
55
addSuffix: true,
66
})

examples/blog-with-comment/lib/deleteComment.js renamed to examples/blog-with-comment/lib/deleteComment.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,26 @@
1+
import type { NextApiRequest, NextApiResponse } from 'next'
2+
import type { User, Comment } from '../interfaces'
13
import redis from './redis'
24
import getUser from './getUser'
35

4-
export default async function deleteComments(req, res) {
5-
const { url, comment } = req.body
6+
export default async function deleteComments(
7+
req: NextApiRequest,
8+
res: NextApiResponse
9+
) {
10+
const { url, comment }: { url: string; comment: Comment } = req.body
611
const { authorization } = req.headers
712

813
if (!url || !comment || !authorization) {
914
return res.status(400).json({ message: 'Missing parameter.' })
1015
}
1116

17+
if (!redis) {
18+
return res.status(500).json({ message: 'Failed to connect to redis.' })
19+
}
20+
1221
try {
1322
// verify user token
14-
const user = await getUser(authorization)
23+
const user: User = await getUser(authorization)
1524
if (!user) return res.status(400).json({ message: 'Invalid token.' })
1625
comment.user.email = user.email
1726

@@ -25,7 +34,7 @@ export default async function deleteComments(req, res) {
2534
// delete
2635
await redis.lrem(url, 0, JSON.stringify(comment))
2736

28-
return res.status(200).json()
37+
return res.status(200).end()
2938
} catch (err) {
3039
return res.status(400)
3140
}

0 commit comments

Comments
 (0)