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) => {
@ -168,6 +170,17 @@ export async function uploadFile(
} catch (error) { } catch (error) {
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,

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)
@ -48,49 +48,55 @@
type: 'paragraph', type: 'paragraph',
content: block.content ? [{ type: 'text', text: block.content }] : [] content: block.content ? [{ type: 'text', text: block.content }] : []
} }
case 'heading': case 'heading':
return { return {
type: 'heading', type: 'heading',
attrs: { level: block.level || 1 }, attrs: { level: block.level || 1 },
content: block.content ? [{ type: 'text', text: block.content }] : [] content: block.content ? [{ type: 'text', text: block.content }] : []
} }
case 'bulletList': case 'bulletList':
case 'ul': case 'ul':
return { return {
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 }]
}
]
})) }))
} }
case 'orderedList': case 'orderedList':
case 'ol': case 'ol':
return { return {
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':
case 'code': case 'code':
return { return {
@ -98,7 +104,7 @@
attrs: { language: block.language || '' }, attrs: { language: block.language || '' },
content: [{ type: 'text', text: block.content || '' }] content: [{ type: 'text', text: block.content || '' }]
} }
case 'image': case 'image':
return { return {
type: 'image', type: 'image',
@ -108,11 +114,11 @@
title: block.caption || '' title: block.caption || ''
} }
} }
case 'hr': case 'hr':
case 'horizontalRule': case 'horizontalRule':
return { type: 'horizontalRule' } return { type: 'horizontalRule' }
default: default:
// Default to paragraph for unknown types // Default to paragraph for unknown types
return { return {
@ -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':
return { case 'heading':
type: 'heading', return {
level: node.attrs?.level || 1, type: 'heading',
content: extractTextFromNode(node) level: node.attrs?.level || 1,
} content: extractTextFromNode(node)
}
case 'bulletList':
return { case 'bulletList':
type: 'bulletList', return {
content: node.content?.map((item: any) => { type: 'bulletList',
const itemText = extractTextFromNode(item.content?.[0]) content:
return itemText node.content
}).filter(Boolean) || [] ?.map((item: any) => {
} const itemText = extractTextFromNode(item.content?.[0])
return itemText
case 'orderedList': })
return { .filter(Boolean) || []
type: 'orderedList', }
content: node.content?.map((item: any) => {
const itemText = extractTextFromNode(item.content?.[0]) case 'orderedList':
return itemText return {
}).filter(Boolean) || [] type: 'orderedList',
} content:
node.content
case 'blockquote': ?.map((item: any) => {
return { const itemText = extractTextFromNode(item.content?.[0])
type: 'blockquote', return itemText
content: extractTextFromNode(node.content?.[0]) })
} .filter(Boolean) || []
}
case 'codeBlock':
return { case 'blockquote':
type: 'codeBlock', return {
language: node.attrs?.language || '', type: 'blockquote',
content: node.content?.[0]?.text || '' content: extractTextFromNode(node.content?.[0])
} }
case 'image': case 'codeBlock':
return { return {
type: 'image', type: 'codeBlock',
src: node.attrs?.src || '', language: node.attrs?.language || '',
alt: node.attrs?.alt || '', content: node.content?.[0]?.text || ''
caption: node.attrs?.title || '' }
}
case 'image':
case 'horizontalRule': return {
return { type: 'hr' } type: 'image',
src: node.attrs?.src || '',
default: alt: node.attrs?.alt || '',
// Skip unknown types caption: node.attrs?.title || ''
return null }
}
}).filter(Boolean) case 'horizontalRule':
return { type: 'hr' }
default:
// Skip unknown types
return null
}
})
.filter(Boolean)
return { blocks } return { blocks }
} }
@ -244,7 +258,7 @@
status = post.status || 'draft' status = post.status || 'draft'
slug = post.slug || '' slug = post.slug || ''
excerpt = post.excerpt || '' excerpt = post.excerpt || ''
// Convert blocks format to Tiptap format if needed // Convert blocks format to Tiptap format if needed
if (post.content && post.content.blocks) { if (post.content && post.content.blocks) {
content = convertBlocksToTiptap(post.content) content = convertBlocksToTiptap(post.content)
@ -253,7 +267,7 @@
} else { } else {
content = { type: 'doc', content: [] } content = { type: 'doc', content: [] }
} }
tags = post.tags || [] tags = post.tags || []
} else { } else {
if (response.status === 404) { if (response.status === 404) {
@ -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')
@ -291,18 +305,18 @@
} }
saving = true saving = true
// Convert content to blocks format if it's in Tiptap format // Convert content to blocks format if it's in Tiptap format
let saveContent = content let saveContent = content
if (config?.showContent && content && content.type === 'doc') { if (config?.showContent && content && content.type === 'doc') {
saveContent = convertTiptapToBlocks(content) saveContent = convertTiptapToBlocks(content)
} }
const postData = { const postData = {
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;
} }