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;
inset: 0;
background: rgba(0, 0, 0, 0.9);
z-index: 1000;
z-index: 1400;
display: flex;
align-items: center;
justify-content: center;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -220,28 +220,26 @@
</div>
<div class="header-actions">
{#if !isLoading}
{#if formData.status === 'published'}
<Button variant="primary" buttonSize="large" onclick={handleSave} disabled={isSaving}>
{isSaving ? 'Saving...' : 'Save'}
</Button>
{:else}
<StatusDropdown
currentStatus={formData.status}
onStatusChange={handleStatusChange}
disabled={isSaving}
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}
<StatusDropdown
currentStatus={formData.status}
onStatusChange={handleStatusChange}
disabled={isSaving}
isLoading={isSaving}
primaryAction={
formData.status === 'published'
? { label: 'Save', status: 'published' }
: { label: 'Publish', status: 'published' }
}
dropdownActions={[
{ label: 'Save as Draft', status: 'draft', show: formData.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}
</div>
</header>

View file

@ -77,8 +77,11 @@ export async function uploadFile(
customOptions?: any
): Promise<UploadResult> {
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
if (dev || !isCloudinaryConfigured()) {
if ((dev && !FORCE_CLOUDINARY_IN_DEV) || !isCloudinaryConfigured()) {
logger.info('Using local storage for file upload')
const localResult = await uploadFileLocally(file, type)
@ -123,14 +126,13 @@ export async function uploadFile(
}
// Log upload attempt for debugging
if (isSvg) {
logger.info('Attempting SVG upload with options:', {
filename: file.name,
mimeType: file.type,
size: file.size,
uploadOptions
})
}
logger.info('Attempting file upload:', {
filename: file.name,
mimeType: file.type,
size: file.size,
isSvg,
uploadOptions
})
// Upload to Cloudinary
const result = await new Promise<UploadApiResponse>((resolve, reject) => {
@ -168,6 +170,17 @@ export async function uploadFile(
} catch (error) {
logger.error('Cloudinary upload failed', error as Error)
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 {
success: false,

View file

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

View file

@ -8,7 +8,7 @@
import PostMetadataPopover from '$lib/components/admin/PostMetadataPopover.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 StatusDropdown from '$lib/components/admin/StatusDropdown.svelte'
import type { JSONContent } from '@tiptap/core'
let post = $state<any>(null)
@ -48,49 +48,55 @@
type: 'paragraph',
content: block.content ? [{ type: 'text', text: block.content }] : []
}
case 'heading':
return {
type: 'heading',
attrs: { level: block.level || 1 },
content: block.content ? [{ type: 'text', text: block.content }] : []
}
case 'bulletList':
case 'ul':
return {
type: 'bulletList',
content: (block.content || []).map((item: any) => ({
type: 'listItem',
content: [{
type: 'paragraph',
content: [{ type: 'text', text: item.content || item }]
}]
content: [
{
type: 'paragraph',
content: [{ type: 'text', text: item.content || item }]
}
]
}))
}
case 'orderedList':
case 'ol':
return {
type: 'orderedList',
content: (block.content || []).map((item: any) => ({
type: 'listItem',
content: [{
type: 'paragraph',
content: [{ type: 'text', text: item.content || item }]
}]
content: [
{
type: 'paragraph',
content: [{ type: 'text', text: item.content || item }]
}
]
}))
}
case 'blockquote':
return {
type: 'blockquote',
content: [{
type: 'paragraph',
content: [{ type: 'text', text: block.content || '' }]
}]
content: [
{
type: 'paragraph',
content: [{ type: 'text', text: block.content || '' }]
}
]
}
case 'codeBlock':
case 'code':
return {
@ -98,7 +104,7 @@
attrs: { language: block.language || '' },
content: [{ type: 'text', text: block.content || '' }]
}
case 'image':
return {
type: 'image',
@ -108,11 +114,11 @@
title: block.caption || ''
}
}
case 'hr':
case 'horizontalRule':
return { type: 'horizontalRule' }
default:
// Default to paragraph for unknown types
return {
@ -134,66 +140,74 @@
return { blocks: [] }
}
const blocks = tiptapContent.content.map((node: any) => {
switch (node.type) {
case 'paragraph':
const text = extractTextFromNode(node)
return text ? { type: 'paragraph', content: text } : null
case 'heading':
return {
type: 'heading',
level: node.attrs?.level || 1,
content: extractTextFromNode(node)
}
case 'bulletList':
return {
type: 'bulletList',
content: node.content?.map((item: any) => {
const itemText = extractTextFromNode(item.content?.[0])
return itemText
}).filter(Boolean) || []
}
case 'orderedList':
return {
type: 'orderedList',
content: node.content?.map((item: any) => {
const itemText = extractTextFromNode(item.content?.[0])
return itemText
}).filter(Boolean) || []
}
case 'blockquote':
return {
type: 'blockquote',
content: extractTextFromNode(node.content?.[0])
}
case 'codeBlock':
return {
type: 'codeBlock',
language: node.attrs?.language || '',
content: node.content?.[0]?.text || ''
}
case 'image':
return {
type: 'image',
src: node.attrs?.src || '',
alt: node.attrs?.alt || '',
caption: node.attrs?.title || ''
}
case 'horizontalRule':
return { type: 'hr' }
default:
// Skip unknown types
return null
}
}).filter(Boolean)
const blocks = tiptapContent.content
.map((node: any) => {
switch (node.type) {
case 'paragraph':
const text = extractTextFromNode(node)
return text ? { type: 'paragraph', content: text } : null
case 'heading':
return {
type: 'heading',
level: node.attrs?.level || 1,
content: extractTextFromNode(node)
}
case 'bulletList':
return {
type: 'bulletList',
content:
node.content
?.map((item: any) => {
const itemText = extractTextFromNode(item.content?.[0])
return itemText
})
.filter(Boolean) || []
}
case 'orderedList':
return {
type: 'orderedList',
content:
node.content
?.map((item: any) => {
const itemText = extractTextFromNode(item.content?.[0])
return itemText
})
.filter(Boolean) || []
}
case 'blockquote':
return {
type: 'blockquote',
content: extractTextFromNode(node.content?.[0])
}
case 'codeBlock':
return {
type: 'codeBlock',
language: node.attrs?.language || '',
content: node.content?.[0]?.text || ''
}
case 'image':
return {
type: 'image',
src: node.attrs?.src || '',
alt: node.attrs?.alt || '',
caption: node.attrs?.title || ''
}
case 'horizontalRule':
return { type: 'hr' }
default:
// Skip unknown types
return null
}
})
.filter(Boolean)
return { blocks }
}
@ -244,7 +258,7 @@
status = post.status || 'draft'
slug = post.slug || ''
excerpt = post.excerpt || ''
// Convert blocks format to Tiptap format if needed
if (post.content && post.content.blocks) {
content = convertBlocksToTiptap(post.content)
@ -253,7 +267,7 @@
} else {
content = { type: 'doc', content: [] }
}
tags = post.tags || []
} else {
if (response.status === 404) {
@ -283,7 +297,7 @@
tags = tags.filter((t) => t !== tag)
}
async function handleSave(publishStatus?: 'draft' | 'published') {
async function handleSave(newStatus?: string) {
const auth = localStorage.getItem('admin_auth')
if (!auth) {
goto('/admin/login')
@ -291,18 +305,18 @@
}
saving = true
// Convert content to blocks format if it's in Tiptap format
let saveContent = content
if (config?.showContent && content && content.type === 'doc') {
saveContent = convertTiptapToBlocks(content)
}
const postData = {
title: config?.showTitle ? title : null,
slug,
type: postType,
status: publishStatus || status,
status: newStatus || status,
content: config?.showContent ? saveContent : null,
excerpt: postType === 'essay' ? excerpt : undefined,
link_url: undefined,
@ -322,8 +336,8 @@
if (response.ok) {
post = await response.json()
if (publishStatus) {
status = publishStatus
if (newStatus) {
status = newStatus
}
}
} catch (error) {
@ -431,12 +445,15 @@
/>
{/if}
</div>
<SaveActionsGroup
{status}
onSave={handleSave}
<StatusDropdown
currentStatus={status}
onStatusChange={handleSave}
disabled={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>
{/if}
@ -591,7 +608,7 @@
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
min-width: 150px;
z-index: 100;
z-index: 1050;
overflow: hidden;
}

View file

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