Skip to content

Commit

Permalink
refactor(example): reorganize examples, use Tailwind (#77)
Browse files Browse the repository at this point in the history
  • Loading branch information
TheEdoRan authored Mar 7, 2024
1 parent 62dec57 commit 6e73cae
Show file tree
Hide file tree
Showing 36 changed files with 363 additions and 405 deletions.
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion packages/example-app/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"extends": "next/core-web-vitals"
"root": true,
"extends": "next/core-web-vitals"
}
5 changes: 3 additions & 2 deletions packages/example-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "next dev --turbo",
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"license": "MIT",
"author": "Edoardo Ranghieri",
"dependencies": {
"lucide-react": "^0.343.0",
"next": "14.1.0",
"next-safe-action": "file:../next-safe-action",
"react": "18.2.0",
Expand All @@ -23,10 +24,10 @@
"@types/node": "^20.11.19",
"@types/react": "^18.2.57",
"@types/react-dom": "18.2.19",
"postcss": "8.4.35",
"autoprefixer": "10.4.17",
"eslint": "^8.56.0",
"eslint-config-next": "14.1.0",
"postcss": "8.4.35",
"tailwindcss": "3.4.1",
"typescript": "^5.3.3"
}
Expand Down
27 changes: 27 additions & 0 deletions packages/example-app/src/app/(examples)/client-form/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"use client";

import { ResultBox } from "@/app/_components/result-box";
import { StyledButton } from "@/app/_components/styled-button";
import { StyledHeading } from "@/app/_components/styled-heading";
import { StyledInput } from "@/app/_components/styled-input";
import { useFormState } from "react-dom";
import { signupAction } from "./signup-action";

// Temporary implementation.
export default function SignUpPage() {
const [state, action] = useFormState(signupAction, {
message: "Click on the signup button to see the result.",
});

return (
<main className="w-96 max-w-full px-4">
<StyledHeading>Action using client form</StyledHeading>
<form action={action} className="flex flex-col mt-8 space-y-4">
<StyledInput type="text" name="email" placeholder="[email protected]" />
<StyledInput type="password" name="password" placeholder="••••••••" />
<StyledButton type="submit">Signup</StyledButton>
</form>
<ResultBox result={state} />
</main>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"use server";

type PrevState = {
message: string;
};

// Temporary implementation.
export const signupAction = (prevState: PrevState, formData: FormData) => {
return {
message: "Logged in successfully!",
};
};
45 changes: 45 additions & 0 deletions packages/example-app/src/app/(examples)/direct/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"use client";

import { StyledButton } from "@/app/_components/styled-button";
import { StyledHeading } from "@/app/_components/styled-heading";
import { StyledInput } from "@/app/_components/styled-input";
import { useState } from "react";
import { ResultBox } from "../../_components/result-box";
import { loginUser } from "./login-action";

export default function DirectExamplePage() {
const [result, setResult] = useState<any>(undefined);

return (
<main className="w-96 max-w-full px-4">
<StyledHeading>Action using direct call</StyledHeading>
<form
className="flex flex-col mt-8 space-y-4"
onSubmit={async (e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const input = Object.fromEntries(formData) as {
username: string;
password: string;
};
const res = await loginUser(input); // this is the typesafe action directly called
setResult(res);
}}>
<StyledInput
type="text"
name="username"
id="username"
placeholder="Username"
/>
<StyledInput
type="password"
name="password"
id="password"
placeholder="Password"
/>
<StyledButton type="submit">Log in</StyledButton>
</form>
<ResultBox result={result} />
</main>
);
}
20 changes: 20 additions & 0 deletions packages/example-app/src/app/(examples)/hook/deleteuser-action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"use server";

import { ActionError, action } from "@/lib/safe-action";
import { z } from "zod";

const input = z.object({
userId: z.string().min(1).max(10),
});

export const deleteUser = action(input, async ({ userId }) => {
await new Promise((res) => setTimeout(res, 1000));

if (Math.random() > 0.5) {
throw new ActionError("Could not delete user!");
}

return {
deletedUserId: userId,
};
});
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
"use client";

import { StyledButton } from "@/app/_components/styled-button";
import { StyledHeading } from "@/app/_components/styled-heading";
import { StyledInput } from "@/app/_components/styled-input";
import { useAction } from "next-safe-action/hooks";
import { isExecuting } from "next-safe-action/status";
import { ResultBox } from "../../_components/result-box";
import { deleteUser } from "./deleteuser-action";

type Props = {
userId: string;
};

const DeleteUserForm = ({ userId }: Props) => {
// Safe action (`deleteUser`) and optional `onSuccess` and `onError` callbacks
// passed to `useAction` hook.
export default function Hook() {
// Safe action (`deleteUser`) and optional callbacks passed to `useAction` hook.
const { execute, result, status, reset } = useAction(deleteUser, {
onSuccess(data, input, reset) {
console.log("HELLO FROM ONSUCCESS", data, input);
Expand Down Expand Up @@ -38,8 +36,10 @@ const DeleteUserForm = ({ userId }: Props) => {
console.log("status:", status);

return (
<>
<main className="w-96 max-w-full px-4">
<StyledHeading>Action using hook</StyledHeading>
<form
className="flex flex-col mt-8 space-y-4"
onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
Expand All @@ -50,26 +50,18 @@ const DeleteUserForm = ({ userId }: Props) => {
// Action call.
execute(input);
}}>
<input type="text" name="userId" id="userId" placeholder="User ID" />
<button type="submit">Delete user</button>
<button type="button" onClick={reset}>
<StyledInput
type="text"
name="userId"
id="userId"
placeholder="User ID"
/>
<StyledButton type="submit">Delete user</StyledButton>
<StyledButton type="button" onClick={reset}>
Reset
</button>
</StyledButton>
</form>
<div id="result-container">
<pre>Deleted user ID: {userId}</pre>
<pre>Is executing: {JSON.stringify(isExecuting(status))}</pre>
<div>Action result:</div>
<pre className="result">
{
result // if got back a result,
? JSON.stringify(result, null, 1)
: "fill in form and click on the delete user button" // if action never ran
}
</pre>
</div>
</>
<ResultBox result={result} status={status} />
</main>
);
};

export default DeleteUserForm;
}
17 changes: 17 additions & 0 deletions packages/example-app/src/app/(examples)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ChevronLeft } from "lucide-react";
import Link from "next/link";
import { type ReactNode } from "react";

export default function ExamplesLayout({ children }: { children: ReactNode }) {
return (
<div>
<Link
href="/"
className="text-center flex items-center justify-center text-blue-600 dark:text-blue-400 hover:underline w-fit mx-auto">
<ChevronLeft className="w-6 h-6" />
<span className="text-lg font-semibold tracking-tight">Go back</span>
</Link>
<div className="mt-4">{children}</div>
</div>
);
}
34 changes: 34 additions & 0 deletions packages/example-app/src/app/(examples)/nested-schema/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"use client";

import { StyledButton } from "@/app/_components/styled-button";
import { StyledHeading } from "@/app/_components/styled-heading";
import { useAction } from "next-safe-action/hooks";
import { ResultBox } from "../../_components/result-box";
import { buyProduct } from "./shop-action";

export default function NestedSchemaPage() {
const { execute, result, status } = useAction(buyProduct);

return (
<main className="w-96 max-w-full px-4">
<StyledHeading>Action using nested schema</StyledHeading>
<form
className="flex flex-col mt-8 space-y-4"
onSubmit={async (e) => {
e.preventDefault();

// Change one of these two to generate validation errors.
const userId = crypto.randomUUID();
const productId = crypto.randomUUID();

execute({
user: { id: userId },
product: { deeplyNested: { id: productId } },
}); // this is the typesafe action called from client
}}>
<StyledButton type="submit">Buy product</StyledButton>
</form>
<ResultBox result={result} status={status} />
</main>
);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
"use client";

import { StyledButton } from "@/app/_components/styled-button";
import { StyledInput } from "@/app/_components/styled-input";
import { useOptimisticAction } from "next-safe-action/hooks";
import { ResultBox } from "../../_components/result-box";
import { addLikes } from "./addlikes-action";

type Props = {
Expand Down Expand Up @@ -46,6 +49,7 @@ const AddLikesForm = ({ likesCount }: Props) => {
return (
<>
<form
className="flex flex-col mt-8 space-y-4"
onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
Expand All @@ -59,31 +63,18 @@ const AddLikesForm = ({ likesCount }: Props) => {
// data.
execute({ incrementBy: intIncrementBy });
}}>
<input
<StyledInput
type="text"
name="incrementBy"
id="incrementBy"
placeholder="Increment by"
/>
<button type="submit">Add likes</button>
<button type="button" onClick={reset}>
<StyledButton type="submit">Add likes</StyledButton>
<StyledButton type="button" onClick={reset}>
Reset
</button>
</StyledButton>
</form>
<div id="result-container">
{/* This object will update immediately when you execute the action.
Real data will come back once action has finished executing. */}
<pre>Optimistic data: {JSON.stringify(optimisticData)}</pre>{" "}
<pre>Is executing: {JSON.stringify(status === "executing")}</pre>
<div>Action result:</div>
<pre className="result">
{
result // if got back a result,
? JSON.stringify(result, null, 1)
: "fill in form and click on the add likes button" // if action never ran
}
</pre>
</div>
<ResultBox result={optimisticData} status={status} />
</>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
import Link from "next/link";
import { StyledHeading } from "@/app/_components/styled-heading";
import { getLikes } from "./addlikes-action";
import AddLikeForm from "./addlikes-form";

export const metadata = {
title: "Action using optimistic hook",
};

export default function OptimisticHook() {
const likesCount = getLikes();
return (
<>
<Link href="/">Go to home</Link>
<h1>Action using optimistic hook</h1>
<pre style={{ marginTop: "1rem" }}>
<main className="w-96 max-w-full px-4">
<StyledHeading>Action using optimistic hook</StyledHeading>
<pre className="mt-4 text-center">
Server state: {JSON.stringify(likesCount)}
</pre>
{/* Pass the server state to Client Component */}
<AddLikeForm likesCount={likesCount} />
</>
</main>
);
}
17 changes: 17 additions & 0 deletions packages/example-app/src/app/(examples)/server-form/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { StyledButton } from "@/app/_components/styled-button";
import { StyledHeading } from "@/app/_components/styled-heading";
import { StyledInput } from "@/app/_components/styled-input";
import { signup } from "./signup-action";

export default function SignUpPage() {
return (
<main className="w-96 max-w-full px-4">
<StyledHeading>Action using server form</StyledHeading>
<form action={signup} className="flex flex-col mt-8 space-y-4">
<StyledInput type="text" name="email" placeholder="[email protected]" />
<StyledInput type="password" name="password" placeholder="••••••••" />
<StyledButton type="submit">Signup</StyledButton>
</form>
</main>
);
}
Loading

0 comments on commit 6e73cae

Please sign in to comment.