aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--assets/css/main.css11
-rw-r--r--components/Button.vue3
-rw-r--r--components/EventListing.vue2
-rw-r--r--components/Panel.vue75
-rw-r--r--components/Sidebar.vue4
-rw-r--r--pages/agenda.vue5
-rw-r--r--pages/events.vue5
-rw-r--r--pages/index.vue18
-rw-r--r--pages/live.vue100
-rw-r--r--pages/login.vue34
-rw-r--r--pages/register.vue35
-rw-r--r--pages/tracks/[slug].vue3
-rw-r--r--pages/tracks/index.vue3
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"