aboutsummaryrefslogtreecommitdiffstats
path: root/web/pages
diff options
context:
space:
mode:
Diffstat (limited to 'web/pages')
-rw-r--r--web/pages/agenda.vue52
-rw-r--r--web/pages/conferences.vue124
-rw-r--r--web/pages/events.vue11
-rw-r--r--web/pages/index.vue9
-rw-r--r--web/pages/live.vue9
-rw-r--r--web/pages/login/[[provider]].vue98
-rw-r--r--web/pages/tracks/[slug].vue9
-rw-r--r--web/pages/tracks/index.vue12
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"
>