diff options
Diffstat (limited to 'web/pages')
| -rw-r--r-- | web/pages/agenda.vue | 52 | ||||
| -rw-r--r-- | web/pages/conferences.vue | 124 | ||||
| -rw-r--r-- | web/pages/events.vue | 11 | ||||
| -rw-r--r-- | web/pages/index.vue | 9 | ||||
| -rw-r--r-- | web/pages/live.vue | 9 | ||||
| -rw-r--r-- | web/pages/login/[[provider]].vue | 98 | ||||
| -rw-r--r-- | web/pages/tracks/[slug].vue | 9 | ||||
| -rw-r--r-- | web/pages/tracks/index.vue | 12 |
8 files changed, 242 insertions, 82 deletions
diff --git a/web/pages/agenda.vue b/web/pages/agenda.vue index 9b55c9b..5126d78 100644 --- a/web/pages/agenda.vue +++ b/web/pages/agenda.vue @@ -8,6 +8,10 @@ const scheduleStore = useScheduleStore(); const errorStore = useErrorStore(); const config = useRuntimeConfig(); +definePageMeta({ + middleware: ['logged-in', 'conference-selected'] +}) + const favouriteEvents = computed(() => { return scheduleStore.events.filter((event) => favouritesStore.isFavourite(event)); }); @@ -19,44 +23,28 @@ const refConfirmDeleteDialog = ref<typeof Dialog>(); const calendarAction = ref(false); -useFetch(config.public.baseURL + '/calendar', { - method: 'GET', - server: false, - lazy: true, - onResponse: ({ response }) => { - if (!response.ok) { - if (response.status !== 404) { - errorStore.setError(response._data.message || 'An unknown error occurred'); - } - } else if (response._data) { - calendarLink.value = (response._data as any).data.url; - } - calendarStatus.value = 'idle'; - }, -}); - function generateCalendar() { calendarAction.value = true; - useFetch(config.public.baseURL + '/calendar', { + $api(config.public.baseURL + '/calendar', { method: 'POST', server: false, lazy: true, - onResponseError: ({ response }) => { - errorStore.setError(response._data.message || 'An unknown error occurred'); - calendarAction.value = false; - }, onResponse: ({ response }) => { + calendarAction.value = false; + if (!response.ok) { + errorStore.setError(response._data.message || 'An unknown error occurred'); + return + } if (response._data) { calendarLink.value = (response._data as any).data.url; } - calendarAction.value = false; }, }); } function deleteCalendar() { calendarAction.value = true; - useFetch(config.public.baseURL + '/calendar', { + $api(config.public.baseURL + '/calendar', { method: 'DELETE', server: false, onResponseError: ({ response }) => { @@ -70,6 +58,24 @@ function deleteCalendar() { }); } +onMounted(() => { + $api(config.public.baseURL + '/calendar', { + method: 'GET', + server: false, + lazy: true, + onResponse: ({ response }) => { + calendarStatus.value = 'idle'; + if (!response.ok) { + if (response.status !== 404) { + errorStore.setError(response._data.message || 'Could not fetch calendar'); + } + } else if (response._data) { + calendarLink.value = (response._data as any).data.url; + } + }, + }); +}) + </script> <template> 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 diff --git a/web/pages/events.vue b/web/pages/events.vue index 093e959..d369bdc 100644 --- a/web/pages/events.vue +++ b/web/pages/events.vue @@ -2,12 +2,21 @@ import { Calendar, SquareGanttChart } from 'lucide-vue-next'; import { useScheduleStore } from '~/stores/schedule'; +definePageMeta({ + middleware: ['logged-in', 'conference-selected'] +}) + const scheduleStore = useScheduleStore(); </script> <template> - <Panel title="Events" :icon="SquareGanttChart" v-if="scheduleStore.schedule"> + <div v-if="scheduleStore.status === 'pending'" class="loading"> + <span class="loading-text"> + <Spinner color="var(--color-text-muted)" />Updating schedule... + </span> + </div> + <Panel title="Events" :icon="SquareGanttChart" v-else> <div v-for="[day, events] of Object.entries(scheduleStore.eventsPerDay)" :key="day" class="events-container"> <ul class="events-list"> <li v-for="event in events" :key="event.id" class="event-item" :data-index="event.id"> diff --git a/web/pages/index.vue b/web/pages/index.vue index c455678..e662bc7 100644 --- a/web/pages/index.vue +++ b/web/pages/index.vue @@ -1,4 +1,8 @@ <script setup lang="ts"> +definePageMeta({ + middleware: ['logged-in', 'conference-selected'] +}) + const scheduleStore = useScheduleStore(); const destination = ref() @@ -13,6 +17,11 @@ if (scheduleStore.isConferenceOngoing()) { </script> <template> + <div v-if="scheduleStore.status === 'pending'" class="loading"> + <span class="loading-text"> + <Spinner color="var(--color-text-muted)" />Updating schedule... + </span> + </div> <Panel kind="success"> <span class="text-icon"> <Spinner /> diff --git a/web/pages/live.vue b/web/pages/live.vue index d69dce5..638910e 100644 --- a/web/pages/live.vue +++ b/web/pages/live.vue @@ -4,6 +4,10 @@ import EventListing from '~/components/EventListing.vue'; import Panel from '~/components/Panel.vue'; import { type Event } from '~/stores/schedule'; +definePageMeta({ + middleware: ['logged-in', 'conference-selected'] +}) + const favouritesStore = useFavouritesStore(); const scheduleStore = useScheduleStore(); @@ -56,6 +60,11 @@ function isToday(date: Date): boolean { </script> <template> + <div v-if="scheduleStore.status === 'pending'" class="loading"> + <span class="loading-text"> + <Spinner color="var(--color-text-muted)" />Updating schedule... + </span> + </div> <Panel kind="emphasis" class="ongoing" v-if="happeningNow.length > 0" title="Now" :icon="Radio"> <ul class="events-list"> <li v-for="event in showAllHappeningNow ? happeningNow : favouritesHappeningNow" :key="event.id"> diff --git a/web/pages/login/[[provider]].vue b/web/pages/login/[[provider]].vue index bfc7e69..58cf2b1 100644 --- a/web/pages/login/[[provider]].vue +++ b/web/pages/login/[[provider]].vue @@ -16,6 +16,7 @@ const basicAuthEnabled = ref(false) const route = useRoute() const config = useRuntimeConfig() +const authStore = useAuthStore() const loginOptionsStore = useLoginOptionsStore() const headers = useRequestHeaders(['cookie']) @@ -25,6 +26,17 @@ watch(loginOptions, (options) => { basicAuthEnabled.value = options.some(o => o.type === 'basic') }) +const authFail = (e: any) => { + if ((e as FetchError).data) { + error.value = e.data.message + } else { + error.value = "An unknown error occurred" + } + + authenticating.value = false + authenticatingProvider.value = '' +} + const handleBasicAuth = async (e: Event, providerName: string) => { const target = e.target as HTMLFormElement; const formData = new FormData(target); @@ -32,48 +44,43 @@ const handleBasicAuth = async (e: Event, providerName: string) => { authenticating.value = true authenticatingProvider.value = providerName - try { - await $fetch(config.public.baseURL + '/login/' + providerName, { - method: 'POST', - body: JSON.stringify(Object.fromEntries(formData)), - headers: headers, - server: false, - }); - - navigateTo("/"); - } catch (e: any) { - if ((e as FetchError).data) { - error.value = e.data.message - } else { - error.value = "An unknown error occurred" - } + $fetch(config.public.baseURL + '/login/' + providerName, { + method: 'POST', + body: JSON.stringify(Object.fromEntries(formData)), + headers: headers, + server: false, + onResponse: ({ response }) => { + authStore.token = response._data.data.token + authStore.username = response._data.data.username + authStore.admin = response._data.data.admin - authenticating.value = false - authenticatingProvider.value = '' - } + navigateTo("/"); + }, + onResponseError: authFail + }); + } const handleOIDCAuth = async (providerName: string) => { authenticating.value = true authenticatingProvider.value = providerName - try { - let response: any = await $fetch(config.public.baseURL + '/login/' + providerName, { - method: 'POST', - headers: headers, - server: false, - }); - navigateTo(response.data.url, { external: true }) - } catch (e: any) { - if ((e as FetchError).data) { - error.value = e.data.message - } else { - error.value = "An unknown error occurred" - } + $fetch(config.public.baseURL + '/login/' + providerName, { + method: 'POST', + headers: headers, + server: false, + onResponse: ({ response }) => { + if (response._data.data.url) { + navigateTo(response._data.data.url, { external: true }) + } else { + authStore.token = response._data.data.token + authStore.admin = response._data.data.admin - authenticating.value = false - authenticatingProvider.value = '' - } + navigateTo("/"); + } + }, + onResponseError: authFail + }); } onMounted(async () => { @@ -230,30 +237,9 @@ div.auth-form { gap: 1.5rem; } -form.basic-form { - display: grid; - gap: 1.5rem; -} - div.auth-error { color: var(--color-text-error); - font-style: oblique; -} - -div.form-group { - display: flex; - flex-direction: column; -} - -label.form-label { - display: block; - font-size: 0.875rem; - font-weight: 500; - color: #374151; -} - -div.form-input-container { - margin-top: 0.25rem; + font-style: italic; } div.form-footer { diff --git a/web/pages/tracks/[slug].vue b/web/pages/tracks/[slug].vue index 27fb97d..9e1881d 100644 --- a/web/pages/tracks/[slug].vue +++ b/web/pages/tracks/[slug].vue @@ -2,6 +2,10 @@ import { TrainTrack } from 'lucide-vue-next'; import { useScheduleStore } from '~/stores/schedule'; +definePageMeta({ + middleware: ['logged-in', 'conference-selected'] +}) + const route = useRoute(); const scheduleStore = useScheduleStore(); @@ -9,6 +13,11 @@ const track = scheduleStore.schedule?.tracks.find((track) => track.slug === rout </script> <template> + <div v-if="scheduleStore.status === 'pending'" class="loading"> + <span class="loading-text"> + <Spinner color="var(--color-text-muted)" />Updating schedule... + </span> + </div> <Panel v-if="track" :title="track.name" :breadcrumbs="[{ text: 'Tracks', to: '/tracks' }]" :icon="TrainTrack"> <ul class="events-list"> <li diff --git a/web/pages/tracks/index.vue b/web/pages/tracks/index.vue index 8d7534e..c3ec883 100644 --- a/web/pages/tracks/index.vue +++ b/web/pages/tracks/index.vue @@ -2,14 +2,22 @@ import { TrainTrack } from 'lucide-vue-next'; import Panel from '~/components/Panel.vue'; +definePageMeta({ + middleware: ['logged-in', 'conference-selected'] +}) const scheduleStore = useScheduleStore(); </script> <template> - <Panel v-if="scheduleStore.schedule" title="Tracks" :icon="TrainTrack"> + <div v-if="scheduleStore.status === 'pending'" class="loading"> + <span class="loading-text"> + <Spinner color="var(--color-text-muted)" />Updating schedule... + </span> + </div> + <Panel v-else title="Tracks" :icon="TrainTrack"> <ul class="tracks-list"> <li - v-for="track in scheduleStore.schedule.tracks" + v-for="track in scheduleStore.schedule?.tracks" :key="track.name" class="tracks-item" > |
