diff options
| -rw-r--r-- | assets/css/main.css | 11 | ||||
| -rw-r--r-- | components/Button.vue | 3 | ||||
| -rw-r--r-- | components/EventListing.vue | 2 | ||||
| -rw-r--r-- | components/Panel.vue | 75 | ||||
| -rw-r--r-- | components/Sidebar.vue | 4 | ||||
| -rw-r--r-- | pages/agenda.vue | 5 | ||||
| -rw-r--r-- | pages/events.vue | 5 | ||||
| -rw-r--r-- | pages/index.vue | 18 | ||||
| -rw-r--r-- | pages/live.vue | 100 | ||||
| -rw-r--r-- | pages/login.vue | 34 | ||||
| -rw-r--r-- | pages/register.vue | 35 | ||||
| -rw-r--r-- | pages/tracks/[slug].vue | 3 | ||||
| -rw-r--r-- | pages/tracks/index.vue | 3 |
13 files changed, 230 insertions, 68 deletions
diff --git a/assets/css/main.css b/assets/css/main.css index c93c22d..320bedb 100644 --- a/assets/css/main.css +++ b/assets/css/main.css @@ -5,6 +5,7 @@ --color-accent: #6366f1; --color-primary: #4f46e5; --color-primary-dark: #4338ca; + --color-primary-light: #cdcde9; --color-favourite: #f6e05e; --color-error: #cf2f27; --color-error-light: #f8d7da; @@ -16,11 +17,19 @@ --color-text-success: #3d9970; --color-text-muted: #4b5563; --color-text-muted-light: #6b7280; - --color-background-muted: #f3f4f6; --color-background: #fff; + --color-background-muted: #f3f4f6; + --color-background-primary: var(--color-primary-light); + --color-background-error: var(--color-error-light); + --color-background-error-muted: #f8d7da; + --color-background-success: var(--color-success-light); + --color-background-success-muted: #b5d0c4; --color-border: #d1d9e0; --color-border-error-light: #f1aeb5; --color-border-success-light: #a3cfbb; + --color-border-primary-light: #c4b5fd; + + --color-hover: rgba(0, 0, 0, 0.04); --text-smaller: 0.75rem; --text-small: 0.875rem; diff --git a/components/Button.vue b/components/Button.vue index 2d24bad..ce9eefc 100644 --- a/components/Button.vue +++ b/components/Button.vue @@ -42,7 +42,6 @@ defineProps({ <style scoped> button { - width: 100%; display: flex; justify-content: center; align-items: center; @@ -90,7 +89,7 @@ button.primary:hover { } button.secondary { - background-color: var(--color-background); + background-color: unset; border: 1px solid var(--color-primary); color: var(--color-primary); } diff --git a/components/EventListing.vue b/components/EventListing.vue index 287fbfc..5c04189 100644 --- a/components/EventListing.vue +++ b/components/EventListing.vue @@ -127,7 +127,7 @@ const removeFavourite = async () => { } .event-details:hover { - background-color: var(--color-background-muted); + background-color: var(--color-hover); } .event { diff --git a/components/Panel.vue b/components/Panel.vue index 285db57..1f2d22e 100644 --- a/components/Panel.vue +++ b/components/Panel.vue @@ -1,10 +1,10 @@ <script setup lang="ts"> import { ArrowRight } from 'lucide-vue-next'; -import { defineProps, type PropType } from 'vue'; +import { defineProps, type FunctionalComponent, type PropType } from 'vue'; defineProps({ kind: { - type: String as PropType<"normal" | "error" | "success">, + type: String as PropType<"normal" | "error" | "success" | "emphasis">, required: false, default: 'normal', }, @@ -18,46 +18,66 @@ defineProps({ required: false, default: () => [], }, + icon: { + type: Object as PropType<FunctionalComponent>, + default: null + } }); </script> <template> <div class="card" :class="kind"> - <div v-if="title" class="card-title-container"> - <span v-for="(breadcrumb, index) in breadcrumbs" class="breadcrumb" :key="index"> - <NuxtLink :to="breadcrumb.to">{{ breadcrumb.text }}</NuxtLink> - <ArrowRight /> - </span> - <span class="card-title"> {{ title }} </span> + <div v-if="title" class="card-header"> + <div class="header-left"> + <component :is="icon" v-if="icon" /> + <div class="card-title-container"> + <span v-for="(breadcrumb, index) in breadcrumbs" class="breadcrumb" :key="index"> + <NuxtLink :to="breadcrumb.to">{{ breadcrumb.text }}</NuxtLink> + <ArrowRight v-if="index != breadcrumbs.length - 1" /> + </span> + <span class="card-title"> {{ title }} </span> + </div> + </div> + + <slot name="actions" /> </div> <slot /> </div> </template> <style> -.card { +div.card { padding: 1rem; border-radius: 0.5rem; } -.card-title-container { - line-height: 1; - background-color: var(--color-background-muted); +div.card-header { padding: 1rem 1rem; - margin: -1rem -1rem 1rem -1rem; + margin: -1rem -1rem 0 -1rem; border-top-left-radius: 0.5rem; border-top-right-radius: 0.5rem;; - border-bottom: 1px solid var(--color-border); display: flex; + justify-content: space-between; + align-items: center; +} + +div.header-left { + display: flex; + line-height: 1; align-items: center; gap: 0.5rem; } -.card-title { +div.header-left > svg { + width: 1.3rem; + height: 1.3rem; +} + +span.card-title { font-size: 1.5rem; font-weight: 700; } -.breadcrumb { +span.breadcrumb { font-size: 1.2rem; font-weight: 600; display: flex; @@ -65,32 +85,43 @@ defineProps({ gap: 0.5rem; } -.breadcrumb > svg { +span.breadcrumb > svg { width: 1.2rem; height: 1.2rem; color: var(--color-text-muted); } -.breadcrumb > a { +span.breadcrumb > a { color: var(--color-text-muted); } -.breadcrumb > a:hover { +span.breadcrumb > a:hover { color: var(--color-primary); } -.normal { +div.normal { background-color: white; border: 0.1rem solid var(--color-border); } -.error { +div.normal .card-header { + background-color: var(--color-background-muted); + border-bottom: 1px solid var(--color-border); + margin: -1rem -1rem 1rem -1rem; +} + +div.error { background-color: var(--color-error-light); border: 0.1rem solid var(--color-border-error-light); } -.success { +div.success { background-color: var(--color-success-light); border: 0.1rem solid var(--color-border-success-light); } + +div.emphasis { + background-color: var(--color-background-primary); + border: 0.1rem solid var(--color-border-primary-light); +} </style>
\ No newline at end of file diff --git a/components/Sidebar.vue b/components/Sidebar.vue index bdbcd51..5fc42d3 100644 --- a/components/Sidebar.vue +++ b/components/Sidebar.vue @@ -78,6 +78,10 @@ onBeforeUnmount(() => { text-align: center; } +.ongoing button { + width: 100%; +} + .finished svg, .ongoing svg, .upcoming svg{ height: var(--text-small) ; width: var(--text-small); diff --git a/pages/agenda.vue b/pages/agenda.vue index 9f83210..5e0c643 100644 --- a/pages/agenda.vue +++ b/pages/agenda.vue @@ -1,6 +1,7 @@ <script setup lang="ts"> import Panel from '~/components/Panel.vue'; import Dialog from '~/components/Dialog.vue'; +import { Calendar } from 'lucide-vue-next'; const favouritesStore = useFavouritesStore(); const scheduleStore = useScheduleStore(); @@ -79,9 +80,9 @@ function deleteCalendar() { <span>Updating favourites...</span> </Panel> - <template v-else-if="favouriteEvents.length > 0"> + <template v-else-if="favouriteEvents.length > 0" > <div class="page"> - <Panel title="Agenda"> + <Panel title="Agenda" :icon="Calendar"> <ul class="agenda-list"> <li v-for="event in favouriteEvents" :key="event.id" class="agenda-item" > <EventListing :event="event" /> diff --git a/pages/events.vue b/pages/events.vue index 9ae35ed..093e959 100644 --- a/pages/events.vue +++ b/pages/events.vue @@ -1,4 +1,5 @@ <script setup lang="ts"> +import { Calendar, SquareGanttChart } from 'lucide-vue-next'; import { useScheduleStore } from '~/stores/schedule'; const scheduleStore = useScheduleStore(); @@ -6,11 +7,11 @@ const scheduleStore = useScheduleStore(); </script> <template> - <Panel v-if="scheduleStore.schedule" title="Events"> + <Panel title="Events" :icon="SquareGanttChart" v-if="scheduleStore.schedule"> <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" :show-relative-time="true" /> + <EventListing :event="event" /> </li> </ul> </div> diff --git a/pages/index.vue b/pages/index.vue index 941a378..c455678 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -1,6 +1,22 @@ <script setup lang="ts"> -navigateTo('/events'); +const scheduleStore = useScheduleStore(); + +const destination = ref() + +if (scheduleStore.isConferenceOngoing()) { + destination.value = "/live"; + navigateTo('/live'); +} else { + destination.value = "/events"; + navigateTo('/events'); +} </script> <template> + <Panel kind="success"> + <span class="text-icon"> + <Spinner /> + <span>Successfully logged in. Navigating to {{ destination }}...</span> + </span> + </Panel> </template> diff --git a/pages/live.vue b/pages/live.vue index 1c5679f..092f99f 100644 --- a/pages/live.vue +++ b/pages/live.vue @@ -1,29 +1,52 @@ <script setup lang="ts"> +import { Radio } from 'lucide-vue-next'; +import EventListing from '~/components/EventListing.vue'; 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 showAllHappeningNow = ref(false); +const showAllUpcoming = ref(false); +const timer = ref(); +const refreshKey = ref(0); + const todayEvents = computed(() => { return scheduleStore.events.filter((event) => isToday(new Date(event.start))); }); const happeningNow = computed(() => { + refreshKey.value; return todayEvents.value.filter((event) => isEventHappeningNow(event)); }); const favouritesHappeningNow = computed(() => { + refreshKey.value; return happeningNow.value.filter((event) => favouritesStore.isFavourite(event)); }); +const upcomingToday = computed(() => { + refreshKey.value; + return todayEvents.value.filter((event) => !isEventHappeningNow(event) && isInFuture(event)); +}); +const favouritesUpcomingToday = computed(() => { + refreshKey.value; + return upcomingToday.value.filter((event) => favouritesStore.isFavourite(event)); +}); + +onMounted(() => { + timer.value = setInterval(() => { + refreshKey.value++; + }, 1000); +}); function isEventHappeningNow(event: Event): boolean { const now = new Date(); return event.start <= now && event.end >= now; } +function isInFuture(event: Event): boolean { + return event.start > new Date(); +} + function isToday(date: Date): boolean { const today = new Date(); return date.getDate() === today.getDate() && @@ -33,11 +56,78 @@ function isToday(date: Date): boolean { </script> <template> - <Panel kind="success" class="ongoing" v-if="happeningNow.length > 0"> + <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"> + <EventListing :event="event" /> + </li> + <span class="other" v-if="!showAllHappeningNow && happeningNow.length != favouritesHappeningNow.length">{{ happeningNow.length }} other events happening right now</span> + </ul> + <template #actions> + <div class="actions"> + <span class="status" v-if="!showAllHappeningNow">Showing only agenda items</span> + <Button @click="showAllHappeningNow = !showAllHappeningNow" :kind="showAllHappeningNow ? 'secondary' : 'primary'"> + {{ showAllHappeningNow ? 'Show only agenda items' : 'Show all' }} + </Button> + </div> + </template> + </Panel> + + <Panel class="upcoming" v-if="upcomingToday.length > 0" title="Upcoming"> + <ul class="events-list"> + <li v-for="event in showAllUpcoming ? upcomingToday : favouritesUpcomingToday" :key="event.id"> + <EventListing :event="event" :show-relative-time="true" /> + </li> + <span class="other" v-if="!showAllUpcoming">{{ upcomingToday.length }} other upcoming events today</span> + </ul> + + <template #actions> + <div class="actions"> + <span class="status" v-if="!showAllUpcoming">Showing only agenda items</span> + <Button @click="showAllUpcoming = !showAllUpcoming" :kind="showAllUpcoming ? 'secondary' : 'primary'"> + {{ showAllUpcoming ? 'Show only agenda items' : 'Show all' }} + </Button> + </div> + </template> + </Panel> + + <Panel kind="error" v-if="todayEvents.length === 0"> + <span>There are no more events today</span> </Panel> </template> <style scoped> +ul.events-list { + list-style: none; + margin: 0; + padding: 0; + display: grid; + margin-top: -1rem; +} + +ul.events-list > .other { + font-style: italic; +} + +ul.events-list > li { + border-bottom: 1px solid var(--color-hover); +} + +ul.events-list > li:last-of-type { + border-bottom: none; +} + +div.actions { + display: flex; + align-items: center; + gap: 1rem; +} + +div.actions > span.status { + color: var(--color-text); + font-style: italic; + font-size: var(--text-small); +} </style>
\ No newline at end of file diff --git a/pages/login.vue b/pages/login.vue index 06d1b77..2900a5e 100644 --- a/pages/login.vue +++ b/pages/login.vue @@ -28,7 +28,7 @@ const handleSubmit = async (e: Event) => { server: false, }); - navigateTo("/events"); + navigateTo("/"); } catch (e: any) { if ((e as FetchError).data) { error.value = e.data.message @@ -102,7 +102,7 @@ onMounted(() => { </template> <style scoped> -.auth-container { +div.auth-container { min-height: 100vh; background-color: var(--color-background-muted); display: flex; @@ -111,7 +111,7 @@ onMounted(() => { gap: 1rem; } -.auth-header { +div.auth-header { margin: 0 auto; width: 100%; max-width: 28rem; @@ -121,53 +121,61 @@ onMounted(() => { flex-direction: column; } -.auth-title { +h2.auth-title { margin-top: 1.5rem; font-size: 1.875rem; font-weight: 800; color: #1f2937; } -.auth-body { +div.auth-body { margin-top: 2rem; margin: 0 auto; width: 100%; max-width: 28rem; } -.auth-form { +form.auth-form { display: grid; gap: 1.5rem; } -.auth-error { +div.auth-error { color: var(--color-text-error); font-style: oblique; } -.form-group { +div.form-group { display: flex; flex-direction: column; } -.form-label { +label.form-label { display: block; font-size: 0.875rem; font-weight: 500; color: #374151; } -.form-input-container { +div.form-input-container { margin-top: 0.25rem; } -.form-footer { +div.form-footer { display: flex; justify-content: flex-end; margin: 0 auto; max-width: 28rem; } +div.form-submit { + display: flex; +} + +div.form-submit button { + width: 100%; +} + .version { font-size: var(--text-smaller); margin: 0 auto; @@ -179,10 +187,6 @@ onMounted(() => { font-weight: 500; } -.form-submit { - display: flex; -} - input[name="username"] { text-transform: lowercase; } diff --git a/pages/register.vue b/pages/register.vue index 35e77dd..33a02a7 100644 --- a/pages/register.vue +++ b/pages/register.vue @@ -94,7 +94,7 @@ const handleSubmit = async (e: Event) => { </template> <style scoped> -.auth-container { +div.auth-container { min-height: 100vh; background-color: var(--color-background-muted); display: flex; @@ -103,7 +103,7 @@ const handleSubmit = async (e: Event) => { gap: 1rem; } -.auth-header { +div.auth-header { margin: 0 auto; width: 100%; max-width: 28rem; @@ -113,61 +113,66 @@ const handleSubmit = async (e: Event) => { flex-direction: column; } -.auth-title { +h2.auth-title { margin-top: 1.5rem; font-size: 1.875rem; font-weight: 800; color: #1f2937; } -.auth-body { +div.auth-body { margin-top: 2rem; margin: 0 auto; width: 100%; max-width: 28rem; } -.auth-form { +form.auth-form { display: grid; gap: 1.5rem; } -.auth-error { - color: var(--color-error-light); +div.auth-error { + color: var(--color-text-error); + font-style: oblique; } -.form-group { +div.form-group { display: flex; flex-direction: column; } -.form-label { +label.form-label { display: block; font-size: 0.875rem; font-weight: 500; color: #374151; } -.form-input-container { +div.form-input-container { margin-top: 0.25rem; } -.form-footer { +div.form-footer { display: flex; justify-content: flex-end; margin: 0 auto; max-width: 28rem; } +div.form-submit { + display: flex; +} + +div.form-submit button { + width: 100%; +} + .register-link { font-size: var(--text-small); font-weight: 500; } -.form-submit { - display: flex; -} - .auth-error { color: var(--color-text-error); font-style: oblique; diff --git a/pages/tracks/[slug].vue b/pages/tracks/[slug].vue index 85f00b6..27fb97d 100644 --- a/pages/tracks/[slug].vue +++ b/pages/tracks/[slug].vue @@ -1,4 +1,5 @@ <script setup lang="ts"> +import { TrainTrack } from 'lucide-vue-next'; import { useScheduleStore } from '~/stores/schedule'; const route = useRoute(); @@ -8,7 +9,7 @@ const track = scheduleStore.schedule?.tracks.find((track) => track.slug === rout </script> <template> - <Panel v-if="track" :title="track.name" :breadcrumbs="[{ text: 'Tracks', to: '/tracks' }]"> + <Panel v-if="track" :title="track.name" :breadcrumbs="[{ text: 'Tracks', to: '/tracks' }]" :icon="TrainTrack"> <ul class="events-list"> <li v-for="event in scheduleStore.eventsPerTrack[track.name]" diff --git a/pages/tracks/index.vue b/pages/tracks/index.vue index 544847b..8d7534e 100644 --- a/pages/tracks/index.vue +++ b/pages/tracks/index.vue @@ -1,11 +1,12 @@ <script setup lang="ts"> +import { TrainTrack } from 'lucide-vue-next'; import Panel from '~/components/Panel.vue'; const scheduleStore = useScheduleStore(); </script> <template> - <Panel v-if="scheduleStore.schedule" title="Tracks"> + <Panel v-if="scheduleStore.schedule" title="Tracks" :icon="TrainTrack"> <ul class="tracks-list"> <li v-for="track in scheduleStore.schedule.tracks" |
