|
| 1 | +<template> |
| 2 | + <div class="min-h-screen bg-gray-900 text-white"> |
| 3 | + <div class="max-w-4xl mx-auto p-6"> |
| 4 | + <h1 class="text-2xl font-bold mb-4">Event JSON Editor</h1> |
| 5 | + <div class="mb-4 flex flex-wrap gap-2 items-center"> |
| 6 | + <label class="font-semibold">Select year:</label> |
| 7 | + <select v-model="selectedFile" @change="loadJson" class="border rounded px-2 py-1"> |
| 8 | + <option v-for="file in jsonFiles" :key="file" :value="file">{{ file }}</option> |
| 9 | + </select> |
| 10 | + <button |
| 11 | + @click="createNewJson" |
| 12 | + class="ml-4 px-3 py-1 bg-green-600 text-white rounded cursor-pointer" |
| 13 | + > |
| 14 | + New JSON |
| 15 | + </button> |
| 16 | + <button |
| 17 | + @click="downloadJson" |
| 18 | + class="ml-2 px-3 py-1 bg-blue-600 text-white rounded cursor-pointer" |
| 19 | + :disabled="!jsonData" |
| 20 | + > |
| 21 | + Download |
| 22 | + </button> |
| 23 | + </div> |
| 24 | + |
| 25 | + <div v-if="jsonData"> |
| 26 | + <form @submit.prevent> |
| 27 | + <div class="mb-2"> |
| 28 | + <label class="block font-semibold">Title</label> |
| 29 | + <input v-model="jsonData.title" class="border rounded px-2 py-1 w-full" /> |
| 30 | + </div> |
| 31 | + <div class="mb-2"> |
| 32 | + <label class="block font-semibold">Location</label> |
| 33 | + <input v-model="jsonData.location" class="border rounded px-2 py-1 w-full" /> |
| 34 | + </div> |
| 35 | + <div class="mb-2 flex gap-2"> |
| 36 | + <div> |
| 37 | + <label class="block font-semibold">Start</label> |
| 38 | + <input |
| 39 | + v-model.number="jsonData.start" |
| 40 | + type="number" |
| 41 | + class="border rounded px-2 py-1 w-20" |
| 42 | + /> |
| 43 | + </div> |
| 44 | + <div> |
| 45 | + <label class="block font-semibold">End</label> |
| 46 | + <input |
| 47 | + v-model.number="jsonData.end" |
| 48 | + type="number" |
| 49 | + class="border rounded px-2 py-1 w-20" |
| 50 | + /> |
| 51 | + </div> |
| 52 | + <div> |
| 53 | + <label class="block font-semibold">Month</label> |
| 54 | + <input v-model="jsonData.month" class="border rounded px-2 py-1 w-24" /> |
| 55 | + </div> |
| 56 | + <div> |
| 57 | + <label class="block font-semibold">Year</label> |
| 58 | + <input |
| 59 | + v-model.number="jsonData.year" |
| 60 | + type="number" |
| 61 | + class="border rounded px-2 py-1 w-24" |
| 62 | + /> |
| 63 | + </div> |
| 64 | + </div> |
| 65 | + <div class="mb-2"> |
| 66 | + <label class="block font-semibold">Live</label> |
| 67 | + <input v-model="jsonData.live" class="border rounded px-2 py-1 w-full" /> |
| 68 | + </div> |
| 69 | + <div class="mb-2"> |
| 70 | + <label class="block font-semibold">Video</label> |
| 71 | + <input v-model="jsonData.video" class="border rounded px-2 py-1 w-full" /> |
| 72 | + </div> |
| 73 | + <div class="mb-4"> |
| 74 | + <label class="block font-semibold">Days & Events</label> |
| 75 | + <div v-for="(day, dIdx) in jsonData.days" :key="dIdx" class="border rounded p-2 mb-2"> |
| 76 | + <div class="flex flex-col md:flex-row gap-2 mb-1"> |
| 77 | + <input |
| 78 | + v-model="day.day" |
| 79 | + class="border rounded px-2 py-1 w-full md:w-40" |
| 80 | + placeholder="Day" |
| 81 | + /> |
| 82 | + <input |
| 83 | + v-model="day.description" |
| 84 | + class="border rounded px-2 py-1 w-full md:w-60" |
| 85 | + placeholder="Description" |
| 86 | + /> |
| 87 | + <button |
| 88 | + @click.prevent="removeDay(dIdx)" |
| 89 | + class="text-red-600 cursor-pointer md:ml-2" |
| 90 | + > |
| 91 | + Remove Day |
| 92 | + </button> |
| 93 | + </div> |
| 94 | + <div class="ml-0 md:ml-4"> |
| 95 | + <div |
| 96 | + v-for="(event, eIdx) in day.events" |
| 97 | + :key="eIdx" |
| 98 | + class="mb-4 p-4 rounded bg-gray-700 flex flex-col md:grid md:grid-cols-2 gap-4" |
| 99 | + > |
| 100 | + <div class="flex flex-col gap-2"> |
| 101 | + <input |
| 102 | + v-model="event.name" |
| 103 | + class="border rounded px-2 py-1 w-full" |
| 104 | + placeholder="Event Name" |
| 105 | + /> |
| 106 | + <input |
| 107 | + v-model="event.by" |
| 108 | + class="border rounded px-2 py-1 w-full" |
| 109 | + placeholder="By" |
| 110 | + /> |
| 111 | + <input |
| 112 | + v-model="event.time" |
| 113 | + class="border rounded px-2 py-1 w-full" |
| 114 | + placeholder="Time" |
| 115 | + /> |
| 116 | + <input |
| 117 | + v-model="event.place.id" |
| 118 | + class="border rounded px-2 py-1 w-full" |
| 119 | + placeholder="Place ID" |
| 120 | + /> |
| 121 | + <input |
| 122 | + v-model="event.place.map" |
| 123 | + class="border rounded px-2 py-1 w-full" |
| 124 | + placeholder="Map" |
| 125 | + /> |
| 126 | + </div> |
| 127 | + <div class="flex flex-col gap-2 h-full"> |
| 128 | + <label class="font-semibold">Description</label> |
| 129 | + <textarea |
| 130 | + v-model="event.description" |
| 131 | + class="border rounded px-2 py-1 w-full min-h-20 resize-y" |
| 132 | + placeholder="Description" |
| 133 | + ></textarea> |
| 134 | + <button |
| 135 | + @click.prevent="removeEvent(dIdx, eIdx)" |
| 136 | + class="text-red-600 cursor-pointer mt-2 self-end" |
| 137 | + > |
| 138 | + Remove |
| 139 | + </button> |
| 140 | + </div> |
| 141 | + </div> |
| 142 | + <button @click.prevent="addEvent(dIdx)" class="text-green-600 cursor-pointer mt-2"> |
| 143 | + + Add Event |
| 144 | + </button> |
| 145 | + </div> |
| 146 | + </div> |
| 147 | + <button @click.prevent="addDay" class="text-green-600 cursor-pointer mt-2"> |
| 148 | + + Add Day |
| 149 | + </button> |
| 150 | + </div> |
| 151 | + </form> |
| 152 | + </div> |
| 153 | + <div v-else class="text-gray-500">Select a file or create a new one to begin.</div> |
| 154 | + </div> |
| 155 | + </div> |
| 156 | +</template> |
| 157 | + |
| 158 | +<script setup> |
| 159 | +import { ref } from "vue" |
| 160 | +
|
| 161 | +const jsonFiles = ["2014.json", "2015.json", "2016.json", "2017.json"] |
| 162 | +const selectedFile = ref("2014.json") |
| 163 | +const jsonData = ref(null) |
| 164 | +
|
| 165 | +async function loadJson() { |
| 166 | + // Use dynamic import for local JSON files |
| 167 | + const year = selectedFile.value.replace(".json", "") |
| 168 | + try { |
| 169 | + const module = await import(`@/data/${year}.json`) |
| 170 | + jsonData.value = module.default |
| 171 | + } catch (e) { |
| 172 | + jsonData.value = null |
| 173 | + console.error("Failed to load JSON:", e) |
| 174 | + } |
| 175 | +} |
| 176 | +
|
| 177 | +function createNewJson() { |
| 178 | + jsonData.value = { |
| 179 | + title: "", |
| 180 | + location: "", |
| 181 | + start: 1, |
| 182 | + end: 1, |
| 183 | + month: "", |
| 184 | + year: new Date().getFullYear(), |
| 185 | + live: "", |
| 186 | + archive: false, |
| 187 | + video: "", |
| 188 | + days: [], |
| 189 | + } |
| 190 | + selectedFile.value = "" |
| 191 | +} |
| 192 | +
|
| 193 | +function addDay() { |
| 194 | + jsonData.value.days.push({ day: "", description: "", events: [] }) |
| 195 | +} |
| 196 | +function removeDay(idx) { |
| 197 | + jsonData.value.days.splice(idx, 1) |
| 198 | +} |
| 199 | +function addEvent(dayIdx) { |
| 200 | + jsonData.value.days[dayIdx].events.push({ |
| 201 | + name: "", |
| 202 | + by: "", |
| 203 | + time: "", |
| 204 | + place: { id: "", map: "" }, |
| 205 | + description: "", |
| 206 | + }) |
| 207 | +} |
| 208 | +function removeEvent(dayIdx, eventIdx) { |
| 209 | + jsonData.value.days[dayIdx].events.splice(eventIdx, 1) |
| 210 | +} |
| 211 | +
|
| 212 | +function downloadJson() { |
| 213 | + const blob = new Blob([JSON.stringify(jsonData.value, null, 2)], { type: "application/json" }) |
| 214 | + const url = URL.createObjectURL(blob) |
| 215 | + const a = document.createElement("a") |
| 216 | + a.href = url |
| 217 | + a.download = selectedFile.value || "new-event.json" |
| 218 | + a.click() |
| 219 | + URL.revokeObjectURL(url) |
| 220 | +} |
| 221 | +
|
| 222 | +// Auto-load first file |
| 223 | +if (!jsonData.value) loadJson() |
| 224 | +</script> |
| 225 | + |
| 226 | +<style scoped> |
| 227 | +/* Add your styles here */ |
| 228 | +</style> |
0 commit comments