diff --git a/admin/schemas/index.js b/admin/schemas/index.js new file mode 100644 index 0000000..34ee67d --- /dev/null +++ b/admin/schemas/index.js @@ -0,0 +1,49 @@ +export const schemas = { + works: { + title: { type: 'text', label: 'Title' }, + date: { type: 'text', label: 'Date' }, + type: { type: 'text', label: 'Type' }, + score: { type: 'text', label: 'Score URL' }, + gallery: { type: 'text', label: 'Gallery' }, + soundcloud_trackid: { type: 'text', label: 'SoundCloud Track ID' }, + vimeo_trackid: { type: 'text', label: 'Vimeo Track ID' }, + instrument_tags: { type: 'text', label: 'Instrument Tags' }, + priority: { type: 'number', label: 'Priority' } + }, + publications: { + citationKey: { type: 'text', label: 'Citation Key' }, + entryType: { type: 'text', label: 'Entry Type' }, + entryTags: { type: 'textarea', label: 'Entry Tags (JSON)' } + }, + events: { + title: { type: 'text', label: 'Title' }, + start_date: { type: 'text', label: 'Start Date' }, + end_date: { type: 'text', label: 'End Date' }, + location: { type: 'text', label: 'Location' }, + type: { type: 'text', label: 'Type' }, + program: { type: 'textarea', label: 'Program' }, + works_list: { type: 'textarea', label: 'Works List' } + }, + releases: { + title: { type: 'text', label: 'Title' }, + year: { type: 'text', label: 'Year' }, + album_art: { type: 'text', label: 'Album Art' }, + discogs_id: { type: 'text', label: 'Discogs ID' }, + buy_link: { type: 'text', label: 'Buy Link' }, + spotify_id: { type: 'text', label: 'Spotify ID' } + }, + talks: { + title: { type: 'text', label: 'Title' }, + date: { type: 'text', label: 'Date' }, + location: { type: 'text', label: 'Location' }, + type: { type: 'text', label: 'Type' } + } +} + +export const collections = [ + { key: 'works', label: 'Works', file: 'works.json' }, + { key: 'publications', label: 'Publications', file: 'publications.json' }, + { key: 'events', label: 'Events', file: 'events.json' }, + { key: 'releases', label: 'Releases', file: 'releases.json' }, + { key: 'talks', label: 'Talks', file: 'talks.json' } +] diff --git a/package-lock.json b/package-lock.json index 4465fb0..37a9d7d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,8 @@ "name": "nuxt-app", "hasInstallScript": true, "dependencies": { + "@formkit/themes": "^1.7.2", + "@formkit/vue": "^1.7.2", "@pinia/nuxt": "^0.11.3", "mongodb": "^7.1.0", "nuxt": "^4.3.1", @@ -1050,6 +1052,113 @@ "license": "MIT", "optional": true }, + "node_modules/@formkit/core": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@formkit/core/-/core-1.7.2.tgz", + "integrity": "sha512-XDRVqkDtOziU3z44hdvgWEqGpiv6nTNeCmP8cnESKkr24a4keQuEwmfDVvibYV0ywyTMg6ylp2lyklCYeRHykQ==", + "license": "MIT", + "dependencies": { + "@formkit/utils": "^1.7.2" + } + }, + "node_modules/@formkit/dev": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@formkit/dev/-/dev-1.7.2.tgz", + "integrity": "sha512-W38xbbFS4h4LTV22kUC6ZbPHBmlM2lVbAz5PSYU3SPaNc4FeXp4GTe4GzLEmcS82B+5L1zbSOgGB43kWZts7wA==", + "license": "MIT", + "dependencies": { + "@formkit/core": "^1.7.2", + "@formkit/utils": "^1.7.2" + } + }, + "node_modules/@formkit/i18n": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@formkit/i18n/-/i18n-1.7.2.tgz", + "integrity": "sha512-Zs6f+rtP2j2Nnt1HkNrL85WU6rtS1l1Q0BAQjCMGQPsbrppiL7/TZ5bNIrVm0DTntLgG3firDS7bNWAxPSwerQ==", + "license": "MIT", + "dependencies": { + "@formkit/core": "^1.7.2", + "@formkit/utils": "^1.7.2", + "@formkit/validation": "^1.7.2" + } + }, + "node_modules/@formkit/inputs": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@formkit/inputs/-/inputs-1.7.2.tgz", + "integrity": "sha512-4xs7RJN8EFGctTCNRXBTvor2O8RvuEEK3q2Hl/RMU5lhhn5tmck90fkkeAr9o+rWRoSNiV8XgbLQArQ/B1IYPw==", + "license": "MIT", + "dependencies": { + "@formkit/core": "^1.7.2", + "@formkit/utils": "^1.7.2" + } + }, + "node_modules/@formkit/observer": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@formkit/observer/-/observer-1.7.2.tgz", + "integrity": "sha512-KUr5mcu2SAeHOOK1FaMyFigtA8hkHLsUkaPFWpTC81j79o1AUjJppmfPaX6PbtlHLeMs2Zq8Qhp6VAhi7D2WJg==", + "license": "MIT", + "dependencies": { + "@formkit/core": "^1.7.2", + "@formkit/utils": "^1.7.2" + } + }, + "node_modules/@formkit/rules": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@formkit/rules/-/rules-1.7.2.tgz", + "integrity": "sha512-2e5qKlXzmL9LAetncFp7xkHEX/UAJsU3bo1iL3idloH3HALf5fIdg3lgOKwCNDMdLjbfiKJ6lVwFaeFfFpcDKw==", + "license": "MIT", + "dependencies": { + "@formkit/core": "^1.7.2", + "@formkit/utils": "^1.7.2", + "@formkit/validation": "^1.7.2" + } + }, + "node_modules/@formkit/themes": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@formkit/themes/-/themes-1.7.2.tgz", + "integrity": "sha512-AaZHy7l9D44Ya3cQRqZ8RIpaTTsCjTB1gX13NlpqYSs6yG0yBTmJ7L7kW60bOGdhs/vmIwSel578EBwz98nybQ==", + "license": "MIT", + "dependencies": { + "@formkit/core": "^1.7.2" + } + }, + "node_modules/@formkit/utils": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@formkit/utils/-/utils-1.7.2.tgz", + "integrity": "sha512-bBF6alUBOqFfJHjVB95Vck0hp36vlw4QfFJxGfTO6BX68AEaFzzzabtpwfy0DbcHtwHh4Yn7l/rOWGxXEve+QQ==", + "license": "MIT" + }, + "node_modules/@formkit/validation": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@formkit/validation/-/validation-1.7.2.tgz", + "integrity": "sha512-JJsLP7AI/++VujdwtBeUcPhCqY3FXSKZ7hajT7Ow5Feab7JLqVPjxVMFbXDgEOLjq0ex1NX/I+EQHfm5sqMRsA==", + "license": "MIT", + "dependencies": { + "@formkit/core": "^1.7.2", + "@formkit/observer": "^1.7.2", + "@formkit/utils": "^1.7.2" + } + }, + "node_modules/@formkit/vue": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@formkit/vue/-/vue-1.7.2.tgz", + "integrity": "sha512-jrvXl2ZhS6X5klTbP4N5zc0Dg37AjbAf8SCVoz7mfligVkmwrrF4SN3waMBcH+n9gqT+vrkOYb6bQXdvjUdnCQ==", + "license": "MIT", + "dependencies": { + "@formkit/core": "^1.7.2", + "@formkit/dev": "^1.7.2", + "@formkit/i18n": "^1.7.2", + "@formkit/inputs": "^1.7.2", + "@formkit/observer": "^1.7.2", + "@formkit/rules": "^1.7.2", + "@formkit/themes": "^1.7.2", + "@formkit/utils": "^1.7.2", + "@formkit/validation": "^1.7.2" + }, + "peerDependencies": { + "vue": "^3.4.0" + } + }, "node_modules/@headlessui/vue": { "version": "1.7.23", "resolved": "https://registry.npmjs.org/@headlessui/vue/-/vue-1.7.23.tgz", diff --git a/package.json b/package.json index 83b5e73..70e3b3b 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,8 @@ "nuxt-icon": "^1.0.0-beta.7" }, "dependencies": { + "@formkit/themes": "^1.7.2", + "@formkit/vue": "^1.7.2", "@pinia/nuxt": "^0.11.3", "mongodb": "^7.1.0", "nuxt": "^4.3.1", diff --git a/pages/admin.vue b/pages/admin.vue new file mode 100644 index 0000000..85f998d --- /dev/null +++ b/pages/admin.vue @@ -0,0 +1,151 @@ + + + + + + + + + + + Admin + + + {{ col.label }} + + + + Logout + + + + + + {{ collections.find(c => c.key === selectedCollection)?.label }} + + Add New + + + + + + + + Title + Actions + + + + + {{ getTitle(item) }} + + JSON + Delete + + + + + + + + + Raw JSON + + + Cancel + Save + + + + + + + + + diff --git a/plugins/formkit.client.js b/plugins/formkit.client.js new file mode 100644 index 0000000..c464bc0 --- /dev/null +++ b/plugins/formkit.client.js @@ -0,0 +1,5 @@ +import { defaultConfig, plugin } from '@formkit/vue' + +export default defineNuxtPlugin((nuxtApp) => { + nuxtApp.vueApp.use(plugin, defaultConfig) +}) diff --git a/server/api/admin/[collection].js b/server/api/admin/[collection].js new file mode 100644 index 0000000..e9fd0cd --- /dev/null +++ b/server/api/admin/[collection].js @@ -0,0 +1,80 @@ +import { readFileSync, writeFileSync, existsSync } from 'node:fs' +import { join } from 'node:path' + +const dataDir = './server/data' + +function getFilePath(collection) { + const fileMap = { + works: 'works.json', + publications: 'publications.json', + events: 'events.json', + releases: 'releases.json', + talks: 'talks.json' + } + return join(dataDir, fileMap[collection] || `${collection}.json`) +} + +function generateId() { + return Math.random().toString(36).substring(2, 15) +} + +export default defineEventHandler(async (event) => { + const method = event.method + const collection = event.context.params?.collection + const id = event.context.params?.id + + if (!collection) { + throw createError({ statusCode: 400, message: 'Collection required' }) + } + + const filePath = getFilePath(collection) + + if (!existsSync(filePath)) { + throw createError({ statusCode: 404, message: 'Collection not found' }) + } + + const readData = () => { + try { + return JSON.parse(readFileSync(filePath, 'utf-8')) + } catch { + return [] + } + } + + const writeData = (data) => { + writeFileSync(filePath, JSON.stringify(data, null, 2)) + } + + if (method === 'GET') { + return readData() + } + + if (method === 'POST') { + const body = await readBody(event) + const data = readData() + body.id = generateId() + data.push(body) + writeData(data) + return body + } + + if (method === 'PUT') { + const body = await readBody(event) + const data = readData() + const index = data.findIndex(item => item.id === body.id) + if (index !== -1) { + data[index] = body + writeData(data) + } + return body + } + + if (method === 'DELETE' && id) { + const data = readData() + const filtered = data.filter(item => item.id !== id) + writeData(filtered) + return { success: true } + } + + throw createError({ statusCode: 405, message: 'Method not allowed' }) +}) diff --git a/server/api/admin/[collection]/[id].js b/server/api/admin/[collection]/[id].js new file mode 100644 index 0000000..d8a34dd --- /dev/null +++ b/server/api/admin/[collection]/[id].js @@ -0,0 +1,52 @@ +import { readFileSync, writeFileSync, existsSync } from 'node:fs' +import { join } from 'node:path' + +const dataDir = './server/data' + +function getFilePath(collection) { + const fileMap = { + works: 'works.json', + publications: 'publications.json', + events: 'events.json', + releases: 'releases.json', + talks: 'talks.json' + } + return join(dataDir, fileMap[collection] || `${collection}.json`) +} + +export default defineEventHandler(async (event) => { + const method = event.method + const collection = event.context.params?.collection + const id = event.context.params?.id + + if (!collection || !id) { + throw createError({ statusCode: 400, message: 'Collection and ID required' }) + } + + const filePath = getFilePath(collection) + + if (!existsSync(filePath)) { + throw createError({ statusCode: 404, message: 'Collection not found' }) + } + + const readData = () => { + try { + return JSON.parse(readFileSync(filePath, 'utf-8')) + } catch { + return [] + } + } + + const writeData = (data) => { + writeFileSync(filePath, JSON.stringify(data, null, 2)) + } + + if (method === 'DELETE') { + const data = readData() + const filtered = data.filter(item => item.id !== id) + writeData(filtered) + return { success: true } + } + + throw createError({ statusCode: 405, message: 'Method not allowed' }) +})