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