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