From 462729de58c0f972c6a261218f9bbe552060dca0 Mon Sep 17 00:00:00 2001 From: Wayne Sutton Date: Sun, 14 Dec 2025 11:30:22 -0800 Subject: [PATCH] chore: prepare v1.0.0 for Netlify deployment Update version to 1.0.0 across package.json and changelog. Configure netlify.toml with Convex deployment URL (agreeable-trout-200.convex.site). Verify TypeScript type-safety for src and convex directories. Confirm Netlify build passes with SPA 404 fallback configured. Update TASK.md with deployment steps and files.md with complete file structure. --- .cursor/rules/convex-write-conflicts.mdc | 637 ++ .cursor/rules/convex2.mdc | 720 +++ .cursor/rules/dev2.mdc | 81 + .cursor/rules/help.mdc | 85 + .cursor/rules/rulesforconvex.mdc | 45 + .cursor/rules/sec-check.mdc | 286 + .cursor/rules/task.mdc | 111 + .eslintrc.cjs | 20 + .gitignore | 32 + README.md | 73 +- TASK.md | 46 + changelog.md | 50 + content/blog/about-this-blog.md | 98 + content/blog/how-to-publish.md | 211 + content/blog/markdown-with-code-examples.md | 181 + content/blog/setup-guide.md | 499 ++ content/blog/using-images-in-posts.md | 99 + content/pages/about.md | 30 + content/pages/contact.md | 43 + content/pages/docs.md | 261 + content/pages/projects.md | 48 + convex/README.md | 90 + convex/_generated/api.d.ts | 55 + convex/_generated/api.js | 23 + convex/_generated/dataModel.d.ts | 60 + convex/_generated/server.d.ts | 143 + convex/_generated/server.js | 93 + convex/convex.config.ts | 6 + convex/http.ts | 276 + convex/pages.ts | 141 + convex/posts.ts | 284 + convex/rss.ts | 154 + convex/schema.ts | 53 + convex/tsconfig.json | 18 + files.md | 143 + index.html | 91 + netlify.toml | 68 + netlify/edge-functions/botMeta.ts | 95 + package-lock.json | 6414 +++++++++++++++++++ package.json | 46 + public/_redirects | 8 + public/favicon.svg | 10 + public/images/logo.svg | 10 + public/images/og-default.svg | 10 + public/llms.txt | 31 + public/robots.txt | 33 + scripts/sync-posts.ts | 289 + src/App.tsx | 18 + src/components/BlogPost.tsx | 413 ++ src/components/CopyPageDropdown.tsx | 188 + src/components/Layout.tsx | 41 + src/components/PostList.tsx | 69 + src/components/ThemeToggle.tsx | 49 + src/context/ThemeContext.tsx | 87 + src/main.tsx | 22 + src/pages/Home.tsx | 97 + src/pages/Post.tsx | 260 + src/styles/global.css | 947 +++ src/vite-env.d.ts | 10 + tsconfig.json | 22 + tsconfig.node.json | 12 + vite.config.ts | 11 + 62 files changed, 14537 insertions(+), 9 deletions(-) create mode 100644 .cursor/rules/convex-write-conflicts.mdc create mode 100644 .cursor/rules/convex2.mdc create mode 100644 .cursor/rules/dev2.mdc create mode 100644 .cursor/rules/help.mdc create mode 100644 .cursor/rules/rulesforconvex.mdc create mode 100644 .cursor/rules/sec-check.mdc create mode 100644 .cursor/rules/task.mdc create mode 100644 .eslintrc.cjs create mode 100644 .gitignore create mode 100644 TASK.md create mode 100644 changelog.md create mode 100644 content/blog/about-this-blog.md create mode 100644 content/blog/how-to-publish.md create mode 100644 content/blog/markdown-with-code-examples.md create mode 100644 content/blog/setup-guide.md create mode 100644 content/blog/using-images-in-posts.md create mode 100644 content/pages/about.md create mode 100644 content/pages/contact.md create mode 100644 content/pages/docs.md create mode 100644 content/pages/projects.md create mode 100644 convex/README.md create mode 100644 convex/_generated/api.d.ts create mode 100644 convex/_generated/api.js create mode 100644 convex/_generated/dataModel.d.ts create mode 100644 convex/_generated/server.d.ts create mode 100644 convex/_generated/server.js create mode 100644 convex/convex.config.ts create mode 100644 convex/http.ts create mode 100644 convex/pages.ts create mode 100644 convex/posts.ts create mode 100644 convex/rss.ts create mode 100644 convex/schema.ts create mode 100644 convex/tsconfig.json create mode 100644 files.md create mode 100644 index.html create mode 100644 netlify.toml create mode 100644 netlify/edge-functions/botMeta.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/_redirects create mode 100644 public/favicon.svg create mode 100644 public/images/logo.svg create mode 100644 public/images/og-default.svg create mode 100644 public/llms.txt create mode 100644 public/robots.txt create mode 100644 scripts/sync-posts.ts create mode 100644 src/App.tsx create mode 100644 src/components/BlogPost.tsx create mode 100644 src/components/CopyPageDropdown.tsx create mode 100644 src/components/Layout.tsx create mode 100644 src/components/PostList.tsx create mode 100644 src/components/ThemeToggle.tsx create mode 100644 src/context/ThemeContext.tsx create mode 100644 src/main.tsx create mode 100644 src/pages/Home.tsx create mode 100644 src/pages/Post.tsx create mode 100644 src/styles/global.css create mode 100644 src/vite-env.d.ts create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts diff --git a/.cursor/rules/convex-write-conflicts.mdc b/.cursor/rules/convex-write-conflicts.mdc new file mode 100644 index 0000000..6457a3d --- /dev/null +++ b/.cursor/rules/convex-write-conflicts.mdc @@ -0,0 +1,637 @@ +--- +description: Guidelines for preventing write conflicts when using React, useEffect, and Convex +globs: **/*.ts,**/*.tsx,**/*.js,**/*.jsx +alwaysApply: true +--- + +# Preventing Write Conflicts in Convex with React + +Write conflicts occur when two functions running in parallel make conflicting changes to the same table or document. This rule provides patterns to avoid these conflicts. + +## Understanding Write Conflicts + +According to [Convex documentation](https://docs.convex.dev/error#1), write conflicts happen when: + +1. Multiple mutations update the same document concurrently +2. A mutation reads data that changes during execution +3. Mutations are called more rapidly than Convex can execute them + +Convex uses optimistic concurrency control and will retry mutations automatically, but will eventually fail permanently if conflicts persist. + +## Backend Protection: Idempotent Mutations + +### Always Make Mutations Idempotent + +Mutations should be safe to call multiple times with the same result. + +**Good Pattern:** + +```typescript +export const completeTask = mutation({ + args: { taskId: v.id("tasks") }, + returns: v.null(), + handler: async (ctx, args) => { + const task = await ctx.db.get(args.taskId); + + // Early return if document doesn't exist + if (!task) { + return null; + } + + // Early return if already in desired state + if (task.status === "completed") { + return null; + } + + // Only update if state change is needed + await ctx.db.patch(args.taskId, { + status: "completed", + completedAt: Date.now(), + }); + + return null; + }, +}); +``` + +**Bad Pattern:** + +```typescript +// This will cause conflicts if called multiple times rapidly +export const completeTask = mutation({ + args: { taskId: v.id("tasks") }, + returns: v.null(), + handler: async (ctx, args) => { + const task = await ctx.db.get(args.taskId); + // No check if already completed + await ctx.db.patch(args.taskId, { + status: "completed", + completedAt: Date.now(), + }); + return null; + }, +}); +``` + +### Avoid Unnecessary Reads - Patch Directly + +When you only need to update fields, patch directly without reading first. Database operations throw if the document doesn't exist. + +**Good Pattern:** + +```typescript +export const updateNote = mutation({ + args: { id: v.id("notes"), content: v.string() }, + returns: v.null(), + handler: async (ctx, args) => { + // Patch directly without reading first + // ctx.db.patch throws if document doesn't exist + await ctx.db.patch(args.id, { content: args.content }); + return null; + }, +}); +``` + +**Bad Pattern:** + +```typescript +export const updateNote = mutation({ + args: { id: v.id("notes"), content: v.string() }, + returns: v.null(), + handler: async (ctx, args) => { + // Reading the document first creates a conflict window + const note = await ctx.db.get(args.id); + if (!note) throw new Error("Not found"); + + // When typing rapidly, multiple mutations fire + // Each reads the same version, then all try to write, causing conflicts + await ctx.db.patch(args.id, { content: args.content }); + return null; + }, +}); +``` + +### Minimize Data Reads + +Only read the data you need. Avoid querying entire tables when you only need specific documents. + +**Good Pattern:** + +```typescript +export const updateUserCount = mutation({ + args: { userId: v.id("users") }, + returns: v.null(), + handler: async (ctx, args) => { + // Only query tasks for this specific user + const tasks = await ctx.db + .query("tasks") + .withIndex("by_user", (q) => q.eq("userId", args.userId)) + .collect(); + + await ctx.db.patch(args.userId, { + taskCount: tasks.length, + }); + + return null; + }, +}); +``` + +**Bad Pattern:** + +```typescript +export const updateUserCount = mutation({ + args: { userId: v.id("users") }, + returns: v.null(), + handler: async (ctx, args) => { + // Reading entire table creates conflicts with any task change + const allTasks = await ctx.db.query("tasks").collect(); + const userTasks = allTasks.filter((t) => t.userId === args.userId); + + await ctx.db.patch(args.userId, { + taskCount: userTasks.length, + }); + + return null; + }, +}); +``` + +### Use Indexes to Reduce Read Scope + +Always define and use indexes to limit the scope of data reads. + +```typescript +// In schema.ts +tasks: defineTable({ + userId: v.string(), + status: v.string(), + content: v.string(), +}).index("by_user", ["userId"]) + .index("by_user_and_status", ["userId", "status"]), +``` + +## Frontend Protection: Preventing Duplicate Calls + +### Use Refs to Track Mutation Calls + +When mutations should only be called once per state change, use refs to track calls. + +**Good Pattern:** + +```typescript +export function TimerComponent() { + const [session, setSession] = useState(null); + const hasCalledComplete = useRef(false); + const completeSession = useMutation(api.timer.completeSession); + + useEffect(() => { + if (timeRemaining <= 0 && session && !hasCalledComplete.current) { + hasCalledComplete.current = true; + completeSession({ sessionId: session._id }); + } + }, [timeRemaining, session, completeSession]); + + // Reset ref when starting new session + const handleStartNewSession = async () => { + hasCalledComplete.current = false; + await startSession(); + }; + + return
...
; +} +``` + +**Bad Pattern:** + +```typescript +export function TimerComponent() { + const [session, setSession] = useState(null); + const completeSession = useMutation(api.timer.completeSession); + + useEffect(() => { + // This can be called multiple times if timeRemaining updates + if (timeRemaining <= 0 && session) { + completeSession({ sessionId: session._id }); + } + }, [timeRemaining, session, completeSession]); + + return
...
; +} +``` + +### Debounce Rapid Mutations + +For user-triggered actions that can happen rapidly (typing, dragging), debounce the mutation calls. Recommended delay: 300-500ms. + +**Good Pattern:** + +```typescript +import { useMutation } from "convex/react"; +import { useCallback } from "react"; +import debounce from "lodash/debounce"; + +export function EditableNote() { + const updateNote = useMutation(api.notes.updateNote); + + // Debounce updates to prevent conflicts during rapid typing (300-500ms) + const debouncedUpdate = useCallback( + debounce((noteId: Id<"notes">, content: string) => { + updateNote({ noteId, content }); + }, 500), // 500ms delay recommended + [updateNote] + ); + + const handleChange = (e: React.ChangeEvent) => { + const content = e.target.value; + setLocalContent(content); + debouncedUpdate(noteId, content); + }; + + return