your personal website on atproto - mirror
blento.app
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>