ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto
16
fork

Configure Feed

Select the types of activity you want to include in your feed.

replace custom validation with Zod

byarielm.fyi 88177fe1 c71e2f56

verified
+180 -96
+113 -4
package-lock.json
··· 25 25 "jszip": "^3.10.1", 26 26 "lucide-react": "^0.544.0", 27 27 "react": "^18.3.1", 28 - "react-dom": "^18.3.1" 28 + "react-dom": "^18.3.1", 29 + "zod": "^4.2.1" 29 30 }, 30 31 "devDependencies": { 31 32 "@types/jszip": "^3.4.0", ··· 112 113 "zod": "^3.23.8" 113 114 } 114 115 }, 116 + "node_modules/@atproto-labs/did-resolver/node_modules/zod": { 117 + "version": "3.25.76", 118 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 119 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 120 + "license": "MIT", 121 + "funding": { 122 + "url": "https://github.com/sponsors/colinhacks" 123 + } 124 + }, 115 125 "node_modules/@atproto-labs/fetch": { 116 126 "version": "0.2.3", 117 127 "resolved": "https://registry.npmjs.org/@atproto-labs/fetch/-/fetch-0.2.3.tgz", ··· 162 172 "node": ">=18.7.0" 163 173 } 164 174 }, 175 + "node_modules/@atproto-labs/handle-resolver/node_modules/zod": { 176 + "version": "3.25.76", 177 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 178 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 179 + "license": "MIT", 180 + "funding": { 181 + "url": "https://github.com/sponsors/colinhacks" 182 + } 183 + }, 165 184 "node_modules/@atproto-labs/identity-resolver": { 166 185 "version": "0.3.1", 167 186 "resolved": "https://registry.npmjs.org/@atproto-labs/identity-resolver/-/identity-resolver-0.3.1.tgz", ··· 216 235 "zod": "^3.23.8" 217 236 } 218 237 }, 238 + "node_modules/@atproto/api/node_modules/zod": { 239 + "version": "3.25.76", 240 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 241 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 242 + "license": "MIT", 243 + "funding": { 244 + "url": "https://github.com/sponsors/colinhacks" 245 + } 246 + }, 219 247 "node_modules/@atproto/common-web": { 220 248 "version": "0.4.3", 221 249 "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.3.tgz", ··· 226 254 "multiformats": "^9.9.0", 227 255 "uint8arrays": "3.0.0", 228 256 "zod": "^3.23.8" 257 + } 258 + }, 259 + "node_modules/@atproto/common-web/node_modules/zod": { 260 + "version": "3.25.76", 261 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 262 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 263 + "license": "MIT", 264 + "funding": { 265 + "url": "https://github.com/sponsors/colinhacks" 229 266 } 230 267 }, 231 268 "node_modules/@atproto/crypto": { ··· 251 288 "zod": "^3.23.8" 252 289 } 253 290 }, 291 + "node_modules/@atproto/did/node_modules/zod": { 292 + "version": "3.25.76", 293 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 294 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 295 + "license": "MIT", 296 + "funding": { 297 + "url": "https://github.com/sponsors/colinhacks" 298 + } 299 + }, 254 300 "node_modules/@atproto/identity": { 255 301 "version": "0.4.9", 256 302 "resolved": "https://registry.npmjs.org/@atproto/identity/-/identity-0.4.9.tgz", ··· 304 350 "zod": "^3.23.8" 305 351 } 306 352 }, 353 + "node_modules/@atproto/jwk-webcrypto/node_modules/zod": { 354 + "version": "3.25.76", 355 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 356 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 357 + "license": "MIT", 358 + "funding": { 359 + "url": "https://github.com/sponsors/colinhacks" 360 + } 361 + }, 362 + "node_modules/@atproto/jwk/node_modules/zod": { 363 + "version": "3.25.76", 364 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 365 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 366 + "license": "MIT", 367 + "funding": { 368 + "url": "https://github.com/sponsors/colinhacks" 369 + } 370 + }, 307 371 "node_modules/@atproto/lexicon": { 308 372 "version": "0.5.1", 309 373 "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.5.1.tgz", ··· 315 379 "iso-datestring-validator": "^2.2.2", 316 380 "multiformats": "^9.9.0", 317 381 "zod": "^3.23.8" 382 + } 383 + }, 384 + "node_modules/@atproto/lexicon/node_modules/zod": { 385 + "version": "3.25.76", 386 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 387 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 388 + "license": "MIT", 389 + "funding": { 390 + "url": "https://github.com/sponsors/colinhacks" 318 391 } 319 392 }, 320 393 "node_modules/@atproto/oauth-client": { ··· 357 430 "node": ">=18.7.0" 358 431 } 359 432 }, 433 + "node_modules/@atproto/oauth-client/node_modules/zod": { 434 + "version": "3.25.76", 435 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 436 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 437 + "license": "MIT", 438 + "funding": { 439 + "url": "https://github.com/sponsors/colinhacks" 440 + } 441 + }, 360 442 "node_modules/@atproto/oauth-types": { 361 443 "version": "0.4.1", 362 444 "resolved": "https://registry.npmjs.org/@atproto/oauth-types/-/oauth-types-0.4.1.tgz", ··· 367 449 "zod": "^3.23.8" 368 450 } 369 451 }, 452 + "node_modules/@atproto/oauth-types/node_modules/zod": { 453 + "version": "3.25.76", 454 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 455 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 456 + "license": "MIT", 457 + "funding": { 458 + "url": "https://github.com/sponsors/colinhacks" 459 + } 460 + }, 370 461 "node_modules/@atproto/syntax": { 371 462 "version": "0.4.1", 372 463 "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.1.tgz", ··· 381 472 "dependencies": { 382 473 "@atproto/lexicon": "^0.5.1", 383 474 "zod": "^3.23.8" 475 + } 476 + }, 477 + "node_modules/@atproto/xrpc/node_modules/zod": { 478 + "version": "3.25.76", 479 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 480 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 481 + "license": "MIT", 482 + "funding": { 483 + "url": "https://github.com/sponsors/colinhacks" 384 484 } 385 485 }, 386 486 "node_modules/@babel/code-frame": { ··· 1996 2096 }, 1997 2097 "engines": { 1998 2098 "node": ">=10" 2099 + } 2100 + }, 2101 + "node_modules/@netlify/zip-it-and-ship-it/node_modules/zod": { 2102 + "version": "3.25.76", 2103 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 2104 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 2105 + "license": "MIT", 2106 + "funding": { 2107 + "url": "https://github.com/sponsors/colinhacks" 1999 2108 } 2000 2109 }, 2001 2110 "node_modules/@noble/curves": { ··· 8164 8273 } 8165 8274 }, 8166 8275 "node_modules/zod": { 8167 - "version": "3.25.76", 8168 - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 8169 - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 8276 + "version": "4.2.1", 8277 + "resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz", 8278 + "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", 8170 8279 "license": "MIT", 8171 8280 "funding": { 8172 8281 "url": "https://github.com/sponsors/colinhacks"
+2 -1
package.json
··· 30 30 "jszip": "^3.10.1", 31 31 "lucide-react": "^0.544.0", 32 32 "react": "^18.3.1", 33 - "react-dom": "^18.3.1" 33 + "react-dom": "^18.3.1", 34 + "zod": "^4.2.1" 34 35 }, 35 36 "devDependencies": { 36 37 "@types/jszip": "^3.4.0",
+65 -91
src/lib/validation.ts
··· 1 1 /** 2 - * Validation utilities for forms 2 + * Validation utilities using Zod schemas 3 3 */ 4 + import { z } from "zod"; 4 5 5 6 export interface ValidationResult { 6 7 isValid: boolean; ··· 8 9 } 9 10 10 11 /** 11 - * Validate AT Protocol handle 12 + * Helper to convert Zod validation to ValidationResult 12 13 */ 13 - export function validateHandle(handle: string): ValidationResult { 14 - const trimmed = handle.trim(); 15 - 16 - if (!trimmed) { 17 - return { 18 - isValid: false, 19 - error: "Please enter your handle", 20 - }; 21 - } 22 - 23 - // Remove @ if user included it 24 - const cleanHandle = trimmed.startsWith("@") ? trimmed.slice(1) : trimmed; 25 - 26 - // Basic format validation 27 - if (cleanHandle.length < 3) { 28 - return { 29 - isValid: false, 30 - error: "Handle is too short", 31 - }; 32 - } 33 - 34 - // Check for valid characters (alphanumeric, dots, hyphens) 35 - const validFormat = /^[a-zA-Z0-9.-]+$/; 36 - if (!validFormat.test(cleanHandle)) { 37 - return { 38 - isValid: false, 39 - error: "Handle contains invalid characters", 40 - }; 14 + function validateWithZod<T>( 15 + schema: z.ZodSchema<T>, 16 + value: unknown, 17 + ): ValidationResult { 18 + const result = schema.safeParse(value); 19 + if (result.success) { 20 + return { isValid: true }; 41 21 } 22 + return { 23 + isValid: false, 24 + error: result.error.errors[0]?.message || "Validation failed", 25 + }; 26 + } 42 27 43 - // Must contain at least one dot (domain required) 44 - if (!cleanHandle.includes(".")) { 45 - return { 46 - isValid: false, 47 - error: "Handle must include a domain (e.g., username.bsky.social)", 48 - }; 49 - } 28 + /** 29 + * Zod Schemas 30 + */ 31 + const handleSchema = z 32 + .string() 33 + .trim() 34 + .min(1, "Please enter your handle") 35 + .transform((val) => (val.startsWith("@") ? val.slice(1) : val)) 36 + .pipe( 37 + z 38 + .string() 39 + .min(3, "Handle is too short") 40 + .regex(/^[a-zA-Z0-9.-]+$/, "Handle contains invalid characters") 41 + .refine((val) => val.includes("."), { 42 + message: "Handle must include a domain (e.g., username.bsky.social)", 43 + }) 44 + .refine((val) => !/^[.-]|[.-]$/.test(val), { 45 + message: "Handle cannot start or end with . or -", 46 + }), 47 + ); 50 48 51 - // Can't start or end with dot or hyphen 52 - if (/^[.-]|[.-]$/.test(cleanHandle)) { 53 - return { 54 - isValid: false, 55 - error: "Handle cannot start or end with . or -", 56 - }; 57 - } 49 + const emailSchema = z 50 + .string() 51 + .trim() 52 + .min(1, "Please enter your email") 53 + .email("Please enter a valid email address"); 58 54 59 - return { isValid: true }; 55 + /** 56 + * Validate AT Protocol handle 57 + */ 58 + export function validateHandle(handle: string): ValidationResult { 59 + return validateWithZod(handleSchema, handle); 60 60 } 61 61 62 62 /** 63 63 * Validate email format 64 64 */ 65 65 export function validateEmail(email: string): ValidationResult { 66 - const trimmed = email.trim(); 67 - 68 - if (!trimmed) { 69 - return { 70 - isValid: false, 71 - error: "Please enter your email", 72 - }; 73 - } 74 - 75 - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 76 - if (!emailRegex.test(trimmed)) { 77 - return { 78 - isValid: false, 79 - error: "Please enter a valid email address", 80 - }; 81 - } 82 - 83 - return { isValid: true }; 66 + return validateWithZod(emailSchema, email); 84 67 } 85 68 86 69 /** ··· 90 73 value: string, 91 74 fieldName: string = "This field", 92 75 ): ValidationResult { 93 - const trimmed = value.trim(); 94 - 95 - if (!trimmed) { 96 - return { 97 - isValid: false, 98 - error: `${fieldName} is required`, 99 - }; 100 - } 101 - 102 - return { isValid: true }; 76 + const schema = z.string().trim().min(1, `${fieldName} is required`); 77 + return validateWithZod(schema, value); 103 78 } 104 79 105 80 /** ··· 110 85 minLength: number, 111 86 fieldName: string = "This field", 112 87 ): ValidationResult { 113 - const trimmed = value.trim(); 114 - 115 - if (trimmed.length < minLength) { 116 - return { 117 - isValid: false, 118 - error: `${fieldName} must be at least ${minLength} characters`, 119 - }; 120 - } 121 - 122 - return { isValid: true }; 88 + const schema = z 89 + .string() 90 + .trim() 91 + .min(minLength, `${fieldName} must be at least ${minLength} characters`); 92 + return validateWithZod(schema, value); 123 93 } 124 94 125 95 /** ··· 130 100 maxLength: number, 131 101 fieldName: string = "This field", 132 102 ): ValidationResult { 133 - if (value.length > maxLength) { 134 - return { 135 - isValid: false, 136 - error: `${fieldName} must be ${maxLength} characters or less`, 137 - }; 138 - } 103 + const schema = z 104 + .string() 105 + .max(maxLength, `${fieldName} must be ${maxLength} characters or less`); 106 + return validateWithZod(schema, value); 107 + } 139 108 140 - return { isValid: true }; 141 - } 109 + /** 110 + * Export schemas for advanced usage 111 + */ 112 + export const schemas = { 113 + handle: handleSchema, 114 + email: emailSchema, 115 + };