aboutsummaryrefslogtreecommitdiffstats
path: root/web/components
diff options
context:
space:
mode:
authorLeonardo Bishop <me@leonardobishop.com>2025-08-23 22:29:28 +0100
committerLeonardo Bishop <me@leonardobishop.com>2025-08-23 22:29:28 +0100
commitecc6a55aba7bb35fc778e7a53848396b88214151 (patch)
tree1b37a2dc5f4594155114da1ae0c4529d20a4c548 /web/components
parent8f7dec8ba6b2f9bde01afd0a110596ebbd43e0ed (diff)
Add multiple conferences feature
Diffstat (limited to 'web/components')
-rw-r--r--web/components/AddConference.vue54
-rw-r--r--web/components/Button.vue2
-rw-r--r--web/components/Dialog.vue2
-rw-r--r--web/components/EventDetail.vue2
-rw-r--r--web/components/EventListing.vue10
-rw-r--r--web/components/Input.vue2
-rw-r--r--web/components/Panel.vue15
-rw-r--r--web/components/Sidebar.vue45
8 files changed, 97 insertions, 35 deletions
diff --git a/web/components/AddConference.vue b/web/components/AddConference.vue
new file mode 100644
index 0000000..95154d4
--- /dev/null
+++ b/web/components/AddConference.vue
@@ -0,0 +1,54 @@
+<script setup lang="ts">
+import { format } from 'date-fns';
+import { type Event as ScheduledEvent } from '~/stores/schedule';
+
+const errorStore = useErrorStore();
+const config = useRuntimeConfig();
+const loading = ref(false)
+
+const emit = defineEmits(['update']);
+
+const addConference = async (e: Event) => {
+ const target = e.target as HTMLFormElement;
+ const formData = new FormData(target);
+ loading.value = true
+
+ $api(config.public.baseURL + '/conference', {
+ method: 'POST',
+ body: JSON.stringify(Object.fromEntries(formData)),
+ onResponse: ({ response }) => {
+ loading.value = false
+ if (!response.ok) {
+ errorStore.setError(response._data?.message || 'An unknown error occurred');
+ return
+ }
+ emit('update')
+ },
+ });
+}
+
+</script>
+
+<template>
+ <div>
+ <form @submit.prevent="(e) => addConference(e)">
+ <div class="form-group">
+ <label for="url" class="form-label">
+ Schedule data URL
+ </label>
+ <div class="form-input-container">
+ <Input id="url" name="url" required />
+ </div>
+ </div>
+
+ <div class="form-submit">
+ <Button type="submit" :loading="loading">
+ Add
+ </Button>
+ </div>
+ </form>
+ </div>
+</template>
+
+<style scoped>
+</style> \ No newline at end of file
diff --git a/web/components/Button.vue b/web/components/Button.vue
index ce9eefc..27cfa4e 100644
--- a/web/components/Button.vue
+++ b/web/components/Button.vue
@@ -48,7 +48,7 @@ button {
gap: 0.4rem;
padding: 0.5rem 1rem;
border: 1px solid transparent;
- border-radius: 0.375rem;
+ border-radius: 2px;
font-size: var(--text-small);
font-family: var(--font-family);
font-weight: 500;
diff --git a/web/components/Dialog.vue b/web/components/Dialog.vue
index 7772f23..04e1461 100644
--- a/web/components/Dialog.vue
+++ b/web/components/Dialog.vue
@@ -72,7 +72,7 @@ const onDialogClick = (e: MouseEvent) => {
<style scoped>
dialog {
outline: none;
- border-radius: 0.5rem;
+ border-radius: 2px;
padding: 1rem;
width: 1000px;
margin: 0;
diff --git a/web/components/EventDetail.vue b/web/components/EventDetail.vue
index b4f7bd9..8fd5b03 100644
--- a/web/components/EventDetail.vue
+++ b/web/components/EventDetail.vue
@@ -43,7 +43,7 @@ const getHostname = (url: string) => new URL(url).hostname;
</div>
<div>
- <span class="event-track"><NuxtLink :to="'/tracks/' + event.track.slug">{{ event.track.name }}</NuxtLink> &bull;</span> <a v-if="event.url" class="event-url" :href="event.url" target="_blank">view on {{ getHostname(event.url)}}</a>
+ <span v-if="event.track" class="event-track"><NuxtLink :to="'/tracks/' + event.track.slug">{{ event.track.name }}</NuxtLink> &bull;</span> <a v-if="event.url" class="event-url" :href="event.url" target="_blank">view on {{ getHostname(event.url)}}</a>
</div>
</div>
</template>
diff --git a/web/components/EventListing.vue b/web/components/EventListing.vue
index 5c04189..0cc546c 100644
--- a/web/components/EventListing.vue
+++ b/web/components/EventListing.vue
@@ -10,6 +10,7 @@ const { event, showRelativeTime } = defineProps<{
}>();
const selectedEventStore = useSelectedEventStore();
+const conferenceStore = useConferenceStore();
const favouritesStore = useFavouritesStore();
const errorStore = useErrorStore();
const config = useRuntimeConfig();
@@ -43,9 +44,10 @@ const addFavourite = async () => {
addingToFavourite.value = true;
try {
- const res = await $fetch(config.public.baseURL + '/favourites', {
+ const res = await $api(config.public.baseURL + '/favourites', {
method: 'POST',
body: JSON.stringify({
+ conferenceId: conferenceStore.id,
eventGuid: event.guid,
eventId: event.id,
}),
@@ -70,7 +72,7 @@ const removeFavourite = async () => {
addingToFavourite.value = true;
try {
- await $fetch(config.public.baseURL + '/favourites', {
+ await $api(config.public.baseURL + '/favourites', {
method: 'DELETE',
body: JSON.stringify({
eventGuid: event.guid,
@@ -102,9 +104,9 @@ const removeFavourite = async () => {
</span>
<span class="event-title">{{ event.title }}</span>
<span class="event-speaker">{{ event.persons.map(p => p.name).join(", ") }}</span>
- <span class="event-track">{{ event.track.name }}</span>
+ <span class="event-track">{{ event.track?.name }}</span>
</div>
- <template v-if="!addingToFavourite" class="event-button">
+ <template v-if="!addingToFavourite && favouritesStore.status !== 'pending'" class="event-button">
<StarIcon v-if="favouritesStore.isFavourite(event)" color="var(--color-favourite)" fill="var(--color-favourite)" class="event-button" @click="removeFavourite" />
<StarIcon v-else color="var(--color-text-muted)" class="event-button" @click="addFavourite" />
</template>
diff --git a/web/components/Input.vue b/web/components/Input.vue
index b541566..aebece6 100644
--- a/web/components/Input.vue
+++ b/web/components/Input.vue
@@ -82,7 +82,7 @@ input {
width: 100%;
padding: 0.5rem 0.75rem;
border: 1px solid #d1d5db;
- border-radius: 0.375rem;
+ border-radius: 2px;
box-sizing: border-box;
/* box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); */
font-family: var(--font-family);
diff --git a/web/components/Panel.vue b/web/components/Panel.vue
index 1f2d22e..d417691 100644
--- a/web/components/Panel.vue
+++ b/web/components/Panel.vue
@@ -4,7 +4,7 @@ import { defineProps, type FunctionalComponent, type PropType } from 'vue';
defineProps({
kind: {
- type: String as PropType<"normal" | "error" | "success" | "emphasis">,
+ type: String as PropType<"normal" | "top" | "error" | "success" | "emphasis">,
required: false,
default: 'normal',
},
@@ -47,14 +47,11 @@ defineProps({
<style>
div.card {
padding: 1rem;
- border-radius: 0.5rem;
}
div.card-header {
padding: 1rem 1rem;
margin: -1rem -1rem 0 -1rem;
- border-top-left-radius: 0.5rem;
- border-top-right-radius: 0.5rem;;
display: flex;
justify-content: space-between;
align-items: center;
@@ -73,7 +70,7 @@ div.header-left > svg {
}
span.card-title {
- font-size: 1.5rem;
+ font-size: 1.3rem;
font-weight: 700;
}
@@ -99,17 +96,21 @@ span.breadcrumb > a:hover {
color: var(--color-primary);
}
-div.normal {
+div.normal, div.top {
background-color: white;
border: 0.1rem solid var(--color-border);
}
-div.normal .card-header {
+div.normal .card-header, div.top .card-header {
background-color: var(--color-background-muted);
border-bottom: 1px solid var(--color-border);
margin: -1rem -1rem 1rem -1rem;
}
+div.top {
+ border-top: 3px solid var(--color-primary)
+}
+
div.error {
background-color: var(--color-error-light);
border: 0.1rem solid var(--color-border-error-light);
diff --git a/web/components/Sidebar.vue b/web/components/Sidebar.vue
index 5fc42d3..fd64434 100644
--- a/web/components/Sidebar.vue
+++ b/web/components/Sidebar.vue
@@ -3,6 +3,7 @@ import { formatDistanceToNow } from "date-fns";
import { LucideClock, LucideRadio } from "lucide-vue-next";
const scheduleStore = useScheduleStore();
+const conferenceStore = useConferenceStore();
const errorStore = useErrorStore();
const timer = ref();
@@ -30,31 +31,35 @@ onBeforeUnmount(() => {
<template>
<div class="sidebar">
- <Panel class="conference">
- <span class="conference-title">{{ scheduleStore.schedule?.conference.title }}</span>
- <span class="conference-venue">{{ scheduleStore.schedule?.conference.venue }}</span>
- <span class="conference-city">{{ scheduleStore.schedule?.conference.city }}</span>
+ <Panel class="conference" kind="top">
+ <template v-if="conferenceStore.id && conferenceStore.title">
+ <span class="conference-title">{{ conferenceStore.title }}</span>
+ <span class="conference-venue">{{ conferenceStore.venue }}</span>
+ <span class="conference-city">{{ conferenceStore.city }}</span>
+ </template>
- <Button kind="secondary" @click="errorStore.setError('This doesn\'t do anything yet :-)')">Change conference</Button>
+ <Button kind="secondary" @click="navigateTo('/conferences')">Change conference</Button>
</Panel>
- <Panel kind="success" class="ongoing" v-if="ongoing">
- <span>This conference is ongoing</span>
- <Button kind="primary" :icon="LucideRadio" @click="navigateTo('/live')">View live</Button>
- </Panel>
+ <template v-if="scheduleStore.schedule != null">
+ <Panel kind="success" class="ongoing" v-if="ongoing">
+ <span>This conference is ongoing</span>
+ <Button kind="primary" :icon="LucideRadio" @click="navigateTo('/live')">View live</Button>
+ </Panel>
- <Panel kind="error" class="finished" v-else-if="finished">
- <span>This conference has finished</span>
- </Panel>
-
- <Panel class="upcoming" v-else>
- <span class="text-icon"><LucideClock /> <span>Starts in {{ startsIn }}</span></span>
- </Panel>
-
- <Nav />
+ <Panel kind="error" class="finished" v-else-if="finished">
+ <span>This conference has finished</span>
+ </Panel>
+
+ <Panel class="upcoming" v-else>
+ <span class="text-icon"><LucideClock /> <span>Starts in {{ startsIn }}</span></span>
+ </Panel>
+
+ <Nav />
+ </template>
<div class="info">
- <span>Times listed are in local time ({{ scheduleStore.schedule?.conference.timeZoneName }})</span>
+ <span v-if="scheduleStore.schedule != null">Times listed are in local time ({{ scheduleStore.schedule?.conference.timeZoneName }})</span>
<Version />
</div>
@@ -74,7 +79,7 @@ onBeforeUnmount(() => {
align-items: center;
gap: 1rem;
font-size: var(--text-small);
- font-style: oblique;
+ font-style: italic;
text-align: center;
}