your personal website on atproto - mirror blento.app
at card-command-bar 166 lines 4.5 kB view raw
1<script lang="ts"> 2 import { Alert, Button, Input, Modal, Subheading } from '@foxui/core'; 3 import type { CreationModalComponentProps } from '../types'; 4 import { createPost } from '$lib/atproto/methods'; 5 import { user } from '$lib/atproto/auth.svelte'; 6 import { parseBlueskyPostUrl } from '../BlueskyPostCard/utils'; 7 8 let { item = $bindable(), oncreate, oncancel }: CreationModalComponentProps = $props(); 9 10 let mode = $state<'create' | 'existing'>('create'); 11 12 const profileUrl = `https://blento.app/${user.profile?.handle ?? ''}`; 13 let postText = $state(`Comment on this post to appear on my Blento! ${profileUrl}`); 14 let postUrl = $state(''); 15 let isPosting = $state(false); 16 let errorMessage = $state(''); 17 18 function buildFacets(text: string, url: string) { 19 const encoder = new TextEncoder(); 20 const encoded = encoder.encode(text); 21 const urlBytes = encoder.encode(url); 22 23 let byteStart = -1; 24 for (let i = 0; i <= encoded.length - urlBytes.length; i++) { 25 let match = true; 26 for (let j = 0; j < urlBytes.length; j++) { 27 if (encoded[i + j] !== urlBytes[j]) { 28 match = false; 29 break; 30 } 31 } 32 if (match) { 33 byteStart = i; 34 break; 35 } 36 } 37 38 if (byteStart === -1) return undefined; 39 40 return [ 41 { 42 index: { byteStart, byteEnd: byteStart + urlBytes.length }, 43 features: [{ $type: 'app.bsky.richtext.facet#link', uri: url }] 44 } 45 ]; 46 } 47 48 async function handleCreateNew() { 49 if (!postText.trim()) { 50 errorMessage = 'Post text cannot be empty.'; 51 return; 52 } 53 54 isPosting = true; 55 errorMessage = ''; 56 57 try { 58 const facets = buildFacets(postText, profileUrl); 59 const response = await createPost({ text: postText, facets }); 60 61 if (!response.ok) { 62 throw new Error('Failed to create post'); 63 } 64 65 item.cardData.uri = response.data.uri; 66 67 const rkey = response.data.uri.split('/').pop(); 68 item.cardData.href = `https://bsky.app/profile/${user.profile?.handle}/post/${rkey}`; 69 70 oncreate(); 71 } catch (err) { 72 errorMessage = 73 err instanceof Error ? err.message : 'Failed to create post. Please try again.'; 74 } finally { 75 isPosting = false; 76 } 77 } 78 79 function handleExisting() { 80 errorMessage = ''; 81 const parsed = parseBlueskyPostUrl(postUrl.trim()); 82 83 if (!parsed) { 84 errorMessage = 85 'Invalid URL. Please enter a valid Bluesky post URL (e.g., https://bsky.app/profile/handle/post/...)'; 86 return; 87 } 88 89 item.cardData.uri = `at://${parsed.handle}/app.bsky.feed.post/${parsed.rkey}`; 90 item.cardData.href = postUrl.trim(); 91 92 oncreate(); 93 } 94 95 async function handleSubmit() { 96 if (mode === 'create') { 97 await handleCreateNew(); 98 } else { 99 handleExisting(); 100 } 101 } 102</script> 103 104<Modal open={true} closeButton={false}> 105 <form 106 onsubmit={(e) => { 107 e.preventDefault(); 108 handleSubmit(); 109 }} 110 class="flex flex-col gap-2" 111 > 112 <Subheading>Guestbook</Subheading> 113 114 <div class="flex gap-2"> 115 <Button 116 size="sm" 117 variant="ghost" 118 class={mode === 'create' ? 'bg-base-200 dark:bg-base-700' : ''} 119 onclick={() => (mode = 'create')} 120 > 121 Create new post 122 </Button> 123 <Button 124 size="sm" 125 variant="ghost" 126 class={mode === 'existing' ? 'bg-base-200 dark:bg-base-700' : ''} 127 onclick={() => (mode = 'existing')} 128 > 129 Use existing post 130 </Button> 131 </div> 132 133 {#if mode === 'create'} 134 <p class="text-base-500 dark:text-base-400 text-sm"> 135 This will create a post on your Bluesky account. Replies to that post will appear on your 136 guestbook card. 137 </p> 138 <textarea 139 bind:value={postText} 140 rows="4" 141 class="bg-base-100 dark:bg-base-800 border-base-300 dark:border-base-600 mt-2 w-full rounded-lg border p-3 text-sm focus:outline-none" 142 ></textarea> 143 {:else} 144 <p class="text-base-500 dark:text-base-400 text-sm"> 145 Paste a Bluesky post URL to use as your guestbook. Replies to that post will appear on your 146 card. 147 </p> 148 <Input bind:value={postUrl} placeholder="https://bsky.app/profile/handle/post/..." /> 149 {/if} 150 151 {#if errorMessage} 152 <Alert type="error" title="Error"><span>{errorMessage}</span></Alert> 153 {/if} 154 155 <div class="mt-4 flex justify-end gap-2"> 156 <Button onclick={oncancel} variant="ghost">Cancel</Button> 157 {#if mode === 'create'} 158 <Button type="submit" disabled={isPosting || !postText.trim()}> 159 {isPosting ? 'Posting...' : 'Post to Bluesky & Create'} 160 </Button> 161 {:else} 162 <Button type="submit" disabled={!postUrl.trim()}>Create</Button> 163 {/if} 164 </div> 165 </form> 166</Modal>