Skip to content

Commit 9d4648d

Browse files
committed
Initial commit
1 parent 3c33454 commit 9d4648d

22 files changed

+748
-171
lines changed

.prettierrc.json

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"semi": false,
3+
"singleQuote": true,
4+
"plugins": ["prettier-plugin-tailwindcss"]
5+
}

README.md

+23-22
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,28 @@
1-
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
1+
# Next.js Server Actions Form
22

3-
## Getting Started
3+
This example is based on [Forms with Next.js and Server Actions](https://github.com/vercel/next.js/tree/canary/examples/next-forms) published by [Next.js](https://nextjs.org/), with the following additions:
44

5-
First, run the development server:
5+
- Using [shadcn/ui](https://ui.shadcn.com/)
6+
- Some components
7+
- Dark mode
8+
- Using [Tailwind CSS](https://tailwindcss.com/)
9+
- Implementing form reset
10+
11+
## Run locally
12+
13+
First, create `.env.local` in the root and set your Vercel Postgres secrets to it:
14+
15+
```
16+
POSTGRES_URL="************"
17+
POSTGRES_PRISMA_URL="************"
18+
POSTGRES_URL_NON_POOLING="************"
19+
POSTGRES_USER="************"
20+
POSTGRES_HOST="************"
21+
POSTGRES_PASSWORD="************"
22+
POSTGRES_DATABASE="************"
23+
```
24+
25+
Then, run the development server:
626

727
```bash
828
npm run dev
@@ -15,22 +35,3 @@ bun dev
1535
```
1636

1737
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18-
19-
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20-
21-
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
22-
23-
## Learn More
24-
25-
To learn more about Next.js, take a look at the following resources:
26-
27-
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28-
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29-
30-
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
31-
32-
## Deploy on Vercel
33-
34-
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35-
36-
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

app/actions.ts

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
'use server'
2+
3+
import { revalidatePath } from 'next/cache'
4+
import { sql } from '@vercel/postgres'
5+
import { z } from 'zod'
6+
7+
export async function createTodo(prevState: any, formData: FormData) {
8+
const schema = z.object({
9+
todo: z.string().nonempty(),
10+
})
11+
const data = schema.parse({
12+
todo: formData.get('todo'),
13+
})
14+
15+
try {
16+
await sql`
17+
INSERT INTO todos (text)
18+
VALUES (${data.todo})
19+
`
20+
21+
revalidatePath('/')
22+
return { message: `Added todo ${data.todo}` }
23+
} catch (e) {
24+
return { message: 'Failed to create todo' }
25+
}
26+
}
27+
28+
export async function deleteTodo(prevState: any, formData: FormData) {
29+
const schema = z.object({
30+
id: z.string().nonempty(),
31+
todo: z.string().nonempty(),
32+
})
33+
const data = schema.parse({
34+
id: formData.get('id'),
35+
todo: formData.get('todo'),
36+
})
37+
38+
try {
39+
await sql`
40+
DELETE FROM todos
41+
WHERE id = ${data.id};
42+
`
43+
44+
revalidatePath('/')
45+
return { message: `Deleted todo ${data.todo}` }
46+
} catch (e) {
47+
return { message: 'Failed to delete todo' }
48+
}
49+
}

app/add-form.tsx

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
'use client'
2+
3+
import { useRef } from 'react'
4+
import { experimental_useFormState as useFormState } from 'react-dom'
5+
import { experimental_useFormStatus as useFormStatus } from 'react-dom'
6+
import { createTodo } from '@/app/actions'
7+
8+
import { Button } from '@/components/ui/button'
9+
import { Input } from '@/components/ui/input'
10+
import { Label } from '@/components/ui/label'
11+
12+
const initialState = {
13+
message: null,
14+
}
15+
16+
function SubmitButton() {
17+
const { pending } = useFormStatus()
18+
19+
return (
20+
<Button
21+
type="submit"
22+
aria-disabled={pending}
23+
className={`my-2 w-full ${
24+
pending ? 'cursor-not-allowed opacity-50' : ''
25+
}`}
26+
>
27+
Add
28+
</Button>
29+
)
30+
}
31+
32+
export function AddForm() {
33+
const [state, formAction] = useFormState(createTodo, initialState)
34+
const ref = useRef<HTMLFormElement>(null)
35+
36+
return (
37+
<form
38+
ref={ref}
39+
action={async (formData) => {
40+
await formAction(formData)
41+
ref.current?.reset()
42+
}}
43+
>
44+
<Label htmlFor="todo">Enter Task</Label>
45+
<Input type="text" id="todo" name="todo" autoComplete="off" required />
46+
<SubmitButton />
47+
<p aria-live="polite" className="sr-only" role="status">
48+
{state?.message}
49+
</p>
50+
</form>
51+
)
52+
}

app/delete-form.tsx

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
'use client'
2+
3+
import { experimental_useFormState as useFormState } from 'react-dom'
4+
import { experimental_useFormStatus as useFormStatus } from 'react-dom'
5+
import { deleteTodo } from '@/app/actions'
6+
7+
import { Button } from '@/components/ui/button'
8+
import { Input } from '@/components/ui/input'
9+
10+
const initialState = {
11+
message: null,
12+
}
13+
14+
function DeleteButton() {
15+
const { pending } = useFormStatus()
16+
17+
return (
18+
<Button
19+
type="submit"
20+
aria-disabled={pending}
21+
className={pending ? 'cursor-not-allowed opacity-50' : ''}
22+
>
23+
Delete
24+
</Button>
25+
)
26+
}
27+
28+
export function DeleteForm({ id, todo }: { id: number; todo: string }) {
29+
const [state, formAction] = useFormState(deleteTodo, initialState)
30+
31+
return (
32+
<form action={formAction}>
33+
<Input type="hidden" name="id" value={id} />
34+
<Input type="hidden" name="todo" value={todo} />
35+
<DeleteButton />
36+
<p aria-live="polite" className="sr-only" role="status">
37+
{state?.message}
38+
</p>
39+
</form>
40+
)
41+
}

app/globals.css

+69-20
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,76 @@
11
@tailwind base;
22
@tailwind components;
33
@tailwind utilities;
4+
5+
@layer base {
6+
:root {
7+
--background: 0 0% 100%;
8+
--foreground: 222.2 84% 4.9%;
49

5-
:root {
6-
--foreground-rgb: 0, 0, 0;
7-
--background-start-rgb: 214, 219, 220;
8-
--background-end-rgb: 255, 255, 255;
9-
}
10+
--card: 0 0% 100%;
11+
--card-foreground: 222.2 84% 4.9%;
12+
13+
--popover: 0 0% 100%;
14+
--popover-foreground: 222.2 84% 4.9%;
15+
16+
--primary: 222.2 47.4% 11.2%;
17+
--primary-foreground: 210 40% 98%;
18+
19+
--secondary: 210 40% 96.1%;
20+
--secondary-foreground: 222.2 47.4% 11.2%;
21+
22+
--muted: 210 40% 96.1%;
23+
--muted-foreground: 215.4 16.3% 46.9%;
24+
25+
--accent: 210 40% 96.1%;
26+
--accent-foreground: 222.2 47.4% 11.2%;
27+
28+
--destructive: 0 84.2% 60.2%;
29+
--destructive-foreground: 210 40% 98%;
1030

11-
@media (prefers-color-scheme: dark) {
12-
:root {
13-
--foreground-rgb: 255, 255, 255;
14-
--background-start-rgb: 0, 0, 0;
15-
--background-end-rgb: 0, 0, 0;
31+
--border: 214.3 31.8% 91.4%;
32+
--input: 214.3 31.8% 91.4%;
33+
--ring: 222.2 84% 4.9%;
34+
35+
--radius: 0.5rem;
36+
}
37+
38+
.dark {
39+
--background: 222.2 84% 4.9%;
40+
--foreground: 210 40% 98%;
41+
42+
--card: 222.2 84% 4.9%;
43+
--card-foreground: 210 40% 98%;
44+
45+
--popover: 222.2 84% 4.9%;
46+
--popover-foreground: 210 40% 98%;
47+
48+
--primary: 210 40% 98%;
49+
--primary-foreground: 222.2 47.4% 11.2%;
50+
51+
--secondary: 217.2 32.6% 17.5%;
52+
--secondary-foreground: 210 40% 98%;
53+
54+
--muted: 217.2 32.6% 17.5%;
55+
--muted-foreground: 215 20.2% 65.1%;
56+
57+
--accent: 217.2 32.6% 17.5%;
58+
--accent-foreground: 210 40% 98%;
59+
60+
--destructive: 0 62.8% 30.6%;
61+
--destructive-foreground: 210 40% 98%;
62+
63+
--border: 217.2 32.6% 17.5%;
64+
--input: 217.2 32.6% 17.5%;
65+
--ring: 212.7 26.8% 83.9%;
1666
}
1767
}
18-
19-
body {
20-
color: rgb(var(--foreground-rgb));
21-
background: linear-gradient(
22-
to bottom,
23-
transparent,
24-
rgb(var(--background-end-rgb))
25-
)
26-
rgb(var(--background-start-rgb));
27-
}
68+
69+
@layer base {
70+
* {
71+
@apply border-border;
72+
}
73+
body {
74+
@apply bg-background text-foreground;
75+
}
76+
}

app/layout.tsx

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import './globals.css'
22
import type { Metadata } from 'next'
33
import { Inter } from 'next/font/google'
4+
import { ThemeProvider } from '@/components/theme-provider'
45

56
const inter = Inter({ subsets: ['latin'] })
67

@@ -16,7 +17,16 @@ export default function RootLayout({
1617
}) {
1718
return (
1819
<html lang="en">
19-
<body className={inter.className}>{children}</body>
20+
<body className={inter.className}>
21+
<ThemeProvider
22+
attribute="class"
23+
defaultTheme="system"
24+
enableSystem
25+
disableTransitionOnChange
26+
>
27+
{children}
28+
</ThemeProvider>
29+
</body>
2030
</html>
2131
)
2232
}

0 commit comments

Comments
 (0)