From 4e6f5d36b88f38849d8eb0bd9dc00c3c4c0d0fb4 Mon Sep 17 00:00:00 2001 From: Jennie Robinson Faber Date: Sun, 5 Apr 2026 13:26:51 +0100 Subject: [PATCH] UX/UI improvements. --- app/components/EventsMiniSidebar.vue | 8 +- app/components/TopStrip.vue | 16 +- app/layouts/admin.vue | 104 +++- app/layouts/default.vue | 28 +- app/pages/admin/members.vue | 599 ++++++++++++---------- app/pages/admin/members/[id].vue | 402 +++++++++++++++ app/pages/events/[id].vue | 511 ------------------- app/pages/events/[slug].vue | 725 +++++++++++++++++++++++++++ app/pages/events/index.vue | 321 +++++++++--- app/pages/index.vue | 2 +- app/pages/members/[id].vue | 12 + scripts/migrate-event-slugs.js | 116 +++-- server/api/admin/members/[id].get.js | 17 + server/models/event.js | 27 +- 14 files changed, 1964 insertions(+), 924 deletions(-) create mode 100644 app/pages/admin/members/[id].vue delete mode 100644 app/pages/events/[id].vue create mode 100644 app/pages/events/[slug].vue create mode 100644 server/api/admin/members/[id].get.js diff --git a/app/components/EventsMiniSidebar.vue b/app/components/EventsMiniSidebar.vue index 1cf35ce..de6066d 100644 --- a/app/components/EventsMiniSidebar.vue +++ b/app/components/EventsMiniSidebar.vue @@ -7,9 +7,11 @@
{{ formatDate(event.startDate) }} - {{ - event.title - }} + {{ event.title }} ghostguild.org @@ -79,7 +83,8 @@ const breadcrumbs = computed(() => { let path = ""; return segments.map((segment) => { path += "/" + segment.replace(/\s+/g, "-"); - return { label: segment, path }; + const label = segment.charAt(0).toUpperCase() + segment.slice(1); + return { label, path }; }); }); @@ -130,4 +135,7 @@ const breadcrumbs = computed(() => { .breadcrumb-sep { color: var(--text-faint); } +.breadcrumb-current { + color: var(--text-dim); +} diff --git a/app/layouts/admin.vue b/app/layouts/admin.vue index 7f1aa80..91d20b6 100644 --- a/app/layouts/admin.vue +++ b/app/layouts/admin.vue @@ -1,6 +1,10 @@ diff --git a/app/pages/index.vue b/app/pages/index.vue index 2cba24e..9e0519c 100644 --- a/app/pages/index.vue +++ b/app/pages/index.vue @@ -49,7 +49,7 @@
{{ formatDate(event.startDate) }} - {{ + {{ event.title }} diff --git a/app/pages/members/[id].vue b/app/pages/members/[id].vue index 4491279..b055b27 100644 --- a/app/pages/members/[id].vue +++ b/app/pages/members/[id].vue @@ -233,6 +233,18 @@ const getInitials = (name) => { const { data, pending, error: fetchError } = useFetch(`/api/members/${id}`); const member = computed(() => data.value?.member || null); +const pageBreadcrumbTitle = useState("pageBreadcrumbTitle", () => ""); +watch( + member, + (val) => { + pageBreadcrumbTitle.value = val?.name || ""; + }, + { immediate: true }, +); +onUnmounted(() => { + pageBreadcrumbTitle.value = ""; +}); + // Page head useHead({ title: computed(() => diff --git a/scripts/migrate-event-slugs.js b/scripts/migrate-event-slugs.js index 0a5d877..c97e89a 100644 --- a/scripts/migrate-event-slugs.js +++ b/scripts/migrate-event-slugs.js @@ -1,66 +1,90 @@ -import mongoose from 'mongoose' -import Event from '../server/models/event.js' -import { connectDB } from '../server/utils/mongoose.js' +import mongoose from "mongoose"; +import dotenv from "dotenv"; +import { connectDB } from "../server/utils/mongoose.js"; +import Event from "../server/models/event.js"; -// Generate slug from title -function generateSlug(title) { - return title +dotenv.config(); + +function generateSlug(title, startDate) { + const titleSlug = title .toLowerCase() - .replace(/[^a-z0-9]+/g, '-') - .replace(/^-+|-+$/g, '') + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, ""); + + if (startDate) { + const d = new Date(startDate); + const year = d.getUTCFullYear(); + const month = String(d.getUTCMonth() + 1).padStart(2, "0"); + const day = String(d.getUTCDate()).padStart(2, "0"); + return `${year}-${month}-${day}-${titleSlug}`; + } + + return titleSlug; } async function migrateEventSlugs() { try { - // Connect to database - await connectDB() - console.log('Connected to database') + await connectDB(); + console.log("Connected to database\n"); - // Find all events without slugs - const eventsWithoutSlugs = await Event.find({ - $or: [ - { slug: { $exists: false } }, - { slug: null }, - { slug: '' } - ] - }) + const events = await Event.find({}).sort({ startDate: 1 }).lean(); + console.log(`Found ${events.length} event(s) to process\n`); - console.log(`Found ${eventsWithoutSlugs.length} events without slugs`) - - if (eventsWithoutSlugs.length === 0) { - console.log('All events already have slugs!') - return + if (events.length === 0) { + console.log("No events found."); + return; } - // Generate and assign unique slugs - for (const event of eventsWithoutSlugs) { - let baseSlug = generateSlug(event.title) - let slug = baseSlug - let counter = 1 + // Build new slugs first so uniqueness checks don't conflict with slugs + // we're about to replace in the same run. + const updates = []; + const usedSlugs = new Set(); - // Ensure slug is unique - while (await Event.findOne({ slug, _id: { $ne: event._id } })) { - slug = `${baseSlug}-${counter}` - counter++ + for (const event of events) { + let baseSlug = generateSlug(event.title, event.startDate); + let slug = baseSlug; + let counter = 1; + + // Ensure slug is unique within this batch and across the DB + // (excluding the current event's own existing slug) + while ( + usedSlugs.has(slug) || + (await Event.findOne({ slug, _id: { $ne: event._id } }).lean()) + ) { + slug = `${baseSlug}-${counter}`; + counter++; } - event.slug = slug - await event.save({ validateBeforeSave: false }) // Skip validation to avoid pre-save hook - console.log(`✓ Generated slug "${slug}" for event "${event.title}"`) + usedSlugs.add(slug); + updates.push({ event, oldSlug: event.slug, newSlug: slug }); } - console.log(`Successfully migrated ${eventsWithoutSlugs.length} events!`) + // Apply updates + let changed = 0; + let skipped = 0; + + for (const { event, oldSlug, newSlug } of updates) { + if (oldSlug === newSlug) { + console.log(` — ${event.title}`); + console.log(` slug unchanged: ${newSlug}`); + skipped++; + } else { + // Use updateOne to bypass the pre-save hook (avoids re-triggering slug generation) + await Event.updateOne({ _id: event._id }, { $set: { slug: newSlug } }); + console.log(` ✓ ${event.title}`); + console.log(` ${oldSlug || "(none)"} → ${newSlug}`); + changed++; + } + } + + console.log(`\nDone. ${changed} updated, ${skipped} already correct.`); } catch (error) { - console.error('Error migrating event slugs:', error) + console.error("\nMigration failed:", error); + process.exit(1); } finally { - await mongoose.connection.close() - console.log('Database connection closed') + await mongoose.connection.close(); + console.log("Database connection closed."); } } -// Run migration if called directly -if (import.meta.url === `file://${process.argv[1]}`) { - migrateEventSlugs() -} - -export default migrateEventSlugs \ No newline at end of file +migrateEventSlugs(); diff --git a/server/api/admin/members/[id].get.js b/server/api/admin/members/[id].get.js new file mode 100644 index 0000000..12fd54d --- /dev/null +++ b/server/api/admin/members/[id].get.js @@ -0,0 +1,17 @@ +import Member from '../../../models/member.js' +import { connectDB } from '../../../utils/mongoose.js' + +export default defineEventHandler(async (event) => { + await requireAdmin(event) + + const memberId = getRouterParam(event, 'id') + + await connectDB() + + const member = await Member.findById(memberId).lean() + if (!member) { + throw createError({ statusCode: 404, statusMessage: 'Member not found' }) + } + + return member +}) diff --git a/server/models/event.js b/server/models/event.js index a914634..16245bc 100644 --- a/server/models/event.js +++ b/server/models/event.js @@ -187,20 +187,35 @@ const eventSchema = new mongoose.Schema({ updatedAt: { type: Date, default: Date.now }, }); -// Generate slug from title -function generateSlug(title) { - return title +// Generate slug from title and startDate +function generateSlug(title, startDate) { + const titleSlug = title .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); + + if (startDate) { + const d = new Date(startDate); + const year = d.getUTCFullYear(); + const month = String(d.getUTCMonth() + 1).padStart(2, "0"); + const day = String(d.getUTCDate()).padStart(2, "0"); + return `${year}-${month}-${day}-${titleSlug}`; + } + + return titleSlug; } // Pre-save hook to generate slug eventSchema.pre("save", async function (next) { try { - // Always generate slug if it doesn't exist or if title has changed - if (!this.slug || this.isNew || this.isModified("title")) { - let baseSlug = generateSlug(this.title); + // Always generate slug if it doesn't exist or if title/startDate has changed + if ( + !this.slug || + this.isNew || + this.isModified("title") || + this.isModified("startDate") + ) { + let baseSlug = generateSlug(this.title, this.startDate); let slug = baseSlug; let counter = 1;