your personal website on atproto - mirror blento.app

improve saving

+187 -28
+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 - for (const item of currentItems) { 458 + for (let item of currentItems) { 459 459 const originalItem = data.cards.find((i) => cardsEqual(i, item)); 460 460 461 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(); 462 + console.log('updated or new item', item); 463 + item.updatedAt = new Date().toISOString(); 465 464 // run optional upload function for this card type 466 - const cardDef = CardDefinitionsByType[parsedItem.cardType]; 465 + const cardDef = CardDefinitionsByType[item.cardType]; 467 466 468 467 if (cardDef?.upload) { 469 - parsedItem = await cardDef?.upload(parsedItem); 468 + item = await cardDef?.upload(item); 470 469 } 470 + 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 - 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 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 - import { Button, Input, Modal, Navbar, Popover, Toggle } from '@foxui/core'; 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 + hasUnsavedChanges, 15 16 16 17 save, 17 18 ··· 26 27 showingMobileView: boolean; 27 28 28 29 isSaving: boolean; 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 - let shareModalOpen = $state(false); 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 + } 42 55 </script> 43 56 44 57 <input ··· 58 71 multiple 59 72 bind:this={videoInputRef} 60 73 /> 61 - 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 - <Button 245 - disabled={isSaving} 246 - onclick={async () => { 247 - save(); 248 - }}>{isSaving ? 'Saving...' : 'Save'}</Button 249 - > 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} 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 + import SaveModal from './SaveModal.svelte'; 35 36 import { user } from '$lib/atproto'; 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 + 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 + }); 50 75 51 76 let container: HTMLDivElement | undefined = $state(); 52 77 ··· 128 153 } 129 154 130 155 let isSaving = $state(false); 156 + let showSaveModal = $state(false); 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 + saveSuccess = false; 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 + 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'); 146 186 } catch (error) { 147 187 console.log(error); 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 - const processedFile = isGif ? file : await compressImage(file); 324 - const objectUrl = URL.createObjectURL(processedFile); 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 - image: { blob: processedFile, objectUrl } 370 + image: { blob: file, objectUrl } 331 371 }; 332 372 333 373 // If grid position is provided ··· 559 599 /> 560 600 {/if} 561 601 602 + <SaveModal 603 + bind:open={showSaveModal} 604 + success={saveSuccess} 605 + handle={data.handle} 606 + page={data.page} 607 + /> 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 + {hasUnsavedChanges} 787 835 {newCard} 788 836 {addLink} 789 837 {save}
+88
src/lib/website/SaveModal.svelte
··· 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>