Add dropdown to change object publish state and fix z-index

This commit is contained in:
Justin Edmund 2025-06-02 17:00:52 -07:00
parent f124fd1e69
commit 9ba787cd8b
15 changed files with 180 additions and 145 deletions

View file

@ -131,7 +131,7 @@
position: fixed; position: fixed;
inset: 0; inset: 0;
background: rgba(0, 0, 0, 0.9); background: rgba(0, 0, 0, 0.9);
z-index: 1000; z-index: 1400;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;

View file

@ -173,7 +173,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
z-index: 1000; z-index: 1400;
padding: $unit-2x; padding: $unit-2x;
@include breakpoint('phone') { @include breakpoint('phone') {

View file

@ -237,7 +237,7 @@
border-radius: 12px; border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
min-width: 150px; min-width: 150px;
z-index: 1000; z-index: 1050;
overflow: hidden; overflow: hidden;
animation: slideDown 0.2s ease; animation: slideDown 0.2s ease;
} }

View file

@ -272,7 +272,7 @@
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
overflow: hidden; overflow: hidden;
min-width: 180px; min-width: 180px;
z-index: 10; z-index: 1050;
} }
.dropdown-item { .dropdown-item {

View file

@ -97,7 +97,7 @@
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
overflow: hidden; overflow: hidden;
min-width: 180px; min-width: 180px;
z-index: 1000; z-index: 1050;
} }
.dropdown-item { .dropdown-item {

View file

@ -23,6 +23,6 @@
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
overflow: hidden; overflow: hidden;
min-width: 180px; min-width: 180px;
z-index: 10; z-index: 1050;
} }
</style> </style>

View file

