Standardize metadata popovers

This commit is contained in:
Justin Edmund 2025-06-02 08:32:36 -07:00
parent 9f7b408bc7
commit 9a09dde557
10 changed files with 1134 additions and 332 deletions

View file

@ -0,0 +1,101 @@
<script lang="ts">
import GenericMetadataPopover, { type MetadataConfig } from './GenericMetadataPopover.svelte'
type Props = {
album: any
triggerElement: HTMLElement
onUpdate: (key: string, value: any) => void
onDelete: () => void
}
let {
album = $bindable(),
triggerElement,
onUpdate,
onDelete
}: Props = $props()
// Convert album date to YYYY-MM-DD format for date input
const albumDate = $derived(album.date ? new Date(album.date).toISOString().split('T')[0] : '')
// Handle date changes - convert back to ISO string
function handleDateChange(key: string, value: string) {
if (key === 'date') {
const isoDate = value ? new Date(value).toISOString() : null
onUpdate(key, isoDate)
} else {
onUpdate(key, value)
}
}
const config: MetadataConfig = {
title: 'Album Settings',
fields: [
{
type: 'input',
key: 'slug',
label: 'Slug',
placeholder: 'album-url-slug',
helpText: 'Used in the album URL.'
},
{
type: 'date',
key: 'date',
label: 'Date',
helpText: 'When was this album created or photos taken?'
},
{
type: 'input',
key: 'location',
label: 'Location',
placeholder: 'Location where photos were taken'
},
{
type: 'section',
key: 'display-options',
label: 'Display Options'
},
{
type: 'toggle',
key: 'isPhotography',
label: 'Show in Photos',
helpText: 'Show this album in the photography experience'
},
{
type: 'toggle',
key: 'showInUniverse',
label: 'Show in Universe',
helpText: 'Display this album in the Universe feed'
},
{
type: 'metadata',
key: 'metadata'
}
],
deleteButton: {
label: 'Delete Album',
action: onDelete
}
}
// Create a reactive data object that includes the formatted date
let popoverData = $state({
...album,
date: albumDate
})
// Sync changes back to album
$effect(() => {
popoverData = {
...album,
date: albumDate
}
})
</script>
<GenericMetadataPopover
{config}
bind:data={popoverData}
{triggerElement}
onUpdate={handleDateChange}
/>

View file

