Skip to content

Commit 6e73cae

Browse files
authoredMar 7, 2024··
refactor(example): reorganize examples, use Tailwind (#77)
1 parent 62dec57 commit 6e73cae

36 files changed

+363
-405
lines changed
 

‎package-lock.json

+9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/example-app/.eslintrc.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
{
2-
"extends": "next/core-web-vitals"
2+
"root": true,
3+
"extends": "next/core-web-vitals"
34
}

‎packages/example-app/package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33
"version": "0.0.0",
44
"private": true,
55
"scripts": {
6-
"dev": "next dev --turbo",
6+
"dev": "next dev",
77
"build": "next build",
88
"start": "next start",
99
"lint": "next lint"
1010
},
1111
"license": "MIT",
1212
"author": "Edoardo Ranghieri",
1313
"dependencies": {
14+
"lucide-react": "^0.343.0",
1415
"next": "14.1.0",
1516
"next-safe-action": "file:../next-safe-action",
1617
"react": "18.2.0",
@@ -23,10 +24,10 @@
2324
"@types/node": "^20.11.19",
2425
"@types/react": "^18.2.57",
2526
"@types/react-dom": "18.2.19",
26-
"postcss": "8.4.35",
2727
"autoprefixer": "10.4.17",
2828
"eslint": "^8.56.0",
2929
"eslint-config-next": "14.1.0",
30+
"postcss": "8.4.35",
3031
"tailwindcss": "3.4.1",
3132
"typescript": "^5.3.3"
3233
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"use client";
2+
3+
import { ResultBox } from "@/app/_components/result-box";
4+
import { StyledButton } from "@/app/_components/styled-button";
5+
import { StyledHeading } from "@/app/_components/styled-heading";
6+
import { StyledInput } from "@/app/_components/styled-input";
7+
import { useFormState } from "react-dom";
8+
import { signupAction } from "./signup-action";
9+
10+
// Temporary implementation.
11+
export default function SignUpPage() {
12+
const [state, action] = useFormState(signupAction, {
13+
message: "Click on the signup button to see the result.",
14+
});
15+
16+
return (
17+
<main className="w-96 max-w-full px-4">
18+
<StyledHeading>Action using client form</StyledHeading>
19+
<form action={action} className="flex flex-col mt-8 space-y-4">
20+
<StyledInput type="text" name="email" placeholder="name@example.com" />
21+
<StyledInput type="password" name="password" placeholder="••••••••" />
22+
<StyledButton type="submit">Signup</StyledButton>
23+
</form>
24+
<ResultBox result={state} />
25+
</main>
26+
);
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"use server";
2+
3+
type PrevState = {
4+
message: string;
5+
};
6+
7+
// Temporary implementation.
8+
export const signupAction = (prevState: PrevState, formData: FormData) => {
9+
return {
10+
message: "Logged in successfully!",
11+
};
12+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"use client";
2+
3+
import { StyledButton } from "@/app/_components/styled-button";
4+
import { StyledHeading } from "@/app/_components/styled-heading";
5+
import { StyledInput } from "@/app/_components/styled-input";
6+
import { useState } from "react";
7+
import { ResultBox } from "../../_components/result-box";
8+
import { loginUser } from "./login-action";
9+
10+
export default function DirectExamplePage() {
11+
const [result, setResult] = useState<any>(undefined);
12+
13+
return (
14+
<main className="w-96 max-w-full px-4">
15+
<StyledHeading>Action using direct call</StyledHeading>
16+
<form
17+
className="flex flex-col mt-8 space-y-4"
18+
onSubmit={async (e) => {
19+
e.preventDefault();
20+
const formData = new FormData(e.currentTarget);
21+
const input = Object.fromEntries(formData) as {
22+
username: string;
23+
password: string;
24+
};
25+
const res = await loginUser(input); // this is the typesafe action directly called
26+
setResult(res);
27+
}}>
28+
<StyledInput
29+
type="text"
30+
name="username"
31+
id="username"
32+
placeholder="Username"
33+
/>
34+
<StyledInput
35+
type="password"
36+
name="password"
37+
id="password"
38+
placeholder="Password"
39+
/>
40+
<StyledButton type="submit">Log in</StyledButton>
41+
</form>
42+
<ResultBox result={result} />
43+
</main>
44+
);
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"use server";
2+
3+
import { ActionError, action } from "@/lib/safe-action";
4+
import { z } from "zod";
5+
6+
const input = z.object({
7+
userId: z.string().min(1).max(10),
8+
});
9+
10+
export const deleteUser = action(input, async ({ userId }) => {
11+
await new Promise((res) => setTimeout(res, 1000));
12+
13+
if (Math.random() > 0.5) {
14+
throw new ActionError("Could not delete user!");
15+
}
16+
17+
return {
18+
deletedUserId: userId,
19+
};
20+
});
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
"use client";
22

3+
import { StyledButton } from "@/app/_components/styled-button";
4+
import { StyledHeading } from "@/app/_components/styled-heading";
5+
import { StyledInput } from "@/app/_components/styled-input";
36
import { useAction } from "next-safe-action/hooks";
4-
import { isExecuting } from "next-safe-action/status";
7+
import { ResultBox } from "../../_components/result-box";
58
import { deleteUser } from "./deleteuser-action";
69

7-
type Props = {
8-
userId: string;
9-
};
10-
11-
const DeleteUserForm = ({ userId }: Props) => {
12-
// Safe action (`deleteUser`) and optional `onSuccess` and `onError` callbacks
13-
// passed to `useAction` hook.
10+
export default function Hook() {
11+
// Safe action (`deleteUser`) and optional callbacks passed to `useAction` hook.
1412
const { execute, result, status, reset } = useAction(deleteUser, {
1513
onSuccess(data, input, reset) {
1614
console.log("HELLO FROM ONSUCCESS", data, input);
@@ -38,8 +36,10 @@ const DeleteUserForm = ({ userId }: Props) => {
3836
console.log("status:", status);
3937

4038
return (
41-
<>
39+
<main className="w-96 max-w-full px-4">
40+
<StyledHeading>Action using hook</StyledHeading>
4241
<form
42+
className="flex flex-col mt-8 space-y-4"
4343
onSubmit={(e) => {
4444
e.preventDefault();
4545
const formData = new FormData(e.currentTarget);
@@ -50,26 +50,18 @@ const DeleteUserForm = ({ userId }: Props) => {
5050
// Action call.
5151
execute(input);
5252
}}>
53-
<input type="text" name="userId" id="userId" placeholder="User ID" />
54-
<button type="submit">Delete user</button>
55-
<button type="button" onClick={reset}>
53+
<StyledInput
54+
type="text"
55+
name="userId"
56+
id="userId"
57+
placeholder="User ID"
58+
/>
59+
<StyledButton type="submit">Delete user</StyledButton>
60+
<StyledButton type="button" onClick={reset}>
5661
Reset
57-
</button>
62+
</StyledButton>
5863
</form>
59-
<div id="result-container">
60-
<pre>Deleted user ID: {userId}</pre>
61-
<pre>Is executing: {JSON.stringify(isExecuting(status))}</pre>
62-
<div>Action result:</div>
63-
<pre className="result">
64-
{
65-
result // if got back a result,
66-
? JSON.stringify(result, null, 1)
67-
: "fill in form and click on the delete user button" // if action never ran
68-
}
69-
</pre>
70-
</div>
71-
</>
64+
<ResultBox result={result} status={status} />
65+
</main>
7266
);
73-
};
74-
75-
export default DeleteUserForm;
67+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { ChevronLeft } from "lucide-react";
2+
import Link from "next/link";
3+
import { type ReactNode } from "react";
4+
5+
export default function ExamplesLayout({ children }: { children: ReactNode }) {
6+
return (
7+
<div>
8+
<Link
9+
href="/"
10+
className="text-center flex items-center justify-center text-blue-600 dark:text-blue-400 hover:underline w-fit mx-auto">
11+
<ChevronLeft className="w-6 h-6" />
12+
<span className="text-lg font-semibold tracking-tight">Go back</span>
13+
</Link>
14+
<div className="mt-4">{children}</div>
15+
</div>
16+
);
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"use client";
2+
3+
import { StyledButton } from "@/app/_components/styled-button";
4+
import { StyledHeading } from "@/app/_components/styled-heading";
5+
import { useAction } from "next-safe-action/hooks";
6+
import { ResultBox } from "../../_components/result-box";
7+
import { buyProduct } from "./shop-action";
8+
9+
export default function NestedSchemaPage() {
10+
const { execute, result, status } = useAction(buyProduct);
11+
12+
return (
13+
<main className="w-96 max-w-full px-4">
14+
<StyledHeading>Action using nested schema</StyledHeading>
15+
<form
16+
className="flex flex-col mt-8 space-y-4"
17+
onSubmit={async (e) => {
18+
e.preventDefault();
19+
20+
// Change one of these two to generate validation errors.
21+
const userId = crypto.randomUUID();
22+
const productId = crypto.randomUUID();
23+
24+
execute({
25+
user: { id: userId },
26+
product: { deeplyNested: { id: productId } },
27+
}); // this is the typesafe action called from client
28+
}}>
29+
<StyledButton type="submit">Buy product</StyledButton>
30+
</form>
31+
<ResultBox result={result} status={status} />
32+
</main>
33+
);
34+
}

‎packages/example-app/src/app/optimistic-hook/addlikes-form.tsx ‎packages/example-app/src/app/(examples)/optimistic-hook/addlikes-form.tsx

+9-18
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
"use client";
22

3+
import { StyledButton } from "@/app/_components/styled-button";
4+
import { StyledInput } from "@/app/_components/styled-input";
35
import { useOptimisticAction } from "next-safe-action/hooks";
6+
import { ResultBox } from "../../_components/result-box";
47
import { addLikes } from "./addlikes-action";
58

69
type Props = {
@@ -46,6 +49,7 @@ const AddLikesForm = ({ likesCount }: Props) => {
4649
return (
4750
<>
4851
<form
52+
className="flex flex-col mt-8 space-y-4"
4953
onSubmit={(e) => {
5054
e.preventDefault();
5155
const formData = new FormData(e.currentTarget);
@@ -59,31 +63,18 @@ const AddLikesForm = ({ likesCount }: Props) => {
5963
// data.
6064
execute({ incrementBy: intIncrementBy });
6165
}}>
62-
<input
66+
<StyledInput
6367
type="text"
6468
name="incrementBy"
6569
id="incrementBy"
6670
placeholder="Increment by"
6771
/>
68-
<button type="submit">Add likes</button>
69-
<button type="button" onClick={reset}>
72+
<StyledButton type="submit">Add likes</StyledButton>
73+
<StyledButton type="button" onClick={reset}>
7074
Reset
71-
</button>
75+
</StyledButton>
7276
</form>
73-
<div id="result-container">
74-
{/* This object will update immediately when you execute the action.
75-
Real data will come back once action has finished executing. */}
76-
<pre>Optimistic data: {JSON.stringify(optimisticData)}</pre>{" "}
77-
<pre>Is executing: {JSON.stringify(status === "executing")}</pre>
78-
<div>Action result:</div>
79-
<pre className="result">
80-
{
81-
result // if got back a result,
82-
? JSON.stringify(result, null, 1)
83-
: "fill in form and click on the add likes button" // if action never ran
84-
}
85-
</pre>
86-
</div>
77+
<ResultBox result={optimisticData} status={status} />
8778
</>
8879
);
8980
};
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,17 @@
1-
import Link from "next/link";
1+
import { StyledHeading } from "@/app/_components/styled-heading";
22
import { getLikes } from "./addlikes-action";
33
import AddLikeForm from "./addlikes-form";
44

5-
export const metadata = {
6-
title: "Action using optimistic hook",
7-
};
8-
95
export default function OptimisticHook() {
106
const likesCount = getLikes();
117
return (
12-
<>
13-
<Link href="/">Go to home</Link>
14-
<h1>Action using optimistic hook</h1>
15-
<pre style={{ marginTop: "1rem" }}>
8+
<main className="w-96 max-w-full px-4">
9+
<StyledHeading>Action using optimistic hook</StyledHeading>
10+
<pre className="mt-4 text-center">
1611
Server state: {JSON.stringify(likesCount)}
1712
</pre>
1813
{/* Pass the server state to Client Component */}
1914
<AddLikeForm likesCount={likesCount} />
20-
</>
15+
</main>
2116
);
2217
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { StyledButton } from "@/app/_components/styled-button";
2+
import { StyledHeading } from "@/app/_components/styled-heading";
3+
import { StyledInput } from "@/app/_components/styled-input";
4+
import { signup } from "./signup-action";
5+
6+
export default function SignUpPage() {
7+
return (
8+
<main className="w-96 max-w-full px-4">
9+
<StyledHeading>Action using server form</StyledHeading>
10+
<form action={signup} className="flex flex-col mt-8 space-y-4">
11+
<StyledInput type="text" name="email" placeholder="name@example.com" />
12+
<StyledInput type="password" name="password" placeholder="••••••••" />
13+
<StyledButton type="submit">Signup</StyledButton>
14+
</form>
15+
</main>
16+
);
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"use client";
2+
3+
import { StyledButton } from "@/app/_components/styled-button";
4+
import { StyledHeading } from "@/app/_components/styled-heading";
5+
import { StyledInput } from "@/app/_components/styled-input";
6+
import { useAction } from "next-safe-action/hooks";
7+
import { ResultBox } from "../../_components/result-box";
8+
import { editUser } from "./edituser-action";
9+
10+
export default function WithAuth() {
11+
const { execute, result, status } = useAction(editUser);
12+
13+
return (
14+
<main className="w-96 max-w-full px-4">
15+
<StyledHeading>Action with auth</StyledHeading>
16+
<form
17+
className="flex flex-col mt-8 space-y-4"
18+
onSubmit={async (e) => {
19+
e.preventDefault();
20+
const formData = new FormData(e.currentTarget);
21+
const input = Object.fromEntries(formData) as {
22+
fullName: string;
23+
age: string;
24+
};
25+
execute(input);
26+
}}>
27+
<StyledInput
28+
type="text"
29+
name="fullName"
30+
id="fullName"
31+
placeholder="Full name"
32+
/>
33+
<StyledInput type="text" name="age" id="age" placeholder="Age" />
34+
<StyledButton type="submit">Update profile</StyledButton>
35+
</form>
36+
<ResultBox result={result} status={status} />
37+
</main>
38+
);
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Link as LinkIcon } from "lucide-react";
2+
import Link from "next/link";
3+
import { ReactNode } from "react";
4+
5+
type Props = {
6+
href: string;
7+
children: ReactNode;
8+
};
9+
10+
export function ExampleLink({ href, children }: Props) {
11+
return (
12+
<Link href={href} className="text-lg">
13+
<span className="flex items-center justify-center space-x-2 hover:underline">
14+
<LinkIcon className="w-4 h-4" />
15+
<span>{children}</span>
16+
</span>
17+
</Link>
18+
);
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { HookActionStatus } from "next-safe-action/hooks";
2+
3+
type Props = {
4+
result: any;
5+
status?: HookActionStatus;
6+
};
7+
8+
export function ResultBox({ result, status }: Props) {
9+
return (
10+
<div className="mt-8">
11+
{status ? (
12+
<p className="text-lg font-semibold">Execution status: {status}</p>
13+
) : null}
14+
<p className="text-lg font-semibold">Action result:</p>
15+
<pre className="mt-4">{JSON.stringify(result, null, 1)}</pre>
16+
</div>
17+
);
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { ComponentProps } from "react";
2+
3+
type Props = ComponentProps<"button">;
4+
5+
export function StyledButton(props: Props) {
6+
return (
7+
<button
8+
{...props}
9+
className={`${props.className ?? ""} bg-slate-950 text-slate-50 px-3 py-2 rounded-md w-full font-medium dark:bg-slate-50 dark:text-slate-950`}
10+
/>
11+
);
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { ReactNode } from "react";
2+
3+
type Props = {
4+
children: ReactNode;
5+
};
6+
7+
export function StyledHeading({ children }: Props) {
8+
return <h1 className="text-2xl font-semibold text-center">{children}</h1>;
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { ComponentProps } from "react";
2+
3+
type Props = ComponentProps<"input">;
4+
5+
export function StyledInput(props: Props) {
6+
return (
7+
<>
8+
<input
9+
{...props}
10+
className={`${props.className ?? ""} py-1 px-2 border rounded-md dark:bg-slate-800 dark:border-slate-700`}
11+
/>
12+
</>
13+
);
14+
}

‎packages/example-app/src/app/form/page.tsx

-16
This file was deleted.
+3-86
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,3 @@
1-
*,
2-
*::after,
3-
*::before {
4-
box-sizing: border-box;
5-
margin: 0;
6-
padding: 0;
7-
font-size: 20px;
8-
}
9-
10-
body {
11-
width: 100%;
12-
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
13-
font-size: 20px;
14-
padding: 1rem 1rem;
15-
min-height: 100vh;
16-
display: flex;
17-
flex-direction: column;
18-
align-items: center;
19-
justify-content: center;
20-
}
21-
22-
a {
23-
display: block;
24-
color: #008cff;
25-
text-align: center;
26-
text-decoration: none;
27-
font-weight: 500;
28-
margin-bottom: 1rem;
29-
}
30-
31-
#github-link {
32-
color: #000;
33-
}
34-
35-
a:hover {
36-
text-decoration: underline;
37-
}
38-
39-
form {
40-
width: 100%;
41-
max-width: 20rem;
42-
margin: 1rem 0;
43-
display: flex;
44-
flex-direction: column;
45-
gap: 0.7rem;
46-
}
47-
48-
form input {
49-
border-radius: 0.3rem;
50-
border: 1px solid #cccccc;
51-
padding: 0.3rem 0.5rem;
52-
}
53-
54-
form button {
55-
cursor: pointer;
56-
padding: 0.5rem 2rem;
57-
color: #fff;
58-
background-color: #008cff;
59-
border: none;
60-
border-radius: 0.3rem;
61-
}
62-
63-
form button:hover {
64-
background-color: #0062d1;
65-
}
66-
67-
#result-container {
68-
width: 100%;
69-
max-width: 20rem;
70-
}
71-
72-
@media (prefers-color-scheme: dark) {
73-
body {
74-
color: #fff;
75-
background-color: #000;
76-
}
77-
78-
#github-link {
79-
color: #fff;
80-
}
81-
form input {
82-
color: white;
83-
border: 1px solid #333333;
84-
background-color: #1f1f1f;
85-
}
86-
}
1+
@tailwind base;
2+
@tailwind components;
3+
@tailwind utilities;

‎packages/example-app/src/app/hook/deleteuser-action.ts

-36
This file was deleted.

‎packages/example-app/src/app/hook/page.tsx

-20
This file was deleted.

‎packages/example-app/src/app/layout.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@ export default function RootLayout({
1313
}) {
1414
return (
1515
<html lang="en">
16-
<body>
16+
<body className="antialiased bg-slate-50 dark:bg-slate-950 dark:text-slate-50 text-slate-950 flex flex-col min-h-screen items-center pt-24">
1717
<a
1818
id="github-link"
1919
href="https://github.com/TheEdoRan/next-safe-action"
2020
target="_blank"
21-
rel="noopener noreferrer">
21+
rel="noopener noreferrer"
22+
className="mb-8">
2223
<GitHubLogo width={40} height={40} />
2324
</a>
2425
{children}

‎packages/example-app/src/app/login-form.tsx

-46
This file was deleted.

‎packages/example-app/src/app/nested-schema/page.tsx

-13
This file was deleted.

‎packages/example-app/src/app/nested-schema/shop-form.tsx

-38
This file was deleted.

‎packages/example-app/src/app/page.tsx

+17-15
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
1-
import Link from "next/link";
2-
import LoginForm from "./login-form";
3-
4-
export const metadata = {
5-
title: "Action without auth",
6-
};
1+
import { ExampleLink } from "./_components/example-link";
72

83
export default function Home() {
94
return (
10-
<>
11-
<Link href="/with-context">Go to /with-context</Link>
12-
<Link href="/hook">Go to /hook</Link>
13-
<Link href="/optimistic-hook">Go to /optimistic-hook</Link>
14-
<Link href="/form">Go to /form</Link>
15-
<Link href="/nested-schema">Go to /nested-schema</Link>
16-
<h1>Action without auth</h1>
17-
<LoginForm />
18-
</>
5+
<main className="text-center">
6+
<h1 className="text-4xl font-semibold">Examples</h1>
7+
<div className="mt-4 flex flex-col space-y-2">
8+
<ExampleLink href="/direct">Direct call</ExampleLink>
9+
<ExampleLink href="/with-context">With Context</ExampleLink>
10+
<ExampleLink href="/nested-schema">Nested schema</ExampleLink>
11+
<ExampleLink href="/hook">
12+
<span className="font-mono">useAction</span> hook
13+
</ExampleLink>
14+
<ExampleLink href="/optimistic-hook">
15+
<span className="font-mono">useOptimisticAction</span> hook
16+
</ExampleLink>
17+
<ExampleLink href="/server-form">Server Form</ExampleLink>
18+
<ExampleLink href="/client-form">Client Form</ExampleLink>
19+
</div>
20+
</main>
1921
);
2022
}

‎packages/example-app/src/app/with-context/edituser-form.tsx

-41
This file was deleted.

‎packages/example-app/src/app/with-context/page.tsx

-16
This file was deleted.
+8-16
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,10 @@
11
/** @type {import('tailwindcss').Config} */
22
module.exports = {
3-
content: [
4-
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
5-
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
6-
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
7-
],
8-
theme: {
9-
extend: {
10-
backgroundImage: {
11-
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
12-
'gradient-conic':
13-
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
14-
},
15-
},
16-
},
17-
plugins: [],
18-
}
3+
// darkMode: "class",
4+
content: [
5+
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
6+
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
7+
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
8+
],
9+
plugins: [],
10+
};

0 commit comments

Comments
 (0)
Please sign in to comment.