@ -106,7 +106,7 @@
popoverElement.style.position = 'fixed' popoverElement.style.position = 'fixed'
popoverElement.style.top = `${top}px` popoverElement.style.top = `${top}px`
popoverElement.style.left = `${left}px` popoverElement.style.left = `${left}px`
popoverElement.style.zIndex = '1000' popoverElement.style.zIndex = '1200'
} }
function handleFieldUpdate(key: string, value: any) { function handleFieldUpdate(key: string, value: any) {

View file

@ -90,7 +90,7 @@
popoverElement.style.position = 'fixed' popoverElement.style.position = 'fixed'
popoverElement.style.top = `${top}px` popoverElement.style.top = `${top}px`
popoverElement.style.left = `${left}px` popoverElement.style.left = `${left}px`
popoverElement.style.zIndex = '1000' popoverElement.style.zIndex = '1200'
} }
onMount(() => { onMount(() => {

View file

@ -89,7 +89,7 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
z-index: 1000; z-index: 1400;
padding: $unit-2x; padding: $unit-2x;
} }

View file

@ -146,7 +146,7 @@
border-radius: $unit-2x; border-radius: $unit-2x;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
min-width: 140px; min-width: 140px;
z-index: 100; z-index: 1050;
overflow: hidden; overflow: hidden;
margin: 0; margin: 0;
padding: 0; padding: 0;

View file

@ -220,28 +220,26 @@
</div> </div>
<div class="header-actions"> <div class="header-actions">
{#if !isLoading} {#if !isLoading}
{#if formData.status === 'published'} <StatusDropdown
<Button variant="primary" buttonSize="large" onclick={handleSave} disabled={isSaving}> currentStatus={formData.status}
{isSaving ? 'Saving...' : 'Save'} onStatusChange={handleStatusChange}
</Button> disabled={isSaving}
{:else} isLoading={isSaving}
<StatusDropdown primaryAction={
currentStatus={formData.status} formData.status === 'published'
onStatusChange={handleStatusChange} ? { label: 'Save', status: 'published' }
disabled={isSaving} : { label: 'Publish', status: 'published' }
isLoading={isSaving} }
primaryAction={{ label: 'Publish', status: 'published' }} dropdownActions={[
dropdownActions={[ { label: 'Save as Draft', status: 'draft', show: formData.status !== 'draft' },
{ label: 'Save as Draft', status: 'draft' }, { label: 'List Only', status: 'list-only', show: formData.status !== 'list-only' },
{ label: 'List Only', status: 'list-only', show: formData.status !== 'list-only' }, {
{ label: 'Password Protected',
label: 'Password Protected', status: 'password-protected',
status: 'password-protected', show: formData.status !== 'password-protected'
show: formData.status !== 'password-protected' }
} ]}
]} />
/>
{/if}
{/if} {/if}
</div> </div>
</header> </header>

View file

@ -77,8 +77,11 @@ export async function uploadFile(
customOptions?: any customOptions?: any
): Promise<UploadResult> { ): Promise<UploadResult> {
try { try {
// TEMPORARY: Force Cloudinary usage for testing
const FORCE_CLOUDINARY_IN_DEV = true; // Toggle this to test
// Use local storage in development or when Cloudinary is not configured // Use local storage in development or when Cloudinary is not configured
if (dev || !isCloudinaryConfigured()) { if ((dev && !FORCE_CLOUDINARY_IN_DEV) || !isCloudinaryConfigured()) {
logger.info('Using local storage for file upload') logger.info('Using local storage for file upload')
const localResult = await uploadFileLocally(file, type) const localResult = await uploadFileLocally(file, type)
@ -123,14 +126,13 @@ export async function uploadFile(
} }
// Log upload attempt for debugging // Log upload attempt for debugging
if (isSvg) { logger.info('Attempting file upload:', {
logger.info('Attempting SVG upload with options:', { filename: file.name,
filename: file.name, mimeType: file.type,
mimeType: file.type, size: file.size,
size: file.size, isSvg,
uploadOptions uploadOptions
}) })
}
// Upload to Cloudinary // Upload to Cloudinary
const result = await new Promise<UploadApiResponse>((resolve, reject) => { const result = await new Promise<UploadApiResponse>((resolve, reject) => {
@ -169,6 +171,17 @@ export async function uploadFile(
logger.error('Cloudinary upload failed', error as Error) logger.error('Cloudinary upload failed', error as Error)
logger.mediaUpload(file.name, file.size, file.type, false) logger.mediaUpload(file.name, file.size, file.type, false)
// Enhanced error logging
if (error instanceof Error) {
logger.error('Upload error details:', {
filename: file.name,
mimeType: file.type,
size: file.size,
errorMessage: error.message,
errorStack: error.stack
})
}
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : 'Upload failed' error: error instanceof Error ? error.message : 'Upload failed'

View file

@ -10,7 +10,7 @@
import MediaLibraryModal from '$lib/components/admin/MediaLibraryModal.svelte' import MediaLibraryModal from '$lib/components/admin/MediaLibraryModal.svelte'
import MediaDetailsModal from '$lib/components/admin/MediaDetailsModal.svelte' import MediaDetailsModal from '$lib/components/admin/MediaDetailsModal.svelte'
import GalleryUploader from '$lib/components/admin/GalleryUploader.svelte' import GalleryUploader from '$lib/components/admin/GalleryUploader.svelte'
import SaveActionsGroup from '$lib/components/admin/SaveActionsGroup.svelte' import StatusDropdown from '$lib/components/admin/StatusDropdown.svelte'
import AlbumMetadataPopover from '$lib/components/admin/AlbumMetadataPopover.svelte' import AlbumMetadataPopover from '$lib/components/admin/AlbumMetadataPopover.svelte'
// Form state // Form state
@ -93,7 +93,7 @@
} }
} }
async function handleSave(publishStatus?: 'draft' | 'published') { async function handleSave(newStatus?: string) {
if (!title.trim()) { if (!title.trim()) {
error = 'Title is required' error = 'Title is required'
return return
@ -122,7 +122,7 @@
location: location.trim() || null, location: location.trim() || null,
isPhotography, isPhotography,
showInUniverse, showInUniverse,
status: publishStatus || status status: newStatus || status
} }
const response = await fetch(`/api/albums/${album.id}`, { const response = await fetch(`/api/albums/${album.id}`, {
@ -142,8 +142,8 @@
const updatedAlbum = await response.json() const updatedAlbum = await response.json()
album = updatedAlbum album = updatedAlbum
if (publishStatus) { if (newStatus) {
status = publishStatus status = newStatus
} }
} catch (err) { } catch (err) {
error = err instanceof Error ? err.message : 'Failed to update album' error = err instanceof Error ? err.message : 'Failed to update album'
@ -591,12 +591,19 @@
/> />
{/if} {/if}
</div> </div>
<SaveActionsGroup <StatusDropdown
{status} currentStatus={status}
onSave={handleSave} onStatusChange={handleSave}
disabled={isSaving} disabled={isSaving}
isLoading={isSaving} isLoading={isSaving}
{canSave} primaryAction={
status === 'published'
? { label: 'Save', status: 'published' }
: { label: 'Publish', status: 'published' }
}
dropdownActions={[
{ label: 'Save as Draft', status: 'draft', show: status !== 'draft' }
]}
/> />
</div> </div>
{/if} {/if}

View file

@ -8,7 +8,7 @@
import PostMetadataPopover from '$lib/components/admin/PostMetadataPopover.svelte' import PostMetadataPopover from '$lib/components/admin/PostMetadataPopover.svelte'
import DeleteConfirmationModal from '$lib/components/admin/DeleteConfirmationModal.svelte' import DeleteConfirmationModal from '$lib/components/admin/DeleteConfirmationModal.svelte'
import Button from '$lib/components/admin/Button.svelte' import Button from '$lib/components/admin/Button.svelte'
import SaveActionsGroup from '$lib/components/admin/SaveActionsGroup.svelte' import StatusDropdown from '$lib/components/admin/StatusDropdown.svelte'
import type { JSONContent } from '@tiptap/core' import type { JSONContent } from '@tiptap/core'
let post = $state<any>(null) let post = $state<any>(null)
@ -62,10 +62,12 @@
type: 'bulletList', type: 'bulletList',
content: (block.content || []).map((item: any) => ({ content: (block.content || []).map((item: any) => ({
type: 'listItem', type: 'listItem',
content: [{ content: [
type: 'paragraph', {
content: [{ type: 'text', text: item.content || item }] type: 'paragraph',
}] content: [{ type: 'text', text: item.content || item }]
}
]
})) }))
} }
@ -75,20 +77,24 @@
type: 'orderedList', type: 'orderedList',
content: (block.content || []).map((item: any) => ({ content: (block.content || []).map((item: any) => ({
type: 'listItem', type: 'listItem',
content: [{ content: [
type: 'paragraph', {
content: [{ type: 'text', text: item.content || item }] type: 'paragraph',
}] content: [{ type: 'text', text: item.content || item }]
}
]
})) }))
} }
case 'blockquote': case 'blockquote':
return { return {
type: 'blockquote', type: 'blockquote',
content: [{ content: [
type: 'paragraph', {
content: [{ type: 'text', text: block.content || '' }] type: 'paragraph',
}] content: [{ type: 'text', text: block.content || '' }]
}
]
} }
case 'codeBlock': case 'codeBlock':
@ -134,66 +140,74 @@
return { blocks: [] } return { blocks: [] }
} }
const blocks = tiptapContent.content.map((node: any) => { const blocks = tiptapContent.content
switch (node.type) { .map((node: any) => {
case 'paragraph': switch (node.type) {
const text = extractTextFromNode(node) case 'paragraph':
return text ? { type: 'paragraph', content: text } : null const text = extractTextFromNode(node)
return text ? { type: 'paragraph', content: text } : null
case 'heading': case 'heading':
return { return {
type: 'heading', type: 'heading',
level: node.attrs?.level || 1, level: node.attrs?.level || 1,
content: extractTextFromNode(node) content: extractTextFromNode(node)
} }
case 'bulletList': case 'bulletList':
return { return {
type: 'bulletList', type: 'bulletList',
content: node.content?.map((item: any) => { content:
const itemText = extractTextFromNode(item.content?.[0]) node.content
return itemText ?.map((item: any) => {
}).filter(Boolean) || [] const itemText = extractTextFromNode(item.content?.[0])
} return itemText
})
.filter(Boolean) || []
}
case 'orderedList': case 'orderedList':
return { return {
type: 'orderedList', type: 'orderedList',
content: node.content?.map((item: any) => { content:
const itemText = extractTextFromNode(item.content?.[0]) node.content
return itemText ?.map((item: any) => {
}).filter(Boolean) || [] const itemText = extractTextFromNode(item.content?.[0])
} return itemText
})
.filter(Boolean) || []
}
case 'blockquote': case 'blockquote':
return { return {
type: 'blockquote', type: 'blockquote',
content: extractTextFromNode(node.content?.[0]) content: extractTextFromNode(node.content?.[0])
} }
case 'codeBlock': case 'codeBlock':
return { return {
type: 'codeBlock', type: 'codeBlock',
language: node.attrs?.language || '', language: node.attrs?.language || '',
content: node.content?.[0]?.text || '' content: node.content?.[0]?.text || ''
} }
case 'image': case 'image':
return { return {
type: 'image', type: 'image',
src: node.attrs?.src || '', src: node.attrs?.src || '',
alt: node.attrs?.alt || '', alt: node.attrs?.alt || '',
caption: node.attrs?.title || '' caption: node.attrs?.title || ''
} }
case 'horizontalRule': case 'horizontalRule':
return { type: 'hr' } return { type: 'hr' }
default: default:
// Skip unknown types // Skip unknown types
return null return null
} }
}).filter(Boolean) })
.filter(Boolean)
return { blocks } return { blocks }
} }
@ -283,7 +297,7 @@
tags = tags.filter((t) => t !== tag) tags = tags.filter((t) => t !== tag)
} }
async function handleSave(publishStatus?: 'draft' | 'published') { async function handleSave(newStatus?: string) {
const auth = localStorage.getItem('admin_auth') const auth = localStorage.getItem('admin_auth')
if (!auth) { if (!auth) {
goto('/admin/login') goto('/admin/login')
@ -302,7 +316,7 @@
title: config?.showTitle ? title : null, title: config?.showTitle ? title : null,
slug, slug,
type: postType, type: postType,
status: publishStatus || status, status: newStatus || status,
content: config?.showContent ? saveContent : null, content: config?.showContent ? saveContent : null,
excerpt: postType === 'essay' ? excerpt : undefined, excerpt: postType === 'essay' ? excerpt : undefined,
link_url: undefined, link_url: undefined,
@ -322,8 +336,8 @@
if (response.ok) { if (response.ok) {
post = await response.json() post = await response.json()
if (publishStatus) { if (newStatus) {
status = publishStatus status = newStatus
} }
} }
} catch (error) { } catch (error) {
@ -431,12 +445,15 @@
/> />
{/if} {/if}
</div> </div>
<SaveActionsGroup <StatusDropdown
{status} currentStatus={status}
onSave={handleSave} onStatusChange={handleSave}
disabled={saving} disabled={saving}
isLoading={saving} isLoading={saving}
canSave={true} primaryAction={status === 'published'
? { label: 'Save', status: 'published' }
: { label: 'Publish', status: 'published' }}
dropdownActions={[{ label: 'Save as Draft', status: 'draft', show: status !== 'draft' }]}
/> />
</div> </div>
{/if} {/if}
@ -591,7 +608,7 @@
border-radius: 8px; border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
min-width: 150px; min-width: 150px;
z-index: 100; z-index: 1050;
overflow: hidden; overflow: hidden;
} }

View file

@ -276,7 +276,7 @@
border-radius: 8px; border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
min-width: 150px; min-width: 150px;
z-index: 100; z-index: 1050;
overflow: hidden; overflow: hidden;
} }