aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLeonardo Bishop <me@leonardobishop.com>2025-01-21 01:21:09 +0000
committerLeonardo Bishop <me@leonardobishop.com>2025-01-21 01:21:09 +0000
commit5d162cb8e0f5d594905a56a9b16f4f68df5f6631 (patch)
treecd244bada4f4749a62cf8f8bd533ab54b585fb8f
parentbfb60f146724219379879468802ee65d02746156 (diff)
Improve frontend
-rw-r--r--app.vue4
-rw-r--r--assets/css/main.css11
-rw-r--r--components/Button.vue71
-rw-r--r--components/Nav.vue4
-rw-r--r--components/Panel.vue29
-rw-r--r--components/Sidebar.vue43
-rw-r--r--layouts/default.vue12
-rw-r--r--pages/agenda.vue2
-rw-r--r--pages/live.vue13
-rw-r--r--stores/error.ts6
-rw-r--r--stores/schedule.ts33
11 files changed, 173 insertions, 55 deletions
diff --git a/app.vue b/app.vue
index e05aae2..236bbb8 100644
--- a/app.vue
+++ b/app.vue
@@ -3,9 +3,9 @@
</script>
<template>
+ <NuxtPwaManifest />
+
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
-
- <NuxtPwaManifest />
</template>
diff --git a/assets/css/main.css b/assets/css/main.css
index 88b8e3e..1ece5c0 100644
--- a/assets/css/main.css
+++ b/assets/css/main.css
@@ -5,7 +5,8 @@
--color-accent: #6366f1;
--color-primary: #4f46e5;
--color-primary-dark: #4338ca;
- --color-error: #ff4136;
+ --color-error: #f8d7da;
+ --color-success: #d1e7dd;
--color-text: #000;
--color-text-muted: #4b5563;
--color-background-muted: #f3f4f6;
@@ -13,6 +14,8 @@
--color-background-error: #cf2f27;
--color-background-error-dark: #c12820;
--color-border: #d1d9e0;
+ --color-border-error: #f1aeb5;
+ --color-border-success: #a3cfbb;
--color-favourite: #f6e05e;
--text-smaller: 0.75rem;
@@ -77,4 +80,10 @@ ul {
list-style-position: inside;
margin: 0.5rem 0 1rem 1rem;
line-height: 1.3;
+}
+
+span.text-icon {
+ display: flex;
+ align-items: centerg;
+ gap: 0.4em;
} \ No newline at end of file
diff --git a/components/Button.vue b/components/Button.vue
index 00cc1e7..97ff928 100644
--- a/components/Button.vue
+++ b/components/Button.vue
@@ -1,9 +1,33 @@
-<script setup>
+<script setup lang="ts">
import { Loader2Icon } from 'lucide-vue-next'
+import { defineProps } from 'vue';
+
+defineProps({
+ isLoading: {
+ type: Boolean,
+ default: false,
+ },
+ disabled: {
+ type: Boolean,
+ default: false
+ },
+ loading: {
+ type: Boolean,
+ default: false
+ },
+ type: {
+ type: String as PropType<"button" | "submit" | "reset">,
+ default: "",
+ },
+ kind: {
+ type: String as PropType<"primary" | "secondary">,
+ default: "primary",
+ },
+});
</script>
<template>
- <button :type="type" :disabled="disabled || loading">
+ <button :type="type" :disabled="disabled || loading" :class="kind">
<Loader2Icon v-if="loading" class="icon-loader" />
<span>
<slot />
@@ -11,30 +35,6 @@ import { Loader2Icon } from 'lucide-vue-next'
</button>
</template>
-<script>
-export default {
- name: "Button",
- props: {
- isLoading: {
- type: Boolean,
- default: false,
- },
- disabled: {
- type: Boolean,
- default: false
- },
- loading: {
- type: Boolean,
- default: false
- },
- type: {
- type: String,
- default: "",
- },
- },
-};
-</script>
-
<style scoped>
button {
width: 100%;
@@ -43,12 +43,10 @@ button {
padding: 0.5rem 1rem;
border: 1px solid transparent;
border-radius: 0.375rem;
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
font-size: var(--text-small);
font-family: var(--font-family);
font-weight: 500;
color: white;
- background-color: var(--color-primary);
cursor: pointer;
transition: background-color 0.2s ease;
}
@@ -60,7 +58,6 @@ button:hover {
button:focus {
outline: none;
border-color: var(--color-accent);
- box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.4);
}
button:disabled {
@@ -76,6 +73,22 @@ button:disabled {
width: 1.25rem;
color: white;
}
+
+button.primary {
+ background-color: var(--color-primary);
+}
+
+button.secondary {
+ background-color: var(--color-background);
+ border: 1px solid var(--color-primary);
+ color: var(--color-primary);
+}
+
+button.secondary:hover {
+ background-color: var(--color-background-muted);
+ border: 1px solid var(--color-primary-dark);
+ color: var(--color-primary-dark);
+}
@keyframes spin {
0% {
diff --git a/components/Nav.vue b/components/Nav.vue
index 6ab4c9f..06a0295 100644
--- a/components/Nav.vue
+++ b/components/Nav.vue
@@ -48,7 +48,7 @@ route.afterEach(() => {
display: flex;
align-items: center;
gap: 0.5rem;
- color: var(--color-text-muted);
+ color: var(--color-accent);
padding: 0.4rem 1rem;
left: -1rem;
width: calc(100%);
@@ -56,7 +56,7 @@ route.afterEach(() => {
}
.nav-list > li.active > a {
- color: var(--color-text);
+ color: var(--color-primary);
font-weight: 600;
background-color: var(--color-background-muted);
}
diff --git a/components/Panel.vue b/components/Panel.vue
index 007d2b6..8e72414 100644
--- a/components/Panel.vue
+++ b/components/Panel.vue
@@ -1,15 +1,38 @@
+<script setup lang="ts">
+import { defineProps, type PropType } from 'vue';
+
+defineProps({
+ kind: {
+ type: String as PropType<"normal" | "error" | "success">,
+ required: false,
+ default: 'normal',
+ },
+});
+</script>
<template>
- <div class="card">
+ <div class="card" :class="kind">
<slot />
</div>
</template>
<style>
.card {
- background-color: white;
padding: 1rem;
border-radius: 0.5rem;
- /* box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06); */
+}
+
+.normal {
+ background-color: white;
border: 0.1rem solid var(--color-border);
}
+
+.error {
+ background-color: var(--color-error);
+ border: 0.1rem solid var(--color-border-error);
+}
+
+.success {
+ background-color: var(--color-success);
+ border: 0.1rem solid var(--color-border-success);
+}
</style> \ No newline at end of file
diff --git a/components/Sidebar.vue b/components/Sidebar.vue
index 755f200..9366696 100644
--- a/components/Sidebar.vue
+++ b/components/Sidebar.vue
@@ -1,5 +1,21 @@
<script setup lang="ts">
+import { formatDistanceToNow } from "date-fns";
+import { LucideClock, LucideRadio } from "lucide-vue-next";
+
const scheduleStore = useScheduleStore();
+const errorStore = useErrorStore();
+
+const timeUntilConferenceStart = computed(() => {
+ if (!scheduleStore.schedule) {
+ return 0;
+ }
+
+ const now = new Date();
+ const conferenceStart = new Date(scheduleStore.schedule.conference.start);
+ const diff = conferenceStart.getTime() - now.getTime();
+
+ return diff;
+});
</script>
<template>
@@ -8,6 +24,21 @@ const scheduleStore = useScheduleStore();
<span class="conference-title">{{ scheduleStore.schedule?.conference.title }}</span>
<span class="conference-venue">{{ scheduleStore.schedule?.conference.venue }}</span>
<span class="conference-city">{{ scheduleStore.schedule?.conference.city }}</span>
+
+ <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 size="var(--text-small)"/> <span>This conference is ongoing</span></span>
+ <Button kind="primary" @click="navigateTo('/live')">View live</Button>
+ </Panel>
+
+ <Panel kind="error" class="finished" v-else-if="scheduleStore.isConferenceFinished()">
+ <span>This conference has finished</span>
+ </Panel>
+
+ <Panel class="upcoming" v-else>
+ <span class="text-icon"><LucideClock size="var(--text-small)" /> <span>Starts in {{ formatDistanceToNow(scheduleStore.getStartDate()) }}</span></span>
</Panel>
<Nav />
@@ -18,13 +49,23 @@ const scheduleStore = useScheduleStore();
</div>
</template>
-<style>
+<style scoped>
.sidebar {
display: flex;
flex-direction: column;
gap: 1rem;
}
+.finished, .ongoing, .upcoming {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 1rem;
+ font-size: var(--text-small);
+ font-style: oblique;
+ text-align: center;
+}
+
.conference {
display: flex;
flex-direction: column;
diff --git a/layouts/default.vue b/layouts/default.vue
index 1f291ef..cd9fa09 100644
--- a/layouts/default.vue
+++ b/layouts/default.vue
@@ -1,5 +1,5 @@
<script setup lang="ts">
-import { LucideCross, LucideMenu, LucideX } from "lucide-vue-next";
+import { LucideMenu, LucideX } from "lucide-vue-next";
import Dialog from "~/components/Dialog.vue";
import EventDetail from "~/components/EventDetail.vue";
import Sidebar from "~/components/Sidebar.vue";
@@ -78,6 +78,10 @@ router.beforeEach((to, from) => {
refErrorDialog.value?.close();
});
+router.afterEach(() => {
+ showHamburger.value = false;
+});
+
</script>
<template>
@@ -155,6 +159,9 @@ header {
.planner-content {
max-width: 1000px;
width: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
}
.planner-layout {
@@ -211,7 +218,8 @@ header {
.hamburger-content {
background-color: var(--color-background-muted);
padding: 1rem;
- border-bottom: 2px solid var(--color-border)
+ border-bottom: 2px solid var(--color-border);
+ position: fixed;
}
.planner-sidebar {
diff --git a/pages/agenda.vue b/pages/agenda.vue
index 316c823..97fdbad 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";
const favouritesStore = useFavouritesStore();
const scheduleStore = useScheduleStore();
diff --git a/pages/live.vue b/pages/live.vue
new file mode 100644
index 0000000..b181ff0
--- /dev/null
+++ b/pages/live.vue
@@ -0,0 +1,13 @@
+<script setup lang="ts">
+import Panel from '~/components/Panel.vue';
+
+</script>
+
+<template>
+ <Panel>
+ </Panel>
+
+</template>
+
+<style scoped>
+</style> \ No newline at end of file
diff --git a/stores/error.ts b/stores/error.ts
index 19ff289..dfb5850 100644
--- a/stores/error.ts
+++ b/stores/error.ts
@@ -2,9 +2,9 @@ import { type Event } from "./schedule";
import { defineStore } from "pinia";
export const useErrorStore = defineStore('error', () => {
- const error = ref(null as Event | null)
- const setError = (event: Event) => {
- error.value = event
+ const error = ref(null as string | null)
+ const setError = (newError: string) => {
+ error.value = newError
}
const clearError = () => {
error.value = null
diff --git a/stores/schedule.ts b/stores/schedule.ts
index b1bf536..df2c1ec 100644
--- a/stores/schedule.ts
+++ b/stores/schedule.ts
@@ -81,20 +81,14 @@ export const useScheduleStore = defineStore('schedule', () => {
const tracks = ref({} as { [key: string]: Track })
const setSchedule = (newSchedule: Schedule) => {
- schedule.value = newSchedule
-
- console.log(newSchedule)
-
tracks.value = {}
- schedule.value.tracks.forEach(track => {
+ newSchedule.tracks.forEach(track => {
track.slug = convertToSlug(track.name)
tracks.value[track.name] = track
});
- console.log("hi")
-
events.value = []
- schedule.value.days.forEach(day => {
+ newSchedule.days.forEach(day => {
day.start = new TZDate(day.start, newSchedule.conference.timeZoneName)
day.end = new TZDate(day.end, newSchedule.conference.timeZoneName)
day.rooms.forEach(room => {
@@ -111,8 +105,7 @@ export const useScheduleStore = defineStore('schedule', () => {
return a.start.getTime() - b.start.getTime()
})
- console.log("hi2")
- console.log(newSchedule)
+ schedule.value = newSchedule
eventsPerDay.value = {}
events.value.forEach(event => {
@@ -132,7 +125,25 @@ export const useScheduleStore = defineStore('schedule', () => {
});
}
- return {schedule, events, eventsPerDay, eventsPerTrack, setSchedule}
+ const isConferenceOngoing = () => {
+ if (!schedule.value) {
+ return false
+ }
+ return new Date() >= schedule.value.conference.start && new Date() < schedule.value.conference.end
+ }
+
+ const isConferenceFinished = () => {
+ if (!schedule.value) {
+ return false
+ }
+ return new Date() > schedule.value.conference.end
+ }
+
+ const getStartDate = () => {
+ return schedule.value?.conference.start || 0
+ }
+
+ return {schedule, events, eventsPerDay, eventsPerTrack, setSchedule, isConferenceOngoing, isConferenceFinished, getStartDate}
})
function normalizeDates(event: Event, timeZone: string) {