@@ -83,10 +139,22 @@ import { collections } from '@/admin/schemas'
const password = ref('')
const authenticated = ref(false)
+const selectedView = ref('collections')
const selectedCollection = ref('works')
+const selectedFolder = ref('scores')
const items = ref([])
+const files = ref([])
const rawJsonItem = ref(null)
const rawJsonContent = ref('')
+const isUploading = ref(false)
+
+const fileFolders = [
+ { key: 'scores', label: 'Scores' },
+ { key: 'pubs', label: 'Publications' },
+ { key: 'album_art', label: 'Album Art' },
+ { key: 'images', label: 'Images' },
+ { key: 'hdp_images', label: 'HDP Images' }
+]
function checkPassword(data) {
if (data.password === 'admin123') {
@@ -103,6 +171,16 @@ function logout() {
}
function getTitle(item) {
+ if (selectedCollection.value === 'publications' && item.entryTags?.title) {
+ return item.entryTags.title
+ }
+ if (selectedCollection.value === 'events' && item.start_date) {
+ const v = item.venue
+ return `${item.start_date}: ${v?.name} - ${v?.city}, ${v?.state}`
+ }
+ if (selectedCollection.value === 'talks' && item.date) {
+ return `${item.date}: ${item.location}`
+ }
return item.title || item.citationKey || item.name || item.id
}
@@ -111,6 +189,11 @@ async function loadItems() {
items.value = data.value || []
}
+async function loadFiles() {
+ const { data } = await useFetch(`/api/admin/files?folder=${selectedFolder.value}`)
+ files.value = data.value || []
+}
+
function createNew() {
rawJsonItem.value = { id: null }
rawJsonContent.value = '{\n \n}'
@@ -145,7 +228,55 @@ async function deleteItem(item) {
}
}
+async function handleFileUpload(event) {
+ const file = event.target.files?.[0]
+ if (!file) return
+
+ isUploading.value = true
+ const formData = new FormData()
+ formData.append('file', file)
+
+ try {
+ await $fetch(`/api/admin/files/upload?folder=${selectedFolder.value}`, {
+ method: 'POST',
+ body: formData
+ })
+ loadFiles()
+ } catch (e) {
+ alert('Upload failed: ' + e.message)
+ } finally {
+ isUploading.value = false
+ event.target.value = ''
+ }
+}
+
+async function deleteFile(file) {
+ if (confirm(`Delete ${file.name}?`)) {
+ await $fetch(`/api/admin/files?folder=${selectedFolder.value}&file=${file.name}`, {
+ method: 'DELETE'
+ })
+ loadFiles()
+ }
+}
+
+function copyUrl(url) {
+ navigator.clipboard.writeText(window.location.origin + url)
+ alert('URL copied to clipboard!')
+}
+
watch(selectedCollection, () => {
loadItems()
})
+
+watch(selectedFolder, () => {
+ loadFiles()
+})
+
+watch(selectedView, (newView) => {
+ if (newView === 'collections') {
+ loadItems()
+ } else if (newView === 'files') {
+ loadFiles()
+ }
+})
diff --git a/server/api/admin/files.js b/server/api/admin/files.js
new file mode 100644
index 0000000..0bcb1fc
--- /dev/null
+++ b/server/api/admin/files.js
@@ -0,0 +1,52 @@
+import { readdirSync, statSync, unlinkSync, writeFileSync, existsSync } from 'node:fs'
+import { join, extname } from 'node:path'
+
+const publicDir = './public'
+
+const allowedFolders = ['scores', 'pubs', 'album_art', 'images', 'hdp_images']
+
+function getFolderPath(folder) {
+ return join(publicDir, folder)
+}
+
+export default defineEventHandler(async (event) => {
+ const method = event.method
+ const query = getQuery(event)
+ const folder = query.folder
+
+ if (!folder || !allowedFolders.includes(folder)) {
+ throw createError({ statusCode: 400, message: 'Invalid folder' })
+ }
+
+ const folderPath = getFolderPath(folder)
+
+ if (!existsSync(folderPath)) {
+ throw createError({ statusCode: 404, message: 'Folder not found' })
+ }
+
+ if (method === 'GET') {
+ const files = readdirSync(folderPath)
+ .filter(f => statSync(join(folderPath, f)).isFile())
+ .map(f => ({
+ name: f,
+ size: statSync(join(folderPath, f)).size,
+ url: `/${folder}/${f}`
+ }))
+ return files
+ }
+
+ if (method === 'DELETE') {
+ const filename = query.file
+ if (!filename) {
+ throw createError({ statusCode: 400, message: 'Filename required' })
+ }
+ const filePath = join(folderPath, filename)
+ if (existsSync(filePath)) {
+ unlinkSync(filePath)
+ return { success: true }
+ }
+ throw createError({ statusCode: 404, message: 'File not found' })
+ }
+
+ throw createError({ statusCode: 405, message: 'Method not allowed' })
+})
diff --git a/server/api/admin/files.upload.js b/server/api/admin/files.upload.js
new file mode 100644
index 0000000..11ed733
--- /dev/null
+++ b/server/api/admin/files.upload.js
@@ -0,0 +1,60 @@
+import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
+import { join } from 'node:path'
+import { readMultipartFormData } from 'h3'
+
+const publicDir = './public'
+
+const allowedFolders = ['scores', 'pubs', 'album_art', 'images', 'hdp_images']
+const allowedExtensions = ['.pdf', '.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.mp3', '.wav', '.ogg']
+
+function getFolderPath(folder) {
+ return join(publicDir, folder)
+}
+
+function sanitizeFilename(filename) {
+ return filename.replace(/[^a-zA-Z0-9._-]/g, '_')
+}
+
+export default defineEventHandler(async (event) => {
+ const query = getQuery(event)
+ const folder = query.folder
+
+ if (!folder || !allowedFolders.includes(folder)) {
+ throw createError({ statusCode: 400, message: 'Invalid folder' })
+ }
+
+ const folderPath = getFolderPath(folder)
+
+ if (!existsSync(folderPath)) {
+ mkdirSync(folderPath, { recursive: true })
+ }
+
+ const formData = await readMultipartFormData(event)
+
+ if (!formData || formData.length === 0) {
+ throw createError({ statusCode: 400, message: 'No file uploaded' })
+ }
+
+ const fileField = formData.find(f => f.name === 'file')
+
+ if (!fileField || !fileField.data) {
+ throw createError({ statusCode: 400, message: 'No file data' })
+ }
+
+ const ext = fileField.filename ? fileField.filename.toLowerCase().slice(fileField.filename.lastIndexOf('.')) : ''
+
+ if (!allowedExtensions.includes(ext)) {
+ throw createError({ statusCode: 400, message: `File type not allowed. Allowed: ${allowedExtensions.join(', ')}` })
+ }
+
+ const filename = fileField.filename ? sanitizeFilename(fileField.filename) : `upload${ext}`
+ const filePath = join(folderPath, filename)
+
+ writeFileSync(filePath, fileField.data)
+
+ return {
+ success: true,
+ filename,
+ url: `/${folder}/${filename}`
+ }
+})