2023-06-14LT資料@TSUQREA
- https://github.com/yicru/tsuqrea-lt_20230614
- https://tsuqrea-lt-20230614-sample.vercel.app
- https://tsuqrea-lt-20230614-slidev.vercel.app
npx create-next-app@latest web --ts --eslint --app --src-dir --tailwind --import-alias "@/*"
npm install -D prettier
echo '{"singleQuote": true, "semi": false}' > .prettierrc
echo '.next' >> .prettierignore
echo $(jq '.scripts.format="prettier --write ."' package.json) > package.json
npm install graphql-yoga graphql
https://the-guild.dev/graphql/yoga-server/docs/integrations/integration-with-nextjs#example
mkdir -p src/app/api/graphql && touch src/app/api/graphql/route.ts
// src/app/api/graphql/route.ts
import { createYoga, createSchema } from 'graphql-yoga'
const { handleRequest } = createYoga({
schema: createSchema({
typeDefs: /* GraphQL */ `
type Query {
greetings: String
}
`,
resolvers: {
Query: {
greetings: () =>
'This is the `greetings` field of the root `Query` type'
}
}
}),
// While using Next.js file convention for routing, we need to configure Yoga to use the correct endpoint
graphqlEndpoint: '/api/graphql',
// Yoga needs to know how to create a valid Next response
fetchAPI: { Response }
})
export { handleRequest as GET, handleRequest as POST }
// src/app/api/graphql/route.ts
import { createYoga, createSchema } from 'graphql-yoga'
const todos = [
{ id: 1, text: 'Buy milk', completed: false },
{ id: 2, text: 'Buy eggs', completed: false },
{ id: 3, text: 'Buy bread', completed: false },
]
const { handleRequest } = createYoga({
schema: createSchema({
typeDefs: /* GraphQL */ `
type Todo {
id: ID!
text: String!
completed: Boolean!
}
type Query {
greetings: String
todos: [Todo!]!
}
type Mutation {
addTodo(text: String!): Todo!
toggleTodo(id: ID!): Todo!
}
`,
resolvers: {
Query: {
greetings: () =>
'This is the `greetings` field of the root `Query` type',
todos: () => todos,
},
Mutation: {
addTodo: (_, { text }) => {
const todo = { id: todos.length + 1, text, completed: false }
todos.push(todo)
return todo
},
toggleTodo: (_, { id }) => {
const todo = todos.find((todo) => todo.id == id)
if (!todo) throw new Error(`Todo with id ${id} not found`)
todo.completed = !todo.completed
return todo
},
},
},
}),
// While using Next.js file convention for routing, we need to configure Yoga to use the correct endpoint
graphqlEndpoint: '/api/graphql',
// Yoga needs to know how to create a valid Next response
fetchAPI: { Response },
})
export { handleRequest as GET, handleRequest as POST }
npm i -D ts-node @graphql-codegen/cli @graphql-codegen/client-preset npm-run-all
touch codegen.ts
// codegen.ts
import { CodegenConfig } from '@graphql-codegen/cli'
const config: CodegenConfig = {
schema: 'http://localhost:3000/api/graphql',
documents: ['src/**/*.tsx'],
ignoreNoDocuments: true, // for better experience with the watcher
generates: {
'./src/lib/gql/': {
preset: 'client',
},
},
}
export default config
{
"dev": "run-p dev:*",
"dev:next": "next dev",
"dev:codegen": "graphql-codegen --watch"
}
npm install urql
mkdir -p src/providers && touch src/providers/index.tsx
// src/providers/index.tsx
'use client'
import { ReactNode } from 'react'
import { Client, cacheExchange, fetchExchange, Provider } from 'urql'
type Props = {
children: ReactNode
}
const client = new Client({
url: '/api/graphql',
exchanges: [cacheExchange, fetchExchange],
})
export const Providers = ({ children }: Props) => {
return <Provider value={client}>{children}</Provider>
}
// src/app/layout.tsx
import './globals.css'
import { Inter } from 'next/font/google'
import { Providers } from '@/providers'
const inter = Inter({ subsets: ['latin'] })
export const metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>
<Providers>{children}</Providers>
</body>
</html>
)
}
npx shadcn-ui init # tailwind.config.jsのcontentを修正
npx shadcn-ui add button input checkbox label
mkdir -p src/features/todo/components
touch src/features/todo/components/AddTodoForm.tsx src/features/todo/components/TodoListItem.tsx
https://ui.shadcn.com/docs/forms/react-hook-form
// src/app/layout.tsx
import './globals.css'
import { Inter } from 'next/font/google'
import { Providers } from '@/providers'
import { cn } from '@/lib/utils'
const inter = Inter({ subsets: ['latin'] })
export const metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" className={'h-full'}>
<body className={cn(inter.className, 'h-full')}>
<Providers>
<div className={'h-full w-full max-w-lg mx-auto p-8'}>{children}</div>
</Providers>
</body>
</html>
)
}
// src/app/page.tsx
'use client'
import { graphql } from '@/lib/gql'
import { useQuery } from 'urql'
import { TodoListItem } from '@/features/todo/components/TodoListItem'
import { AddTodoForm } from '@/features/todo/components/AddTodoForm'
const HomePageQuery = graphql(/* GraphQL */ `
query HomePageQuery {
todos {
id
...TodoListItem_todo
}
}
`)
export default function Home() {
const [{ data }] = useQuery({ query: HomePageQuery })
return (
<main className={'flex flex-col h-full'}>
<div className={'flex-1'}>
<div className={'divide-y'}>
{data?.todos.map((todo) => (
<TodoListItem todo={todo} key={todo.id} />
))}
</div>
</div>
<AddTodoForm />
</main>
)
}
// src/features/components/TodoListItem.tsx
import { Checkbox } from '@/components/ui/checkbox'
import { Label } from '@/components/ui/label'
import { cn } from '@/lib/utils'
import { FragmentType, graphql, useFragment } from '@/lib/gql'
import { useMutation } from 'urql'
const TodoFragment = graphql(/* GraphQL */ `
fragment TodoListItem_todo on Todo {
id
text
completed
}
`)
const ToggleTodoMutation = graphql(/* GraphQL */ `
mutation ToggleTodoMutation($id: ID!) {
toggleTodo(id: $id) {
id
completed
}
}
`)
type Props = {
todo: FragmentType<typeof TodoFragment>
}
export const TodoListItem = (props: Props) => {
const todo = useFragment(TodoFragment, props.todo)
const [, toggleTodo] = useMutation(ToggleTodoMutation)
const handleOnChange = async () => {
await toggleTodo({
id: todo.id,
})
}
return (
<div className="flex items-center space-x-2 py-4">
<Checkbox
checked={todo.completed}
id={`todo-${todo.id}`}
onCheckedChange={handleOnChange}
/>
<Label
className={cn(todo.completed && 'line-through')}
htmlFor={`todo-${todo.id}`}
>
{todo.text}
</Label>
</div>
)
}
// src/features/components/AddTodoForm.tsx
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { zodResolver } from '@hookform/resolvers/zod'
import * as z from 'zod'
import { useForm } from 'react-hook-form'
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from '@/components/ui/form'
import { graphql } from '@/lib/gql'
import { useMutation } from 'urql'
const AddTodoMutation = graphql(/* GraphQL */ `
mutation addTodoMutation($text: String!) {
addTodo(text: $text) {
id
}
}
`)
const formSchema = z.object({
text: z.string().min(1),
})
export const AddTodoForm = () => {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
text: '',
},
})
const [{ fetching }, addTodo] = useMutation(AddTodoMutation)
const onSubmit = async (values: z.infer<typeof formSchema>) => {
await addTodo(values)
form.reset()
}
return (
<Form {...form}>
<form className={'flex space-x-4'} onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name="text"
render={({ field }) => (
<FormItem className={'flex-1'}>
<FormControl>
<Input disabled={fetching} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button disabled={fetching} type="submit">
Add
</Button>
</form>
</Form>
)
}
vercel