Track which account is interested in which events
Store a list of ids of events and slots that accounts have marked as being interested in, and show aggeregate counts in the schedule.
This commit is contained in:
parent
ca51c07065
commit
db9a12250e
5 changed files with 137 additions and 0 deletions
|
@ -2,10 +2,35 @@
|
||||||
<section class="event">
|
<section class="event">
|
||||||
<h3>{{ event.name }}</h3>
|
<h3>{{ event.name }}</h3>
|
||||||
<p>{{ event.description ?? "No description provided" }}</p>
|
<p>{{ event.description ?? "No description provided" }}</p>
|
||||||
|
<p v-if="event.interested">
|
||||||
|
{{ event.interested }} interested
|
||||||
|
</p>
|
||||||
|
<p v-if="session">
|
||||||
|
<button
|
||||||
|
class="interested"
|
||||||
|
:class="{ active: interestedIds.has(event.id) }"
|
||||||
|
@click="toggle(event.id, event.slots.map(slot => slot.id))"
|
||||||
|
>
|
||||||
|
{{ interestedIds.has(event.id) ? "✔ interested" : "🔔 interested?" }}
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
|
||||||
<h4>Timeslots</h4>
|
<h4>Timeslots</h4>
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="slot in event.slots" :key="slot.id">
|
<li v-for="slot in event.slots" :key="slot.id">
|
||||||
{{ slot.start }} - {{ slot.end }}
|
{{ slot.start }} - {{ slot.end }}
|
||||||
|
<button
|
||||||
|
v-if="session && event.slots.length > 1"
|
||||||
|
class="interested"
|
||||||
|
:disabled="interestedIds.has(event.id)"
|
||||||
|
:class="{ active: interestedIds.has(event.id) || interestedIds.has(slot.id) }"
|
||||||
|
@click="toggle(slot.id)"
|
||||||
|
>
|
||||||
|
{{ interestedIds.has(event.id) || interestedIds.has(slot.id) ? "✔ interested" : "🔔 interested?" }}
|
||||||
|
</button>
|
||||||
|
<template v-if="slot.interested">
|
||||||
|
({{ slot.interested }} interested)
|
||||||
|
</template>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
@ -17,6 +42,27 @@ import type { ScheduleEvent } from '~/shared/types/schedule';
|
||||||
defineProps<{
|
defineProps<{
|
||||||
event: ScheduleEvent
|
event: ScheduleEvent
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const { data: session, refresh: refreshSession } = useAccountSession();
|
||||||
|
const interestedIds = computed(() => new Set(session.value?.account.interestedIds ?? []));
|
||||||
|
|
||||||
|
async function toggle(id: string, slotIds?: string[]) {
|
||||||
|
let newIds = [...session.value!.account.interestedIds ?? []];
|
||||||
|
if (interestedIds.value.has(id)) {
|
||||||
|
newIds = newIds.filter(newId => newId !== id);
|
||||||
|
} else {
|
||||||
|
newIds.push(id);
|
||||||
|
if (slotIds) {
|
||||||
|
const filterIds = new Set(slotIds);
|
||||||
|
newIds = newIds.filter(newId => !filterIds.has(newId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await $fetch("/api/account", {
|
||||||
|
method: "PATCH",
|
||||||
|
body: { interestedIds: newIds },
|
||||||
|
})
|
||||||
|
await refreshSession();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
@ -31,4 +77,11 @@ defineProps<{
|
||||||
.event + .event {
|
.event + .event {
|
||||||
margin-block-start: 0.5rem;
|
margin-block-start: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding-inline: 0.2em;
|
||||||
|
}
|
||||||
|
button.active {
|
||||||
|
color: color-mix(in oklab, var(--foreground), green 50%);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
46
server/api/account.patch.ts
Normal file
46
server/api/account.patch.ts
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import { Account } from "~/shared/types/account";
|
||||||
|
import { readAccounts, readSchedule, writeAccounts, writeSchedule } from "~/server/database";
|
||||||
|
import { broadcastUpdate } from "~/server/streams";
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const session = await requireAccountSession(event);
|
||||||
|
const body: Pick<Account, "interestedIds"> = await readBody(event);
|
||||||
|
if (
|
||||||
|
!(body.interestedIds instanceof Array)
|
||||||
|
|| !body.interestedIds.every(id => typeof id === "string")
|
||||||
|
) {
|
||||||
|
throw createError({
|
||||||
|
status: 400,
|
||||||
|
message: "Invalid interestedIds",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const accounts = await readAccounts();
|
||||||
|
const sessionAccount = accounts.find(account => account.id === session.accountId);
|
||||||
|
if (!sessionAccount) {
|
||||||
|
throw Error("Account does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.interestedIds.length) {
|
||||||
|
sessionAccount.interestedIds = body.interestedIds;
|
||||||
|
} else {
|
||||||
|
delete sessionAccount.interestedIds;
|
||||||
|
}
|
||||||
|
await writeAccounts(accounts);
|
||||||
|
|
||||||
|
const counts = new Map();
|
||||||
|
for (const account of accounts)
|
||||||
|
if (account.interestedIds)
|
||||||
|
for (const id of account.interestedIds)
|
||||||
|
counts.set(id, (counts.get(id) ?? 0) + 1);
|
||||||
|
|
||||||
|
const schedule = await readSchedule();
|
||||||
|
for (const event of schedule.events) {
|
||||||
|
event.interested = counts.get(event.id);
|
||||||
|
for (const slot of event.slots) {
|
||||||
|
slot.interested = counts.get(slot.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await writeSchedule(schedule);
|
||||||
|
broadcastUpdate(schedule);
|
||||||
|
})
|
|
@ -175,5 +175,40 @@ export function generateDemoAccounts(): Account[] {
|
||||||
type: (["regular", "crew", "admin"] as const)[Math.floor(random() ** 5 * 3)],
|
type: (["regular", "crew", "admin"] as const)[Math.floor(random() ** 5 * 3)],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// These have a much higher probability of being in someone's interested list.
|
||||||
|
const desiredEvent = ["opening", "closing", "fursuit-games"];
|
||||||
|
|
||||||
|
for (const account of accounts) {
|
||||||
|
const interestedIds: string[] = [];
|
||||||
|
for (const id of desiredEvent) {
|
||||||
|
if (random() < 0.5) {
|
||||||
|
interestedIds.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventsToAdd = Math.floor(random() * 10);
|
||||||
|
while (interestedIds.length < eventsToAdd) {
|
||||||
|
const event = events[Math.floor(random() * events.length)];
|
||||||
|
const eventId = toId(event.name);
|
||||||
|
if (interestedIds.some(id => id.replace(/-\d+$/, "") === eventId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.slots.length === 1 || random() < 0.8) {
|
||||||
|
interestedIds.push(toId(event.name))
|
||||||
|
} else {
|
||||||
|
for (const index of event.slots.map((_, index) => index)) {
|
||||||
|
if (random() < 0.5) {
|
||||||
|
interestedIds.push(toId(`${toId(event.name)}-${index}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (interestedIds.length) {
|
||||||
|
account.interestedIds = interestedIds;
|
||||||
|
}
|
||||||
|
}
|
||||||
return accounts;
|
return accounts;
|
||||||
}
|
}
|
||||||
|
|
1
shared/types/account.d.ts
vendored
1
shared/types/account.d.ts
vendored
|
@ -3,6 +3,7 @@ export interface Account {
|
||||||
type: "anonymous" | "regular" | "crew" | "admin",
|
type: "anonymous" | "regular" | "crew" | "admin",
|
||||||
/** Name of the account. Not present on anonymous accounts */
|
/** Name of the account. Not present on anonymous accounts */
|
||||||
name?: string,
|
name?: string,
|
||||||
|
interestedIds?: string[],
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Subscription {
|
export interface Subscription {
|
||||||
|
|
2
shared/types/schedule.d.ts
vendored
2
shared/types/schedule.d.ts
vendored
|
@ -4,6 +4,7 @@ export interface ScheduleEvent {
|
||||||
host?: string,
|
host?: string,
|
||||||
cancelled?: boolean,
|
cancelled?: boolean,
|
||||||
description?: string,
|
description?: string,
|
||||||
|
interested?: number,
|
||||||
slots: TimeSlot[],
|
slots: TimeSlot[],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,6 +19,7 @@ export interface TimeSlot {
|
||||||
start: string,
|
start: string,
|
||||||
end: string,
|
end: string,
|
||||||
locations: string[],
|
locations: string[],
|
||||||
|
interested?: number,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Schedule {
|
export interface Schedule {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue