Monorepo for Tangled tangled.org
2

Configure Feed

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

web/routes: refactor string form into its own component

Signed-off-by: oppiliappan <me@oppi.li>

author
oppiliappan
committer
dawn
date (Jul 27, 2026, 3:40 PM +0300) commit 8f69075d parent 01647c10
+155 -102
+134
web/src/lib/components/strings/StringForm.svelte
··· 1 + <script module lang="ts"> 2 + export interface StringFormData { 3 + filename: string; 4 + description: string; 5 + content: string; 6 + } 7 + </script> 8 + 9 + <script lang="ts"> 10 + import { untrack } from "svelte"; 11 + import Button from "$lib/components/ui/Button.svelte"; 12 + import Spinner from "$lib/components/ui/Spinner.svelte"; 13 + import ErrorAlert from "$lib/components/ui/Error.svelte"; 14 + import ArrowUp from "$icon/arrow-up"; 15 + import X from "$icon/x"; 16 + 17 + interface Props { 18 + mode?: "create" | "edit"; 19 + filename?: string; 20 + description?: string; 21 + content?: string; 22 + onsubmit: (data: StringFormData) => Promise<void>; 23 + oncancel?: () => void; 24 + } 25 + 26 + let { 27 + mode = "create", 28 + filename: initialFilename = "", 29 + description: initialDescription = "", 30 + content: initialContent = "", 31 + onsubmit, 32 + oncancel 33 + }: Props = $props(); 34 + 35 + // seed once; the props can't change while the form is open. 36 + let filename = $state(untrack(() => initialFilename)); 37 + let description = $state(untrack(() => initialDescription)); 38 + let content = $state(untrack(() => initialContent)); 39 + let isPublishing = $state(false); 40 + let error = $state<string | null>(null); 41 + 42 + const lineCount = $derived(content === "" ? 0 : content.split("\n").length); 43 + const byteCount = $derived(new TextEncoder().encode(content).length); 44 + 45 + const handleSubmit = async (e: SubmitEvent) => { 46 + e.preventDefault(); 47 + isPublishing = true; 48 + error = null; 49 + try { 50 + await onsubmit({ filename, description, content }); 51 + } catch (err) { 52 + error = err instanceof Error ? err.message : "Failed to publish string"; 53 + } finally { 54 + isPublishing = false; 55 + } 56 + }; 57 + </script> 58 + 59 + <div class="rounded border border-border-default bg-background-default"> 60 + <form onsubmit={handleSubmit}> 61 + <div class="flex flex-col gap-2 p-4"> 62 + <div class="flex flex-col gap-2 md:flex-row"> 63 + <input 64 + type="text" 65 + id="filename" 66 + name="filename" 67 + placeholder="Filename" 68 + required 69 + bind:value={filename} 70 + disabled={isPublishing} 71 + class="rounded border border-border-default bg-background-default px-2 py-2 text-sm text-foreground-default placeholder:text-foreground-placeholder focus:ring-1 focus:ring-border-strong focus:outline-none disabled:opacity-50 md:max-w-64" 72 + /> 73 + <input 74 + type="text" 75 + id="description" 76 + name="description" 77 + placeholder="Description ..." 78 + maxlength={280} 79 + bind:value={description} 80 + disabled={isPublishing} 81 + class="flex-1 rounded border border-border-default bg-background-default px-2 py-2 text-sm text-foreground-default placeholder:text-foreground-placeholder focus:ring-1 focus:ring-border-strong focus:outline-none disabled:opacity-50" 82 + /> 83 + </div> 84 + 85 + <textarea 86 + id="content" 87 + name="content" 88 + placeholder="Paste your string here!" 89 + required 90 + rows={20} 91 + spellcheck={false} 92 + bind:value={content} 93 + disabled={isPublishing} 94 + class="w-full resize-y rounded border border-border-default bg-background-default px-2 py-2 font-mono text-sm text-foreground-default placeholder:text-foreground-placeholder focus:ring-1 focus:ring-border-strong focus:outline-none disabled:opacity-50" 95 + ></textarea> 96 + </div> 97 + 98 + <div class="flex items-center justify-between px-4 py-3"> 99 + <div class="text-sm text-foreground-subtle"> 100 + <span>{lineCount} line{lineCount !== 1 ? "s" : ""}</span> 101 + <span class="px-1 select-none">·</span> 102 + <span>{byteCount} byte{byteCount !== 1 ? "s" : ""}</span> 103 + </div> 104 + 105 + <div class="flex items-center gap-2"> 106 + {#if mode === "edit"} 107 + <Button 108 + type="button" 109 + variant="default" 110 + icon={X} 111 + disabled={isPublishing} 112 + onclick={oncancel} 113 + > 114 + Cancel 115 + </Button> 116 + {/if} 117 + <Button 118 + type="submit" 119 + variant="primary" 120 + icon={isPublishing ? Spinner : ArrowUp} 121 + disabled={isPublishing} 122 + > 123 + Publish 124 + </Button> 125 + </div> 126 + </div> 127 + 128 + {#if error} 129 + <div class="px-4 pb-4"> 130 + <ErrorAlert label={error} /> 131 + </div> 132 + {/if} 133 + </form> 134 + </div>
+21 -102
web/src/routes/strings/new/+page.svelte
··· 6 6 import { getAuth } from "$lib/auth.svelte"; 7 7 import { createClient } from "$lib/auth/agent"; 8 8 import { rkeyFromUri } from "$lib/api/uri"; 9 - import Button from "$lib/components/ui/Button.svelte"; 10 - import Spinner from "$lib/components/ui/Spinner.svelte"; 11 - import ErrorAlert from "$lib/components/ui/Error.svelte"; 12 - import ArrowUp from "$icon/arrow-up"; 9 + import StringForm, { type StringFormData } from "$lib/components/strings/StringForm.svelte"; 13 10 14 11 const auth = getAuth(); 15 12 const user = $derived(auth.currentUser); 16 13 17 - let filename = $state(""); 18 - let description = $state(""); 19 - let content = $state(""); 20 - let isPublishing = $state(false); 21 - let error = $state<string | null>(null); 22 - 23 - const lineCount = $derived(content === "" ? 0 : content.split("\n").length); 24 - const byteCount = $derived(new TextEncoder().encode(content).length); 25 - 26 - const handleSubmit = async (e: SubmitEvent) => { 27 - e.preventDefault(); 14 + const handleSubmit = async ({ filename, description, content }: StringFormData) => { 28 15 if (!auth.agent) return; 29 - isPublishing = true; 30 - error = null; 31 - try { 32 - const rpc = createClient(auth.agent); 33 - const result = await ok( 34 - rpc.call(createRecordSchema, { 35 - input: { 36 - repo: auth.agent.sub, 37 - collection: "sh.tangled.string" as Nsid, 38 - record: { 39 - $type: "sh.tangled.string", 40 - filename, 41 - description, 42 - contents: content, 43 - createdAt: new Date().toISOString() 44 - } 16 + const rpc = createClient(auth.agent); 17 + const result = await ok( 18 + rpc.call(createRecordSchema, { 19 + input: { 20 + repo: auth.agent.sub, 21 + collection: "sh.tangled.string" as Nsid, 22 + record: { 23 + $type: "sh.tangled.string", 24 + filename, 25 + description, 26 + contents: content, 27 + createdAt: new Date().toISOString() 45 28 } 46 - }) 47 - ); 48 - const rkey = rkeyFromUri(result.uri); 49 - await goto(`/strings/${user?.handle}/${rkey}`); 50 - } catch (err) { 51 - error = err instanceof Error ? err.message : "Failed to publish string"; 52 - } finally { 53 - isPublishing = false; 54 - } 29 + } 30 + }) 31 + ); 32 + const rkey = rkeyFromUri(result.uri); 33 + await goto(`/strings/${user?.handle}/${rkey}`); 55 34 }; 56 35 </script> 57 36 ··· 65 44 <p class="text-sm text-foreground-subtle">Store and share code snippets with ease.</p> 66 45 </div> 67 46 68 - <div class="mt-4 rounded border border-border-default bg-background-default"> 69 - <form onsubmit={handleSubmit}> 70 - <div class="flex flex-col gap-2 p-4"> 71 - <div class="flex flex-col gap-2 md:flex-row"> 72 - <input 73 - type="text" 74 - id="filename" 75 - name="filename" 76 - placeholder="Filename" 77 - required 78 - bind:value={filename} 79 - disabled={isPublishing} 80 - class="rounded border border-border-default bg-background-default px-2 py-2 text-sm text-foreground-default placeholder:text-foreground-placeholder focus:ring-1 focus:ring-border-strong focus:outline-none disabled:opacity-50 md:max-w-64" 81 - /> 82 - <input 83 - type="text" 84 - id="description" 85 - name="description" 86 - placeholder="Description ..." 87 - maxlength={280} 88 - bind:value={description} 89 - disabled={isPublishing} 90 - class="flex-1 rounded border border-border-default bg-background-default px-2 py-2 text-sm text-foreground-default placeholder:text-foreground-placeholder focus:ring-1 focus:ring-border-strong focus:outline-none disabled:opacity-50" 91 - /> 92 - </div> 93 - 94 - <textarea 95 - id="content" 96 - name="content" 97 - placeholder="Paste your string here!" 98 - required 99 - rows={20} 100 - spellcheck={false} 101 - bind:value={content} 102 - disabled={isPublishing} 103 - class="w-full resize-y rounded border border-border-default bg-background-default px-2 py-2 font-mono text-sm text-foreground-default placeholder:text-foreground-placeholder focus:ring-1 focus:ring-border-strong focus:outline-none disabled:opacity-50" 104 - ></textarea> 105 - </div> 106 - 107 - <div class="flex items-center justify-between px-4 py-3"> 108 - <div class="text-sm text-foreground-subtle"> 109 - <span>{lineCount} line{lineCount !== 1 ? "s" : ""}</span> 110 - <span class="px-1 select-none">·</span> 111 - <span>{byteCount} byte{byteCount !== 1 ? "s" : ""}</span> 112 - </div> 113 - 114 - <Button 115 - type="submit" 116 - variant="primary" 117 - icon={isPublishing ? Spinner : ArrowUp} 118 - disabled={isPublishing} 119 - > 120 - Publish 121 - </Button> 122 - </div> 123 - 124 - {#if error} 125 - <div class="px-4 pb-4"> 126 - <ErrorAlert label={error} /> 127 - </div> 128 - {/if} 129 - </form> 47 + <div class="mt-4"> 48 + <StringForm mode="create" onsubmit={handleSubmit} /> 130 49 </div> 131 50 </div>