diff options
Diffstat (limited to 'components')
| -rw-r--r-- | components/Button.vue | 33 | ||||
| -rw-r--r-- | components/Dialog.vue | 41 | ||||
| -rw-r--r-- | components/EventListing.vue | 34 | ||||
| -rw-r--r-- | components/Nav.vue | 2 | ||||
| -rw-r--r-- | components/Panel.vue | 8 | ||||
| -rw-r--r-- | components/Sidebar.vue | 36 |
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 /> |
