From bc56c90539d79663dfee40d545a8625b3bbd7829 Mon Sep 17 00:00:00 2001 From: Dan BROOKS Date: Sat, 10 Jan 2026 13:56:45 -0800 Subject: [PATCH 01/33] chore(Admin > Edit Album): Select a thumbnail and generate XML --- src/components/AdminAlbum/Fields.tsx | 254 ++++++++++++++++++++++++++- src/components/AdminAlbum/Thumbs.tsx | 4 +- src/models/album.ts | 43 +++-- 3 files changed, 276 insertions(+), 25 deletions(-) diff --git a/src/components/AdminAlbum/Fields.tsx b/src/components/AdminAlbum/Fields.tsx index 374b0433..b5f0abf4 100644 --- a/src/components/AdminAlbum/Fields.tsx +++ b/src/components/AdminAlbum/Fields.tsx @@ -1,7 +1,14 @@ +import Button from '@mui/joy/Button' import Input from '@mui/joy/Input' +import Option from '@mui/joy/Option' +import Select from '@mui/joy/Select' import Stack from '@mui/joy/Stack' +import Textarea from '@mui/joy/Textarea' +import { useEffect, useState } from 'react' +import xml2js from 'xml2js' import { type AlbumResponseBody } from '../../lib/album' +import type { Item } from '../../types/common' import { type ItemState } from './AdminAlbumClient' import Xml from './Xml' @@ -9,24 +16,253 @@ export default function Fields( { albumEntity, item, children }: { albumEntity: AlbumResponseBody['album'] | undefined, item: ItemState, children: React.ReactElement }, ) { + const [editedItem, setEditedItem] = useState(item) + const [xmlOutput, setXmlOutput] = useState('') + + useEffect(() => { + setEditedItem(item) + setXmlOutput('') + }, [item]) + + const handleFieldChange = (field: keyof Item, value: string) => { + if (editedItem) { + setEditedItem({ ...editedItem, [field]: value }) + } + } + + const generateXml = () => { + if (!editedItem || !albumEntity) return + + // Create a copy of the album with the updated item + const updatedAlbum = { ...albumEntity } + const items = [...updatedAlbum.items] + const itemIndex = items.findIndex(i => i?.filename === editedItem.filename) + + if (itemIndex !== -1) { + items[itemIndex] = editedItem + } + + // Convert to XML - need to transform back to XML format with snake_case fields + const xmlItems = items.map(item => { + const filename = Array.isArray(item.filename) ? item.filename[0] : item.filename + const isVideo = filename.toLowerCase().endsWith('.mp4') || + filename.toLowerCase().endsWith('.mov') || + filename.toLowerCase().endsWith('.avi') + + const xmlItem: any = { + $: { id: item.id }, + } + + if (isVideo) xmlItem.type = 'video' + + xmlItem.filename = item.filename + xmlItem.photo_city = item.city + + if (item.location) xmlItem.photo_loc = item.location + + // Strip "Video: " prefix from caption if it's a video + let caption = item.caption + if (isVideo && caption.startsWith('Video: ')) { + caption = caption.substring(7) + } + xmlItem.thumb_caption = caption + + if (item.description) xmlItem.photo_desc = item.description + if (item.search) xmlItem.search = item.search + + if (item.coordinates) { + xmlItem.geo = { + lat: item.coordinates[1].toString(), + lon: item.coordinates[0].toString(), + } + if (item.coordinateAccuracy) { + xmlItem.geo.accuracy = item.coordinateAccuracy.toString() + } + } + + if (item.reference && item.reference[0] && item.reference[1]) { + // item.reference is [url, name] - need to extract source type from URL + const url = item.reference[0] + const name = item.reference[1] + + let source = 'wikipedia' // default + if (url.includes('facebook.com')) source = 'facebook' + else if (url.includes('google.com')) source = 'google' + else if (url.includes('instagram.com')) source = 'instagram' + else if (url.includes('wikipedia.org')) source = 'wikipedia' + else if (url.includes('youtube.com')) source = 'youtube' + + xmlItem.ref = { + name, + source, + } + } + + return xmlItem + }) + + // Transform meta to XML format with snake_case + const xmlMeta = { + gallery: updatedAlbum.meta.gallery, + album_name: updatedAlbum.meta.albumName, + album_version: updatedAlbum.meta.albumVersion, + marker_zoom: updatedAlbum.meta.geo?.zoom.toString(), + cluster_max_zoom: updatedAlbum.meta.clusterMaxZoom, + } + + const xmlAlbum = { + meta: xmlMeta, + item: xmlItems.length === 1 ? xmlItems[0] : xmlItems, + } + + // Convert to XML + const builder = new xml2js.Builder({ + rootName: 'album', + renderOpts: { pretty: true, indent: '\t' }, + xmldec: { version: '1.0', encoding: 'UTF-8' }, + }) + + const xml = builder.buildObject(xmlAlbum) + setXmlOutput(xml) + } + return ( <> {children} - - - - - - - - + + + handleFieldChange('city', e.target.value)} + placeholder="City" + /> + handleFieldChange('location', e.target.value)} + placeholder="Location" + /> + handleFieldChange('caption', e.target.value)} + placeholder="Caption" + /> +