aboutsummaryrefslogtreecommitdiffstats
path: root/components
diff options
context:
space:
mode:
Diffstat (limited to 'components')
-rw-r--r--components/Button.vue33
-rw-r--r--components/Dialog.vue41
-rw-r--r--components/EventListing.vue34
-rw-r--r--components/Nav.vue2
-rw-r--r--components/Panel.vue8
-rw-r--r--components/Sidebar.vue36
6 files changed, 119 insertions, 35 deletions
diff --git a/components/Button.vue b/components/Button.vue
index 97ff928..2d24bad 100644
--- a/components/Button.vue
+++ b/components/Button.vue
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { Loader2Icon } from 'lucide-vue-next'
-import { defineProps } from 'vue';
+import { defineProps, type FunctionalComponent } from 'vue';
defineProps({
isLoading: {
@@ -20,15 +20,20 @@ defineProps({
default: "",
},
kind: {
- type: String as PropType<"primary" | "secondary">,
+ type: String as PropType<"primary" | "secondary" | "danger">,
default: "primary",
},
+ icon: {
+ type: Object as PropType<FunctionalComponent>,
+ default: null
+ }
});
</script>
<template>
<button :type="type" :disabled="disabled || loading" :class="kind">
<Loader2Icon v-if="loading" class="icon-loader" />
+ <component :is="icon" v-else-if="icon" />
<span>
<slot />
</span>
@@ -40,6 +45,8 @@ button {
width: 100%;
display: flex;
justify-content: center;
+ align-items: center;
+ gap: 0.4rem;
padding: 0.5rem 1rem;
border: 1px solid transparent;
border-radius: 0.375rem;
@@ -51,6 +58,11 @@ button {
transition: background-color 0.2s ease;
}
+button svg {
+ height: var(--text-small);
+ width: var(--text-small);
+}
+
button:hover {
background-color: var(--color-primary-dark);
}
@@ -67,16 +79,15 @@ button:disabled {
.icon-loader {
animation: spin 1s linear infinite;
- margin-left: -0.25rem;
- margin-right: 0.75rem;
- height: 1.25rem;
- width: 1.25rem;
- color: white;
}
button.primary {
background-color: var(--color-primary);
}
+
+button.primary:hover {
+ background-color: var(--color-primary-dark);
+}
button.secondary {
background-color: var(--color-background);
@@ -89,6 +100,14 @@ button.secondary:hover {
border: 1px solid var(--color-primary-dark);
color: var(--color-primary-dark);
}
+
+button.danger {
+ background-color: var(--color-error);
+}
+
+button.danger:hover {
+ background-color: var(--color-error-dark);
+}
@keyframes spin {
0% {
diff --git a/components/Dialog.vue b/components/Dialog.vue
index 3d91de0..7772f23 100644
--- a/components/Dialog.vue
+++ b/components/Dialog.vue
@@ -7,6 +7,7 @@ const refDialog = ref<HTMLDialogElement | null>(null);
const props = defineProps<{
kind?: 'normal' | 'error';
fitContents?: boolean;
+ title?: string;
}>();
const showModal = () => {
@@ -18,7 +19,7 @@ const closeModal = () => {
refDialog.value?.close();
};
-const emit = defineEmits(['close']);
+const emit = defineEmits(['close', 'submit']);
defineExpose({
show: showModal,
@@ -31,6 +32,15 @@ const onClose = () => {
emit('close');
};
+const onSubmit = (e: Event) => {
+ e.preventDefault();
+ const formData = new FormData(e.target as HTMLFormElement);
+ const formValue = Object.fromEntries(formData.entries());
+ emit('submit', formValue);
+
+ closeModal();
+};
+
const onDivClick = (e: MouseEvent) => {
e.stopPropagation()
};
@@ -43,10 +53,16 @@ const onDialogClick = (e: MouseEvent) => {
</script>
<template>
- <dialog ref="refDialog" @click="onDialogClick" @close="onClose" :class="[props.kind, { fit: props.fitContents }]">
+ <dialog ref="refDialog" @click="onDialogClick" @close="onClose" @submit="onSubmit" :class="[props.kind ?? 'normal', { fit: props.fitContents }]">
<div @click="onDivClick">
<form v-if="visible" method="dialog">
+ <div class="title" v-if="title">{{ props.title }}</div>
+
<slot />
+
+ <div class="actions" v-if="$slots.actions">
+ <slot name="actions" class="actions" />
+ </div>
</form>
</div>
</dialog>
@@ -72,8 +88,8 @@ dialog.normal {
}
dialog.error {
- border: 2px solid var(--color-border-error);
- background-color: var(--color-error);
+ border: 2px solid var(--color-border-error-light);
+ background-color: var(--color-error-light);
}
dialog.fit {
@@ -86,10 +102,21 @@ dialog::backdrop {
background-color: rgba(0, 0, 0, 0.1);
}
-div.actions {
+form {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+div.title {
+ font-size: var(--text-larger);
+ font-weight: 700;
+}
+
+.actions {
display: flex;
- margin-top: 12px;
- gap: 8px;
+ gap: 1rem;
+ align-self: flex-end;
justify-content: flex-end;
}
</style> \ No newline at end of file
diff --git a/components/EventListing.vue b/components/EventListing.vue
index 9271692..287fbfc 100644
--- a/components/EventListing.vue
+++ b/components/EventListing.vue
@@ -1,11 +1,12 @@
<script setup lang="ts">
import { StarIcon } from 'lucide-vue-next';
-import { add, format } from 'date-fns';
+import { format, formatDistanceToNow } from 'date-fns';
import { type Event as ScheduledEvent } from '~/stores/schedule';
import Spinner from './Spinner.vue';
-const { event } = defineProps<{
+const { event, showRelativeTime } = defineProps<{
event: ScheduledEvent;
+ showRelativeTime?: boolean;
}>();
const selectedEventStore = useSelectedEventStore();
@@ -14,6 +15,29 @@ const errorStore = useErrorStore();
const config = useRuntimeConfig();
const addingToFavourite = ref(false);
+const relativeTime = ref();
+const timer = ref();
+
+const updateRelativeTime = () => {
+ if (event.start < new Date() && event.end > new Date()) {
+ relativeTime.value = 'now';
+ } else {
+ relativeTime.value = `in ${formatDistanceToNow(event.start)}`
+ }
+};
+
+onMounted(() => {
+ if (showRelativeTime) {
+ updateRelativeTime();
+ timer.value = setInterval(updateRelativeTime, 1000);
+ }
+});
+
+onUnmounted(() => {
+ if (timer.value) {
+ clearInterval(timer.value);
+ }
+});
const addFavourite = async () => {
addingToFavourite.value = true;
@@ -74,7 +98,7 @@ const removeFavourite = async () => {
<div class="event">
<div class="event-details" @click="selectedEventStore.setSelectedEvent(event)">
<span class="event-info">
- {{ format(event.start, "kk:mm") }} - {{ format(event.end, "kk:mm") }}, {{ event.room }}
+ <span>{{ format(event.start, "kk:mm") }} - {{ format(event.end, "kk:mm") }},</span> <span>{{ event.room }}</span> <span v-if="showRelativeTime">-</span> <span v-if="showRelativeTime" class="relative-time">{{ relativeTime }}</span>
</span>
<span class="event-title">{{ event.title }}</span>
<span class="event-speaker">{{ event.persons.map(p => p.name).join(", ") }}</span>
@@ -147,5 +171,9 @@ const removeFavourite = async () => {
.event-button-loading {
cursor: progress;
}
+
+.relative-time {
+ color: var(--color-text-success);
+}
</style> \ No newline at end of file
diff --git a/components/Nav.vue b/components/Nav.vue
index 280af02..1e08e54 100644
--- a/components/Nav.vue
+++ b/components/Nav.vue
@@ -1,5 +1,5 @@
<script setup lang="ts">
-import { Calendar, Icon, SquareGanttChart, TrainTrack } from 'lucide-vue-next';
+import { Calendar, SquareGanttChart, TrainTrack } from 'lucide-vue-next';
const route = useRouter();
diff --git a/components/Panel.vue b/components/Panel.vue
index 1e86ed1..285db57 100644
--- a/components/Panel.vue
+++ b/components/Panel.vue
@@ -85,12 +85,12 @@ defineProps({
}
.error {
- background-color: var(--color-error);
- border: 0.1rem solid var(--color-border-error);
+ background-color: var(--color-error-light);
+ border: 0.1rem solid var(--color-border-error-light);
}
.success {
- background-color: var(--color-success);
- border: 0.1rem solid var(--color-border-success);
+ background-color: var(--color-success-light);
+ border: 0.1rem solid var(--color-border-success-light);
}
</style> \ No newline at end of file
diff --git a/components/Sidebar.vue b/components/Sidebar.vue
index 3d33586..bdbcd51 100644
--- a/components/Sidebar.vue
+++ b/components/Sidebar.vue
@@ -5,17 +5,27 @@ import { LucideClock, LucideRadio } from "lucide-vue-next";
const scheduleStore = useScheduleStore();
const errorStore = useErrorStore();
-const timeUntilConferenceStart = computed(() => {
- if (!scheduleStore.schedule) {
- return 0;
- }
+const timer = ref();
+const startsIn = ref();
+const ongoing = ref(false);
+const finished = ref(false);
- const now = new Date();
- const conferenceStart = new Date(scheduleStore.schedule.conference.start);
- const diff = conferenceStart.getTime() - now.getTime();
+onMounted(() => {
+ startsIn.value = formatDistanceToNow(scheduleStore.getStartDate());
+ ongoing.value = scheduleStore.isConferenceOngoing();
+ finished.value = scheduleStore.isConferenceFinished();
- return diff;
+ timer.value = setInterval(() => {
+ startsIn.value = formatDistanceToNow(scheduleStore.getStartDate());
+ ongoing.value = scheduleStore.isConferenceOngoing();
+ finished.value = scheduleStore.isConferenceFinished();
+ }, 1000);
});
+
+onBeforeUnmount(() => {
+ clearInterval(timer.value);
+});
+
</script>
<template>
@@ -28,17 +38,17 @@ const timeUntilConferenceStart = computed(() => {
<Button kind="secondary" @click="errorStore.setError('This doesn\'t do anything yet :-)')">Change conference</Button>
</Panel>
- <Panel kind="success" class="ongoing" v-if="scheduleStore.isConferenceOngoing()">
- <span class="text-icon"><LucideRadio /> <span>This conference is ongoing</span></span>
- <Button kind="primary" @click="navigateTo('/live')">View live</Button>
+ <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="scheduleStore.isConferenceFinished()">
+ <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 {{ formatDistanceToNow(scheduleStore.getStartDate()) }}</span></span>
+ <span class="text-icon"><LucideClock /> <span>Starts in {{ startsIn }}</span></span>
</Panel>
<Nav />