aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLeonardo Bishop <me@leonardobishop.com>2025-01-22 02:09:56 +0000
committerLeonardo Bishop <me@leonardobishop.com>2025-01-22 02:09:56 +0000
commit850affbd55fee9cd48a82ade94a3a5e60fd737a8 (patch)
tree3d53005a151f17bdef27afcfaeec91aa1217a25e
parentc6a34e6f7d6c0f592b8e370ee942017d914662bd (diff)
Add version and some icons
-rw-r--r--assets/css/main.css6
-rw-r--r--components/Nav.vue16
-rw-r--r--components/Panel.vue58
-rw-r--r--components/Sidebar.vue5
-rw-r--r--components/Version.vue26
-rw-r--r--composables/fetch-favourites.ts23
-rw-r--r--composables/fetch-schedule.ts24
-rw-r--r--layouts/default.vue75
-rw-r--r--nuxt.config.ts16
-rw-r--r--pages/agenda.vue16
-rw-r--r--pages/events.vue13
-rw-r--r--pages/login.vue19
-rw-r--r--pages/register.vue13
-rw-r--r--pages/tracks/[slug].vue23
-rw-r--r--pages/tracks/index.vue5
-rw-r--r--stores/error.ts1
16 files changed, 243 insertions, 96 deletions
diff --git a/assets/css/main.css b/assets/css/main.css
index 1ece5c0..4af7ef0 100644
--- a/assets/css/main.css
+++ b/assets/css/main.css
@@ -8,11 +8,11 @@
--color-error: #f8d7da;
--color-success: #d1e7dd;
--color-text: #000;
+ --color-text-error: #cf2f27;
--color-text-muted: #4b5563;
+ --color-text-muted-light: #6b7280;
--color-background-muted: #f3f4f6;
--color-background: #fff;
- --color-background-error: #cf2f27;
- --color-background-error-dark: #c12820;
--color-border: #d1d9e0;
--color-border-error: #f1aeb5;
--color-border-success: #a3cfbb;
@@ -84,6 +84,6 @@ ul {
span.text-icon {
display: flex;
- align-items: centerg;
+ align-items: center;
gap: 0.4em;
} \ No newline at end of file
diff --git a/components/Nav.vue b/components/Nav.vue
index 06a0295..280af02 100644
--- a/components/Nav.vue
+++ b/components/Nav.vue
@@ -1,10 +1,12 @@
<script setup lang="ts">
+import { Calendar, Icon, SquareGanttChart, TrainTrack } from 'lucide-vue-next';
+
const route = useRouter();
const navList = ref([
- { title: "Agenda", path: "/agenda", navigating: false },
- { title: "Events", path: "/events", navigating: false },
- { title: "Tracks", path: "/tracks", navigating: false },
+ { title: "Agenda", path: "/agenda", icon: Calendar, navigating: false },
+ { title: "Events", path: "/events", icon: SquareGanttChart, navigating: false },
+ { title: "Tracks", path: "/tracks", icon: TrainTrack, navigating: false },
])
route.beforeEach((to, from, next) => {
@@ -26,14 +28,18 @@ route.afterEach(() => {
<ul class="nav-list">
<li v-for="item in navList" :key="item.title" :class="{ active: $route.path === item.path }">
<NuxtLink :to="item.path">
- <span>{{ item.title }}</span> <Spinner v-if="item.navigating" color="var(--color-text-muted)" :size="16"/></NuxtLink>
-
+ <span class="text-icon"><component :is="item.icon" /> <span>{{ item.title }}</span></span> <Spinner v-if="item.navigating" color="var(--color-text-muted)" :size="16"/></NuxtLink>
</li>
</ul>
</Panel>
</template>
<style scoped>
+.text-icon > svg {
+ width: var(--text-normal);
+ height: var(--text-normal);
+}
+
.nav-list {
list-style: none;
padding: 0;
diff --git a/components/Panel.vue b/components/Panel.vue
index 8e72414..1e86ed1 100644
--- a/components/Panel.vue
+++ b/components/Panel.vue
@@ -1,4 +1,5 @@
<script setup lang="ts">
+import { ArrowRight } from 'lucide-vue-next';
import { defineProps, type PropType } from 'vue';
defineProps({
@@ -7,10 +8,27 @@ defineProps({
required: false,
default: 'normal',
},
+ title: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ breadcrumbs: {
+ type: Array as PropType<Array<{ text: string, to: string }>>,
+ required: false,
+ default: () => [],
+ },
});
</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>
<slot />
</div>
</template>
@@ -21,6 +39,46 @@ defineProps({
border-radius: 0.5rem;
}
+.card-title-container {
+ line-height: 1;
+ background-color: var(--color-background-muted);
+ padding: 1rem 1rem;
+ margin: -1rem -1rem 1rem -1rem;
+ border-top-left-radius: 0.5rem;
+ border-top-right-radius: 0.5rem;;
+ border-bottom: 1px solid var(--color-border);
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.card-title {
+ font-size: 1.5rem;
+ font-weight: 700;
+}
+
+.breadcrumb {
+ font-size: 1.2rem;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.breadcrumb > svg {
+ width: 1.2rem;
+ height: 1.2rem;
+ color: var(--color-text-muted);
+}
+
+.breadcrumb > a {
+ color: var(--color-text-muted);
+}
+
+.breadcrumb > a:hover {
+ color: var(--color-primary);
+}
+
.normal {
background-color: white;
border: 0.1rem solid var(--color-border);
diff --git a/components/Sidebar.vue b/components/Sidebar.vue
index 0dca27a..3d33586 100644
--- a/components/Sidebar.vue
+++ b/components/Sidebar.vue
@@ -45,6 +45,8 @@ const timeUntilConferenceStart = computed(() => {
<div class="info">
<span>Times listed are in local time ({{ scheduleStore.schedule?.conference.timeZoneName }})</span>
+
+ <Version />
</div>
</div>
</template>
@@ -91,6 +93,9 @@ const timeUntilConferenceStart = computed(() => {
font-size: var(--text-smaller);
color: var(--color-text-muted);
margin: 0 1rem;
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
}
</style> \ No newline at end of file
diff --git a/components/Version.vue b/components/Version.vue
new file mode 100644
index 0000000..a5bca5e
--- /dev/null
+++ b/components/Version.vue
@@ -0,0 +1,26 @@
+<script setup lang="ts">
+import { Loader2Icon } from 'lucide-vue-next'
+
+const config = useRuntimeConfig();
+</script>
+
+<template>
+ <span>confplanner-web v{{ config.public.version }} ({{ config.public.gitSha }})</span>
+</template>
+
+<style scoped>
+.icon-loader {
+ animation: spin 1s linear infinite;
+ color: var(--color-text);
+}
+
+@keyframes spin {
+ 0% {
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ transform: rotate(360deg);
+ }
+}
+</style>
diff --git a/composables/fetch-favourites.ts b/composables/fetch-favourites.ts
new file mode 100644
index 0000000..97b443a
--- /dev/null
+++ b/composables/fetch-favourites.ts
@@ -0,0 +1,23 @@
+export default function useFetchFavourites() {
+ const favouritesStore = useFavouritesStore();
+ const errorStore = useErrorStore();
+ const config = useRuntimeConfig();
+
+ favouritesStore.setStatus('pending')
+
+ useFetch(config.public.baseURL + '/favourites', {
+ method: 'GET',
+ server: false,
+ lazy: true,
+ onResponseError: ({ response }) => {
+ favouritesStore.setStatus('idle')
+ errorStore.setError(response._data.message || 'An unknown error occurred');
+ },
+ onResponse: ({ response }) => {
+ if (response._data) {
+ favouritesStore.setFavourites((response._data as any).data);
+ }
+ favouritesStore.setStatus('idle')
+ },
+ });
+} \ No newline at end of file
diff --git a/composables/fetch-schedule.ts b/composables/fetch-schedule.ts
new file mode 100644
index 0000000..09d0a25
--- /dev/null
+++ b/composables/fetch-schedule.ts
@@ -0,0 +1,24 @@
+export default function useFetchFavourites() {
+ const scheduleStore = useScheduleStore();
+ const errorStore = useErrorStore();
+ const config = useRuntimeConfig();
+
+ useFetch(config.public.baseURL + '/schedule', {
+ method: 'GET',
+ server: false,
+ lazy: true,
+ onResponse: ({ response }) => {
+ if (!response.ok) {
+ if (response.status === 401) {
+ navigateTo({ name: 'login', state: { error: 'Sorry, your session has expired' } });
+ } else {
+ errorStore.setError(response._data.message || 'An unknown error occurred');
+ }
+ }
+
+ if (response._data) {
+ scheduleStore.setSchedule((response._data as any).data.schedule);
+ }
+ },
+ });
+} \ No newline at end of file
diff --git a/layouts/default.vue b/layouts/default.vue
index 92641a0..9c477aa 100644
--- a/layouts/default.vue
+++ b/layouts/default.vue
@@ -1,5 +1,5 @@
<script setup lang="ts">
-import { LucideMenu, LucideX } from "lucide-vue-next";
+import { BookHeart, LucideMenu, LucideX } from "lucide-vue-next";
import Dialog from "~/components/Dialog.vue";
import EventDetail from "~/components/EventDetail.vue";
import Sidebar from "~/components/Sidebar.vue";
@@ -9,9 +9,7 @@ definePageMeta({
})
const scheduleStore = useScheduleStore();
-const config = useRuntimeConfig()
const selectedEventStore = useSelectedEventStore();
-const favouritesStore = useFavouritesStore();
const errorStore = useErrorStore();
const router = useRouter();
@@ -23,43 +21,8 @@ const refErrorDialog = ref<typeof Dialog>();
const showHamburger = ref(false);
-useFetch(config.public.baseURL + '/schedule', {
- method: 'GET',
- server: false,
- lazy: true,
- onResponse: ({ response }) => {
- if (!response.ok) {
- if (response.status === 401) {
- navigateTo('/login');
- } else {
- errorStore.setError(response._data.message || 'An unknown error occurred');
- }
- }
-
- if (response._data) {
- scheduleStore.setSchedule((response._data as any).data.schedule);
- }
- },
-});
-
-favouritesStore.setStatus('pending')
-
-await useFetch(config.public.baseURL + '/favourites', {
- method: 'GET',
- server: false,
- lazy: true,
- onResponseError: ({ response }) => {
- favouritesStore.setStatus('idle')
- errorStore.setError(response._data.message || 'An unknown error occurred');
- },
- onResponse: ({ response }) => {
- if (response._data) {
- favouritesStore.setFavourites((response._data as any).data);
- }
- favouritesStore.setStatus('idle')
- },
-});
-
+fetchSchedule();
+fetchFavourites();
watch(selectedEvent, () => {
if (selectedEvent.value != null) {
@@ -88,7 +51,7 @@ router.afterEach(() => {
<div class="planner-container">
<header>
<div class="planner-header">
- <h1 class="planner-title">Conference Planner</h1>
+ <span class="text-icon planner-title" @click="navigateTo('/')"><BookHeart /> confplanner</span>
<span class="hamburger" @click="showHamburger = !showHamburger">
<LucideMenu :size="24" v-if="!showHamburger"/>
<LucideX :size="24" v-else />
@@ -143,30 +106,38 @@ header {
z-index: 9999;
}
-.planner-header {
- background-color: var(--color-primary);
- color: white;
- padding: 1rem;
+div.planner-header {
+ background-color: var(--color-background-muted);
+ color: var(--color-text-muted);
+ border-bottom: 2px solid var(--color-border);
+ height: 3.5rem;
display: flex;
justify-content: space-between;
+ align-items: center;
z-index: 9999;
- position: relative;
+ padding: 0 1rem;
}
-.planner-title {
+span.planner-title {
font-size: 1.5rem;
font-weight: 700;
}
-.planner-content {
+span.planner-title:hover {
+ cursor: pointer;
+ color: var(--color-primary);
+}
+
+main.planner-content {
max-width: 1000px;
width: 100%;
display: flex;
flex-direction: column;
gap: 1rem;
+ flex-grow: 1;
}
-.planner-layout {
+div.planner-layout {
display: flex;
flex-direction: row;
gap: 1rem;
@@ -174,13 +145,15 @@ header {
box-sizing: border-box;
padding: 1rem;
justify-content: center;
+ width: 100%;
}
-.planner-sidebar {
+aside.planner-sidebar {
width: 100%;
max-width: 300px;
position: sticky;
- top: 0;
+ align-self: flex-start;
+ top: calc(4.5rem + 2px);
}
.loading-text {
diff --git a/nuxt.config.ts b/nuxt.config.ts
index 724f521..bb1bfce 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -1,4 +1,14 @@
-// https://nuxt.com/docs/api/configuration/nuxt-config
+import { execSync } from "child_process";
+let gitSha: string | null = null;
+let version: string | null = null;
+try {
+ gitSha = execSync("git rev-parse HEAD").toString().trim().substring(0, 7);
+ version = execSync("git log -1 --format=%cd --date=format:'%Y.%m.%d'").toString().trim();
+} catch (e) {
+ gitSha = "git unknown";
+ version = new Date().toISOString().slice(0, 10).replace(/-/g, ".");
+}
+
export default defineNuxtConfig({
compatibilityDate: "2024-11-01",
devtools: { enabled: true },
@@ -8,6 +18,8 @@ export default defineNuxtConfig({
runtimeConfig: {
public: {
baseURL: process.env.BASE_URL || "/api",
+ gitSha: gitSha,
+ version: version,
},
},
@@ -24,4 +36,4 @@ export default defineNuxtConfig({
},
modules: ["@pinia/nuxt", "@vite-pwa/nuxt"],
-}); \ No newline at end of file
+});
diff --git a/pages/agenda.vue b/pages/agenda.vue
index 97fdbad..7ef5f44 100644
--- a/pages/agenda.vue
+++ b/pages/agenda.vue
@@ -82,32 +82,26 @@ function deleteCalendar() {
<template v-else-if="favouriteEvents.length > 0">
<div class="page">
- <Panel>
- <h2 class="agenda-title">Agenda</h2>
+ <Panel title="Agenda">
<ul class="agenda-list">
- <li
- v-for="event in favouriteEvents"
- :key="event.id"
- class="agenda-item"
- >
+ <li v-for="event in favouriteEvents" :key="event.id" class="agenda-item" >
<EventListing :event="event" />
</li>
</ul>
</Panel>
<Panel>
- <h2 class="calendar-title">Calendar</h2>
<template v-if="calendarStatus === 'pending'">
<span>Fetching calendar status...</span>
</template>
<div v-else-if="calendarStatus === 'idle'" class="calendar">
<template v-if="calendarLink">
- <span>You can add your agenda to your own calendar using the iCal link below.</span>
+ <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>
</template>
<template v-else>
- <span>You do not have a calendar link yet. Use the button below to request a calendar link synchronise with your own calendar app.</span>
+ <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>
<Button @click="generateCalendar" :loading="calendarAction">Request calendar</Button>
</template>
</div>
@@ -123,7 +117,7 @@ function deleteCalendar() {
<style scoped>
.agenda-list {
list-style: none;
- margin: 0;
+ margin: -1rem 0;
padding: 0;
display: grid;
}
diff --git a/pages/events.vue b/pages/events.vue
index 080bd8f..bfbe553 100644
--- a/pages/events.vue
+++ b/pages/events.vue
@@ -6,9 +6,8 @@ const scheduleStore = useScheduleStore();
</script>
<template>
- <Panel v-if="scheduleStore.schedule">
- <h2 class="events-title">Events</h2>
- <div v-for="[day, events] of Object.entries(scheduleStore.eventsPerDay)" :key="day">
+ <Panel v-if="scheduleStore.schedule" title="Events">
+ <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" />
@@ -19,6 +18,10 @@ const scheduleStore = useScheduleStore();
</template>
<style>
+.events-container {
+ margin: -1rem 0;
+}
+
.events-list {
list-style: none;
margin: 0;
@@ -30,10 +33,6 @@ const scheduleStore = useScheduleStore();
border-bottom: 1px solid var(--color-background-muted);
}
-.events-title {
- margin-bottom: 1rem;
-}
-
.event-item:last-child {
border-bottom: none;
}
diff --git a/pages/login.vue b/pages/login.vue
index 2c38f61..06d1b77 100644
--- a/pages/login.vue
+++ b/pages/login.vue
@@ -40,6 +40,12 @@ const handleSubmit = async (e: Event) => {
isLoading.value = false
}
+onMounted(() => {
+ if (history.state.error) {
+ error.value = history.state.error as string
+ }
+})
+
</script>
<template>
@@ -79,8 +85,11 @@ const handleSubmit = async (e: Event) => {
Sign in
</Button>
</div>
+
+ <Version class="version" />
</form>
</Panel>
+
</div>
<div class="form-footer">
@@ -88,6 +97,7 @@ const handleSubmit = async (e: Event) => {
Register
</NuxtLink>
</div>
+
</div>
</template>
@@ -131,7 +141,8 @@ const handleSubmit = async (e: Event) => {
}
.auth-error {
- color: var(--color-error);
+ color: var(--color-text-error);
+ font-style: oblique;
}
.form-group {
@@ -157,6 +168,12 @@ const handleSubmit = async (e: Event) => {
max-width: 28rem;
}
+.version {
+ font-size: var(--text-smaller);
+ margin: 0 auto;
+ color: var(--color-text-muted-light);
+}
+
.register-link {
font-size: var(--text-small);
font-weight: 500;
diff --git a/pages/register.vue b/pages/register.vue
index ad9a041..b06578f 100644
--- a/pages/register.vue
+++ b/pages/register.vue
@@ -79,6 +79,8 @@ const handleSubmit = async (e: Event) => {
Register
</Button>
</div>
+
+ <Version class="version" />
</form>
</Panel>
</div>
@@ -166,6 +168,17 @@ const handleSubmit = async (e: Event) => {
display: flex;
}
+.auth-error {
+ color: var(--color-text-error);
+ font-style: oblique;
+}
+
+.version {
+ font-size: var(--text-smaller);
+ margin: 0 auto;
+ color: var(--color-text-muted-light);
+}
+
input[name="username"] {
text-transform: lowercase;
}
diff --git a/pages/tracks/[slug].vue b/pages/tracks/[slug].vue
index 38bce61..85f00b6 100644
--- a/pages/tracks/[slug].vue
+++ b/pages/tracks/[slug].vue
@@ -8,24 +8,23 @@ const track = scheduleStore.schedule?.tracks.find((track) => track.slug === rout
</script>
<template>
- <Panel v-if="track">
- <h2 class="events-title">{{ track.name }}</h2>
- <ul class="events-list">
- <li
- v-for="event in scheduleStore.eventsPerTrack[track.name]"
- :key="event.id"
- class="event-item"
- >
- <EventListing :event="event" />
- </li>
- </ul>
+ <Panel v-if="track" :title="track.name" :breadcrumbs="[{ text: 'Tracks', to: '/tracks' }]">
+ <ul class="events-list">
+ <li
+ v-for="event in scheduleStore.eventsPerTrack[track.name]"
+ :key="event.id"
+ class="event-item"
+ >
+ <EventListing :event="event" />
+ </li>
+ </ul>
</Panel>
</template>
<style>
.events-list {
list-style: none;
- margin: 0;
+ margin: -1rem 0;
padding: 0;
display: grid;
}
diff --git a/pages/tracks/index.vue b/pages/tracks/index.vue
index 35641ab..544847b 100644
--- a/pages/tracks/index.vue
+++ b/pages/tracks/index.vue
@@ -5,8 +5,7 @@ const scheduleStore = useScheduleStore();
</script>
<template>
- <Panel v-if="scheduleStore.schedule">
- <h2 class="tracks-title">Tracks</h2>
+ <Panel v-if="scheduleStore.schedule" title="Tracks">
<ul class="tracks-list">
<li
v-for="track in scheduleStore.schedule.tracks"
@@ -24,7 +23,7 @@ const scheduleStore = useScheduleStore();
<style scoped>
.tracks-list {
list-style: none;
- margin: 0.5rem 0 0 0;
+ margin: -0.5rem 0 0 0;
padding: 0;
display: grid;
}
diff --git a/stores/error.ts b/stores/error.ts
index dfb5850..2c234f4 100644
--- a/stores/error.ts
+++ b/stores/error.ts
@@ -1,4 +1,3 @@
-import { type Event } from "./schedule";
import { defineStore } from "pinia";
export const useErrorStore = defineStore('error', () => {