feat: reskin admin pages to zine design system

Migrate the entire admin section from the dark guild-* Tailwind theme
to the zine design system (dashed borders, CSS custom properties,
Brygada 1918 + Commit Mono, cream/dark mode palette).

- Replace admin top-nav layout with sidebar matching default layout
- Reskin dashboard, members, events, series management pages
- Reskin events/create and series/create form pages
- Add dev-only test login endpoint (GET /api/dev/test-login)
- Redirect duplicate admin/dashboard.vue to /admin
- Update CLAUDE.md design system docs
This commit is contained in:
Jennie Robinson Faber 2026-04-03 10:56:01 +01:00
parent f16f9ada64
commit fcd6f4cdf4
23 changed files with 3845 additions and 3827 deletions

View file

@ -1,143 +1,123 @@
<template>
<div>
<div class="bg-guild-900 border-b border-guild-700">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="py-6">
<div class="flex items-center gap-4 mb-2">
<NuxtLink to="/admin/series-management" class="text-guild-500 hover:text-guild-100">
<Icon name="heroicons:arrow-left" class="w-5 h-5" />
</NuxtLink>
<h1 class="text-display font-bold text-guild-100">Create New Series</h1>
</div>
<p class="text-guild-400">Create a new event series to group related events together</p>
</div>
</div>
<div class="create-form">
<div class="page-header">
<NuxtLink to="/admin/series-management" class="back-link">&larr; Series</NuxtLink>
<h1>Create New Series</h1>
<p>Create a new event series to group related events together</p>
</div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="form-body">
<!-- Error Summary -->
<div v-if="formErrors.length > 0" class="mb-6 p-4 bg-ember-900/20 border border-ember-800 rounded-lg">
<div class="flex">
<Icon name="heroicons:exclamation-circle" class="w-5 h-5 text-ember-400 mr-3 mt-0.5" />
<div>
<h3 class="text-sm font-medium text-ember-400 mb-2">Please fix the following errors:</h3>
<ul class="text-sm text-ember-400 space-y-1">
<li v-for="error in formErrors" :key="error"> {{ error }}</li>
</ul>
</div>
<div v-if="formErrors.length > 0" class="error-box">
<Icon name="heroicons:exclamation-circle" class="box-icon" />
<div>
<strong>Please fix the following errors:</strong>
<ul>
<li v-for="error in formErrors" :key="error">{{ error }}</li>
</ul>
</div>
</div>
<!-- Success Message -->
<div v-if="showSuccessMessage" class="mb-6 p-4 bg-candlelight-900/20 border border-candlelight-800 rounded-lg">
<div class="flex">
<Icon name="heroicons:check-circle" class="w-5 h-5 text-candlelight-400 mr-3 mt-0.5" />
<div>
<h3 class="text-sm font-medium text-candlelight-400">Series created successfully!</h3>
</div>
<div v-if="showSuccessMessage" class="success-box">
<Icon name="heroicons:check-circle" class="box-icon" />
<div>
<strong>Series created successfully!</strong>
</div>
</div>
<form @submit.prevent="createSeries">
<!-- Series Information -->
<div class="mb-8">
<h2 class="text-lg font-semibold text-guild-100 mb-4">Series Information</h2>
<div class="grid grid-cols-1 gap-6">
<div>
<label class="block text-sm font-medium text-guild-100 mb-2">
Series Title <span class="text-ember-400">*</span>
</label>
<input
v-model="seriesForm.title"
type="text"
placeholder="e.g., Cooperative Game Development Fundamentals"
required
class="w-full bg-guild-800 border border-guild-700 rounded-lg px-3 py-2 text-guild-100 placeholder-guild-500 focus:ring-2 focus:ring-earth-500 focus:border-transparent"
:class="{ 'border-ember-700 focus:ring-ember-500': fieldErrors.title }"
@input="generateSlugFromTitle"
/>
<p v-if="fieldErrors.title" class="mt-1 text-sm text-ember-400">{{ fieldErrors.title }}</p>
<div class="form-section">
<h2 class="section-heading">Series Information</h2>
<div class="field">
<label>
Series Title <span class="required">*</span>
</label>
<input
v-model="seriesForm.title"
type="text"
placeholder="e.g., Cooperative Game Development Fundamentals"
required
:class="{ 'has-error': fieldErrors.title }"
@input="generateSlugFromTitle"
/>
<p v-if="fieldErrors.title" class="field-error">{{ fieldErrors.title }}</p>
</div>
<div v-if="generatedSlug" class="field">
<label>Generated Series ID</label>
<div class="slug-display">
{{ generatedSlug }}
</div>
<p class="help-text">
This unique identifier will be automatically generated from your title
</p>
</div>
<div class="field">
<label>
Series Description <span class="required">*</span>
</label>
<textarea
v-model="seriesForm.description"
placeholder="Describe what the series covers and its goals"
required
rows="4"
:class="{ 'has-error': fieldErrors.description }"
></textarea>
<p v-if="fieldErrors.description" class="field-error">{{ fieldErrors.description }}</p>
</div>
<div class="form-grid">
<div class="field">
<label>Series Type</label>
<select v-model="seriesForm.type">
<option value="workshop_series">Workshop Series</option>
<option value="recurring_meetup">Recurring Meetup</option>
<option value="multi_day">Multi-Day Event</option>
<option value="course">Course</option>
<option value="tournament">Tournament</option>
</select>
</div>
<div v-if="generatedSlug">
<label class="block text-sm font-medium text-guild-100 mb-2">Generated Series ID</label>
<div class="w-full bg-guild-800 border border-guild-700 rounded-lg px-3 py-2 text-guild-100 font-mono text-sm">
{{ generatedSlug }}
</div>
<p class="mt-1 text-sm text-guild-500">
This unique identifier will be automatically generated from your title
</p>
</div>
<div>
<label class="block text-sm font-medium text-guild-100 mb-2">
Series Description <span class="text-ember-400">*</span>
</label>
<textarea
v-model="seriesForm.description"
placeholder="Describe what the series covers and its goals"
required
rows="4"
class="w-full bg-guild-800 border border-guild-700 rounded-lg px-3 py-2 text-guild-100 placeholder-guild-500 focus:ring-2 focus:ring-earth-500 focus:border-transparent"
:class="{ 'border-ember-700 focus:ring-ember-500': fieldErrors.description }"
></textarea>
<p v-if="fieldErrors.description" class="mt-1 text-sm text-ember-400">{{ fieldErrors.description }}</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="block text-sm font-medium text-guild-100 mb-2">Series Type</label>
<select
v-model="seriesForm.type"
class="w-full bg-guild-800 border border-guild-700 rounded-lg px-3 py-2 text-guild-100 focus:ring-2 focus:ring-earth-500 focus:border-transparent"
>
<option value="workshop_series">Workshop Series</option>
<option value="recurring_meetup">Recurring Meetup</option>
<option value="multi_day">Multi-Day Event</option>
<option value="course">Course</option>
<option value="tournament">Tournament</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-guild-100 mb-2">Total Events Planned</label>
<input
v-model.number="seriesForm.totalEvents"
type="number"
min="1"
placeholder="e.g., 4"
class="w-full bg-guild-800 border border-guild-700 rounded-lg px-3 py-2 text-guild-100 placeholder-guild-500 focus:ring-2 focus:ring-earth-500 focus:border-transparent"
/>
<p class="text-sm text-guild-500 mt-1">How many events will be in this series? (optional)</p>
</div>
<div class="field">
<label>Total Events Planned</label>
<input
v-model.number="seriesForm.totalEvents"
type="number"
min="1"
placeholder="e.g., 4"
/>
<p class="help-text">How many events will be in this series? (optional)</p>
</div>
</div>
</div>
<!-- Form Actions -->
<div class="flex justify-between items-center pt-6 border-t border-guild-700">
<NuxtLink
to="/admin/series-management"
class="px-4 py-2 text-guild-400 hover:text-guild-100 font-medium"
<div class="form-actions">
<NuxtLink
to="/admin/series-management"
class="btn"
>
Cancel
</NuxtLink>
<div class="flex gap-3">
<button
<div class="form-actions-right">
<button
type="button"
@click="createAndAddEvent"
:disabled="creating"
class="px-4 py-2 bg-candlelight-600 text-white rounded-lg hover:bg-candlelight-700 disabled:opacity-50 disabled:cursor-not-allowed font-medium"
:disabled="creating"
class="btn"
>
{{ creating ? 'Creating...' : 'Create & Add Event' }}
</button>
<button
type="submit"
:disabled="creating"
class="px-6 py-2 bg-earth-600 text-white rounded-lg hover:bg-earth-700 disabled:opacity-50 disabled:cursor-not-allowed font-medium"
<button
type="submit"
:disabled="creating"
class="btn btn-primary"
>
{{ creating ? 'Creating...' : 'Create Series' }}
</button>
@ -150,7 +130,8 @@
<script setup>
definePageMeta({
layout: 'admin'
layout: 'admin',
middleware: 'admin',
})
const router = useRouter()
@ -190,22 +171,22 @@ const generateSlugFromTitle = () => {
const validateForm = () => {
formErrors.value = []
fieldErrors.value = {}
if (!seriesForm.title.trim()) {
formErrors.value.push('Series title is required')
fieldErrors.value.title = 'Please enter a series title'
}
if (!seriesForm.description.trim()) {
formErrors.value.push('Series description is required')
fieldErrors.value.description = 'Please provide a description for the series'
}
if (!generatedSlug.value) {
formErrors.value.push('Series title must generate a valid ID')
fieldErrors.value.title = 'Please enter a title that can generate a valid series ID'
}
return formErrors.value.length === 0
}
@ -214,7 +195,7 @@ const createSeries = async (redirectAfter = true) => {
window.scrollTo({ top: 0, behavior: 'smooth' })
return false
}
creating.value = true
try {
const response = await $fetch('/api/admin/series', {
@ -224,16 +205,16 @@ const createSeries = async (redirectAfter = true) => {
id: generatedSlug.value
}
})
showSuccessMessage.value = true
setTimeout(() => { showSuccessMessage.value = false }, 5000)
if (redirectAfter) {
setTimeout(() => {
router.push('/admin/series-management')
}, 1500)
}
return response.data
} catch (error) {
console.error('Failed to create series:', error)
@ -260,9 +241,127 @@ const createAndAddEvent = async () => {
totalEvents: seriesForm.totalEvents
}
}
sessionStorage.setItem('seriesEventData', JSON.stringify(seriesData))
router.push('/admin/events/create?series=true')
}
}
</script>
</script>
<style scoped>
.create-form {
max-width: 800px;
margin: 0 auto;
}
.page-header {
padding: 28px 28px 20px;
border-bottom: 1px dashed var(--border);
}
.page-header h1 {
font-family: 'Brygada 1918', serif;
font-size: 24px;
font-weight: 500;
color: var(--text-bright);
line-height: 1.2;
margin-bottom: 4px;
}
.page-header p { font-size: 12px; color: var(--text-dim); }
.back-link {
font-size: 12px;
color: var(--candle);
text-decoration: none;
margin-bottom: 8px;
display: inline-block;
}
.back-link:hover { text-decoration: underline; }
.form-body { padding: 24px 28px; }
.form-section { margin-bottom: 32px; }
.section-heading {
font-family: 'Brygada 1918', serif;
font-size: 16px;
font-weight: 500;
color: var(--text-bright);
padding-bottom: 10px;
border-bottom: 1px dashed var(--border);
margin-bottom: 16px;
}
.error-box {
display: flex;
gap: 10px;
padding: 16px 20px;
border: 1px dashed var(--ember);
margin-bottom: 20px;
font-size: 12px;
color: var(--ember);
}
.error-box ul { margin-top: 6px; padding: 0; list-style: none; }
.error-box li::before { content: '\2022\00a0'; }
.success-box {
display: flex;
gap: 10px;
padding: 16px 20px;
border: 1px dashed var(--candle);
margin-bottom: 20px;
font-size: 12px;
color: var(--candle);
}
.box-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
margin-top: 1px;
}
.required { color: var(--ember); }
.has-error { border-color: var(--ember) !important; }
.help-text { font-size: 11px; color: var(--text-faint); margin-top: 4px; }
.field-error { font-size: 11px; color: var(--ember); margin-top: 4px; }
.slug-display {
padding: 5px 8px;
font-family: 'Commit Mono', monospace;
font-size: 13px;
color: var(--text-bright);
background: var(--surface);
border: 1px dashed var(--border);
}
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.form-actions {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 20px;
border-top: 1px dashed var(--border);
margin-top: 32px;
}
.form-actions-right {
display: flex;
gap: 8px;
}
@media (max-width: 768px) {
.page-header { padding: 24px 20px 16px; }
.form-body { padding: 20px; }
.form-grid { grid-template-columns: 1fr; }
}
</style>