Add landing page
This commit is contained in:
parent
3fea484585
commit
bce86ee840
47 changed files with 7119 additions and 439 deletions
|
|
@ -217,6 +217,42 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Series Ticketing Info -->
|
||||
<div
|
||||
v-if="series.tickets?.enabled"
|
||||
class="px-6 py-3 bg-blue-50 dark:bg-blue-950/20 border-t border-default"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<Icon name="heroicons:ticket" class="w-5 h-5 text-blue-600" />
|
||||
<div>
|
||||
<span class="text-sm font-medium text-default">
|
||||
Series Pass Ticketing Enabled
|
||||
</span>
|
||||
<p class="text-xs text-dimmed">
|
||||
<span v-if="series.tickets.public?.available">
|
||||
Public: ${{ series.tickets.public.price || 0 }}
|
||||
</span>
|
||||
<span v-if="series.tickets.member?.available" class="ml-2">
|
||||
| Members:
|
||||
{{
|
||||
series.tickets.member.isFree
|
||||
? "Free"
|
||||
: `$${series.tickets.member.price || 0}`
|
||||
}}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@click="manageSeriesTickets(series)"
|
||||
class="text-sm text-blue-600 hover:text-blue-700 font-medium"
|
||||
>
|
||||
Manage Tickets
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Series Actions -->
|
||||
<div class="px-6 py-3 bg-muted border-t border-default">
|
||||
<div class="flex justify-between items-center">
|
||||
|
|
@ -224,6 +260,12 @@
|
|||
{{ formatDateRange(series.startDate, series.endDate) }}
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
@click="manageSeriesTickets(series)"
|
||||
class="text-sm text-primary hover:text-primary font-medium"
|
||||
>
|
||||
Ticketing
|
||||
</button>
|
||||
<button
|
||||
@click="editSeries(series)"
|
||||
class="text-sm text-primary hover:text-primary font-medium"
|
||||
|
|
@ -459,6 +501,384 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Series Ticketing Modal -->
|
||||
<div
|
||||
v-if="editingTicketsSeriesId"
|
||||
class="fixed inset-0 bg-gray-600 bg-opacity-50 flex items-center justify-center p-4 z-50"
|
||||
>
|
||||
<div
|
||||
class="bg-elevated rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-y-auto"
|
||||
>
|
||||
<div class="px-6 py-4 border-b border-default">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-highlighted">
|
||||
Series Pass Ticketing
|
||||
</h3>
|
||||
<p class="text-sm text-muted">{{ editingTicketsData.title }}</p>
|
||||
</div>
|
||||
<button
|
||||
@click="cancelTicketsEdit"
|
||||
class="text-muted hover:text-default"
|
||||
>
|
||||
<Icon name="heroicons:x-mark" class="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-6 space-y-6">
|
||||
<!-- Enable Ticketing Toggle -->
|
||||
<div class="p-4 bg-muted rounded-lg">
|
||||
<label class="flex items-start cursor-pointer">
|
||||
<input
|
||||
v-model="editingTicketsData.tickets.enabled"
|
||||
type="checkbox"
|
||||
class="rounded border-default text-blue-600 focus:ring-blue-500 mt-1"
|
||||
/>
|
||||
<div class="ml-3">
|
||||
<span class="text-sm font-medium text-default"
|
||||
>Enable Series Pass Ticketing</span
|
||||
>
|
||||
<p class="text-xs text-dimmed">
|
||||
Allow users to purchase a pass for all events in this series
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="editingTicketsData.tickets.enabled" class="space-y-6">
|
||||
<!-- Ticketing Behavior -->
|
||||
<div class="space-y-4">
|
||||
<h4 class="text-sm font-semibold text-highlighted">
|
||||
Ticketing Behavior
|
||||
</h4>
|
||||
|
||||
<label class="flex items-start cursor-pointer">
|
||||
<input
|
||||
v-model="editingTicketsData.tickets.requiresSeriesTicket"
|
||||
type="checkbox"
|
||||
class="rounded border-default text-blue-600 focus:ring-blue-500 mt-1"
|
||||
/>
|
||||
<div class="ml-3">
|
||||
<span class="text-sm font-medium text-default"
|
||||
>Require Series Pass</span
|
||||
>
|
||||
<p class="text-xs text-dimmed">
|
||||
Users must buy the series pass; individual event tickets are
|
||||
not available
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="flex items-start cursor-pointer">
|
||||
<input
|
||||
v-model="
|
||||
editingTicketsData.tickets.allowIndividualEventTickets
|
||||
"
|
||||
type="checkbox"
|
||||
class="rounded border-default text-blue-600 focus:ring-blue-500 mt-1"
|
||||
:disabled="editingTicketsData.tickets.requiresSeriesTicket"
|
||||
/>
|
||||
<div class="ml-3">
|
||||
<span class="text-sm font-medium text-default"
|
||||
>Allow Individual Event Tickets</span
|
||||
>
|
||||
<p class="text-xs text-dimmed">
|
||||
Users can attend single events without buying the full
|
||||
series pass
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Member Tickets -->
|
||||
<div class="border border-default rounded-lg p-4">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h4 class="text-sm font-semibold text-highlighted">
|
||||
Member Series Pass
|
||||
</h4>
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<span class="text-xs text-muted mr-2">Available</span>
|
||||
<input
|
||||
v-model="editingTicketsData.tickets.member.available"
|
||||
type="checkbox"
|
||||
class="rounded border-default text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="editingTicketsData.tickets.member.available"
|
||||
class="space-y-4"
|
||||
>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-default mb-2"
|
||||
>Pass Name</label
|
||||
>
|
||||
<UInput
|
||||
v-model="editingTicketsData.tickets.member.name"
|
||||
placeholder="e.g., Member Series Pass"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-default mb-2"
|
||||
>Price (CAD)</label
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<UInput
|
||||
v-model.number="editingTicketsData.tickets.member.price"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
placeholder="0.00"
|
||||
:disabled="editingTicketsData.tickets.member.isFree"
|
||||
class="flex-1 w-full"
|
||||
/>
|
||||
<label
|
||||
class="flex items-center whitespace-nowrap cursor-pointer"
|
||||
>
|
||||
<input
|
||||
v-model="editingTicketsData.tickets.member.isFree"
|
||||
type="checkbox"
|
||||
class="rounded border-default text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<span class="ml-1 text-xs text-muted">Free</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-default mb-2"
|
||||
>Description</label
|
||||
>
|
||||
<UTextarea
|
||||
v-model="editingTicketsData.tickets.member.description"
|
||||
placeholder="Describe what's included with the member series pass..."
|
||||
:rows="2"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Public Tickets -->
|
||||
<div class="border border-default rounded-lg p-4">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h4 class="text-sm font-semibold text-highlighted">
|
||||
Public Series Pass
|
||||
</h4>
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<span class="text-xs text-muted mr-2">Available</span>
|
||||
<input
|
||||
v-model="editingTicketsData.tickets.public.available"
|
||||
type="checkbox"
|
||||
class="rounded border-default text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="editingTicketsData.tickets.public.available"
|
||||
class="space-y-4"
|
||||
>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-default mb-2"
|
||||
>Pass Name</label
|
||||
>
|
||||
<UInput
|
||||
v-model="editingTicketsData.tickets.public.name"
|
||||
placeholder="e.g., Series Pass"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-default mb-2"
|
||||
>Price (CAD)</label
|
||||
>
|
||||
<UInput
|
||||
v-model.number="editingTicketsData.tickets.public.price"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
placeholder="0.00"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-default mb-2"
|
||||
>Description</label
|
||||
>
|
||||
<UTextarea
|
||||
v-model="editingTicketsData.tickets.public.description"
|
||||
placeholder="Describe what's included with the public series pass..."
|
||||
:rows="2"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-default mb-2"
|
||||
>Quantity Available</label
|
||||
>
|
||||
<UInput
|
||||
v-model.number="
|
||||
editingTicketsData.tickets.public.quantity
|
||||
"
|
||||
type="number"
|
||||
min="1"
|
||||
placeholder="Leave blank for unlimited"
|
||||
class="w-full"
|
||||
/>
|
||||
<p class="text-xs text-dimmed mt-1">
|
||||
{{ editingTicketsData.tickets.public.sold || 0 }} sold,
|
||||
{{ editingTicketsData.tickets.public.reserved || 0 }}
|
||||
reserved
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-default mb-2"
|
||||
>Early Bird Price (Optional)</label
|
||||
>
|
||||
<UInput
|
||||
v-model.number="
|
||||
editingTicketsData.tickets.public.earlyBirdPrice
|
||||
"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
placeholder="0.00"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="editingTicketsData.tickets.public.earlyBirdPrice > 0"
|
||||
>
|
||||
<label class="block text-sm font-medium text-default mb-2"
|
||||
>Early Bird Deadline</label
|
||||
>
|
||||
<UInput
|
||||
v-model="
|
||||
editingTicketsData.tickets.public.earlyBirdDeadline
|
||||
"
|
||||
type="datetime-local"
|
||||
class="w-full"
|
||||
/>
|
||||
<p class="text-xs text-dimmed mt-1">
|
||||
Price increases to ${{
|
||||
editingTicketsData.tickets.public.price
|
||||
}}
|
||||
after this date
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Capacity Management -->
|
||||
<div class="border border-default rounded-lg p-4">
|
||||
<h4 class="text-sm font-semibold text-highlighted mb-4">
|
||||
Capacity Management
|
||||
</h4>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-default mb-2"
|
||||
>Total Capacity</label
|
||||
>
|
||||
<UInput
|
||||
v-model.number="editingTicketsData.tickets.capacity.total"
|
||||
type="number"
|
||||
min="1"
|
||||
placeholder="Leave blank for unlimited"
|
||||
class="w-full"
|
||||
/>
|
||||
<p class="text-xs text-dimmed mt-1">
|
||||
Maximum series pass holders across all types
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-default mb-2"
|
||||
>Currently Reserved</label
|
||||
>
|
||||
<UInput
|
||||
v-model.number="
|
||||
editingTicketsData.tickets.capacity.reserved
|
||||
"
|
||||
type="number"
|
||||
min="0"
|
||||
disabled
|
||||
class="w-full bg-accented"
|
||||
/>
|
||||
<p class="text-xs text-dimmed mt-1">
|
||||
Auto-calculated during checkout
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Waitlist Configuration -->
|
||||
<div class="border border-default rounded-lg p-4">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h4 class="text-sm font-semibold text-highlighted">Waitlist</h4>
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<span class="text-xs text-muted mr-2">Enable Waitlist</span>
|
||||
<input
|
||||
v-model="editingTicketsData.tickets.waitlist.enabled"
|
||||
type="checkbox"
|
||||
class="rounded border-default text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="editingTicketsData.tickets.waitlist.enabled">
|
||||
<label class="block text-sm font-medium text-default mb-2"
|
||||
>Max Waitlist Size</label
|
||||
>
|
||||
<UInput
|
||||
v-model.number="editingTicketsData.tickets.waitlist.maxSize"
|
||||
type="number"
|
||||
min="1"
|
||||
placeholder="Leave blank for unlimited"
|
||||
class="w-full"
|
||||
/>
|
||||
<p class="text-xs text-dimmed mt-1">
|
||||
{{ editingTicketsData.tickets.waitlist.entries?.length || 0 }}
|
||||
people currently on waitlist
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-6 py-4 border-t border-default flex justify-end gap-3">
|
||||
<button
|
||||
@click="cancelTicketsEdit"
|
||||
class="px-4 py-2 text-muted hover:text-default"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
@click="saveTicketsEdit"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
||||
>
|
||||
Save Ticketing Settings
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -477,6 +897,43 @@ const editingSeriesData = ref({
|
|||
type: "workshop_series",
|
||||
totalEvents: null,
|
||||
});
|
||||
const editingTicketsSeriesId = ref(null);
|
||||
const editingTicketsData = ref({
|
||||
title: "",
|
||||
tickets: {
|
||||
enabled: false,
|
||||
requiresSeriesTicket: false,
|
||||
allowIndividualEventTickets: true,
|
||||
currency: "CAD",
|
||||
member: {
|
||||
available: true,
|
||||
isFree: true,
|
||||
price: 0,
|
||||
name: "Member Series Pass",
|
||||
description: "",
|
||||
},
|
||||
public: {
|
||||
available: false,
|
||||
name: "Series Pass",
|
||||
description: "",
|
||||
price: 0,
|
||||
quantity: null,
|
||||
sold: 0,
|
||||
reserved: 0,
|
||||
earlyBirdPrice: null,
|
||||
earlyBirdDeadline: "",
|
||||
},
|
||||
capacity: {
|
||||
total: null,
|
||||
reserved: 0,
|
||||
},
|
||||
waitlist: {
|
||||
enabled: false,
|
||||
maxSize: null,
|
||||
entries: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Fetch series data
|
||||
const {
|
||||
|
|
@ -715,6 +1172,116 @@ const deleteSeries = async (series) => {
|
|||
}
|
||||
};
|
||||
|
||||
// Ticketing management functions
|
||||
const manageSeriesTickets = (series) => {
|
||||
editingTicketsSeriesId.value = series.id;
|
||||
|
||||
// Deep clone the series data to avoid mutating the original
|
||||
editingTicketsData.value = {
|
||||
title: series.title,
|
||||
tickets: {
|
||||
enabled: series.tickets?.enabled || false,
|
||||
requiresSeriesTicket: series.tickets?.requiresSeriesTicket || false,
|
||||
allowIndividualEventTickets:
|
||||
series.tickets?.allowIndividualEventTickets !== false,
|
||||
currency: series.tickets?.currency || "CAD",
|
||||
member: {
|
||||
available: series.tickets?.member?.available !== false,
|
||||
isFree: series.tickets?.member?.isFree !== false,
|
||||
price: series.tickets?.member?.price || 0,
|
||||
name: series.tickets?.member?.name || "Member Series Pass",
|
||||
description: series.tickets?.member?.description || "",
|
||||
},
|
||||
public: {
|
||||
available: series.tickets?.public?.available || false,
|
||||
name: series.tickets?.public?.name || "Series Pass",
|
||||
description: series.tickets?.public?.description || "",
|
||||
price: series.tickets?.public?.price || 0,
|
||||
quantity: series.tickets?.public?.quantity || null,
|
||||
sold: series.tickets?.public?.sold || 0,
|
||||
reserved: series.tickets?.public?.reserved || 0,
|
||||
earlyBirdPrice: series.tickets?.public?.earlyBirdPrice || null,
|
||||
earlyBirdDeadline: series.tickets?.public?.earlyBirdDeadline
|
||||
? new Date(series.tickets.public.earlyBirdDeadline)
|
||||
.toISOString()
|
||||
.slice(0, 16)
|
||||
: "",
|
||||
},
|
||||
capacity: {
|
||||
total: series.tickets?.capacity?.total || null,
|
||||
reserved: series.tickets?.capacity?.reserved || 0,
|
||||
},
|
||||
waitlist: {
|
||||
enabled: series.tickets?.waitlist?.enabled || false,
|
||||
maxSize: series.tickets?.waitlist?.maxSize || null,
|
||||
entries: series.tickets?.waitlist?.entries || [],
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const cancelTicketsEdit = () => {
|
||||
editingTicketsSeriesId.value = null;
|
||||
editingTicketsData.value = {
|
||||
title: "",
|
||||
tickets: {
|
||||
enabled: false,
|
||||
requiresSeriesTicket: false,
|
||||
allowIndividualEventTickets: true,
|
||||
currency: "CAD",
|
||||
member: {
|
||||
available: true,
|
||||
isFree: true,
|
||||
price: 0,
|
||||
name: "Member Series Pass",
|
||||
description: "",
|
||||
},
|
||||
public: {
|
||||
available: false,
|
||||
name: "Series Pass",
|
||||
description: "",
|
||||
price: 0,
|
||||
quantity: null,
|
||||
sold: 0,
|
||||
reserved: 0,
|
||||
earlyBirdPrice: null,
|
||||
earlyBirdDeadline: "",
|
||||
},
|
||||
capacity: {
|
||||
total: null,
|
||||
reserved: 0,
|
||||
},
|
||||
waitlist: {
|
||||
enabled: false,
|
||||
maxSize: null,
|
||||
entries: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const saveTicketsEdit = async () => {
|
||||
try {
|
||||
// Update the series with new ticketing configuration
|
||||
await $fetch("/api/admin/series/tickets", {
|
||||
method: "PUT",
|
||||
body: {
|
||||
id: editingTicketsSeriesId.value,
|
||||
tickets: editingTicketsData.value.tickets,
|
||||
},
|
||||
});
|
||||
|
||||
await refresh();
|
||||
cancelTicketsEdit();
|
||||
alert("Ticketing settings updated successfully");
|
||||
} catch (error) {
|
||||
console.error("Failed to update ticketing settings:", error);
|
||||
alert(
|
||||
`Failed to update ticketing settings: ${error.data?.statusMessage || error.message}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Bulk operations
|
||||
const reorderAllSeries = async () => {
|
||||
// TODO: Implement auto-reordering
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue