From 6478a8ede817905cd231125c5a0ecf25255d41ab Mon Sep 17 00:00:00 2001 From: johannes-hernehult Date: Tue, 9 Jun 2026 20:40:43 +0200 Subject: [PATCH] feat: add Guide, Frame, and Image Zod schemas and type infer --- package-lock.json | 111 +++++++++++++++++++++++++++++++-- package.json | 7 ++- src/features/guides/schemas.ts | 35 +++++++++++ src/router.tsx | 0 src/store.ts | 0 5 files changed, 148 insertions(+), 5 deletions(-) create mode 100644 src/features/guides/schemas.ts create mode 100644 src/router.tsx create mode 100644 src/store.ts diff --git a/package-lock.json b/package-lock.json index 5bf841a..cbaaf15 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,8 +8,13 @@ "name": "snapsteps", "version": "0.0.0", "dependencies": { + "browser-image-compression": "^2.0.2", + "idb": "^8.0.3", "react": "^19.2.6", - "react-dom": "^19.2.6" + "react-dom": "^19.2.6", + "react-router-dom": "^7.17.0", + "zod": "^4.4.3", + "zustand": "^5.0.14" }, "devDependencies": { "@babel/core": "^7.29.0", @@ -913,7 +918,7 @@ "version": "19.2.17", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", - "dev": true, + "devOptional": true, "dependencies": { "csstype": "^3.2.2" } @@ -1262,6 +1267,14 @@ "node": "18 || 20 || >=22" } }, + "node_modules/browser-image-compression": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/browser-image-compression/-/browser-image-compression-2.0.2.tgz", + "integrity": "sha512-pBLlQyUf6yB8SmmngrcOw3EoS4RpQ1BcylI3T9Yqn7+4nrQTXJD4sJDe5ODnJdrvNMaio5OicFo75rDyJD2Ucw==", + "dependencies": { + "uzip": "0.20201231.0" + } + }, "node_modules/browserslist": { "version": "4.28.2", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", @@ -1321,6 +1334,18 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1339,7 +1364,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true + "devOptional": true }, "node_modules/debug": { "version": "4.4.3", @@ -1716,6 +1741,11 @@ "hermes-estree": "0.25.1" } }, + "node_modules/idb": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/idb/-/idb-8.0.3.tgz", + "integrity": "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2306,6 +2336,42 @@ "react": "^19.2.7" } }, + "node_modules/react-router": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.17.0.tgz", + "integrity": "sha512-FDELK7rTMlCHO5+reyXsPlmfr7N1F91lPHsWYfMEGQm/KQ+F4JFM8jGoeQDmDvdTs93Fw9aSilH+uKRb4/jXvQ==", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.17.0.tgz", + "integrity": "sha512-fyU2yjGups/hE6Xz0I5ZYbVL8Gx29eCjgpHaRaTaVU+OOAdfRX05KsvyRm0GO8YQwOkhpU3MurW1jyMUJn+zSw==", + "dependencies": { + "react-router": "7.17.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/rolldown": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.0.tgz", @@ -2354,6 +2420,11 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2512,6 +2583,11 @@ "punycode": "^2.1.0" } }, + "node_modules/uzip": { + "version": "0.20201231.0", + "resolved": "https://registry.npmjs.org/uzip/-/uzip-0.20201231.0.tgz", + "integrity": "sha512-OZeJfZP+R0z9D6TmBgLq2LHzSSptGMGDGigGiEe0pr8UBe/7fdflgHlHBNDASTXB5jnFuxHpNaJywSg8YFeGng==" + }, "node_modules/vite": { "version": "8.0.16", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", @@ -2919,7 +2995,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", - "dev": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -2935,6 +3010,34 @@ "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } + }, + "node_modules/zustand": { + "version": "5.0.14", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz", + "integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index 37ed56f..a050fa5 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,13 @@ "preview": "vite preview" }, "dependencies": { + "browser-image-compression": "^2.0.2", + "idb": "^8.0.3", "react": "^19.2.6", - "react-dom": "^19.2.6" + "react-dom": "^19.2.6", + "react-router-dom": "^7.17.0", + "zod": "^4.4.3", + "zustand": "^5.0.14" }, "devDependencies": { "@babel/core": "^7.29.0", diff --git a/src/features/guides/schemas.ts b/src/features/guides/schemas.ts new file mode 100644 index 0000000..3178322 --- /dev/null +++ b/src/features/guides/schemas.ts @@ -0,0 +1,35 @@ +import { z } from "zod"; + +export const ImageSchema = z.object({ + id: z.uuid().default(() => crypto.randomUUID()), + file: z.instanceof(File), + alt: z.string().default(""), + width: z.number(), + height: z.number(), + size: z.number(), + mimeType: z.string(), + createdAt: z.date(), + updatedAt: z.date(), +}); + +export const FrameSchema = z.object({ + id: z.uuid().default(() => crypto.randomUUID()), + imageId: z.uuid(), + steps: z.array(z.string()).min(1, "Steps are required"), + order: z.number(), +}); + +export const GuideSchema = z.object({ + id: z.uuid().default(() => crypto.randomUUID()), + title: z.string().min(1, "Title is required"), + description: z.string().optional(), + content: z.string().optional(), + createdAt: z.date(), + updatedAt: z.date(), + deletedAt: z.date().optional(), + frames: z.array(FrameSchema), +}); + +export type Guide = z.infer; +export type Image = z.infer; +export type Frame = z.infer; diff --git a/src/router.tsx b/src/router.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/store.ts b/src/store.ts new file mode 100644 index 0000000..e69de29