tangled
alpha
login
or
join now
flo-bit.dev
/
blento
your personal website on atproto - mirror
blento.app
20
fork
atom
overview
issues
pulls
pipelines
improve saving
Florian
1 week ago
a2fae4c0
814cd48d
+187
-28
4 changed files
expand all
collapse all
unified
split
src
lib
helper.ts
website
EditBar.svelte
EditableWebsite.svelte
SaveModal.svelte
+7
-15
src/lib/helper.ts
···
455
) {
456
const promises = [];
457
// find all cards that have been updated (where items differ from originalItems)
458
-
for (const item of currentItems) {
459
const originalItem = data.cards.find((i) => cardsEqual(i, item));
460
461
if (!originalItem) {
462
-
let parsedItem = JSON.parse(JSON.stringify(item));
463
-
console.log('updated or new item', parsedItem);
464
-
parsedItem.updatedAt = new Date().toISOString();
465
// run optional upload function for this card type
466
-
const cardDef = CardDefinitionsByType[parsedItem.cardType];
467
468
if (cardDef?.upload) {
469
-
parsedItem = await cardDef?.upload(parsedItem);
470
}
0
0
471
472
parsedItem.page = data.page;
473
parsedItem.version = 2;
···
536
}
537
538
await Promise.all(promises);
539
-
540
-
fetch('/' + data.handle + '/api/refresh').then(() => {
541
-
console.log('data refreshed!');
542
-
});
543
-
console.log('refreshing data');
544
-
545
-
toast('Saved', {
546
-
description: 'Your website has been saved!'
547
-
});
548
}
549
550
export function createEmptyCard(page: string) {
···
455
) {
456
const promises = [];
457
// find all cards that have been updated (where items differ from originalItems)
458
+
for (let item of currentItems) {
459
const originalItem = data.cards.find((i) => cardsEqual(i, item));
460
461
if (!originalItem) {
462
+
console.log('updated or new item', item);
463
+
item.updatedAt = new Date().toISOString();
0
464
// run optional upload function for this card type
465
+
const cardDef = CardDefinitionsByType[item.cardType];
466
467
if (cardDef?.upload) {
468
+
item = await cardDef?.upload(item);
469
}
470
+
471
+
const parsedItem = JSON.parse(JSON.stringify(item));
472
473
parsedItem.page = data.page;
474
parsedItem.version = 2;
···
537
}
538
539
await Promise.all(promises);
0
0
0
0
0
0
0
0
0
540
}
541
542
export function createEmptyCard(page: string) {
+41
-10
src/lib/website/EditBar.svelte
···
2
import { dev } from '$app/environment';
3
import { user } from '$lib/atproto';
4
import type { WebsiteData } from '$lib/types';
5
-
import { Button, Input, Modal, Navbar, Popover, Toggle } from '@foxui/core';
6
7
let {
8
data,
···
12
13
showingMobileView = $bindable(),
14
isSaving = $bindable(),
0
15
16
save,
17
···
26
showingMobileView: boolean;
27
28
isSaving: boolean;
0
29
30
save: () => Promise<void>;
31
···
38
let imageInputRef: HTMLInputElement | undefined = $state();
39
let videoInputRef: HTMLInputElement | undefined = $state();
40
41
-
let shareModalOpen = $state(false);
0
0
0
0
0
0
0
0
0
0
0
42
</script>
43
44
<input
···
58
multiple
59
bind:this={videoInputRef}
60
/>
61
-
62
-
<Modal bind:open={shareModalOpen}></Modal>
63
64
{#if dev || (user.isLoggedIn && user.profile?.did === data.did)}
65
<Navbar
···
241
/>
242
</svg>
243
</Toggle>
244
-
<Button
245
-
disabled={isSaving}
246
-
onclick={async () => {
247
-
save();
248
-
}}>{isSaving ? 'Saving...' : 'Save'}</Button
249
-
>
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
250
</div>
251
</Navbar>
252
{/if}
···
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,
···
12
13
showingMobileView = $bindable(),
14
isSaving = $bindable(),
15
+
hasUnsavedChanges,
16
17
save,
18
···
27
showingMobileView: boolean;
28
29
isSaving: boolean;
30
+
hasUnsavedChanges: boolean;
31
32
save: () => Promise<void>;
33
···
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
···
71
multiple
72
bind:this={videoInputRef}
73
/>
0
0
74
75
{#if dev || (user.isLoggedIn && user.profile?.did === data.did)}
76
<Navbar
···
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}
+51
-3
src/lib/website/EditableWebsite.svelte
···
32
import { compressImage } from '../helper';
33
import Account from './Account.svelte';
34
import EditBar from './EditBar.svelte';
0
35
import { user } from '$lib/atproto';
0
36
37
let {
38
data
···
47
48
// svelte-ignore state_referenced_locally
49
let publication = $state(JSON.stringify(data.publication));
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
50
51
let container: HTMLDivElement | undefined = $state();
52
···
128
}
129
130
let isSaving = $state(false);
0
0
131
132
let newItem: { modal?: Component<CreationModalComponentProps>; item?: Item } = $state({});
133
134
async function save() {
135
isSaving = true;
0
0
136
137
try {
138
// Upload profile icon if changed
···
143
await savePage(data, items, publication);
144
145
publication = JSON.stringify(data.publication);
0
0
0
0
0
0
0
0
0
0
0
146
} catch (error) {
147
console.log(error);
0
148
toast.error('Error saving page!');
149
} finally {
150
isSaving = false;
···
320
const isGif = file.type === 'image/gif';
321
322
// Don't compress GIFs to preserve animation
323
-
const processedFile = isGif ? file : await compressImage(file);
324
-
const objectUrl = URL.createObjectURL(processedFile);
325
326
let item = createEmptyCard(data.page);
327
328
item.cardType = isGif ? 'gif' : 'image';
329
item.cardData = {
330
-
image: { blob: processedFile, objectUrl }
331
};
332
333
// If grid position is provided
···
559
/>
560
{/if}
561
0
0
0
0
0
0
0
562
<div
563
class={[
564
'@container/wrapper relative w-full',
···
784
bind:linkValue
785
bind:isSaving
786
bind:showingMobileView
0
787
{newCard}
788
{addLink}
789
{save}
···
32
import { compressImage } from '../helper';
33
import Account from './Account.svelte';
34
import EditBar from './EditBar.svelte';
35
+
import SaveModal from './SaveModal.svelte';
36
import { user } from '$lib/atproto';
37
+
import { launchConfetti } from '@foxui/visual';
38
39
let {
40
data
···
49
50
// svelte-ignore state_referenced_locally
51
let publication = $state(JSON.stringify(data.publication));
52
+
53
+
// Track saved state for comparison
54
+
// svelte-ignore state_referenced_locally
55
+
let savedItems = $state(JSON.stringify(data.cards));
56
+
// svelte-ignore state_referenced_locally
57
+
let savedPublication = $state(JSON.stringify(data.publication));
58
+
59
+
let hasUnsavedChanges = $derived(
60
+
JSON.stringify(items) !== savedItems || JSON.stringify(data.publication) !== savedPublication
61
+
);
62
+
63
+
// Warn user before closing tab if there are unsaved changes
64
+
$effect(() => {
65
+
function handleBeforeUnload(e: BeforeUnloadEvent) {
66
+
if (hasUnsavedChanges) {
67
+
e.preventDefault();
68
+
return '';
69
+
}
70
+
}
71
+
72
+
window.addEventListener('beforeunload', handleBeforeUnload);
73
+
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
74
+
});
75
76
let container: HTMLDivElement | undefined = $state();
77
···
153
}
154
155
let isSaving = $state(false);
156
+
let showSaveModal = $state(false);
157
+
let saveSuccess = $state(false);
158
159
let newItem: { modal?: Component<CreationModalComponentProps>; item?: Item } = $state({});
160
161
async function save() {
162
isSaving = true;
163
+
saveSuccess = false;
164
+
showSaveModal = true;
165
166
try {
167
// Upload profile icon if changed
···
172
await savePage(data, items, publication);
173
174
publication = JSON.stringify(data.publication);
175
+
176
+
// Update saved state
177
+
savedItems = JSON.stringify(items);
178
+
savedPublication = JSON.stringify(data.publication);
179
+
180
+
saveSuccess = true;
181
+
182
+
launchConfetti();
183
+
184
+
// Refresh cached data
185
+
await fetch('/' + data.handle + '/api/refresh');
186
} catch (error) {
187
console.log(error);
188
+
showSaveModal = false;
189
toast.error('Error saving page!');
190
} finally {
191
isSaving = false;
···
361
const isGif = file.type === 'image/gif';
362
363
// Don't compress GIFs to preserve animation
364
+
const objectUrl = URL.createObjectURL(file);
0
365
366
let item = createEmptyCard(data.page);
367
368
item.cardType = isGif ? 'gif' : 'image';
369
item.cardData = {
370
+
image: { blob: file, objectUrl }
371
};
372
373
// If grid position is provided
···
599
/>
600
{/if}
601
602
+
<SaveModal
603
+
bind:open={showSaveModal}
604
+
success={saveSuccess}
605
+
handle={data.handle}
606
+
page={data.page}
607
+
/>
608
+
609
<div
610
class={[
611
'@container/wrapper relative w-full',
···
831
bind:linkValue
832
bind:isSaving
833
bind:showingMobileView
834
+
{hasUnsavedChanges}
835
{newCard}
836
{addLink}
837
{save}
+88
src/lib/website/SaveModal.svelte
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
<script lang="ts">
2
+
import { Button, Modal, toast } from '@foxui/core';
3
+
4
+
let {
5
+
open = $bindable(),
6
+
success,
7
+
handle,
8
+
page
9
+
}: {
10
+
open: boolean;
11
+
success: boolean;
12
+
handle: string;
13
+
page: string;
14
+
} = $props();
15
+
16
+
function getShareUrl() {
17
+
const base = typeof window !== 'undefined' ? window.location.origin : '';
18
+
const pagePath = page && page !== 'blento.self' ? `/${page.replace('blento.', '')}` : '';
19
+
return `${base}/${handle}${pagePath}`;
20
+
}
21
+
22
+
async function copyShareLink() {
23
+
const url = getShareUrl();
24
+
await navigator.clipboard.writeText(url);
25
+
toast.success('Link copied to clipboard!');
26
+
}
27
+
</script>
28
+
29
+
<Modal {open} closeButton={false}>
30
+
<div class="flex flex-col items-center gap-4">
31
+
{#if !success}
32
+
<div class="flex items-center gap-4">
33
+
<svg
34
+
class="text-accent-500 size-8 animate-spin"
35
+
xmlns="http://www.w3.org/2000/svg"
36
+
fill="none"
37
+
viewBox="0 0 24 24"
38
+
>
39
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"
40
+
></circle>
41
+
<path
42
+
class="opacity-75"
43
+
fill="currentColor"
44
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
45
+
></path>
46
+
</svg>
47
+
<p class="text-base-700 dark:text-base-300 text-3xl font-bold">Saving...</p>
48
+
</div>
49
+
{:else}
50
+
<div class="flex items-center gap-4">
51
+
<svg
52
+
xmlns="http://www.w3.org/2000/svg"
53
+
viewBox="0 0 24 24"
54
+
fill="currentColor"
55
+
class="text-accent-500 size-8"
56
+
>
57
+
<path
58
+
fill-rule="evenodd"
59
+
d="M8.603 3.799A4.49 4.49 0 0 1 12 2.25c1.357 0 2.573.6 3.397 1.549a4.49 4.49 0 0 1 3.498 1.307 4.491 4.491 0 0 1 1.307 3.497A4.49 4.49 0 0 1 21.75 12a4.49 4.49 0 0 1-1.549 3.397 4.491 4.491 0 0 1-1.307 3.497 4.491 4.491 0 0 1-3.497 1.307A4.49 4.49 0 0 1 12 21.75a4.49 4.49 0 0 1-3.397-1.549 4.49 4.49 0 0 1-3.498-1.306 4.491 4.491 0 0 1-1.307-3.498A4.49 4.49 0 0 1 2.25 12c0-1.357.6-2.573 1.549-3.397a4.49 4.49 0 0 1 1.307-3.497 4.49 4.49 0 0 1 3.497-1.307Zm7.007 6.387a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z"
60
+
clip-rule="evenodd"
61
+
/>
62
+
</svg>
63
+
64
+
<p class="text-base-700 dark:text-base-300 text-3xl font-bold">Website Saved</p>
65
+
</div>
66
+
<div class="mt-8 flex w-full flex-col gap-2">
67
+
<Button size="lg" onclick={copyShareLink}>
68
+
<svg
69
+
xmlns="http://www.w3.org/2000/svg"
70
+
fill="none"
71
+
viewBox="0 0 24 24"
72
+
stroke-width="1.5"
73
+
stroke="currentColor"
74
+
class="size-5"
75
+
>
76
+
<path
77
+
stroke-linecap="round"
78
+
stroke-linejoin="round"
79
+
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"
80
+
/>
81
+
</svg>
82
+
Share link
83
+
</Button>
84
+
<Button variant="ghost" onclick={() => (open = false)}>Close</Button>
85
+
</div>
86
+
{/if}
87
+
</div>
88
+
</Modal>