Skip to content

Commit

Permalink
feat: APPS 3079 Calendar Component EventCard Popup (#661)
Browse files Browse the repository at this point in the history
* task: setup vuetify config

* task: install vite vuetify plugin

* task: add vuetify lab import to vite config

* task: remove unneeded vuetify plugin folder/file

* task: adjusting/refining vuetify config

* wip: display test events

* wip: calendar navigation

* fix: linting

* task: uninstall vite-plugin-vuetify

* wip: add v-menu config, basic implementation

* wip: implement basic calendar item popup

* clean up code, reposition header today btn

* wip: update styling

* Update prop to set calendar's starting month

* wip: add slot component to calendar

* wip

* Add inner components to calendar, update styles

* update card-meta slot to remove extra margin if slot is unused

* update calendar component slot story

* Update calendar stories, add same-day events

* wip: remove SmartLink component

* rm tagLabels from cardWithImage, set calendar height

* refactor: set base calendar and slot options

* fix: linting

* update documentation

* update template logic, css to reflect highlighted tag labels

* update mock calendar data with isHighlighted field

* add logic to set visual state for selected event item

* refactor deselect event listener

* update calendar mock data

* add documentation to component story

* refactor, cleanup

* refactor logic to handle event selection state styling

* copy yaml file from main branch after pnpm reinstall

* remove event card vertical overflow

* wip: typescript

* add typescript

---------

Co-authored-by: tinuola <[email protected]>
  • Loading branch information
tinuola and tinuola authored Dec 10, 2024
1 parent aa305c4 commit dc24f85
Show file tree
Hide file tree
Showing 8 changed files with 593 additions and 105 deletions.
2 changes: 1 addition & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

185 changes: 155 additions & 30 deletions src/lib-components/BaseCalendar.vue
Original file line number Diff line number Diff line change
@@ -1,63 +1,145 @@
<script setup>
import { computed, onMounted, useTemplateRef } from 'vue'
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref, useTemplateRef } from 'vue'
import type { PropType, Ref } from 'vue'
import format from 'date-fns/format'
import BlockCardWithImage from './BlockCardWithImage.vue'
import BlockEventDetail from './BlockEventDetail.vue'
import BlockTag from './BlockTag.vue'
import { useTheme } from '@/composables/useTheme'
import type { MediaItemType } from '@/types/types'
interface Event {
id: string
startDateWithTime: string
title: string
to: string
location?: { title: string; publicUrl?: string }[]
}
interface TagLabels {
title?: string
isHighlighted?: boolean
}
interface CalendarEvent extends Event {
ftvaEventScreeningDetails: { tagLabels: TagLabels[] }[]
imageCarousel: { image: MediaItemType[] }[]
}
interface SelectedCalendarEvent extends Event {
start: Date
end: Date
time: string
tagLabels?: TagLabels[]
image: MediaItemType
}
const { defaultEventCalendar, events, firstEventMonth } = defineProps({
defaultEventCalendar: {
type: Boolean,
default: true
// True: Default calendar with fixed components
// False: Minimal calendar or with custom components
},
const { events, value } = defineProps({
events: {
type: Array,
type: Array as PropType<CalendarEvent[]>,
default: () => [],
},
value: {
firstEventMonth: {
type: Array,
default: () => [new Date()]
// Sets calendar to month of earliest event
// Default: Calendar opens to month of current date
}
})
// const newDateRef = ref(value)
const calendarRef = useTemplateRef<HTMLDivElement>('calendar')
const firstEventMonthRef = ref(firstEventMonth)
const calendarRef = useTemplateRef('calendar')
// Vuetify Popup/Dialog
const eventItemRef: Ref<CalendarEvent | any> = ref({})
const selectedEventObj: Ref<SelectedCalendarEvent | any> = ref({})
const selectedEventElement = ref<HTMLElement | null>(null)
onMounted(() => {
updateCalendarWeekdays()
updateCalendarHeaderElements()
window.addEventListener('click', handleSelectedEventItemDeselect)
})
// Vuetify calendar day format is single-lettered: S, M, etc.
// Update day to full name: Sunday, Monday, etc.
// Default header button text is 'Today'; update to 'This Month'
function updateCalendarHeaderElements() {
const weekDayLabels = calendarRef.value?.querySelectorAll('.v-calendar-weekly__head-weekday-with-weeknumber')
const todayBtnElem = calendarRef.value?.querySelector('.v-calendar-header__today .v-btn__content') as HTMLElement
const weekDays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
weekDayLabels?.forEach((elem, idx) => (elem as HTMLElement).innerText = weekDays[idx])
todayBtnElem.innerText = 'This Month'
}
// Remove selected style of previous selected event
function handleSelectedEventItemDeselect() {
if (selectedEventElement.value)
selectedEventElement.value.classList.remove('selected-event')
}
const parsedEvents = computed(() => {
if (events.length === 0)
return []
const calendarEvents = events.map((obj) => {
const calendarEvents = events.map((obj: CalendarEvent) => {
const rawDate = obj.startDateWithTime
return {
// Required object keys for Vuetify calendar
title: obj.title,
start: new Date(rawDate),
end: new Date(rawDate),
time: formatEventTime(rawDate),
// All other event data
id: obj.id,
startDateWithTime: obj.startDateWithTime,
tagLabels: obj.ftvaEventScreeningDetails[0]?.tagLabels,
image: obj.imageCarousel[0]?.image[0],
location: obj.location,
to: obj.to
}
})
return calendarEvents
})
// console.log(parsedEvents.value)
// Format time as '00:00 PM'
function formatEventTime(date) {
function formatEventTime(date: string) {
const formattedTime = format(new Date(date), 'h:mm aaa')
return formattedTime.toUpperCase()
}
// Vuetify calendar day format is single-lettered: S, M, etc.
// Update day to full name: Sunday, Monday, etc.
function updateCalendarWeekdays() {
const weekDayLabels = calendarRef.value.querySelectorAll('.v-calendar-weekly__head-weekday-with-weeknumber')
function showEventItemPopup(calendarEventObj: SelectedCalendarEvent | Record <string, unknown>) {
// Remove selected style of previous selected event
handleSelectedEventItemDeselect()
const weekDays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
selectedEventObj.value = calendarEventObj
const selectedElem = eventItemRef.value[`item-${calendarEventObj.id}`]
// Set event as new selected event
selectedEventElement.value = selectedElem
weekDayLabels.forEach((elem, idx) => elem.innerHTML = weekDays[idx])
selectedEventElement.value?.classList.add('selected-event')
}
onUnmounted(() => {
window.removeEventListener('click', handleSelectedEventItemDeselect)
})
const theme = useTheme()
const classes = computed(() => {
Expand All @@ -73,26 +155,69 @@ const classes = computed(() => {
<div class="calendar-wrapper">
<v-sheet class="calendar-body">
<v-calendar
v-model="firstEventMonthRef"
:events="parsedEvents"
view-mode="month"
>
<!-- Vuetify calendar header slot -->
<!--
<template #header="header">
{{ header.title }}
</template>
-->
<!-- Vuetify calendar event slot -->
<!-- Slot prop holds each parsedEvent object -->
<template #event="event">
<div class="calendar-event-item">
<p class="calendar-event-title">
<button :ref="(el) => { eventItemRef[`item-${event.event.id}`] = el }" class="calendar-event-item" @click="showEventItemPopup(event.event)">
<span class="calendar-event-title">
{{ event.event.title }}
</p>
<p class="calendar-event-time">
</span>
<span class="calendar-event-time">
{{ event.event.time }}
</p>
</div>
</span>

<!-- Event item popup -->
<v-menu
activator="parent"
:open-on-click="true"
:close-on-content-click="false"
location="start bottom"
origin="auto"
offset="10"
opacity="0"
>
<v-card width="320" style="overflow: initial; z-index: initial" :hover="false" :link="true" :to="selectedEventObj.to">
<!-- Default Event Calendar -->
<div v-if="defaultEventCalendar" class="calendar-event-popup-wrapper">
<BlockCardWithImage
:image="selectedEventObj.image"
:title="selectedEventObj.title"
/>
<div class="block-tag-wrapper">
<BlockTag
v-for="tag in selectedEventObj.tagLabels"
:key="`tag-${tag.title}`"
:label="tag.title"
:is-secondary="true"
:is-highlighted="tag.isHighlighted ? tag.isHighlighted : false"
:class="{ highlighted: tag.isHighlighted }"
/>
</div>
<BlockEventDetail
:start-date="selectedEventObj.startDateWithTime"
:time="selectedEventObj.startDateWithTime"
:locations="selectedEventObj.location"
/>
</div>

<!-- Slot for new components -->
<div v-else-if="$slots.calendarSlotComponent" class="calendar-slot-wrapper">
<slot name="calendarSlotComponent" :event="selectedEventObj" />
</div>

<!-- Default Vuetify component -->
<v-list v-else>
<v-list-item
:title="selectedEventObj.title"
/>
</v-list>
</v-card>
</v-menu>
</button>
</template>
</v-calendar>
</v-sheet>
Expand Down
2 changes: 1 addition & 1 deletion src/lib-components/CardMeta.vue
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ const classes = computed(() => {
<template>
<div :class="classes">
<div class="linked-category">
<div v-if="$slots.linkedcategoryslot" class="linked-category">
<slot name="linkedcategoryslot" />
</div>
Expand Down
3 changes: 2 additions & 1 deletion src/plugins/vuetify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import 'vuetify/styles'
import { createVuetify } from 'vuetify'
import { VCalendar } from 'vuetify/labs/VCalendar'
import '@mdi/font/css/materialdesignicons.css'
import { VCard, VList, VListItem, VMenu, VSheet } from 'vuetify/lib/components/index.mjs'

export const vuetify = createVuetify({
components: {
VCalendar,
VCalendar, VSheet, VMenu, VList, VCard, VListItem
},
})
84 changes: 81 additions & 3 deletions src/stories/BaseCalendar.stories.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,46 @@
import { computed } from 'vue'

import BaseCalendar from '../lib-components/BaseCalendar.vue'
import { mockCalendarEvents } from './mock/CalendarEvents'

/**
* Calendar component that extends Vuetify's `v-calendar` component and uses the `v-menu` component to display event item popup/dialog.
*
* Props:
*
* 1. defaultEventCalendar: Boolean value
* - When `true` (default), renders an event calendar with preset inner components:
* - BlockCardWithImage, BlockEventDetail, and BlockTag
* - See __Default Event__ stories
* - When `false`, BaseCalendar can either be:
* - a simplified calendar that uses Vuetify's `v-list` component; see __Default Vuetify__ story
* - a calendar with custom components if the slot `calendarSlotComponent` is used
*
*
* 2. events: Array of event objects
*
*
* 3. firstEventMonth: Array with a default `new Date()` object as its only item
* - When used, sets the first month that the calendar displays on page load
* - Example syntax: `[new Date('September 01, 2024 00:00:00')]`
* - See __Set Start Month__ story
*/

export default {
title: 'Base Calendar',
component: BaseCalendar,
}

export function Default() {
export function DefaultVuetify() {
return {
data() {
return { ...mockCalendarEvents }
},
components: { BaseCalendar },
template: '<div style="display: flex;justify-content: center;"><base-calendar :events="events" :defaultEventCalendar="false" /></div>'
}
}

export function DefaultEvent() {
return {
data() {
return { ...mockCalendarEvents }
Expand All @@ -18,7 +50,7 @@ export function Default() {
}
}

export function DefaultFTVA() {
export function DefaultFTVAEvent() {
return {
data() {
return { ...mockCalendarEvents }
Expand All @@ -32,3 +64,49 @@ export function DefaultFTVA() {
template: '<div style="display: flex;justify-content: center;"><base-calendar :events="events" /></div>'
}
}

export function SetStartMonth() {
return {
data() {
return {
...mockCalendarEvents,
mockCalendarStart: [new Date('September 01, 2024 00:00:00')],
}
},
provide() {
return {
theme: computed(() => 'ftva'),
}
},
components: { BaseCalendar },
template: `<div
style="display: flex;justify-content: center;">
<base-calendar
:events="events"
:firstEventMonth="mockCalendarStart" />
</div>`
}
}

export function SameDayEvents() {
return {
data() {
return {
...mockCalendarEvents,
mockCalendarStart: [new Date('October 01, 2024 00:00:00')],
}
},
provide() {
return {
theme: computed(() => 'ftva'),
}
},
components: { BaseCalendar },
template: `<div
style="display: flex;justify-content: center;">
<base-calendar
:events="events"
:firstEventMonth="mockCalendarStart" />
</div>`
}
}
Loading

1 comment on commit dc24f85

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.