From ecc6a55aba7bb35fc778e7a53848396b88214151 Mon Sep 17 00:00:00 2001
From: Leonardo Bishop <me@leonardobishop.com>
Date: Sat, 23 Aug 2025 22:29:28 +0100
Subject: Add multiple conferences feature

---
 web/pages/conferences.vue | 124 ++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 124 insertions(+)
 create mode 100644 web/pages/conferences.vue

(limited to 'web/pages/conferences.vue')

diff --git a/web/pages/conferences.vue b/web/pages/conferences.vue
new file mode 100644
index 0000000..993bc66
--- /dev/null
+++ b/web/pages/conferences.vue
@@ -0,0 +1,124 @@
+<script setup lang="ts">
+import { MapPinPlus, MapPin } from 'lucide-vue-next';
+import { expireAuth } from '~/composables/expire-auth';
+
+definePageMeta({
+  middleware: ['logged-in']
+})
+
+interface Conference {
+  id: number,
+  url: string,
+  title: string,
+  venue: string,
+  city: string,
+}
+
+const setting = ref(false)
+const conferences = ref([] as Conference[])
+
+const conferenceStore = useConferenceStore();
+const errorStore = useErrorStore();
+const authStore = useAuthStore();
+const config = useRuntimeConfig();
+
+const status = ref('idle' as 'loading' | 'idle')
+
+const fetchConferences = () => {
+  status.value = 'loading'
+  $api(config.public.baseURL + '/conference', {
+    method: 'GET',
+    onResponse: ({ response }) => {
+      if (!response.ok) {
+        if (response.status === 401) {
+          expireAuth()
+          return
+        }
+        errorStore.setError(response._data.message || 'An unknown error occurred');
+      }
+      status.value = 'idle'
+      conferences.value = response._data.data
+    },
+  })
+}
+
+const deleteConference = (id: number) => {
+  // todo make this better
+  $api(config.public.baseURL + '/conference', {
+    method: 'DELETE',
+    body: {
+      id: id
+    },
+    onResponse: ({ response }) => {
+      if (!response.ok) {
+        errorStore.setError(response._data.message || 'An unknown error occurred');
+      }
+    },
+  })
+}
+
+const selectConference = async (c) => {
+  setting.value = true
+  conferenceStore.id = c.id
+  try {
+    await fetchSchedule()
+    await fetchFavourites()
+    navigateTo({ path: "/events" })
+  } catch (e) {
+    conferenceStore.clear()
+    setting.value = false
+  }
+}
+
+onMounted(fetchConferences)
+</script>
+
+<template>
+  <template v-if="!setting">
+    <Panel title="Conferences" :icon="MapPin">
+      <span class="loading-text" v-if="status === 'loading'"><Spinner color="var(--color-text-muted)" />Fetching conferences...</span>
+      <div class="conference-list" v-if="conferences.length > 0 && status !== 'loading'">
+        <template v-for="conference of conferences">
+          <span class="title">{{ conference.title }}</span>
+          <span>{{ conference.city }}</span>
+          <span>{{ conference.venue }}</span>
+          <span class="actions">
+            <Button v-if="authStore.admin" kind="secondary" @click="() => { deleteConference(conference.id) }">Delete</Button>
+            <Button @click="() => { selectConference(conference) }">Select</Button>
+          </span>
+        </template>
+      </div>
+      <p v-if="conferences.length == 0 && status !== 'loading'">
+        There are no conferences to display.
+      </p>
+    </Panel>
+    
+    <Panel v-if="authStore.admin" title="Add conference" :icon="MapPinPlus">
+      <AddConference @update="fetchConferences" />
+    </Panel>
+  </template>
+  <template v-else>
+    <div class="loading">
+      <span class="loading-text"><Spinner color="var(--color-text-muted)" />Setting conference...</span>
+    </div>
+  </template>
+</template>
+
+<style>
+.conference-list {
+  display: grid;
+  grid-template-columns: 1fr 1fr 2fr 1fr;
+  align-items: center;
+  gap: 0.5rem;
+}
+
+.conference-list > .title {
+  font-weight: bold;
+}
+
+.conference-list > .actions {
+  display: flex;
+  gap: 0.5rem;
+  justify-self: flex-end;
+}
+</style>
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2

