From 7029b1476420e54635b362c1eaf98a2af914c9b3 Mon Sep 17 00:00:00 2001
From: Johannes Brandt Nielsen
Date: Thu, 22 Jun 2023 06:13:37 +0200
Subject: [PATCH 1/2] install packages
---
package-lock.json | 141 +++++++++++++++++++++++++++++++++++++++++++++-
package.json | 5 +-
2 files changed, 144 insertions(+), 2 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 2d381b2..147001d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,12 +11,15 @@
"@remix-run/node": "^1.17.0",
"@remix-run/react": "^1.17.0",
"@remix-run/serve": "^1.17.0",
+ "@remix-validated-form/with-zod": "^2.0.6",
"bcryptjs": "^2.4.3",
"isbot": "^3.6.8",
"marked": "^5.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
- "tiny-invariant": "^1.3.1"
+ "remix-validated-form": "^5.0.2",
+ "tiny-invariant": "^1.3.1",
+ "zod": "^3.21.4"
},
"devDependencies": {
"@faker-js/faker": "^7.6.0",
@@ -3176,6 +3179,15 @@
"web-streams-polyfill": "^3.1.1"
}
},
+ "node_modules/@remix-validated-form/with-zod": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/@remix-validated-form/with-zod/-/with-zod-2.0.6.tgz",
+ "integrity": "sha512-i8H0PPFSSKIMGPVO/8cUMO1QoGa2bBQZb6RH3DoXGVE1heu52d1vwrFVsYYQB8Vc8lp5BGQk1kbxZuN9RzH1OA==",
+ "peerDependencies": {
+ "remix-validated-form": "^4.x || ^5.x",
+ "zod": "^3.11.x"
+ }
+ },
"node_modules/@rollup/pluginutils": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz",
@@ -8922,6 +8934,15 @@
"node": ">= 4"
}
},
+ "node_modules/immer": {
+ "version": "9.0.21",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
+ "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -10088,6 +10109,11 @@
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="
},
+ "node_modules/lodash.get": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
+ "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ=="
+ },
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -13579,6 +13605,29 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/remeda": {
+ "version": "1.19.0",
+ "resolved": "https://registry.npmjs.org/remeda/-/remeda-1.19.0.tgz",
+ "integrity": "sha512-iwZohiXDhC1K+adRI6OB+tYxOfXyX7DaPXQDZrR5s1k7umrkG3Yd2+QDfSrYFlC7oc0IqeUns6RqSjNkERXeLw=="
+ },
+ "node_modules/remix-validated-form": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/remix-validated-form/-/remix-validated-form-5.0.2.tgz",
+ "integrity": "sha512-jM3uuvCP6AO9G117MAEGgTb3x/aOy5qVrOM85XdDhWnpJFt7WC6u1gBQ/tSd1UBCTxDSFwU6TZPCJex73D9rjQ==",
+ "dependencies": {
+ "immer": "^9.0.12",
+ "lodash.get": "^4.4.2",
+ "nanoid": "3.3.6",
+ "remeda": "^1.2.0",
+ "tiny-invariant": "^1.2.0",
+ "zustand": "^4.3.0"
+ },
+ "peerDependencies": {
+ "@remix-run/react": ">= 1.15.0",
+ "@remix-run/server-runtime": "1.x",
+ "react": "^17.0.2 || ^18.0.0"
+ }
+ },
"node_modules/request-progress": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz",
@@ -15435,6 +15484,14 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/use-sync-external-store": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
+ "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/util": {
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
@@ -16219,6 +16276,37 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/zod": {
+ "version": "3.21.4",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz",
+ "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zustand": {
+ "version": "4.3.8",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.3.8.tgz",
+ "integrity": "sha512-4h28KCkHg5ii/wcFFJ5Fp+k1J3gJoasaIbppdgZFO4BPJnsNxL0mQXBSFgOgAdCdBj35aDTPvdAJReTMntFPGg==",
+ "dependencies": {
+ "use-sync-external-store": "1.2.0"
+ },
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "immer": ">=9.0",
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/zwitch": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
@@ -18352,6 +18440,11 @@
"web-streams-polyfill": "^3.1.1"
}
},
+ "@remix-validated-form/with-zod": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/@remix-validated-form/with-zod/-/with-zod-2.0.6.tgz",
+ "integrity": "sha512-i8H0PPFSSKIMGPVO/8cUMO1QoGa2bBQZb6RH3DoXGVE1heu52d1vwrFVsYYQB8Vc8lp5BGQk1kbxZuN9RzH1OA=="
+ },
"@rollup/pluginutils": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz",
@@ -22658,6 +22751,11 @@
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
"integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ=="
},
+ "immer": {
+ "version": "9.0.21",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
+ "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA=="
+ },
"import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -23497,6 +23595,11 @@
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="
},
+ "lodash.get": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
+ "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ=="
+ },
"lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -25876,6 +25979,24 @@
"unified": "^10.0.0"
}
},
+ "remeda": {
+ "version": "1.19.0",
+ "resolved": "https://registry.npmjs.org/remeda/-/remeda-1.19.0.tgz",
+ "integrity": "sha512-iwZohiXDhC1K+adRI6OB+tYxOfXyX7DaPXQDZrR5s1k7umrkG3Yd2+QDfSrYFlC7oc0IqeUns6RqSjNkERXeLw=="
+ },
+ "remix-validated-form": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/remix-validated-form/-/remix-validated-form-5.0.2.tgz",
+ "integrity": "sha512-jM3uuvCP6AO9G117MAEGgTb3x/aOy5qVrOM85XdDhWnpJFt7WC6u1gBQ/tSd1UBCTxDSFwU6TZPCJex73D9rjQ==",
+ "requires": {
+ "immer": "^9.0.12",
+ "lodash.get": "^4.4.2",
+ "nanoid": "3.3.6",
+ "remeda": "^1.2.0",
+ "tiny-invariant": "^1.2.0",
+ "zustand": "^4.3.0"
+ }
+ },
"request-progress": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz",
@@ -27285,6 +27406,11 @@
"punycode": "^2.1.0"
}
},
+ "use-sync-external-store": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
+ "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA=="
+ },
"util": {
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
@@ -27813,6 +27939,19 @@
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="
},
+ "zod": {
+ "version": "3.21.4",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz",
+ "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw=="
+ },
+ "zustand": {
+ "version": "4.3.8",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.3.8.tgz",
+ "integrity": "sha512-4h28KCkHg5ii/wcFFJ5Fp+k1J3gJoasaIbppdgZFO4BPJnsNxL0mQXBSFgOgAdCdBj35aDTPvdAJReTMntFPGg==",
+ "requires": {
+ "use-sync-external-store": "1.2.0"
+ }
+ },
"zwitch": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
diff --git a/package.json b/package.json
index 8a9a566..f8a6335 100644
--- a/package.json
+++ b/package.json
@@ -29,12 +29,15 @@
"@remix-run/node": "^1.17.0",
"@remix-run/react": "^1.17.0",
"@remix-run/serve": "^1.17.0",
+ "@remix-validated-form/with-zod": "^2.0.6",
"bcryptjs": "^2.4.3",
"isbot": "^3.6.8",
"marked": "^5.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
- "tiny-invariant": "^1.3.1"
+ "remix-validated-form": "^5.0.2",
+ "tiny-invariant": "^1.3.1",
+ "zod": "^3.21.4"
},
"devDependencies": {
"@faker-js/faker": "^7.6.0",
From 7778bab7a0e300d50408fe66de3a972b9b675b9d Mon Sep 17 00:00:00 2001
From: Johannes Brandt Nielsen
Date: Thu, 22 Jun 2023 06:15:34 +0200
Subject: [PATCH 2/2] use ValidatedForm in posts new
---
app/routes/posts.admin.new.tsx | 153 ++++++++++++++++++---------------
1 file changed, 85 insertions(+), 68 deletions(-)
diff --git a/app/routes/posts.admin.new.tsx b/app/routes/posts.admin.new.tsx
index 7d923e1..934fcc6 100644
--- a/app/routes/posts.admin.new.tsx
+++ b/app/routes/posts.admin.new.tsx
@@ -1,90 +1,107 @@
import type { ActionArgs } from "@remix-run/node";
-import { json, redirect } from "@remix-run/node";
-import { Form, useActionData, useNavigation } from "@remix-run/react";
-import invariant from "tiny-invariant";
+import { redirect } from "@remix-run/node";
import { createPost } from "~/models/post.server";
+import {
+ ValidatedForm,
+ useField,
+ useIsSubmitting,
+ validationError,
+} from "remix-validated-form";
+import { withZod } from "@remix-validated-form/with-zod";
+import { z } from "zod";
+
const inputClassName = `w-full rounded border border-gray-500 px-2 py-1 text-lg`;
+export const validator = withZod(
+ z.object({
+ title: z.string().min(1, { message: "Title is required" }),
+ slug: z.string().min(1, { message: "Slug name is required" }),
+ markdown: z.string().min(1, { message: "Markdown content is required" }),
+ })
+);
+
export const action = async ({ request }: ActionArgs) => {
- const formData = await request.formData();
+ const result = await validator.validate(await request.formData());
- const title = formData.get("title");
- const slug = formData.get("slug");
- const markdown = formData.get("markdown");
+ if (result.error) {
+ return validationError(result.error);
+ }
await new Promise((res) => setTimeout(res, 1000));
- const errors = {
- title: title ? null : "Title is required",
- slug: slug ? null : "Slug is required",
- markdown: markdown ? null : "Markdown is required",
- };
+ await createPost(result.data);
- const hasErrors = Object.values(errors).some((errorMessage) => errorMessage);
- if (hasErrors) {
- return json(errors);
- }
+ return redirect("/posts/admin");
+};
+
+export default function NewPost() {
+ return (
+
+
+
- invariant(typeof title === "string", "title must be a string");
- invariant(typeof slug === "string", "slug must be a string");
- invariant(typeof markdown === "string", "markdown must be a string");
+
- await createPost({ title, slug, markdown });
+
+
+
+
+ );
+}
- return redirect("/posts/admin");
+type InputProps = {
+ name: string;
+ label: string;
};
-export default function NewPost() {
- const errors = useActionData();
+export const Input = ({ name, label }: InputProps) => {
+ const { error, getInputProps } = useField(name);
+ return (
+
+
+
+ );
+};
- const navigation = useNavigation();
- const isCreating = Boolean(navigation.state === "submitting");
+type TextareaProps = {
+ name: string;
+ label: string;
+};
+export const Textarea = ({ name, label }: TextareaProps) => {
+ const { error, getInputProps } = useField(name);
return (
-
+
+
);
-}
+};
+
+export const SubmitButton = () => {
+ const isSubmitting = useIsSubmitting();
+
+ return (
+
+ );
+};