@ -2,7 +2,7 @@
import type { HTMLButtonAttributes } from 'svelte/elements'
interface Props extends HTMLButtonAttributes {
variant?: 'primary' | 'secondary' | 'danger' | 'ghost' | 'text' | 'overlay'
variant?: 'primary' | 'secondary' | 'danger' | 'ghost' | 'text' | 'overlay' | 'danger-text'
buttonSize?: 'small' | 'medium' | 'large' | 'icon'
iconOnly?: boolean
iconPosition?: 'left' | 'right'
@ -343,6 +343,23 @@
}
}
.btn-danger-text {
background: none;
color: #dc2626;
padding: $unit;
font-weight: 600;
&:hover:not(:disabled) {
background-color: $grey-90;
color: #dc2626;
}
&:active:not(:disabled) {
background-color: $grey-80;
color: #dc2626;
}
}
.btn-overlay {
background-color: white;
color: $grey-20;

View file

@ -5,7 +5,6 @@
import SmartImage from '../SmartImage.svelte'
import MediaLibraryModal from './MediaLibraryModal.svelte'
import { authenticatedFetch } from '$lib/admin-auth'
import RefreshIcon from '$icons/refresh.svg?component'
interface Props {
label: string
@ -103,7 +102,7 @@
for (let i = 0; i < files.length; i++) {
const file = files[i]
const validationError = validateFile(file)
if (validationError) {
errors.push(`${file.name}: ${validationError}`)
} else if (filesToUpload.length < remainingSlots) {
@ -126,10 +125,10 @@
try {
// Initialize progress tracking
const progressKeys = filesToUpload.map((file, index) => `${file.name}-${index}`)
uploadProgress = Object.fromEntries(progressKeys.map(key => [key, 0]))
uploadProgress = Object.fromEntries(progressKeys.map((key) => [key, 0]))
// Simulate progress for user feedback
const progressIntervals = progressKeys.map(key => {
const progressIntervals = progressKeys.map((key) => {
return setInterval(() => {
if (uploadProgress[key] < 90) {
uploadProgress[key] += Math.random() * 10
@ -139,16 +138,16 @@
})
const uploadedMedia = await uploadFiles(filesToUpload)
// Clear progress intervals
progressIntervals.forEach(interval => clearInterval(interval))
progressIntervals.forEach((interval) => clearInterval(interval))
// Complete progress
progressKeys.forEach(key => {
progressKeys.forEach((key) => {
uploadProgress[key] = 100
})
uploadProgress = { ...uploadProgress }
// Brief delay to show completion
setTimeout(() => {
const newValue = [...(value || []), ...uploadedMedia]
@ -158,7 +157,6 @@
isUploading = false
uploadProgress = {}
}, 500)
} catch (err) {
isUploading = false
uploadProgress = {}
@ -180,7 +178,7 @@
function handleDrop(event: DragEvent) {
event.preventDefault()
isDragOver = false
const files = event.dataTransfer?.files
if (files) {
handleFiles(files)
@ -202,7 +200,7 @@
// Remove individual image - now passes the item to be removed instead of doing it locally
function handleRemoveImage(index: number) {
if (!value || !value[index]) return
const itemToRemove = value[index]
// Call the onRemove callback if provided, otherwise fall back to onUpload
if (onRemove) {
@ -219,7 +217,7 @@
// Update alt text on server
async function handleAltTextChange(item: any, newAltText: string) {
if (!item) return
try {
// For album photos, use mediaId; for direct media objects, use id
const mediaId = item.mediaId || item.id
@ -227,7 +225,7 @@
console.error('No media ID found for alt text update')
return
}
const response = await authenticatedFetch(`/api/media/${mediaId}/metadata`, {
method: 'PATCH',
headers: {
@ -241,9 +239,13 @@
if (response.ok) {
const updatedData = await response.json()
if (value) {
const index = value.findIndex(v => (v.mediaId || v.id) === mediaId)
const index = value.findIndex((v) => (v.mediaId || v.id) === mediaId)
if (index !== -1) {
value[index] = { ...value[index], altText: updatedData.altText, updatedAt: updatedData.updatedAt }
value[index] = {
...value[index],
altText: updatedData.altText,
updatedAt: updatedData.updatedAt
}
value = [...value]
}
}
@ -275,25 +277,25 @@
function handleImageDrop(event: DragEvent, dropIndex: number) {
event.preventDefault()
if (draggedIndex === null || !value) return
const newValue = [...value]
const draggedItem = newValue[draggedIndex]
// Remove from old position
newValue.splice(draggedIndex, 1)
// Insert at new position (adjust index if dragging to later position)
const adjustedDropIndex = draggedIndex < dropIndex ? dropIndex - 1 : dropIndex
newValue.splice(adjustedDropIndex, 0, draggedItem)
value = newValue
onUpload(newValue)
if (onReorder) {
onReorder(newValue)
}
draggedIndex = null
draggedOverIndex = null
}
@ -311,12 +313,12 @@
function handleMediaSelect(selectedMedia: any | any[]) {
// For gallery mode, selectedMedia will be an array
const mediaArray = Array.isArray(selectedMedia) ? selectedMedia : [selectedMedia]
// Add selected media to existing gallery (avoid duplicates)
// Check both id and mediaId to handle different object types
const currentIds = value?.map(m => m.mediaId || m.id) || []
const newMedia = mediaArray.filter(media => !currentIds.includes(media.id))
const currentIds = value?.map((m) => m.mediaId || m.id) || []
const newMedia = mediaArray.filter((media) => !currentIds.includes(media.id))
if (newMedia.length > 0) {
const updatedGallery = [...(value || []), ...newMedia]
value = updatedGallery
@ -331,21 +333,9 @@
</script>
<div class="gallery-uploader">
<!-- Label -->
<label class="uploader-label">
{label}
{#if required}
<span class="required">*</span>
{/if}
</label>
{#if helpText}
<p class="help-text">{helpText}</p>
{/if}
<!-- Upload Area -->
{#if !hasImages || (hasImages && canAddMore)}
<div
<div
class="drop-zone"
class:drag-over={isDragOver}
class:uploading={isUploading}
@ -381,7 +371,7 @@
</circle>
</svg>
<p class="upload-text">Uploading images...</p>
<!-- Individual file progress -->
<div class="file-progress-list">
{#each Object.entries(uploadProgress) as [fileName, progress]}
@ -398,12 +388,53 @@
{:else}
<!-- Upload Prompt -->
<div class="upload-prompt">
<svg class="upload-icon" width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 2H6A2 2 0 0 0 4 4V20A2 2 0 0 0 6 22H18A2 2 0 0 0 20 20V8L14 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<polyline points="14,2 14,8 20,8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="16" y1="13" x2="8" y2="13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<line x1="16" y1="17" x2="8" y2="17" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<polyline points="10,9 9,9 8,9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<svg
class="upload-icon"
width="48"
height="48"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M14 2H6A2 2 0 0 0 4 4V20A2 2 0 0 0 6 22H18A2 2 0 0 0 20 20V8L14 2Z"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<polyline
points="14,2 14,8 20,8"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<line
x1="16"
y1="13"
x2="8"
y2="13"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
/>
<line
x1="16"
y1="17"
x2="8"
y2="17"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
/>
<polyline
points="10,9 9,9 8,9"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
<p class="upload-main-text">{placeholder}</p>
<p class="upload-sub-text">
@ -426,11 +457,9 @@
<Button variant="primary" onclick={handleBrowseClick}>
{hasImages ? 'Add More Images' : 'Choose Images'}
</Button>
{#if showBrowseLibrary}
<Button variant="ghost" onclick={handleBrowseLibrary}>
Browse Library
</Button>
<Button variant="ghost" onclick={handleBrowseLibrary}>Browse Library</Button>
{/if}
</div>
{/if}
@ -439,7 +468,7 @@
{#if hasImages}
<div class="image-gallery">
{#each value as media, index (media.id)}
<div
<div
class="gallery-item"
class:dragging={draggedIndex === index}
class:drag-over={draggedOverIndex === index}
@ -452,19 +481,25 @@
>
<!-- Drag Handle -->
<div class="drag-handle">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="9" cy="6" r="2" fill="currentColor"/>
<circle cx="15" cy="6" r="2" fill="currentColor"/>
<circle cx="9" cy="12" r="2" fill="currentColor"/>
<circle cx="15" cy="12" r="2" fill="currentColor"/>
<circle cx="9" cy="18" r="2" fill="currentColor"/>
<circle cx="15" cy="18" r="2" fill="currentColor"/>
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="9" cy="6" r="2" fill="currentColor" />
<circle cx="15" cy="6" r="2" fill="currentColor" />
<circle cx="9" cy="12" r="2" fill="currentColor" />
<circle cx="15" cy="12" r="2" fill="currentColor" />
<circle cx="9" cy="18" r="2" fill="currentColor" />
<circle cx="15" cy="18" r="2" fill="currentColor" />
</svg>
</div>
<!-- Image Preview -->
<div class="image-preview">
<SmartImage
<SmartImage
media={{
id: media.mediaId || media.id,
filename: media.filename,
@ -487,17 +522,41 @@
aspectRatio="1:1"
class="gallery-image"
/>
<!-- Remove Button -->
<button
<button
class="remove-button"
onclick={() => handleRemoveImage(index)}
type="button"
aria-label="Remove image"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<line x1="18" y1="6" x2="6" y2="18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="6" y1="6" x2="18" y2="18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<line
x1="18"
y1="6"
x2="6"
y2="18"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<line
x1="6"
y1="6"
x2="18"
y2="18"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
</div>
@ -861,4 +920,4 @@
align-items: stretch;
}
}
</style>
</style>

View file

@ -0,0 +1,450 @@
<script lang="ts">
import { onMount } from 'svelte'
import Input from './Input.svelte'
import FormFieldWrapper from './FormFieldWrapper.svelte'
import Button from './Button.svelte'
export interface MetadataField {
type: 'input' | 'textarea' | 'date' | 'toggle' | 'tags' | 'metadata' | 'custom' | 'section'
key: string
label?: string
placeholder?: string
rows?: number
helpText?: string
component?: any // For custom components
props?: any // Additional props for custom components
}
export interface MetadataConfig {
title: string
fields: MetadataField[]
deleteButton?: {
label: string
action: () => void
}
}
type Props = {
config: MetadataConfig
data: any
triggerElement: HTMLElement
onUpdate?: (key: string, value: any) => void
onAddTag?: () => void
onRemoveTag?: (tag: string) => void
}
let {
config,
data = $bindable(),
triggerElement,
onUpdate = () => {},
onAddTag = () => {},
onRemoveTag = () => {}
}: Props = $props()
let popoverElement: HTMLDivElement
let portalTarget: HTMLElement
function updatePosition() {
if (!popoverElement || !triggerElement) return
const triggerRect = triggerElement.getBoundingClientRect()
const popoverRect = popoverElement.getBoundingClientRect()
const viewportWidth = window.innerWidth
const viewportHeight = window.innerHeight
// Find the AdminPage container to align with its right edge
const adminPage =
document.querySelector('.admin-page') || document.querySelector('[data-admin-page]')
const adminPageRect = adminPage?.getBoundingClientRect()
// Position below the trigger button
let top = triggerRect.bottom + 8
// Align closer to the right edge of AdminPage, with some padding
let left: number
if (adminPageRect) {
// Position to align with AdminPage right edge minus padding
left = adminPageRect.right - popoverRect.width - 24
} else {
// Fallback to viewport-based positioning
left = triggerRect.right - popoverRect.width
}
// Ensure we don't go off-screen horizontally
if (left < 16) {
left = 16
} else if (left + popoverRect.width > viewportWidth - 16) {
left = viewportWidth - popoverRect.width - 16
}
// Check if popover would go off-screen vertically (both top and bottom)
if (top + popoverRect.height > viewportHeight - 16) {
// Try positioning above the trigger
const topAbove = triggerRect.top - popoverRect.height - 8
if (topAbove >= 16) {
top = topAbove
} else {
// If neither above nor below works, position with maximum available space
if (triggerRect.top > viewportHeight - triggerRect.bottom) {
// More space above - position at top of viewport with margin
top = 16
} else {
// More space below - position at bottom of viewport with margin
top = viewportHeight - popoverRect.height - 16
}
}
}
// Also check if positioning below would place us off the top (shouldn't happen but be safe)
if (top < 16) {
top = 16
}
popoverElement.style.position = 'fixed'
popoverElement.style.top = `${top}px`
popoverElement.style.left = `${left}px`
popoverElement.style.zIndex = '1000'
}
function handleFieldUpdate(key: string, value: any) {
data[key] = value
onUpdate(key, value)
}
onMount(() => {
// Create portal target
portalTarget = document.createElement('div')
portalTarget.style.position = 'absolute'
portalTarget.style.top = '0'
portalTarget.style.left = '0'
portalTarget.style.pointerEvents = 'none'
document.body.appendChild(portalTarget)
// Initial positioning
updatePosition()
// Update position on scroll/resize
const handleUpdate = () => updatePosition()
window.addEventListener('scroll', handleUpdate, true)
window.addEventListener('resize', handleUpdate)
return () => {
window.removeEventListener('scroll', handleUpdate, true)
window.removeEventListener('resize', handleUpdate)
if (portalTarget) {
document.body.removeChild(portalTarget)
}
}
})
$effect(() => {
if (popoverElement && portalTarget && triggerElement) {
portalTarget.appendChild(popoverElement)
portalTarget.style.pointerEvents = 'auto'
updatePosition()
}
})
</script>
<div class="metadata-popover" bind:this={popoverElement}>
<div class="popover-content">
<h3>{config.title}</h3>
{#each config.fields as field}
{#if field.type === 'input'}
<Input
label={field.label}
bind:value={data[field.key]}
placeholder={field.placeholder}
helpText={field.helpText}
onchange={() => handleFieldUpdate(field.key, data[field.key])}
/>
{:else if field.type === 'textarea'}
<Input
type="textarea"
label={field.label}
bind:value={data[field.key]}
rows={field.rows || 3}
placeholder={field.placeholder}
helpText={field.helpText}
onchange={() => handleFieldUpdate(field.key, data[field.key])}
/>
{:else if field.type === 'date'}
<Input
type="date"
label={field.label}
bind:value={data[field.key]}
helpText={field.helpText}
onchange={() => handleFieldUpdate(field.key, data[field.key])}
/>
{:else if field.type === 'toggle'}
<div class="toggle-wrapper">
<label class="toggle-label">
<input
type="checkbox"
bind:checked={data[field.key]}
class="toggle-input"
onchange={() => handleFieldUpdate(field.key, data[field.key])}
/>
<span class="toggle-slider"></span>
<div class="toggle-content">
<span class="toggle-title">{field.label}</span>
{#if field.helpText}
<span class="toggle-description">{field.helpText}</span>
{/if}
</div>
</label>
</div>
{:else if field.type === 'tags'}
<div class="tags-section">
<Input
label={field.label}
bind:value={data.tagInput}
onkeydown={(e) => e.key === 'Enter' && (e.preventDefault(), onAddTag())}
placeholder={field.placeholder || "Add tags..."}
/>
<button type="button" onclick={onAddTag} class="add-tag-btn">Add</button>
{#if data[field.key] && data[field.key].length > 0}
<div class="tags">
{#each data[field.key] as tag}
<span class="tag">
{tag}
<button onclick={() => onRemoveTag(tag)}>×</button>
</span>
{/each}
</div>
{/if}
</div>
{:else if field.type === 'metadata'}
<div class="metadata">
<p>Created: {new Date(data.createdAt).toLocaleString()}</p>
<p>Updated: {new Date(data.updatedAt).toLocaleString()}</p>
{#if data.publishedAt}
<p>Published: {new Date(data.publishedAt).toLocaleString()}</p>
{/if}
</div>
{:else if field.type === 'section'}
<div class="section-header">
<h4>{field.label}</h4>
</div>
{:else if field.type === 'custom' && field.component}
<svelte:component this={field.component} {...field.props} bind:data />
{/if}
{/each}
</div>
{#if config.deleteButton}
<div class="popover-footer">
<Button variant="danger-text" pill={false} onclick={config.deleteButton.action}>
<svg slot="icon" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path
d="M4 4L12 12M4 12L12 4"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
{config.deleteButton.label}
</Button>
</div>
{/if}
</div>
<style lang="scss">
@import '$styles/variables.scss';
.metadata-popover {
background: white;
border: 1px solid $grey-80;
border-radius: $card-corner-radius;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
min-width: 420px;
max-width: 480px;
max-height: calc(100vh - #{$unit-2x * 2});
display: flex;
flex-direction: column;
pointer-events: auto;
overflow-y: auto;
}
.popover-content {
padding: $unit-3x;
display: flex;
flex-direction: column;
gap: $unit-3x;
h3 {
font-size: 1.125rem;
font-weight: 600;
margin: 0;
color: $grey-10;
}
}
.popover-footer {
padding: $unit-3x;
border-top: 1px solid $grey-90;
display: flex;
justify-content: flex-start;
}
.section-header {
margin: $unit-3x 0 $unit 0;
&:first-child {
margin-top: 0;
}
h4 {
display: block;
margin-bottom: $unit;
font-weight: 500;
color: $grey-20;
font-size: 0.925rem;
}
}
.tags-section {
display: flex;
flex-direction: column;
gap: $unit;
}
.add-tag-btn {
align-self: flex-start;
margin-top: $unit-half;
padding: $unit $unit-2x;
background: $grey-10;
color: white;
border: none;
border-radius: 6px;
font-size: 13px;
cursor: pointer;
transition: background-color 0.15s ease;
&:hover {
background: $grey-20;
}
}
.tags {
display: flex;
flex-wrap: wrap;
gap: $unit;
margin-top: $unit;
}
.tag {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px $unit-2x;
background: $grey-80;
border-radius: 20px;
font-size: 0.75rem;
button {
background: none;
border: none;
color: $grey-40;
cursor: pointer;
padding: 0;
font-size: 1rem;
line-height: 1;
&:hover {
color: $grey-10;
}
}
}
.metadata {
font-size: 0.75rem;
color: $grey-40;
p {
margin: $unit-half 0;
}
}
.toggle-wrapper {
.toggle-label {
display: flex;
align-items: center;
gap: $unit-3x;
cursor: pointer;
user-select: none;
}
.toggle-input {
position: absolute;
opacity: 0;
pointer-events: none;
&:checked + .toggle-slider {
background-color: $blue-60;
&::before {
transform: translateX(20px);
}
}
&:disabled + .toggle-slider {
opacity: 0.5;
cursor: not-allowed;
}
}
.toggle-slider {
position: relative;
width: 44px;
height: 24px;
background-color: $grey-80;
border-radius: 12px;
transition: background-color 0.2s ease;
flex-shrink: 0;
&::before {
content: '';
position: absolute;
top: 2px;
left: 2px;
width: 20px;
height: 20px;
background-color: white;
border-radius: 50%;
transition: transform 0.2s ease;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
}
.toggle-content {
display: flex;
flex-direction: column;
gap: $unit-half;
.toggle-title {
font-weight: 500;
color: $grey-10;
font-size: 0.875rem;
}
.toggle-description {
font-size: 0.75rem;
color: $grey-50;
line-height: 1.4;
}
}
}
@include breakpoint('phone') {
.metadata-popover {
min-width: 280px;
max-width: calc(100vw - 2rem);
}
}
</style>

View file

@ -64,9 +64,27 @@
left = viewportWidth - popoverRect.width - 16
}
// Adjust if would go off-screen vertically
// Check if popover would go off-screen vertically (both top and bottom)
if (top + popoverRect.height > viewportHeight - 16) {
top = triggerRect.top - popoverRect.height - 8
// Try positioning above the trigger
const topAbove = triggerRect.top - popoverRect.height - 8
if (topAbove >= 16) {
top = topAbove
} else {
// If neither above nor below works, position with maximum available space
if (triggerRect.top > viewportHeight - triggerRect.bottom) {
// More space above - position at top of viewport with margin
top = 16
} else {
// More space below - position at bottom of viewport with margin
top = viewportHeight - popoverRect.height - 16
}
}
}
// Also check if positioning below would place us off the top (shouldn't happen but be safe)
if (top < 16) {
top = 16
}
popoverElement.style.position = 'fixed'

View file

@ -0,0 +1,105 @@
<script lang="ts">
import GenericMetadataPopover, { type MetadataConfig } from './GenericMetadataPopover.svelte'
type Props = {
post: any
postType: 'post' | 'essay'
slug: string
excerpt: string
tags: string[]
tagInput: string
triggerElement: HTMLElement
onAddTag: () => void
onRemoveTag: (tag: string) => void
onDelete: () => void
}
let {
post,
postType,
slug = $bindable(),
excerpt = $bindable(),
tags = $bindable(),
tagInput = $bindable(),
triggerElement,
onAddTag,
onRemoveTag,
onDelete
}: Props = $props()
function handleFieldUpdate(key: string, value: any) {
if (key === 'slug') {
slug = value
} else if (key === 'excerpt') {
excerpt = value
} else if (key === 'tagInput') {
tagInput = value
}
}
const config: MetadataConfig = {
title: 'Post Settings',
fields: [
{
type: 'input',
key: 'slug',
label: 'Slug',
placeholder: 'post-slug'
},
...(postType === 'essay' ? [{
type: 'textarea' as const,
key: 'excerpt',
label: 'Excerpt',
rows: 3,
placeholder: 'Brief description...'
}] : []),
{
type: 'tags',
key: 'tags',
label: 'Tags',
placeholder: 'Add tags...'
},
{
type: 'metadata',
key: 'metadata'
}
],
deleteButton: {
label: 'Delete Post',
action: onDelete
}
}
// Create a reactive data object
let popoverData = $state({
slug,
excerpt,
tags,
tagInput,
createdAt: post.createdAt,
updatedAt: post.updatedAt,
publishedAt: post.publishedAt
})
// Sync changes back
$effect(() => {
popoverData = {
slug,
excerpt,
tags,
tagInput,
createdAt: post.createdAt,
updatedAt: post.updatedAt,
publishedAt: post.publishedAt
}
})
</script>
<GenericMetadataPopover
{config}
bind:data={popoverData}
{triggerElement}
onUpdate={handleFieldUpdate}
{onAddTag}
{onRemoveTag}
/>

View file

@ -7,11 +7,11 @@
import Input from '$lib/components/admin/Input.svelte'
import FormFieldWrapper from '$lib/components/admin/FormFieldWrapper.svelte'
import LoadingSpinner from '$lib/components/admin/LoadingSpinner.svelte'
import DeleteConfirmationModal from '$lib/components/admin/DeleteConfirmationModal.svelte'
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 AlbumMetadataPopover from '$lib/components/admin/AlbumMetadataPopover.svelte'
// Form state
let album = $state<any>(null)
@ -28,7 +28,6 @@
let isLoading = $state(true)
let isSaving = $state(false)
let error = $state('')
let isDeleteModalOpen = $state(false)
// Photo management state
let isMediaLibraryOpen = $state(false)
@ -39,6 +38,10 @@
let isMediaDetailsOpen = $state(false)
let selectedMedia = $state<any>(null)
// Metadata popover state
let isMetadataOpen = $state(false)
let metadataButtonElement: HTMLButtonElement
onMount(async () => {
await loadAlbum()
})
@ -443,14 +446,14 @@
try {
if (newPhotos.length > 0) {
// Check if these are new uploads (have File objects) or library selections (have media IDs)
const uploadsToAdd = newPhotos.filter(photo => photo instanceof File || !photo.id)
const libraryPhotosToAdd = newPhotos.filter(photo => photo.id && !(photo instanceof File))
const uploadsToAdd = newPhotos.filter((photo) => photo instanceof File || !photo.id)
const libraryPhotosToAdd = newPhotos.filter((photo) => photo.id && !(photo instanceof File))
// Handle new uploads
if (uploadsToAdd.length > 0) {
await handleAddPhotosFromUpload(uploadsToAdd)
}
// Handle library selections
if (libraryPhotosToAdd.length > 0) {
await handleAddPhotos(libraryPhotosToAdd)
@ -471,7 +474,7 @@
error = 'Cannot remove photo: no photo ID found'
return
}
// Call the existing remove photo function
const success = await handleRemovePhoto(photoId, true) // Skip confirmation since user clicked remove
if (!success) {
@ -487,7 +490,6 @@
}
}
function generateSlug(text: string): string {
return text
.toLowerCase()
@ -503,6 +505,34 @@
})
const canSave = $derived(title.trim().length > 0 && slug.trim().length > 0)
// Metadata popover handlers
function handleMetadataUpdate(key: string, value: any) {
if (key === 'date') {
date = value ? new Date(value).toISOString().split('T')[0] : ''
} else {
// Update the form state variable
switch (key) {
case 'slug':
slug = value
break
case 'location':
location = value
break
case 'isPhotography':
isPhotography = value
break
case 'showInUniverse':
showInUniverse = value
break
}
}
}
function handleMetadataDelete() {
isMetadataOpen = false
handleDelete()
}
</script>
<AdminPage>
@ -522,23 +552,34 @@
</button>
</div>
<div class="header-actions">
<Button
variant="ghost"
buttonSize="large"
onclick={() => (isDeleteModalOpen = true)}
disabled={isSaving}
>
<svg slot="icon" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path
d="M6 3V2C6 1.44772 6.44772 1 7 1H9C9.55228 1 10 1.44772 10 2V3M13 4H3M5 7V12M8 7V12M11 7V12M4 4L4.5 13C4.55228 13.5523 4.99772 14 5.5 14H10.5C11.0023 14 11.4477 13.5523 11.5 13L12 4H4Z"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
<div class="metadata-popover-container">
<button
bind:this={metadataButtonElement}
class="btn btn-text"
onclick={(e) => {
e.stopPropagation()
isMetadataOpen = !isMetadataOpen
}}
disabled={isSaving}
>
<svg width="16" height="16" viewBox="0 0 56 56" fill="none">
<path
fill="currentColor"
d="M 36.4023 19.3164 C 38.8398 19.3164 40.9257 17.7461 41.6992 15.5898 L 49.8085 15.5898 C 50.7695 15.5898 51.6133 14.7461 51.6133 13.6914 C 51.6133 12.6367 50.7695 11.8164 49.8085 11.8164 L 41.7226 11.8164 C 40.9257 9.6367 38.8398 8.0430 36.4023 8.0430 C 33.9648 8.0430 31.8789 9.6367 31.1054 11.8164 L 6.2851 11.8164 C 5.2304 11.8164 4.3867 12.6367 4.3867 13.6914 C 4.3867 14.7461 5.2304 15.5898 6.2851 15.5898 L 31.1054 15.5898 C 31.8789 17.7461 33.9648 19.3164 36.4023 19.3164 Z M 6.1913 26.1133 C 5.2304 26.1133 4.3867 26.9570 4.3867 28.0117 C 4.3867 29.0664 5.2304 29.8867 6.1913 29.8867 L 14.5586 29.8867 C 15.3320 32.0898 17.4179 33.6601 19.8554 33.6601 C 22.3164 33.6601 24.4023 32.0898 25.1757 29.8867 L 49.7149 29.8867 C 50.7695 29.8867 51.6133 29.0664 51.6133 28.0117 C 51.6133 26.9570 50.7695 26.1133 49.7149 26.1133 L 25.1757 26.1133 C 24.3789 23.9570 22.2929 22.3867 19.8554 22.3867 C 17.4413 22.3867 15.3554 23.9570 14.5586 26.1133 Z M 36.4023 47.9570 C 38.8398 47.9570 40.9257 46.3867 41.6992 44.2070 L 49.8085 44.2070 C 50.7695 44.2070 51.6133 43.3867 51.6133 42.3320 C 51.6133 41.2773 50.7695 40.4336 49.8085 40.4336 L 41.6992 40.4336 C 40.9257 38.2539 38.8398 36.7070 36.4023 36.7070 C 33.9648 36.7070 31.8789 38.2539 31.1054 40.4336 L 6.2851 40.4336 C 5.2304 40.4336 4.3867 41.2773 4.3867 42.3320 C 4.3867 43.3867 5.2304 44.2070 6.2851 44.2070 L 31.1054 44.2070 C 31.8789 46.3867 33.9648 47.9570 36.4023 47.9570 Z"
/>
</svg>
Metadata
</button>
{#if isMetadataOpen && metadataButtonElement && album}
<AlbumMetadataPopover
bind:album
triggerElement={metadataButtonElement}
onUpdate={handleMetadataUpdate}
onDelete={handleMetadataDelete}
/>
</svg>
Delete
</Button>
{/if}
</div>
<SaveActionsGroup
{status}
onSave={handleSave}
@ -577,15 +618,6 @@
fullWidth
/>
<Input
label="Slug"
bind:value={slug}
placeholder="album-url-slug"
helpText="Used in the album URL."
disabled={isSaving}
fullWidth
/>
<Input
type="textarea"
label="Description"
@ -595,66 +627,6 @@
disabled={isSaving}
fullWidth
/>
<div class="form-row">
<Input
type="date"
label="Date"
bind:value={date}
helpText="When was this album created or photos taken?"
disabled={isSaving}
/>
<Input
label="Location"
bind:value={location}
placeholder="Location where photos were taken"
disabled={isSaving}
/>
</div>
</div>
<div class="form-section">
<h2>Album Settings</h2>
<!-- Photography Toggle -->
<FormFieldWrapper label="Album Type">
<div class="photography-toggle">
<label class="toggle-label">
<input
type="checkbox"
bind:checked={isPhotography}
disabled={isSaving}
class="toggle-input"
/>
<span class="toggle-slider"></span>
<div class="toggle-content">
<span class="toggle-title">Photography Album</span>
<span class="toggle-description">Show this album in the photography experience</span
>
</div>
</label>
</div>
</FormFieldWrapper>
<!-- Show in Universe Toggle -->
<FormFieldWrapper label="Visibility">
<div class="universe-toggle">
<label class="toggle-label">
<input
type="checkbox"
bind:checked={showInUniverse}
disabled={isSaving}
class="toggle-input"
/>
<span class="toggle-slider"></span>
<div class="toggle-content">
<span class="toggle-title">Show in Universe</span>
<span class="toggle-description">Display this album in the Universe feed</span>
</div>
</label>
</div>
</FormFieldWrapper>
</div>
<!-- Photo Management -->
@ -695,16 +667,6 @@
{/if}
</AdminPage>
<!-- Delete Confirmation Modal -->
<DeleteConfirmationModal
bind:isOpen={isDeleteModalOpen}
title="Delete Album"
message="Are you sure you want to delete this album? This action cannot be undone."
confirmText="Delete Album"
onConfirm={handleDelete}
onCancel={() => (isDeleteModalOpen = false)}
/>
<!-- Media Library Modal -->
<MediaLibraryModal
bind:isOpen={isMediaLibraryOpen}
@ -761,6 +723,49 @@
background: $grey-90;
color: $grey-10;
}
&.metadata-btn {
&:hover {
background: $blue-60;
color: white;
}
}
}
.btn-text {
padding: $unit $unit-2x;
border: none;
background: none;
color: $grey-40;
cursor: pointer;
display: flex;
align-items: center;
gap: $unit;
border-radius: 8px;
font-size: 0.875rem;
transition: all 0.2s ease;
&:hover {
background: $grey-90;
color: $grey-10;
}
}
.btn {
padding: $unit-2x $unit-3x;
border: none;
border-radius: 50px;
font-size: 0.925rem;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: $unit;
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
.loading-container {
@ -1072,7 +1077,6 @@
}
}
// Upload status styles
.upload-status {
background: white;

View file

@ -4,7 +4,11 @@
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'
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 AlbumMetadataPopover from '$lib/components/admin/AlbumMetadataPopover.svelte'
// Form state
let title = $state('')
@ -20,6 +24,19 @@
let isSaving = $state(false)
let error = $state('')
// Photo management state
let isMediaLibraryOpen = $state(false)
let albumPhotos = $state<any[]>([])
let isManagingPhotos = $state(false)
// Media details modal state
let isMediaDetailsOpen = $state(false)
let selectedMedia = $state<any>(null)
// Metadata popover state
let isMetadataOpen = $state(false)
let metadataButtonElement: HTMLButtonElement
// Auto-generate slug from title
$effect(() => {
if (title && !slug) {
@ -96,6 +113,107 @@
goto('/admin/albums')
}
// Photo management functions (simplified for new album - no API calls yet)
function handleMediaLibraryClose() {
isMediaLibraryOpen = false
}
function handlePhotoClick(photo: any) {
// Convert album photo to media format for MediaDetailsModal
selectedMedia = {
id: photo.mediaId || photo.id,
filename: photo.filename,
originalName: photo.filename,
mimeType: photo.mimeType || 'image/jpeg',
size: photo.size || 0,
url: photo.url,
thumbnailUrl: photo.thumbnailUrl,
width: photo.width,
height: photo.height,
altText: photo.altText || '',
description: photo.description || '',
isPhotography: photo.isPhotography || false,
createdAt: photo.createdAt,
updatedAt: photo.updatedAt
}
isMediaDetailsOpen = true
}
function handleMediaDetailsClose() {
isMediaDetailsOpen = false
selectedMedia = null
}
function handleMediaUpdate(updatedMedia: any) {
// Update the photo in the album photos list
const photoIndex = albumPhotos.findIndex(
(photo) => (photo.mediaId || photo.id) === updatedMedia.id
)
if (photoIndex !== -1) {
albumPhotos[photoIndex] = {
...albumPhotos[photoIndex],
filename: updatedMedia.filename,
altText: updatedMedia.altText,
description: updatedMedia.description,
isPhotography: updatedMedia.isPhotography
}
albumPhotos = [...albumPhotos] // Trigger reactivity
}
selectedMedia = updatedMedia
}
function handlePhotoReorder(reorderedPhotos: any[]) {
albumPhotos = reorderedPhotos
}
function handleGalleryAdd(newPhotos: any[]) {
if (newPhotos.length > 0) {
albumPhotos = [...albumPhotos, ...newPhotos]
}
}
function handleGalleryRemove(itemToRemove: any, index: number) {
albumPhotos = albumPhotos.filter((_, i) => i !== index)
}
// Metadata popover handlers
function handleMetadataUpdate(key: string, value: any) {
if (key === 'date') {
date = value ? new Date(value).toISOString().split('T')[0] : ''
} else {
// Update the form state variable
switch (key) {
case 'slug':
slug = value
break
case 'location':
location = value
break
case 'isPhotography':
isPhotography = value
break
case 'showInUniverse':
showInUniverse = value
break
}
}
}
// Mock album object for metadata popover
const mockAlbum = $derived({
id: null,
title,
slug,
description,
date: date ? new Date(date).toISOString() : null,
location,
isPhotography,
showInUniverse,
status,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
})
const canSave = $derived(title.trim().length > 0 && slug.trim().length > 0)
</script>
@ -115,11 +233,40 @@
</button>
</div>
<div class="header-actions">
<PublishDropdown
onPublish={() => handleSave('published')}
onSaveDraft={() => handleSave('draft')}
disabled={!canSave || isSaving}
<div class="metadata-popover-container">
<button
bind:this={metadataButtonElement}
class="btn btn-text"
onclick={(e) => {
e.stopPropagation()
isMetadataOpen = !isMetadataOpen
}}
disabled={isSaving}
>
<svg width="16" height="16" viewBox="0 0 56 56" fill="none">
<path
fill="currentColor"
d="M 36.4023 19.3164 C 38.8398 19.3164 40.9257 17.7461 41.6992 15.5898 L 49.8085 15.5898 C 50.7695 15.5898 51.6133 14.7461 51.6133 13.6914 C 51.6133 12.6367 50.7695 11.8164 49.8085 11.8164 L 41.7226 11.8164 C 40.9257 9.6367 38.8398 8.0430 36.4023 8.0430 C 33.9648 8.0430 31.8789 9.6367 31.1054 11.8164 L 6.2851 11.8164 C 5.2304 11.8164 4.3867 12.6367 4.3867 13.6914 C 4.3867 14.7461 5.2304 15.5898 6.2851 15.5898 L 31.1054 15.5898 C 31.8789 17.7461 33.9648 19.3164 36.4023 19.3164 Z M 6.1913 26.1133 C 5.2304 26.1133 4.3867 26.9570 4.3867 28.0117 C 4.3867 29.0664 5.2304 29.8867 6.1913 29.8867 L 14.5586 29.8867 C 15.3320 32.0898 17.4179 33.6601 19.8554 33.6601 C 22.3164 33.6601 24.4023 32.0898 25.1757 29.8867 L 49.7149 29.8867 C 50.7695 29.8867 51.6133 29.0664 51.6133 28.0117 C 51.6133 26.9570 50.7695 26.1133 49.7149 26.1133 L 25.1757 26.1133 C 24.3789 23.9570 22.2929 22.3867 19.8554 22.3867 C 17.4413 22.3867 15.3554 23.9570 14.5586 26.1133 Z M 36.4023 47.9570 C 38.8398 47.9570 40.9257 46.3867 41.6992 44.2070 L 49.8085 44.2070 C 50.7695 44.2070 51.6133 43.3867 51.6133 42.3320 C 51.6133 41.2773 50.7695 40.4336 49.8085 40.4336 L 41.6992 40.4336 C 40.9257 38.2539 38.8398 36.7070 36.4023 36.7070 C 33.9648 36.7070 31.8789 38.2539 31.1054 40.4336 L 6.2851 40.4336 C 5.2304 40.4336 4.3867 41.2773 4.3867 42.3320 C 4.3867 43.3867 5.2304 44.2070 6.2851 44.2070 L 31.1054 44.2070 C 31.8789 46.3867 33.9648 47.9570 36.4023 47.9570 Z"
/>
</svg>
Metadata
</button>
{#if isMetadataOpen && metadataButtonElement}
<AlbumMetadataPopover
album={mockAlbum}
triggerElement={metadataButtonElement}
onUpdate={handleMetadataUpdate}
onDelete={() => {}}
/>
{/if}
</div>
<SaveActionsGroup
{status}
onSave={handleSave}
disabled={isSaving}
isLoading={isSaving}
{canSave}
/>
</div>
</header>
@ -141,15 +288,6 @@
fullWidth
/>
<Input
label="Slug"
bind:value={slug}
placeholder="album-url-slug"
helpText="Used in the album URL. Auto-generated from title."
disabled={isSaving}
fullWidth
/>
<Input
type="textarea"
label="Description"
@ -159,69 +297,43 @@
disabled={isSaving}
fullWidth
/>
<div class="form-row">
<Input
type="date"
label="Date"
bind:value={date}
helpText="When was this album created or photos taken?"
disabled={isSaving}
/>
<Input
label="Location"
bind:value={location}
placeholder="Location where photos were taken"
disabled={isSaving}
/>
</div>
</div>
<!-- Photo Management -->
<div class="form-section">
<h2>Album Settings</h2>
<h2>Photos ({albumPhotos.length})</h2>
<!-- Photography Toggle -->
<FormFieldWrapper label="Album Type">
<div class="photography-toggle">
<label class="toggle-label">
<input
type="checkbox"
bind:checked={isPhotography}
disabled={isSaving}
class="toggle-input"
/>
<span class="toggle-slider"></span>
<div class="toggle-content">
<span class="toggle-title">Photography Album</span>
<span class="toggle-description">Show this album in the photography experience</span>
</div>
</label>
</div>
</FormFieldWrapper>
<!-- Show in Universe Toggle -->
<FormFieldWrapper label="Visibility">
<div class="universe-toggle">
<label class="toggle-label">
<input
type="checkbox"
bind:checked={showInUniverse}
disabled={isSaving}
class="toggle-input"
/>
<span class="toggle-slider"></span>
<div class="toggle-content">
<span class="toggle-title">Show in Universe</span>
<span class="toggle-description">Display this album in the Universe feed</span>
</div>
</label>
</div>
</FormFieldWrapper>
<GalleryUploader
label="Album Photos"
bind:value={albumPhotos}
onUpload={handleGalleryAdd}
onReorder={handlePhotoReorder}
onRemove={handleGalleryRemove}
showBrowseLibrary={true}
placeholder="Add photos to this album by uploading or selecting from your media library"
helpText="Drag photos to reorder them. Click on photos to edit metadata."
/>
</div>
</div>
</AdminPage>
<!-- Media Library Modal -->
<MediaLibraryModal
bind:isOpen={isMediaLibraryOpen}
mode="multiple"
fileType="image"
onSelect={handleGalleryAdd}
onClose={handleMediaLibraryClose}
/>
<!-- Media Details Modal -->
<MediaDetailsModal
bind:isOpen={isMediaDetailsOpen}
media={selectedMedia}
onClose={handleMediaDetailsClose}
onUpdate={handleMediaUpdate}
/>
<style lang="scss">
@import '$styles/variables.scss';
@ -244,12 +356,6 @@
gap: $unit-2x;
}
.publish-dropdown {
position: relative;
display: flex;
gap: $unit-half;
}
.btn-icon {
width: 40px;
height: 40px;
@ -269,6 +375,29 @@
}
}
.btn-text {
padding: $unit $unit-2x;
border: none;
background: none;
color: $grey-40;
cursor: pointer;
display: flex;
align-items: center;
gap: $unit;
border-radius: 8px;
font-size: 0.875rem;
transition: all 0.2s ease;
&:hover {
background: $grey-90;
color: $grey-10;
}
}
.metadata-popover-container {
position: relative;
}
.album-form {
max-width: 800px;
margin: 0 auto;
@ -298,85 +427,4 @@
padding-bottom: $unit-2x;
}
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: $unit-3x;
@include breakpoint('tablet') {
grid-template-columns: 1fr;
}
}
.photography-toggle,
.universe-toggle {
.toggle-label {
display: flex;
align-items: center;
gap: $unit-3x;
cursor: pointer;
user-select: none;
}
.toggle-input {
position: absolute;
opacity: 0;
pointer-events: none;
&:checked + .toggle-slider {
background-color: $blue-60;
&::before {
transform: translateX(20px);
}
}
&:disabled + .toggle-slider {
opacity: 0.5;
cursor: not-allowed;
}
}
.toggle-slider {
position: relative;
width: 44px;
height: 24px;
background-color: $grey-80;
border-radius: 12px;
transition: background-color 0.2s ease;
flex-shrink: 0;
&::before {
content: '';
position: absolute;
top: 2px;
left: 2px;
width: 20px;
height: 20px;
background-color: white;
border-radius: 50%;
transition: transform 0.2s ease;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
}
.toggle-content {
display: flex;
flex-direction: column;
gap: $unit-half;
.toggle-title {
font-weight: 500;
color: $grey-10;
font-size: 0.875rem;
}
.toggle-description {
font-size: 0.75rem;
color: $grey-50;
line-height: 1.4;
}
}
}
</style>

View file

@ -5,7 +5,7 @@
import AdminPage from '$lib/components/admin/AdminPage.svelte'
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 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'
@ -229,7 +229,7 @@
</button>
{#if showMetadata && metadataButtonRef}
<MetadataPopover
<PostMetadataPopover
{post}
{postType}
triggerElement={metadataButtonRef}

View file

@ -4,7 +4,7 @@
import { onMount } from 'svelte'
import AdminPage from '$lib/components/admin/AdminPage.svelte'
import Editor from '$lib/components/admin/Editor.svelte'
import MetadataPopover from '$lib/components/admin/MetadataPopover.svelte'
import PostMetadataPopover from '$lib/components/admin/PostMetadataPopover.svelte'
import Button from '$lib/components/admin/Button.svelte'
import PublishDropdown from '$lib/components/admin/PublishDropdown.svelte'
import type { JSONContent } from '@tiptap/core'
@ -179,7 +179,7 @@
</button>
{#if showMetadata && metadataButtonRef}
<MetadataPopover
<PostMetadataPopover
post={mockPost}
{postType}
triggerElement={metadataButtonRef}