diff options
| author | Leonardo Bishop <me@leonardobishop.com> | 2025-01-23 15:59:58 +0000 |
|---|---|---|
| committer | Leonardo Bishop <me@leonardobishop.com> | 2025-01-23 15:59:58 +0000 |
| commit | 0248517c6845a6c755d40c89d3d769ce7d60bd03 (patch) | |
| tree | def59a310f2bbe0a1e7913b99547200da686ecc2 | |
| parent | 850affbd55fee9cd48a82ade94a3a5e60fd737a8 (diff) | |
Some more shit
| -rw-r--r-- | assets/css/main.css | 14 | ||||
| -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 | ||||
| -rw-r--r-- | pages/agenda.vue | 20 | ||||
| -rw-r--r-- | pages/events.vue | 2 | ||||
| -rw-r--r-- | pages/live.vue | 32 | ||||
| -rw-r--r-- | pages/register.vue | 2 | ||||
| -rw-r--r-- | stores/schedule.ts | 4 |
12 files changed, 179 insertions, 49 deletions
diff --git a/assets/css/main.css b/assets/css/main.css index 4af7ef0..c93c22d 100644 --- a/assets/css/main.css +++ b/assets/css/main.css @@ -5,18 +5,22 @@ --color-accent: #6366f1; --color-primary: #4f46e5; --color-primary-dark: #4338ca; - --color-error: #f8d7da; - --color-success: #d1e7dd; + --color-favourite: #f6e05e; + --color-error: #cf2f27; + --color-error-light: #f8d7da; + --color-error-dark: #ad1f2b; + --color-success-light: #d1e7dd; + --color-text: #000; --color-text-error: #cf2f27; + --color-text-success: #3d9970; --color-text-muted: #4b5563; --color-text-muted-light: #6b7280; --color-background-muted: #f3f4f6; --color-background: #fff; --color-border: #d1d9e0; - --color-border-error: #f1aeb5; - --color-border-success: #a3cfbb; - --color-favourite: #f6e05e; + --color-border-error-light: #f1aeb5; + --color-border-success-light: #a3cfbb; --text-smaller: 0.75rem; --text-small: 0.875rem; 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 /> diff --git a/pages/agenda.vue b/pages/agenda.vue index 7ef5f44..9f83210 100644 --- a/pages/agenda.vue +++ b/pages/agenda.vue @@ -1,6 +1,6 @@ <script setup lang="ts"> import Panel from '~/components/Panel.vue'; -import { LucideRadio } from "lucide-vue-next"; +import Dialog from '~/components/Dialog.vue'; const favouritesStore = useFavouritesStore(); const scheduleStore = useScheduleStore(); @@ -17,15 +17,14 @@ const calendarLinkWithPageProtocol = computed(() => { return window.location.protocol + '//' + calendarLink.value; }); +const refConfirmDeleteDialog = ref<typeof Dialog>(); + const calendarAction = ref(false); useFetch(config.public.baseURL + '/calendar', { method: 'GET', server: false, lazy: true, - onResponseError: ({ response }) => { - calendarStatus.value = 'idle'; - }, onResponse: ({ response }) => { if (!response.ok) { if (response.status !== 404) { @@ -43,7 +42,7 @@ function generateCalendar() { useFetch(config.public.baseURL + '/calendar', { method: 'POST', server: false, - lazy: true, + lazy: true, onResponseError: ({ response }) => { errorStore.setError(response._data.message || 'An unknown error occurred'); calendarAction.value = false; @@ -98,7 +97,7 @@ function deleteCalendar() { <template v-if="calendarLink"> <span>You can add your agenda to your own calendar using the iCal link below</span> <Input :value="calendarLinkWithPageProtocol" readonly/> - <Button @click="deleteCalendar" :loading="calendarAction">Delete calendar</Button> + <Button @click="refConfirmDeleteDialog!.show()" :loading="calendarAction">Delete calendar</Button> </template> <template v-else> <span>You do not have a calendar link yet. Use the button below to request a calendar link to subscribe to on your own calendar app.</span> @@ -112,6 +111,15 @@ function deleteCalendar() { <Panel v-else> <span>You have not added any favourites yet.</span> </Panel> + + <Dialog ref="refConfirmDeleteDialog" title="Delete calendar" :confirmation="true" @submit="deleteCalendar" :fit-contents="true"> + <span>Are you sure you want to delete your calendar?</span> + <span>Your unique link cannot be recovered if you do so.</span> + <template v-slot:actions> + <Button kind="secondary" type="button" @click="refConfirmDeleteDialog!.close()">Cancel</Button> + <Button kind="danger" type="submit" :loading="calendarAction">Delete</Button> + </template> + </Dialog> </template> <style scoped> diff --git a/pages/events.vue b/pages/events.vue index bfbe553..9ae35ed 100644 --- a/pages/events.vue +++ b/pages/events.vue @@ -10,7 +10,7 @@ const scheduleStore = useScheduleStore(); <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"> - <EventListing :event="event" /> + <EventListing :event="event" :show-relative-time="true" /> </li> </ul> </div> diff --git a/pages/live.vue b/pages/live.vue index b181ff0..1c5679f 100644 --- a/pages/live.vue +++ b/pages/live.vue @@ -1,10 +1,40 @@ <script setup lang="ts"> import Panel from '~/components/Panel.vue'; +import { type Event } from '~/stores/schedule'; +const favouritesStore = useFavouritesStore(); +const scheduleStore = useScheduleStore(); +const errorStore = useErrorStore(); + +const favouriteEvents = computed(() => { + return scheduleStore.events.filter((event) => favouritesStore.isFavourite(event)); +}); +const todayEvents = computed(() => { + return scheduleStore.events.filter((event) => isToday(new Date(event.start))); +}); +const happeningNow = computed(() => { + return todayEvents.value.filter((event) => isEventHappeningNow(event)); +}); +const favouritesHappeningNow = computed(() => { + return happeningNow.value.filter((event) => favouritesStore.isFavourite(event)); +}); + +function isEventHappeningNow(event: Event): boolean { + const now = new Date(); + return event.start <= now && event.end >= now; +} + +function isToday(date: Date): boolean { + const today = new Date(); + return date.getDate() === today.getDate() && + date.getMonth() === today.getMonth() && + date.getFullYear() === today.getFullYear(); +} </script> <template> - <Panel> + <Panel kind="success" class="ongoing" v-if="happeningNow.length > 0"> + </Panel> </template> diff --git a/pages/register.vue b/pages/register.vue index b06578f..35e77dd 100644 --- a/pages/register.vue +++ b/pages/register.vue @@ -133,7 +133,7 @@ const handleSubmit = async (e: Event) => { } .auth-error { - color: var(--color-error); + color: var(--color-error-light); } .form-group { diff --git a/stores/schedule.ts b/stores/schedule.ts index df2c1ec..e81055e 100644 --- a/stores/schedule.ts +++ b/stores/schedule.ts @@ -1,4 +1,5 @@ import { TZDate } from "@date-fns/tz"; +import { addDays } from "date-fns"; import { defineStore } from "pinia"; interface Schedule { @@ -87,6 +88,9 @@ export const useScheduleStore = defineStore('schedule', () => { tracks.value[track.name] = track }); + newSchedule.conference.start = new TZDate(newSchedule.conference.start, newSchedule.conference.timeZoneName) + newSchedule.conference.end = addDays(new TZDate(newSchedule.conference.end, newSchedule.conference.timeZoneName), 1) + events.value = [] newSchedule.days.forEach(day => { day.start = new TZDate(day.start, newSchedule.conference.timeZoneName) |
