From b2ad9efd9c93bcb77849d197c1892bd5d80e12bb Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Mon, 2 Jun 2025 06:19:39 -0700 Subject: [PATCH] Fixes for UniverseComposer and delete modals. Also standardizing Publish buttons and whatnot --- src/assets/styles/variables.scss | 2 + src/lib/components/ProjectItem.svelte | 53 ++++-- src/lib/components/SegmentedController.svelte | 6 +- src/lib/components/admin/AdminPage.svelte | 1 + src/lib/components/admin/Button.svelte | 12 +- .../admin/DeleteConfirmationModal.svelte | 2 +- src/lib/components/admin/DropdownItem.svelte | 59 +++++++ src/lib/components/admin/DropdownMenu.svelte | 134 +++++++++++++++ .../admin/DropdownMenuContainer.svelte | 28 +++ .../components/admin/EditorWithUpload.svelte | 65 ++++--- src/lib/components/admin/EssayForm.svelte | 6 +- .../components/admin/GalleryUploader.svelte | 2 +- src/lib/components/admin/ImageUploader.svelte | 12 +- .../components/admin/MediaDetailsModal.svelte | 2 +- src/lib/components/admin/MediaSelector.svelte | 6 +- .../components/admin/MediaUploadModal.svelte | 6 +- src/lib/components/admin/PostDropdown.svelte | 2 +- src/lib/components/admin/ProjectForm.svelte | 144 ++-------------- .../components/admin/ProjectListItem.svelte | 54 +++--- .../components/admin/PublishDropdown.svelte | 107 ++++++++++++ .../components/admin/SaveActionsGroup.svelte | 68 ++++++++ .../components/admin/StatusDropdown.svelte | 118 +++++++++++++ .../components/admin/UniverseComposer.svelte | 22 +-- src/lib/components/edra/editor.ts | 5 +- .../components/edra/headless/editor.svelte | 8 +- src/routes/admin/albums/+page.svelte | 4 +- .../admin/albums/[id]/edit/+page.svelte | 159 +++++++++++------- src/routes/admin/albums/new/+page.svelte | 54 +++++- src/routes/admin/buttons/+page.svelte | 18 +- src/routes/admin/inputs/+page.svelte | 6 +- src/routes/admin/media/+page.svelte | 12 +- src/routes/admin/media/upload/+page.svelte | 6 +- src/routes/admin/posts/+page.svelte | 1 + src/routes/admin/posts/[id]/edit/+page.svelte | 93 +++------- src/routes/admin/posts/new/+page.svelte | 67 +------- src/routes/admin/projects/+page.svelte | 27 +-- src/routes/api/posts/+server.ts | 14 ++ src/stories/Header.svelte | 6 +- src/stories/admin/ButtonShowcase.svelte | 6 +- 39 files changed, 933 insertions(+), 464 deletions(-) create mode 100644 src/lib/components/admin/DropdownItem.svelte create mode 100644 src/lib/components/admin/DropdownMenu.svelte create mode 100644 src/lib/components/admin/DropdownMenuContainer.svelte create mode 100644 src/lib/components/admin/PublishDropdown.svelte create mode 100644 src/lib/components/admin/SaveActionsGroup.svelte create mode 100644 src/lib/components/admin/StatusDropdown.svelte diff --git a/src/assets/styles/variables.scss b/src/assets/styles/variables.scss index 74108c6..6fd23bb 100644 --- a/src/assets/styles/variables.scss +++ b/src/assets/styles/variables.scss @@ -20,6 +20,8 @@ $unit-8x: $unit * 8; $unit-10x: $unit * 10; $unit-12x: $unit * 12; $unit-14x: $unit * 14; +$unit-16x: $unit * 16; +$unit-18x: $unit * 18; $unit-20x: $unit * 20; /* Page properties diff --git a/src/lib/components/ProjectItem.svelte b/src/lib/components/ProjectItem.svelte index e1fb196..1d24ae9 100644 --- a/src/lib/components/ProjectItem.svelte +++ b/src/lib/components/ProjectItem.svelte @@ -188,22 +188,55 @@

{@html highlightedDescription}

- + {#if isListOnly}
- - - - + + + + Coming Soon
{:else if isPasswordProtected}
- - - - + + + + Password Required
@@ -248,7 +281,7 @@ } &.odd { - flex-direction: row-reverse; + // flex-direction: row-reverse; } } diff --git a/src/lib/components/SegmentedController.svelte b/src/lib/components/SegmentedController.svelte index 08c693b..31ae2df 100644 --- a/src/lib/components/SegmentedController.svelte +++ b/src/lib/components/SegmentedController.svelte @@ -16,8 +16,8 @@ const navItems: NavItem[] = [ { icon: WorkIcon, text: 'Work', href: '/', variant: 'work' }, - { icon: PhotosIcon, text: 'Photos', href: '/photos', variant: 'photos' }, { icon: LabsIcon, text: 'Labs', href: '/labs', variant: 'labs' }, + { icon: PhotosIcon, text: 'Photos', href: '/photos', variant: 'photos' }, { icon: UniverseIcon, text: 'Universe', href: '/universe', variant: 'universe' } ] @@ -28,9 +28,9 @@ const activeIndex = $derived( currentPath === '/' ? 0 - : currentPath.startsWith('/photos') + : currentPath.startsWith('/labs') ? 1 - : currentPath.startsWith('/labs') + : currentPath.startsWith('/photos') ? 2 : currentPath.startsWith('/universe') ? 3 diff --git a/src/lib/components/admin/AdminPage.svelte b/src/lib/components/admin/AdminPage.svelte index 544fc21..5f3d840 100644 --- a/src/lib/components/admin/AdminPage.svelte +++ b/src/lib/components/admin/AdminPage.svelte @@ -31,6 +31,7 @@ margin: 0 auto $unit-6x; width: calc(100% - #{$unit-6x}); max-width: 900px; // Much wider for admin + min-height: calc(100vh - #{$unit-16x}); // Full height minus margins overflow: hidden; // Ensure border-radius clips content &:first-child { diff --git a/src/lib/components/admin/Button.svelte b/src/lib/components/admin/Button.svelte index ea338a2..2a9fde4 100644 --- a/src/lib/components/admin/Button.svelte +++ b/src/lib/components/admin/Button.svelte @@ -3,7 +3,7 @@ interface Props extends HTMLButtonAttributes { variant?: 'primary' | 'secondary' | 'danger' | 'ghost' | 'text' | 'overlay' - size?: 'small' | 'medium' | 'large' | 'icon' + buttonSize?: 'small' | 'medium' | 'large' | 'icon' iconOnly?: boolean iconPosition?: 'left' | 'right' pill?: boolean @@ -16,7 +16,7 @@ let { variant = 'primary', - size = 'medium', + buttonSize = 'medium', iconOnly = false, iconPosition = 'left', pill = true, @@ -41,10 +41,10 @@ // Size if (!iconOnly) { - classes.push(`btn-${size}`) + classes.push(`btn-${buttonSize}`) } else { classes.push('btn-icon') - classes.push(`btn-icon-${size}`) + classes.push(`btn-icon-${buttonSize}`) } // States @@ -254,8 +254,8 @@ } &.btn-icon-large { - width: 40px; - height: 40px; + width: 44px; + height: 44px; border-radius: 10px; } diff --git a/src/lib/components/admin/DeleteConfirmationModal.svelte b/src/lib/components/admin/DeleteConfirmationModal.svelte index 6e4c838..aa3dc38 100644 --- a/src/lib/components/admin/DeleteConfirmationModal.svelte +++ b/src/lib/components/admin/DeleteConfirmationModal.svelte @@ -64,7 +64,7 @@ display: flex; align-items: center; justify-content: center; - z-index: 1000; + z-index: 1050; } .modal { diff --git a/src/lib/components/admin/DropdownItem.svelte b/src/lib/components/admin/DropdownItem.svelte new file mode 100644 index 0000000..315d8a6 --- /dev/null +++ b/src/lib/components/admin/DropdownItem.svelte @@ -0,0 +1,59 @@ + + + + + \ No newline at end of file diff --git a/src/lib/components/admin/DropdownMenu.svelte b/src/lib/components/admin/DropdownMenu.svelte new file mode 100644 index 0000000..70308b8 --- /dev/null +++ b/src/lib/components/admin/DropdownMenu.svelte @@ -0,0 +1,134 @@ + + +{#if isOpen && browser} + +{/if} + + \ No newline at end of file diff --git a/src/lib/components/admin/DropdownMenuContainer.svelte b/src/lib/components/admin/DropdownMenuContainer.svelte new file mode 100644 index 0000000..55d2551 --- /dev/null +++ b/src/lib/components/admin/DropdownMenuContainer.svelte @@ -0,0 +1,28 @@ + + + + + \ No newline at end of file diff --git a/src/lib/components/admin/EditorWithUpload.svelte b/src/lib/components/admin/EditorWithUpload.svelte index 7b5e346..68b161d 100644 --- a/src/lib/components/admin/EditorWithUpload.svelte +++ b/src/lib/components/admin/EditorWithUpload.svelte @@ -206,28 +206,54 @@ } }) - // Custom image paste handler - function handleImagePaste(view: any, event: ClipboardEvent) { - const item = event.clipboardData?.items[0] + // Custom paste handler for both images and text + function handlePaste(view: any, event: ClipboardEvent) { + const clipboardData = event.clipboardData + if (!clipboardData) return false - if (item?.type.indexOf('image') !== 0) { - return false + // Check for images first + const imageItem = Array.from(clipboardData.items).find(item => item.type.indexOf('image') === 0) + if (imageItem) { + const file = imageItem.getAsFile() + if (!file) return false + + // Check file size (2MB max) + const filesize = file.size / 1024 / 1024 + if (filesize > 2) { + alert(`Image too large! File size: ${filesize.toFixed(2)} MB (max 2MB)`) + return true + } + + // Upload to our media API + uploadImage(file) + return true // Prevent default paste behavior } - const file = item.getAsFile() - if (!file) return false - - // Check file size (2MB max) - const filesize = file.size / 1024 / 1024 - if (filesize > 2) { - alert(`Image too large! File size: ${filesize.toFixed(2)} MB (max 2MB)`) - return true + // Handle text paste - strip HTML formatting + const htmlData = clipboardData.getData('text/html') + const plainText = clipboardData.getData('text/plain') + + if (htmlData && plainText) { + // If we have both HTML and plain text, use plain text to strip formatting + event.preventDefault() + + // Use editor commands to insert text so all callbacks are triggered + const editorInstance = (view as any).editor + if (editorInstance) { + editorInstance.chain().focus().insertContent(plainText).run() + } else { + // Fallback to manual transaction + const { state, dispatch } = view + const { selection } = state + const transaction = state.tr.insertText(plainText, selection.from, selection.to) + dispatch(transaction) + } + + return true // Prevent default paste behavior } - // Upload to our media API - uploadImage(file) - - return true // Prevent default paste behavior + // Let default handling take care of plain text only + return false } async function uploadImage(file: File) { @@ -321,9 +347,10 @@ attributes: { class: 'prose prose-sm max-w-none focus:outline-none' }, - handlePaste: handleImagePaste + handlePaste: handlePaste } - } + }, + placeholder ) // Add placeholder diff --git a/src/lib/components/admin/EssayForm.svelte b/src/lib/components/admin/EssayForm.svelte index 1406133..ba17ea8 100644 --- a/src/lib/components/admin/EssayForm.svelte +++ b/src/lib/components/admin/EssayForm.svelte @@ -204,7 +204,7 @@
@@ -307,7 +307,7 @@ - - diff --git a/src/lib/components/admin/MediaSelector.svelte b/src/lib/components/admin/MediaSelector.svelte index 2ace5c0..2849ec5 100644 --- a/src/lib/components/admin/MediaSelector.svelte +++ b/src/lib/components/admin/MediaSelector.svelte @@ -235,9 +235,9 @@ >
- {#if item.thumbnailUrl} + {#if item.mimeType?.startsWith('image/')} {item.filename} @@ -317,7 +317,7 @@ class="load-more-button" > {#if loading} - + Loading... {:else} Load More diff --git a/src/lib/components/admin/MediaUploadModal.svelte b/src/lib/components/admin/MediaUploadModal.svelte index b99ea1a..9dac198 100644 --- a/src/lib/components/admin/MediaUploadModal.svelte +++ b/src/lib/components/admin/MediaUploadModal.svelte @@ -258,17 +258,17 @@

Files to Upload

-
{#if !isLoading} -
- - - {#if showPublishMenu} -
- {#if formData.status !== 'draft'} - - {/if} - {#if formData.status !== 'published'} - - {/if} - {#if formData.status !== 'list-only'} - - {/if} - {#if formData.status !== 'password-protected'} - - {/if} -
- {/if} -
+ isLoading={isSaving} + primaryAction={{ label: 'Publish', status: 'published' }} + dropdownActions={[ + { label: 'Save as Draft', status: 'draft' }, + { label: 'List Only', status: 'list-only', show: formData.status !== 'list-only' }, + { label: 'Password Protected', status: 'password-protected', show: formData.status !== 'password-protected' } + ]} + /> + {/if} {/if}
@@ -414,54 +351,7 @@ } } - .save-actions { - position: relative; - display: flex; - gap: $unit-half; - } - /* Button-specific styles handled by Button component */ - - /* Custom button styles */ - :global(.save-button) { - border-top-right-radius: 0; - border-bottom-right-radius: 0; - padding-right: $unit-2x; - } - - :global(.chevron-button) { - border-top-left-radius: 0; - border-bottom-left-radius: 0; - border-left: 1px solid rgba(255, 255, 255, 0.2); - - svg { - display: block; - transition: transform 0.2s ease; - } - - &.active svg { - transform: rotate(180deg); - } - } - - .publish-menu { - position: absolute; - top: 100%; - right: 0; - margin-top: $unit; - background: white; - border-radius: $unit; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); - overflow: hidden; - min-width: 120px; - z-index: 100; - - /* Menu item styles handled by Button component */ - :global(.menu-item) { - text-align: left; - justify-content: flex-start; - } - } .tab-panels { position: relative; diff --git a/src/lib/components/admin/ProjectListItem.svelte b/src/lib/components/admin/ProjectListItem.svelte index 7f44991..0dae8a2 100644 --- a/src/lib/components/admin/ProjectListItem.svelte +++ b/src/lib/components/admin/ProjectListItem.svelte @@ -1,6 +1,6 @@
@@ -240,7 +255,7 @@ background-color: $grey-95; } - &.delete { + &.danger { color: $red-60; } } @@ -250,4 +265,5 @@ background-color: $grey-90; margin: $unit-half 0; } + diff --git a/src/lib/components/admin/PublishDropdown.svelte b/src/lib/components/admin/PublishDropdown.svelte new file mode 100644 index 0000000..679aa8e --- /dev/null +++ b/src/lib/components/admin/PublishDropdown.svelte @@ -0,0 +1,107 @@ + + +
+ + + {#if showDropdown} + + + {#if isDropdownOpen} + + + {saveDraftText} + + + {/if} + {/if} +
+ + diff --git a/src/lib/components/admin/SaveActionsGroup.svelte b/src/lib/components/admin/SaveActionsGroup.svelte new file mode 100644 index 0000000..bb46063 --- /dev/null +++ b/src/lib/components/admin/SaveActionsGroup.svelte @@ -0,0 +1,68 @@ + + +{#if status === 'draft'} + +{:else if status === 'published'} + +{:else} + + +{/if} \ No newline at end of file diff --git a/src/lib/components/admin/StatusDropdown.svelte b/src/lib/components/admin/StatusDropdown.svelte new file mode 100644 index 0000000..6ea73f3 --- /dev/null +++ b/src/lib/components/admin/StatusDropdown.svelte @@ -0,0 +1,118 @@ + + +
+ + + {#if availableActions.length > 0} + + + {#if isDropdownOpen} + + {#each availableActions as action} + handleDropdownAction(action.status)}> + {action.label} + + {/each} + + {/if} + {/if} +
+ + \ No newline at end of file diff --git a/src/lib/components/admin/UniverseComposer.svelte b/src/lib/components/admin/UniverseComposer.svelte index 65b0c02..2e77ece 100644 --- a/src/lib/components/admin/UniverseComposer.svelte +++ b/src/lib/components/admin/UniverseComposer.svelte @@ -17,6 +17,7 @@ export let initialMode: 'modal' | 'page' = 'modal' export let initialPostType: 'post' | 'essay' = 'post' export let initialContent: JSONContent | undefined = undefined + export let closeOnSave = true type PostType = 'post' | 'essay' type ComposerMode = 'modal' | 'page' @@ -67,8 +68,7 @@ } function resetComposer() { - postType = 'post' - mode = 'modal' + postType = initialPostType content = { type: 'doc', content: [{ type: 'paragraph' }] @@ -248,7 +248,9 @@ if (response.ok) { resetComposer() - isOpen = false + if (closeOnSave) { + isOpen = false + } dispatch('saved') if (postType === 'essay') { goto('/admin/posts') @@ -372,7 +374,7 @@ + {/snippet} @@ -219,7 +219,7 @@
Photography Album - Show this album in the photography experience + Show this album in the photography experience
@@ -675,30 +673,62 @@

Photos ({albumPhotos.length})

- -
- + isDeleteModalOpen = false} + onCancel={() => (isDeleteModalOpen = false)} /> @@ -781,6 +811,7 @@ gap: $unit-2x; } + .btn-icon { width: 40px; height: 40px; @@ -849,7 +880,7 @@ display: flex; align-items: center; justify-content: space-between; - + h2 { border-bottom: none; padding-bottom: 0; @@ -1203,4 +1234,4 @@ } } } - \ No newline at end of file + diff --git a/src/routes/admin/albums/new/+page.svelte b/src/routes/admin/albums/new/+page.svelte index efbfd04..7449edf 100644 --- a/src/routes/admin/albums/new/+page.svelte +++ b/src/routes/admin/albums/new/+page.svelte @@ -4,6 +4,7 @@ import Button from '$lib/components/admin/Button.svelte' import Input from '$lib/components/admin/Input.svelte' import FormFieldWrapper from '$lib/components/admin/FormFieldWrapper.svelte' + import PublishDropdown from '$lib/components/admin/PublishDropdown.svelte' // Form state let title = $state('') @@ -96,6 +97,7 @@ goto('/admin/albums') } + const canSave = $derived(title.trim().length > 0 && slug.trim().length > 0) @@ -116,15 +118,43 @@

New Album

- - - +
+ + + {#if isPublishDropdownOpen} + + {handleSave('draft'); isPublishDropdownOpen = false}}> + Save as Draft + + + {/if} +
@@ -248,6 +278,12 @@ gap: $unit-2x; } + .publish-dropdown { + position: relative; + display: flex; + gap: $unit-half; + } + .btn-icon { width: 40px; height: 40px; diff --git a/src/routes/admin/buttons/+page.svelte b/src/routes/admin/buttons/+page.svelte index 23c06b3..a8b2293 100644 --- a/src/routes/admin/buttons/+page.svelte +++ b/src/routes/admin/buttons/+page.svelte @@ -19,31 +19,31 @@

Sizes

- - - + + +

Icon Buttons

- - - - + - +
diff --git a/src/routes/admin/inputs/+page.svelte b/src/routes/admin/inputs/+page.svelte index d2619b4..c0428bd 100644 --- a/src/routes/admin/inputs/+page.svelte +++ b/src/routes/admin/inputs/+page.svelte @@ -103,19 +103,19 @@

Input Sizes

diff --git a/src/routes/admin/media/+page.svelte b/src/routes/admin/media/+page.svelte index 62fe6f5..049cc39 100644 --- a/src/routes/admin/media/+page.svelte +++ b/src/routes/admin/media/+page.svelte @@ -320,7 +320,7 @@ {#snippet actions()} - + {/snippet} @@ -348,14 +348,14 @@ @@ -366,7 +366,7 @@ bind:value={searchQuery} onkeydown={(e) => e.key === 'Enter' && handleSearch()} placeholder="Search files..." - size="small" + buttonSize="small" fullWidth={false} pill={true} prefixIcon diff --git a/src/routes/admin/media/upload/+page.svelte b/src/routes/admin/media/upload/+page.svelte index 1e28407..3b8958a 100644 --- a/src/routes/admin/media/upload/+page.svelte +++ b/src/routes/admin/media/upload/+page.svelte @@ -200,17 +200,17 @@

Files to Upload

-
diff --git a/src/routes/admin/posts/[id]/edit/+page.svelte b/src/routes/admin/posts/[id]/edit/+page.svelte index aff4675..ba6077b 100644 --- a/src/routes/admin/posts/[id]/edit/+page.svelte +++ b/src/routes/admin/posts/[id]/edit/+page.svelte @@ -6,6 +6,9 @@ import Editor from '$lib/components/admin/Editor.svelte' import LoadingSpinner from '$lib/components/admin/LoadingSpinner.svelte' import MetadataPopover from '$lib/components/admin/MetadataPopover.svelte' + import DeleteConfirmationModal from '$lib/components/admin/DeleteConfirmationModal.svelte' + import Button from '$lib/components/admin/Button.svelte' + import SaveActionsGroup from '$lib/components/admin/SaveActionsGroup.svelte' import type { JSONContent } from '@tiptap/core' let post = $state(null) @@ -22,9 +25,8 @@ let tags = $state([]) let tagInput = $state('') let showMetadata = $state(false) - let isPublishDropdownOpen = $state(false) - let publishButtonRef: HTMLButtonElement let metadataButtonRef: HTMLButtonElement + let showDeleteConfirmation = $state(false) const postTypeConfig = { post: { icon: '💭', label: 'Post', showTitle: false, showContent: true }, @@ -142,9 +144,12 @@ } } - async function handleDelete() { - if (!confirm('Are you sure you want to delete this post?')) return + function openDeleteConfirmation() { + showMetadata = false + showDeleteConfirmation = true + } + async function handleDelete() { const auth = localStorage.getItem('admin_auth') if (!auth) { goto('/admin/login') @@ -158,6 +163,7 @@ }) if (response.ok) { + showDeleteConfirmation = false goto('/admin/posts') } } catch (error) { @@ -165,11 +171,6 @@ } } - function handlePublishDropdown(event: MouseEvent) { - if (!publishButtonRef?.contains(event.target as Node)) { - isPublishDropdownOpen = false - } - } function handleMetadataPopover(event: MouseEvent) { const target = event.target as Node @@ -183,12 +184,6 @@ showMetadata = false } - $effect(() => { - if (isPublishDropdownOpen) { - document.addEventListener('click', handlePublishDropdown) - return () => document.removeEventListener('click', handlePublishDropdown) - } - }) $effect(() => { if (showMetadata) { @@ -244,48 +239,17 @@ bind:tagInput onAddTag={addTag} onRemoveTag={removeTag} - onDelete={handleDelete} + onDelete={openDeleteConfirmation} /> {/if}
- {#if status === 'draft'} -
- - {#if isPublishDropdownOpen} - - {/if} -
- {:else} - - {/if} +
{/if} @@ -319,6 +283,15 @@ {/if} + (showDeleteConfirmation = false)} +/> +