your personal website on atproto - mirror
blento.app
1<script lang="ts">
2 import { dev } from '$app/environment';
3 import { user } from '$lib/atproto';
4 import type { WebsiteData } from '$lib/types';
5 import { Button, Input, Navbar, Popover, Toggle, toast } from '@foxui/core';
6
7 let {
8 data,
9 linkValue = $bindable(),
10 newCard,
11 addLink,
12
13 showingMobileView = $bindable(),
14 isSaving = $bindable(),
15 hasUnsavedChanges,
16
17 save,
18
19 handleImageInputChange,
20 handleVideoInputChange
21 }: {
22 data: WebsiteData;
23 linkValue: string;
24 newCard: (type: string) => void;
25 addLink: (url: string) => void;
26
27 showingMobileView: boolean;
28
29 isSaving: boolean;
30 hasUnsavedChanges: boolean;
31
32 save: () => Promise<void>;
33
34 handleImageInputChange: (evt: Event) => void;
35 handleVideoInputChange: (evt: Event) => void;
36 } = $props();
37
38 let linkPopoverOpen = $state(false);
39
40 let imageInputRef: HTMLInputElement | undefined = $state();
41 let videoInputRef: HTMLInputElement | undefined = $state();
42
43 function getShareUrl() {
44 const base = typeof window !== 'undefined' ? window.location.origin : '';
45 const pagePath =
46 data.page && data.page !== 'blento.self' ? `/${data.page.replace('blento.', '')}` : '';
47 return `${base}/${data.handle}${pagePath}`;
48 }
49
50 async function copyShareLink() {
51 const url = getShareUrl();
52 await navigator.clipboard.writeText(url);
53 toast.success('Link copied to clipboard!');
54 }
55</script>
56
57<input
58 type="file"
59 accept="image/*"
60 onchange={handleImageInputChange}
61 class="hidden"
62 multiple
63 bind:this={imageInputRef}
64/>
65
66<input
67 type="file"
68 accept="video/*"
69 onchange={handleVideoInputChange}
70 class="hidden"
71 multiple
72 bind:this={videoInputRef}
73/>
74
75{#if dev || (user.isLoggedIn && user.profile?.did === data.did)}
76 <Navbar
77 class={[
78 'dark:bg-base-900 bg-base-100 top-auto bottom-2 mx-4 mt-3 max-w-3xl rounded-full px-4 md:mx-auto lg:inline-flex',
79 !dev ? 'hidden' : ''
80 ]}
81 >
82 <div class="flex items-center gap-2">
83 <Button
84 size="iconLg"
85 variant="ghost"
86 class="backdrop-blur-none"
87 onclick={() => {
88 newCard('section');
89 }}
90 >
91 <svg
92 xmlns="http://www.w3.org/2000/svg"
93 viewBox="0 0 24 24"
94 fill="none"
95 stroke="currentColor"
96 stroke-width="2"
97 stroke-linecap="round"
98 stroke-linejoin="round"
99 ><path d="M6 12h12" /><path d="M6 20V4" /><path d="M18 20V4" /></svg
100 >
101 </Button>
102
103 <Button
104 size="iconLg"
105 variant="ghost"
106 class="backdrop-blur-none"
107 onclick={() => {
108 newCard('text');
109 }}
110 >
111 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
112 ><path
113 fill="none"
114 stroke="currentColor"
115 stroke-linecap="round"
116 stroke-linejoin="round"
117 stroke-width="2"
118 d="m15 16l2.536-7.328a1.02 1.02 1 0 1 1.928 0L22 16m-6.303-2h5.606M2 16l4.039-9.69a.5.5 0 0 1 .923 0L11 16m-7.696-3h6.392"
119 /></svg
120 >
121 </Button>
122
123 <Popover sideOffset={16} bind:open={linkPopoverOpen} class="bg-base-100 dark:bg-base-900">
124 {#snippet child({ props })}
125 <Button
126 size="iconLg"
127 variant="ghost"
128 class="backdrop-blur-none"
129 onclick={() => {
130 newCard('link');
131 }}
132 {...props}
133 >
134 <svg
135 xmlns="http://www.w3.org/2000/svg"
136 fill="none"
137 viewBox="-2 -2 28 28"
138 stroke-width="2"
139 stroke="currentColor"
140 >
141 <path
142 stroke-linecap="round"
143 stroke-linejoin="round"
144 d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244"
145 />
146 </svg>
147 </Button>
148 {/snippet}
149 <Input
150 spellcheck={false}
151 type="url"
152 bind:value={linkValue}
153 onkeydown={(event) => {
154 if (event.code === 'Enter') {
155 addLink(linkValue);
156 event.preventDefault();
157 }
158 }}
159 placeholder="Enter link"
160 />
161 <Button onclick={() => addLink(linkValue)} size="icon"
162 ><svg
163 xmlns="http://www.w3.org/2000/svg"
164 fill="none"
165 viewBox="0 0 24 24"
166 stroke-width="2"
167 stroke="currentColor"
168 class="size-6"
169 >
170 <path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
171 </svg>
172 </Button>
173 </Popover>
174
175 <Button
176 size="iconLg"
177 variant="ghost"
178 class="backdrop-blur-none"
179 onclick={() => {
180 imageInputRef?.click();
181 }}
182 >
183 <svg
184 xmlns="http://www.w3.org/2000/svg"
185 fill="none"
186 viewBox="0 0 24 24"
187 stroke-width="2"
188 stroke="currentColor"
189 >
190 <path
191 stroke-linecap="round"
192 stroke-linejoin="round"
193 d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z"
194 />
195 </svg>
196 </Button>
197
198 {#if dev}
199 <Button
200 size="iconLg"
201 variant="ghost"
202 class="backdrop-blur-none"
203 onclick={() => {
204 videoInputRef?.click();
205 }}
206 >
207 <svg
208 xmlns="http://www.w3.org/2000/svg"
209 fill="none"
210 viewBox="0 0 24 24"
211 stroke-width="1.5"
212 stroke="currentColor"
213 >
214 <path
215 stroke-linecap="round"
216 stroke-linejoin="round"
217 d="m15.75 10.5 4.72-4.72a.75.75 0 0 1 1.28.53v11.38a.75.75 0 0 1-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 0 0 2.25-2.25v-9a2.25 2.25 0 0 0-2.25-2.25h-9A2.25 2.25 0 0 0 2.25 7.5v9a2.25 2.25 0 0 0 2.25 2.25Z"
218 />
219 </svg>
220 </Button>
221 {/if}
222
223 <Button size="iconLg" variant="ghost" class="backdrop-blur-none" popovertarget="mobile-menu">
224 <svg
225 xmlns="http://www.w3.org/2000/svg"
226 fill="none"
227 viewBox="0 0 24 24"
228 stroke-width="1.5"
229 stroke="currentColor"
230 >
231 <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
232 </svg>
233 </Button>
234 </div>
235 <div class="flex items-center gap-2">
236 <Toggle
237 class="hidden bg-transparent backdrop-blur-none lg:block dark:bg-transparent"
238 bind:pressed={showingMobileView}
239 >
240 <svg
241 xmlns="http://www.w3.org/2000/svg"
242 fill="none"
243 viewBox="0 0 24 24"
244 stroke-width="1.5"
245 stroke="currentColor"
246 class="size-6"
247 >
248 <path
249 stroke-linecap="round"
250 stroke-linejoin="round"
251 d="M10.5 1.5H8.25A2.25 2.25 0 0 0 6 3.75v16.5a2.25 2.25 0 0 0 2.25 2.25h7.5A2.25 2.25 0 0 0 18 20.25V3.75a2.25 2.25 0 0 0-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3"
252 />
253 </svg>
254 </Toggle>
255 {#if hasUnsavedChanges}
256 <Button
257 disabled={isSaving}
258 onclick={async () => {
259 save();
260 }}>{isSaving ? 'Saving...' : 'Save'}</Button
261 >
262 {:else}
263 <Button onclick={copyShareLink}>
264 <svg
265 xmlns="http://www.w3.org/2000/svg"
266 fill="none"
267 viewBox="0 0 24 24"
268 stroke-width="1.5"
269 stroke="currentColor"
270 class="size-5"
271 >
272 <path
273 stroke-linecap="round"
274 stroke-linejoin="round"
275 d="M7.217 10.907a2.25 2.25 0 100 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186l9.566-5.314m-9.566 7.5l9.566 5.314m0 0a2.25 2.25 0 103.935 2.186 2.25 2.25 0 00-3.935-2.186zm0-12.814a2.25 2.25 0 103.933-2.185 2.25 2.25 0 00-3.933 2.185z"
276 />
277 </svg>
278 Share
279 </Button>
280 {/if}
281 </div>
282 </Navbar>
283{/if}