the universal sandbox runtime for agents and humans. pocketenv.io
sandbox openclaw agent claude-code vercel-sandbox deno-sandbox cloudflare-sandbox atproto sprites daytona
7
fork

Configure Feed

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

Add form handling and zod validation to settings

+254 -61
+44 -8
apps/web/src/pages/settings/Settings.tsx
··· 1 + import { zodResolver } from "@hookform/resolvers/zod"; 1 2 import { useRouterState } from "@tanstack/react-router"; 3 + import { useForm } from "react-hook-form"; 4 + import { z } from "zod"; 5 + import { useSandboxQuery } from "../../hooks/useSandbox"; 2 6 import Main from "../../layouts/Main"; 3 7 import Sidebar from "./sidebar/Sidebar"; 4 - import { useSandboxQuery } from "../../hooks/useSandbox"; 8 + 9 + const settingsSchema = z.object({ 10 + name: z.string().trim().min(1, "Name is required"), 11 + description: z.string().trim().optional(), 12 + topics: z.string().trim().optional(), 13 + }); 14 + 15 + type SettingsFormValues = z.infer<typeof settingsSchema>; 5 16 6 17 function Settings() { 7 18 const routerState = useRouterState(); ··· 10 21 `at:/${pathname.replace("/settings", "").replace("sandbox", "io.pocketenv.sandbox")}`, 11 22 ); 12 23 24 + const { 25 + register, 26 + handleSubmit, 27 + formState: { errors }, 28 + } = useForm<SettingsFormValues>({ 29 + resolver: zodResolver(settingsSchema), 30 + }); 31 + 32 + const onSubmit = (values: SettingsFormValues) => { 33 + console.log(values); 34 + }; 35 + 13 36 return ( 14 37 <Main 15 38 sidebar={<Sidebar />} ··· 17 40 rootLink={pathname.replace("/settings", "")} 18 41 > 19 42 <> 20 - <div className="form-control w-[95%] m-auto"> 43 + <form 44 + className="form-control w-[95%] m-auto" 45 + onSubmit={handleSubmit(onSubmit)} 46 + > 21 47 <label className="label"> 22 48 <span className="label-text font-bold mb-1 text-[14px]">Name</span> 23 49 </label> 24 - <div className="input input-bordered w-md input-lg text-[15px] font-semibold bg-transparent"> 50 + <div 51 + className={`input input-bordered w-md input-lg text-[15px] font-semibold bg-transparent ${errors.name ? "input-error" : ""}`} 52 + > 25 53 <input 26 54 type="text" 27 - className={`grow`} 55 + className="grow" 28 56 autoComplete="off" 29 57 data-1p-ignore 30 58 data-lpignore="true" 31 59 data-form-type="other" 60 + {...register("name")} 32 61 /> 33 62 </div> 63 + {errors.name && ( 64 + <p className="text-error text-sm mt-2">{errors.name.message}</p> 65 + )} 34 66 <div className="mt-8"> 35 67 <label className="label"> 36 68 <span className="label-text font-bold mb-1 text-[14px]"> ··· 38 70 </span> 39 71 </label> 40 72 <textarea 41 - className={`textarea max-w-full h-[150px] text-[14px] font-semibold`} 73 + className="textarea max-w-full h-[150px] text-[14px] font-semibold" 42 74 aria-label="Textarea" 75 + {...register("description")} 43 76 ></textarea> 44 77 </div> 45 78 <div className="mt-8"> ··· 52 85 List of topics separated by spaces. 53 86 </span> 54 87 <textarea 55 - className={`textarea max-w-full h-25 text-[14px] font-semibold mt-2`} 88 + className="textarea max-w-full h-25 text-[14px] font-semibold mt-2" 56 89 aria-label="Textarea" 90 + {...register("topics")} 57 91 ></textarea> 58 92 </div> 59 93 <div className="mt-4"> 60 - <button className="btn btn-primary w-25">Save</button> 94 + <button type="submit" className="btn btn-primary w-25"> 95 + Save 96 + </button> 61 97 </div> 62 - </div> 98 + </form> 63 99 </> 64 100 </Main> 65 101 );
+67 -24
apps/web/src/pages/settings/repository/Repository.tsx
··· 1 + import { zodResolver } from "@hookform/resolvers/zod"; 1 2 import { useRouterState } from "@tanstack/react-router"; 3 + import { useForm } from "react-hook-form"; 4 + import { z } from "zod"; 2 5 import { useSandboxQuery } from "../../../hooks/useSandbox"; 3 6 import Main from "../../../layouts/Main"; 4 7 import Sidebar from "../sidebar/Sidebar"; 5 8 9 + const gitUrlSchema = z.object({ 10 + repositoryUrl: z 11 + .string() 12 + .trim() 13 + .min(1, "Repository URL is required") 14 + .regex( 15 + /^(https?:\/\/.+\/.+\/.+|git@.+:.+\/.+)$/, 16 + "Must be a valid Git URL (e.g. https://tangled.org/user/repo or git@tangled.org:user/repo)", 17 + ), 18 + }); 19 + 20 + type GitUrlFormValues = z.infer<typeof gitUrlSchema>; 21 + 6 22 function Repository() { 7 23 const routerState = useRouterState(); 8 24 const pathname = routerState.location.pathname; ··· 11 27 ); 12 28 const index = Math.floor(Math.random() * 7); 13 29 30 + const { 31 + register, 32 + handleSubmit, 33 + formState: { errors }, 34 + } = useForm<GitUrlFormValues>({ 35 + resolver: zodResolver(gitUrlSchema), 36 + }); 37 + 38 + const onSubmit = (values: GitUrlFormValues) => { 39 + console.log(values); 40 + }; 41 + 14 42 return ( 15 43 <Main 16 44 sidebar={<Sidebar />} ··· 23 51 <p className="opacity-60 mt-1"> 24 52 Bring your project's Git repository into your Sandbox. 25 53 </p> 26 - <div className="input input-bordered w-xl input-lg text-[15px] font-semibold bg-transparent mt-5"> 27 - <input 28 - type="text" 29 - className={`grow`} 30 - placeholder={`e.g. ${ 31 - [ 32 - "https://tangled.org/tranquil.farm/tranquil-pds", 33 - "https://tangled.org/rocksky.app/rocksky", 34 - "https://tangled.org/pocketenv.io/pocketenv", 35 - "https://tangled.org/zat.dev/zat", 36 - "https://tangled.org/pds.ls/pdsls", 37 - "https://tangled.org/teal.fm/piper", 38 - "https://tangled.org/tangled.org/core", 39 - ][index] 40 - }`} 41 - autoComplete="off" 42 - data-1p-ignore 43 - data-lpignore="true" 44 - data-form-type="other" 45 - /> 46 - </div> 47 - <div className="mt-4"> 48 - <button className="btn btn-primary w-25 font-semibold">Save</button> 49 - </div> 54 + <form onSubmit={handleSubmit(onSubmit)}> 55 + <div 56 + className={`input input-bordered w-xl input-lg text-[15px] font-semibold bg-transparent mt-5 ${errors.repositoryUrl ? "input-error" : ""}`} 57 + > 58 + <input 59 + type="text" 60 + className="grow" 61 + placeholder={`e.g. ${ 62 + [ 63 + "https://tangled.org/tranquil.farm/tranquil-pds", 64 + "https://tangled.org/rocksky.app/rocksky", 65 + "https://tangled.org/pocketenv.io/pocketenv", 66 + "https://tangled.org/zat.dev/zat", 67 + "https://tangled.org/pds.ls/pdsls", 68 + "https://tangled.org/teal.fm/piper", 69 + "https://tangled.org/tangled.org/core", 70 + ][index] 71 + }`} 72 + autoComplete="off" 73 + data-1p-ignore 74 + data-lpignore="true" 75 + data-form-type="other" 76 + {...register("repositoryUrl")} 77 + /> 78 + </div> 79 + {errors.repositoryUrl && ( 80 + <p className="text-error text-sm mt-2"> 81 + {errors.repositoryUrl.message} 82 + </p> 83 + )} 84 + <div className="mt-4"> 85 + <button 86 + type="submit" 87 + className="btn btn-primary w-25 font-semibold" 88 + > 89 + Save 90 + </button> 91 + </div> 92 + </form> 50 93 </div> 51 94 </> 52 95 </Main>
+86 -14
apps/web/src/pages/settings/sshkeys/SshKeys.tsx
··· 1 + import { zodResolver } from "@hookform/resolvers/zod"; 1 2 import { useRouterState } from "@tanstack/react-router"; 3 + import { useForm } from "react-hook-form"; 4 + import { z } from "zod"; 2 5 import { useSandboxQuery } from "../../../hooks/useSandbox"; 3 6 import Main from "../../../layouts/Main"; 4 7 import Sidebar from "../sidebar/Sidebar"; 5 8 import { useSshKeys } from "../../../hooks/useSshKeys"; 6 - import { useState } from "react"; 9 + 10 + const sshKeysSchema = z 11 + .object({ 12 + privateKey: z 13 + .string() 14 + .trim() 15 + .refine( 16 + (val) => 17 + val === "" || 18 + (val.startsWith("-----BEGIN OPENSSH PRIVATE KEY-----") && 19 + val.includes("-----END OPENSSH PRIVATE KEY-----")), 20 + { 21 + message: 22 + "Private key must start with -----BEGIN OPENSSH PRIVATE KEY----- and end with -----END OPENSSH PRIVATE KEY-----", 23 + }, 24 + ), 25 + publicKey: z 26 + .string() 27 + .trim() 28 + .refine( 29 + (val) => 30 + val === "" || 31 + val.startsWith("ssh-rsa") || 32 + val.startsWith("ssh-ed25519"), 33 + { 34 + message: "Public key must start with ssh-rsa or ssh-ed25519", 35 + }, 36 + ), 37 + }) 38 + .superRefine((data, ctx) => { 39 + const hasPrivate = data.privateKey !== ""; 40 + const hasPublic = data.publicKey !== ""; 41 + if (hasPrivate && !hasPublic) { 42 + ctx.addIssue({ 43 + code: z.ZodIssueCode.custom, 44 + path: ["publicKey"], 45 + message: "Public key is required when a private key is provided", 46 + }); 47 + } 48 + if (hasPublic && !hasPrivate) { 49 + ctx.addIssue({ 50 + code: z.ZodIssueCode.custom, 51 + path: ["privateKey"], 52 + message: "Private key is required when a public key is provided", 53 + }); 54 + } 55 + }); 56 + 57 + type SshKeysFormValues = z.infer<typeof sshKeysSchema>; 7 58 8 59 function SshKeys() { 9 - const [privateKey, setPrivateKey] = useState(""); 10 - const [publicKey, setPublicKey] = useState(""); 11 60 const routerState = useRouterState(); 12 61 const pathname = routerState.location.pathname; 13 62 const { generateEd25519SSHKeyPair } = useSshKeys(); ··· 15 64 `at:/${pathname.replace("/ssh-keys", "").replace("sandbox", "io.pocketenv.sandbox")}`, 16 65 ); 17 66 67 + const { 68 + register, 69 + handleSubmit, 70 + setValue, 71 + formState: { errors }, 72 + } = useForm<SshKeysFormValues>({ 73 + resolver: zodResolver(sshKeysSchema), 74 + }); 75 + 76 + const onSubmit = (values: SshKeysFormValues) => { 77 + console.log(values); 78 + }; 79 + 18 80 const onGenerate = async () => { 19 81 const keypair = await generateEd25519SSHKeyPair(""); 20 - setPrivateKey(keypair.privateKey); 21 - setPublicKey(keypair.publicKey); 82 + setValue("privateKey", keypair.privateKey, { shouldValidate: true }); 83 + setValue("publicKey", keypair.publicKey, { shouldValidate: true }); 22 84 }; 23 85 return ( 24 86 <Main ··· 40 102 <p className="opacity-60 mb-5"> 41 103 SSH keys used to securely access Git repositories or remote servers. 42 104 </p> 43 - <div className="form-control"> 105 + <form className="form-control" onSubmit={handleSubmit(onSubmit)}> 44 106 <div className="mt-8"> 45 107 <label className="label"> 46 108 <span className="label-text font-bold mb-1 text-[14px]"> ··· 48 110 </span> 49 111 </label> 50 112 <textarea 51 - className={`textarea max-w-full h-[150px] text-[14px] font-semibold`} 113 + className={`textarea max-w-full h-[150px] text-[14px] font-semibold ${errors.privateKey ? "textarea-error" : ""}`} 52 114 aria-label="Textarea" 53 - value={privateKey} 54 - onChange={(e) => setPrivateKey(e.target.value)} 55 115 style={{ fontFamily: "CaskaydiaNerdFontMonoRegular" }} 116 + {...register("privateKey")} 56 117 ></textarea> 118 + {errors.privateKey && ( 119 + <p className="text-error text-sm mt-2"> 120 + {errors.privateKey.message} 121 + </p> 122 + )} 57 123 </div> 58 124 <div className="mt-8"> 59 125 <label className="label"> ··· 62 128 </span> 63 129 </label> 64 130 <textarea 65 - className={`textarea max-w-full h-[150px] text-[14px] font-semibold`} 131 + className={`textarea max-w-full h-[150px] text-[14px] font-semibold ${errors.publicKey ? "textarea-error" : ""}`} 66 132 aria-label="Textarea" 67 - value={publicKey} 68 - onChange={(e) => setPublicKey(e.target.value)} 69 133 style={{ fontFamily: "CaskaydiaNerdFontMonoRegular" }} 134 + {...register("publicKey")} 70 135 ></textarea> 136 + {errors.publicKey && ( 137 + <p className="text-error text-sm mt-2"> 138 + {errors.publicKey.message} 139 + </p> 140 + )} 71 141 </div> 72 142 <div className="mt-4"> 73 - <button className="btn btn-primary w-25">Save</button> 143 + <button type="submit" className="btn btn-primary w-25"> 144 + Save 145 + </button> 74 146 </div> 75 - </div> 147 + </form> 76 148 </div> 77 149 </> 78 150 </Main>
+57 -15
apps/web/src/pages/settings/tailscale/Tailscale.tsx
··· 1 + import { zodResolver } from "@hookform/resolvers/zod"; 1 2 import { useRouterState } from "@tanstack/react-router"; 3 + import { useForm } from "react-hook-form"; 4 + import { z } from "zod"; 2 5 import { useSandboxQuery } from "../../../hooks/useSandbox"; 3 6 import Main from "../../../layouts/Main"; 4 7 import Sidebar from "../sidebar/Sidebar"; 5 8 9 + const tailscaleSchema = z.object({ 10 + authKey: z 11 + .string() 12 + .trim() 13 + .refine((val) => val === "" || val.startsWith("tskey-auth-"), { 14 + message: "Auth Key must start with tskey-auth-", 15 + }) 16 + .optional(), 17 + }); 18 + 19 + type TailscaleFormValues = z.infer<typeof tailscaleSchema>; 20 + 6 21 function Tailscale() { 7 22 const routerState = useRouterState(); 8 23 const pathname = routerState.location.pathname; ··· 10 25 `at:/${pathname.replace("/tailscale", "").replace("sandbox", "io.pocketenv.sandbox")}`, 11 26 ); 12 27 28 + const { 29 + register, 30 + handleSubmit, 31 + formState: { errors }, 32 + } = useForm<TailscaleFormValues>({ 33 + resolver: zodResolver(tailscaleSchema), 34 + }); 35 + 36 + const onSubmit = (values: TailscaleFormValues) => { 37 + console.log(values); 38 + }; 39 + 13 40 return ( 14 41 <Main 15 42 sidebar={<Sidebar />} ··· 25 52 Connect your Sandbox to your Tailscale network for secure private 26 53 access to services and devices. 27 54 </p> 28 - <div className="input input-bordered w-xl input-lg text-[15px] font-semibold bg-transparent"> 29 - <input 30 - type="text" 31 - className={`grow`} 32 - placeholder="Enter your Tailscale Auth Key" 33 - autoComplete="off" 34 - data-1p-ignore 35 - data-lpignore="true" 36 - data-form-type="other" 37 - style={{ fontFamily: "CaskaydiaNerdFontMonoRegular" }} 38 - /> 39 - </div> 40 - <div> 41 - <button className="btn btn-primary font-semibold mt-4">Save</button> 42 - </div> 55 + <form onSubmit={handleSubmit(onSubmit)}> 56 + <div 57 + className={`input input-bordered w-xl input-lg text-[15px] font-semibold bg-transparent ${errors.authKey ? "input-error" : ""}`} 58 + > 59 + <input 60 + type="text" 61 + className="grow" 62 + placeholder="Enter your Tailscale Auth Key" 63 + autoComplete="off" 64 + data-1p-ignore 65 + data-lpignore="true" 66 + data-form-type="other" 67 + style={{ fontFamily: "CaskaydiaNerdFontMonoRegular" }} 68 + {...register("authKey")} 69 + /> 70 + </div> 71 + {errors.authKey && ( 72 + <p className="text-error text-sm mt-2"> 73 + {errors.authKey.message} 74 + </p> 75 + )} 76 + <div> 77 + <button 78 + type="submit" 79 + className="btn btn-primary font-semibold mt-4" 80 + > 81 + Save 82 + </button> 83 + </div> 84 + </form> 43 85 </div> 44 86 </> 45 87 </Main>