Monorepo for Tangled
0

Configure Feed

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

web: add /strings/new route

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

oppiliappan (Jul 17, 2026, 1:20 PM +0100) b05eb322 f2e2b17f

+141 -3
+3 -2
web/src/lib/components/ui/Spinner.svelte
··· 1 1 <script lang="ts"> 2 + import type { ClassValue } from "svelte/elements"; 2 3 import LoaderCircle from "$icon/loader-circle"; 3 4 4 5 interface Props { 5 - class?: string; 6 + class?: ClassValue | null; 6 7 } 7 8 8 9 let { class: className = "size-4" }: Props = $props(); 9 10 </script> 10 11 11 - <LoaderCircle class={`${className} animate-spin`} aria-hidden="true" /> 12 + <LoaderCircle class={[className, "animate-spin"]} aria-hidden="true" />
+1 -1
web/src/routes/repo/new/+page.svelte
··· 30 30 </p> 31 31 </div> 32 32 33 - <div class="rounded bg-background-default p-6 drop-shadow-sm"> 33 + <div class="rounded border border-border-default bg-background-default bg-background-default p-6"> 34 34 <form> 35 35 <div class="relative flex gap-4 border-l border-border-default pl-6"> 36 36 <div class="absolute -top-0 -left-3">
+6
web/src/routes/strings/new/+page.server.ts
··· 1 + import { requireAuth } from "$lib/auth/guards"; 2 + import type { PageServerLoad } from "./$types"; 3 + 4 + export const load: PageServerLoad = (event) => { 5 + requireAuth(event); 6 + };
+131
web/src/routes/strings/new/+page.svelte
··· 1 + <script lang="ts"> 2 + import { goto } from "$app/navigation"; 3 + import { ok } from "@atcute/client"; 4 + import { mainSchema as createRecordSchema } from "@atcute/atproto/types/repo/createRecord"; 5 + import type { Nsid } from "@atcute/lexicons/syntax"; 6 + import { getAuth } from "$lib/auth.svelte"; 7 + import { createClient } from "$lib/auth/agent"; 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"; 13 + 14 + const auth = getAuth(); 15 + const user = $derived(auth.currentUser); 16 + 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(); 28 + 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 + } 45 + } 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 + } 55 + }; 56 + </script> 57 + 58 + <svelte:head> 59 + <title>New string &middot; Tangled</title> 60 + </svelte:head> 61 + 62 + <div class="mx-auto w-full max-w-screen-lg px-4 py-12"> 63 + <div class="px-2 py-2"> 64 + <h1 class="text-xl font-bold text-foreground-default">Create a new string</h1> 65 + <p class="text-sm text-foreground-subtle">Store and share code snippets with ease.</p> 66 + </div> 67 + 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> 130 + </div> 131 + </div>