* feat: agenda page in vue (wip) * feat: scroll to agenda day * fix: vue 3 composition api + eslint settings * fix: agenda day scroll match indicator * fix: convert vite deps to yarn * fix: missing lodash + legacy build step * fix: agenda - move calendar into drawer * fix: improve agenda filter UI * fix: download ics + move agenda into own component * feat: use fullcalendar for agenda calendar view (wip) * feat: add events to agenda calendar * feat: agenda filter UI improvements * feat: agenda add to calendar dropdown * feat: agenda calendar filter + timezone + event coloring * feat: agenda calendar color improvements * chore: exclude dist-neue from git * feat: agenda calendar event modal * fix: rebuild yarn deps * chore: add run migration task to vscode * fix: agenda buttons display flag * feat: agenda event modal component * feat: show calendar event quick info on hover * fix: clear calendar quick info on timezone change * feat: agenda list view improvements * feat: agenda list row coloring * feat: agenda list note * feat: agenda list icons for office hours + hackathon * fix: agenda top links * refactor: use pinia as store for agenda components * feat: agenda jump to now * fix: agenda mobile improvements * feat: agenda search * feat: agenda search improvements * feat: agenda event recordings buttons for post-meeting * fix: agenda switch to meeting timezone on load * feat: agenda pre & live session buttons * fix: remove agenda utc + personalize links in top menu * feat: add pre-vue loading state on page load * feat: filter from agenda picker mode * fix: agenda UI improvements * fix: django-vite non-dev mode * chore: update yarn dependencies for vue + vite * feat: agenda settings panel + UI improvements * feat: agenda settings colors + import/export feature * feat: agenda color assignments + responsive UI improvements * feat: agenda realtime red line + debug datetime offset * feat: agenda add aria labels for settings * feat: add new agenda path + pages/menu * fix: bring base/menu.html up to main * fix: agenda various fixes * test: add new agenda item to meetings menu for item count * chore: restore devcontainer extensions list * fix: agenda UI improvements + montserrat default font * feat: agenda bolder text + hide event icons options * feat: agenda warning badge * fix: agenda various UI improvements + intersectionObserver fix * feat: agenda floorplan page + various UI improvements * feat: agenda floor plan pin * feat: view floor plan room from agenda * feat: agenda floor plan mobile optimization * feat: adjust calendar options + default calendar view in settings * feat: agenda persist picked events + change base font only on new agenda page * feat: agenda mobile view optimizations * fix: add .vite to cached volumes * fix: mobile view for filters, calendar, settings panels * test: upgrade cypress existing tests to work on bs5 + update dependencies * fix: use named url patterns to avoid hardcoded URLs. Add rudimentary test coverage for the neue views. Co-authored-by: Robert Sparks <rjsparks@nostrum.com>
516 lines
12 KiB
Vue
516 lines
12 KiB
Vue
<template lang="pug">
|
|
.agenda(
|
|
:class='{ "bolder-text": agendaStore.bolderText }'
|
|
)
|
|
h1
|
|
span #[strong IETF {{agendaStore.meeting.number}}] Meeting Agenda {{titleExtra}}
|
|
.meeting-h1-badges.d-none.d-sm-flex
|
|
span.meeting-warning(v-if='agendaStore.meeting.warningNote') {{agendaStore.meeting.warningNote}}
|
|
span.meeting-beta BETA
|
|
h4
|
|
span {{agendaStore.meeting.city}}, {{ meetingDate }}
|
|
h6.float-end.d-none.d-lg-inline(v-if='meetingUpdated') #[span.text-muted Updated:] {{ meetingUpdated }}
|
|
|
|
.agenda-topnav.my-3
|
|
meeting-navigation
|
|
n-button.d-none.d-sm-flex(
|
|
quaternary
|
|
@click='toggleSettings'
|
|
)
|
|
template(#icon)
|
|
i.bi.bi-gear
|
|
span Settings
|
|
|
|
.row
|
|
.col
|
|
|
|
// ----------------------------
|
|
// -> Subtitle + Timezone Bar
|
|
// ----------------------------
|
|
.row
|
|
.col.d-none.d-sm-flex.align-items-center
|
|
h2 {{ agendaStore.pickerMode ? 'Session Selection' : 'Schedule'}}
|
|
|
|
n-popover(v-if='!agendaStore.infoNoteShown')
|
|
template(#trigger)
|
|
n-button.ms-2(text, @click='toggleInfoNote')
|
|
i.bi.bi-info-circle.text-muted
|
|
span Show Info Note
|
|
.col-12.col-sm-auto.d-flex.align-items-center
|
|
i.bi.bi-globe.me-2
|
|
small.me-2.d-none.d-md-inline: strong Timezone:
|
|
n-button-group.agenda-tz-selector
|
|
n-button(
|
|
:type='agendaStore.isTimezoneMeeting ? `primary` : `default`'
|
|
@click='setTimezone(`meeting`)'
|
|
) Meeting
|
|
n-button(
|
|
:type='agendaStore.isTimezoneLocal ? `primary` : `default`'
|
|
@click='setTimezone(`local`)'
|
|
) Local
|
|
n-button(
|
|
:type='agendaStore.timezone === `UTC` ? `primary` : `default`'
|
|
@click='setTimezone(`UTC`)'
|
|
) UTC
|
|
n-select.agenda-timezone-ddn(
|
|
v-if='agendaStore.viewport > 1250'
|
|
v-model:value='agendaStore.timezone'
|
|
:options='timezones'
|
|
placeholder='Select Time Zone'
|
|
filterable
|
|
)
|
|
|
|
.alert.alert-warning.mt-3(v-if='agendaStore.isCurrentMeeting') #[strong Note:] IETF agendas are subject to change, up to and during a meeting.
|
|
.agenda-infonote.mt-3(v-if='agendaStore.meeting.infoNote && agendaStore.infoNoteShown')
|
|
n-popover
|
|
template(#trigger)
|
|
n-button(
|
|
text
|
|
aria-label='Close Info Note'
|
|
@click='toggleInfoNote'
|
|
)
|
|
i.bi.bi-x-square
|
|
span Hide Info Note
|
|
div(v-html='agendaStore.meeting.infoNote')
|
|
|
|
// -----------------------------------
|
|
// -> Color Legend
|
|
// -----------------------------------
|
|
|
|
.agenda-colorlegend.mt-3(v-if='colorLegendShown')
|
|
div
|
|
i.bi.bi-palette.me-2
|
|
span Color Legend
|
|
template(v-for='(cl, idx) of agendaStore.colors')
|
|
div(
|
|
v-if='cl.tag !== ``'
|
|
:key='`cl` + idx'
|
|
:style='{ color: cl.hex }'
|
|
)
|
|
span {{cl.tag}}
|
|
|
|
|
|
// -----------------------------------
|
|
// -> Search Bar
|
|
// -----------------------------------
|
|
|
|
.agenda-search.mt-3(v-if='agendaStore.searchVisible')
|
|
n-input-group
|
|
n-input(
|
|
v-model:value='state.searchText'
|
|
ref='searchIpt'
|
|
type='text'
|
|
placeholder='Search...'
|
|
@keyup.esc='closeSearch'
|
|
)
|
|
template(#prefix)
|
|
i.bi.bi-search.me-1
|
|
n-popover
|
|
template(#trigger)
|
|
n-button(
|
|
type='primary'
|
|
ghost
|
|
@click='state.searchText = ``'
|
|
aria-label='Clear Search'
|
|
)
|
|
i.bi.bi-x-lg
|
|
span Clear Search
|
|
|
|
|
|
// -----------------------------------
|
|
// -> Drawers
|
|
// -----------------------------------
|
|
agenda-filter
|
|
agenda-schedule-calendar
|
|
agenda-settings
|
|
|
|
// -----------------------------------
|
|
// -> Schedule List
|
|
// -----------------------------------
|
|
agenda-schedule-list.mt-3(ref='schdList')
|
|
|
|
// -----------------------------------
|
|
// -> Anchored Day Quick Access Menu
|
|
// -----------------------------------
|
|
.col-auto.d-print-none(v-if='agendaStore.viewport >= 990')
|
|
agenda-quick-access
|
|
|
|
agenda-mobile-bar
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
|
import { DateTime } from 'luxon'
|
|
import debounce from 'lodash/debounce'
|
|
|
|
import {
|
|
NButtonGroup,
|
|
NButton,
|
|
NInputGroup,
|
|
NInput,
|
|
NPopover,
|
|
NSelect,
|
|
useMessage
|
|
} from 'naive-ui'
|
|
import AgendaFilter from './AgendaFilter.vue'
|
|
import AgendaScheduleList from './AgendaScheduleList.vue'
|
|
import AgendaScheduleCalendar from './AgendaScheduleCalendar.vue'
|
|
import AgendaQuickAccess from './AgendaQuickAccess.vue'
|
|
import AgendaSettings from './AgendaSettings.vue'
|
|
import AgendaMobileBar from './AgendaMobileBar.vue'
|
|
import MeetingNavigation from './MeetingNavigation.vue'
|
|
|
|
import timezones from '../shared/timezones'
|
|
|
|
import { useAgendaStore } from './store'
|
|
|
|
// MESSAGE PROVIDER
|
|
|
|
const message = useMessage()
|
|
|
|
// STORES
|
|
|
|
const agendaStore = useAgendaStore()
|
|
|
|
// DATA
|
|
|
|
const state = reactive({
|
|
searchText: '',
|
|
})
|
|
|
|
// REFS
|
|
|
|
const schdList = ref(null)
|
|
const searchIpt = ref(null)
|
|
|
|
// WATCHERS
|
|
|
|
watch(() => agendaStore.searchVisible, (newValue) => {
|
|
state.searchText = agendaStore.searchText
|
|
if (newValue) {
|
|
nextTick(() => {
|
|
searchIpt.value?.focus()
|
|
})
|
|
}
|
|
})
|
|
|
|
watch(() => state.searchText, debounce((newValue) => {
|
|
agendaStore.$patch({
|
|
searchText: newValue.toLowerCase()
|
|
})
|
|
}, 500))
|
|
|
|
watch(() => agendaStore.meetingDays, () => {
|
|
nextTick(() => {
|
|
setTimeout(() => {
|
|
reconnectScrollObservers()
|
|
}, 100)
|
|
})
|
|
})
|
|
|
|
// COMPUTED
|
|
|
|
const titleExtra = computed(() => {
|
|
let title = ''
|
|
if (agendaStore.timezone === 'UTC') {
|
|
title = `${title} (UTC)`
|
|
}
|
|
return title
|
|
})
|
|
const meetingDate = computed(() => {
|
|
const start = DateTime.fromISO(agendaStore.meeting.startDate).setZone(agendaStore.timezone)
|
|
const end = DateTime.fromISO(agendaStore.meeting.endDate).setZone(agendaStore.timezone)
|
|
if (start.month === end.month) {
|
|
return `${start.toFormat('MMMM d')} - ${end.toFormat('d, y')}`
|
|
} else {
|
|
return `${start.toFormat('MMMM d')} - ${end.toFormat('MMMM d, y')}`
|
|
}
|
|
})
|
|
const meetingUpdated = computed(() => {
|
|
return agendaStore.meeting.updated ? DateTime.fromISO(agendaStore.meeting.updated).setZone(agendaStore.timezone).toFormat(`DD 'at' tt ZZZZ`) : false
|
|
})
|
|
const colorLegendShown = computed(() => {
|
|
return agendaStore.colorPickerVisible || (agendaStore.colorLegendShown && Object.keys(agendaStore.colorAssignments).length > 0)
|
|
})
|
|
|
|
// METHODS
|
|
|
|
function switchTab (key) {
|
|
state.currentTab = key
|
|
window.history.pushState({}, '', key)
|
|
}
|
|
|
|
function setTimezone (tz) {
|
|
switch (tz) {
|
|
case 'meeting':
|
|
agendaStore.$patch({ timezone: agendaStore.meeting.timezone })
|
|
break
|
|
case 'local':
|
|
agendaStore.$patch({ timezone: DateTime.local().zoneName })
|
|
break
|
|
default:
|
|
agendaStore.$patch({ timezone: tz })
|
|
break
|
|
}
|
|
}
|
|
|
|
function closeSearch () {
|
|
agendaStore.$patch({
|
|
searchText: '',
|
|
searchVisible: false
|
|
})
|
|
}
|
|
|
|
function toggleInfoNote () {
|
|
agendaStore.$patch({ infoNoteShown: !agendaStore.infoNoteShown })
|
|
agendaStore.persistMeetingPreferences()
|
|
}
|
|
|
|
function toggleSettings () {
|
|
agendaStore.$patch({
|
|
settingsShown: !agendaStore.settingsShown
|
|
})
|
|
}
|
|
|
|
// --------------------------------------------------------------------
|
|
// Handle browser resize
|
|
// --------------------------------------------------------------------
|
|
|
|
const resizeObserver = new ResizeObserver(entries => {
|
|
agendaStore.$patch({ viewport: Math.round(window.innerWidth) })
|
|
// for (const entry of entries) {
|
|
// const newWidth = entry.contentBoxSize ? entry.contentBoxSize[0].inlineSize : entry.contentRect.width
|
|
// }
|
|
})
|
|
|
|
onMounted(() => {
|
|
resizeObserver.observe(schdList.value.$el)
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
resizeObserver.unobserve(schdList.value.$el)
|
|
})
|
|
|
|
// --------------------------------------------------------------------
|
|
// Handle day indicator / scroll
|
|
// --------------------------------------------------------------------
|
|
|
|
const visibleDays = []
|
|
const scrollObserver = new IntersectionObserver(entries => {
|
|
for (const entry of entries) {
|
|
if (entry.isIntersecting) {
|
|
if (!visibleDays.some(e => e.id === entry.target.dataset.dayId)) {
|
|
visibleDays.push({
|
|
id: entry.target.dataset.dayId,
|
|
ts: entry.target.dataset.dayTs
|
|
})
|
|
}
|
|
} else {
|
|
const idxToRemove = visibleDays.findIndex(e => e.id === entry.target.dataset.dayId)
|
|
if (idxToRemove >= 0) {
|
|
visibleDays.splice(idxToRemove, 1)
|
|
}
|
|
}
|
|
}
|
|
|
|
let finalDayId = agendaStore.dayIntersectId
|
|
let earliestTs = '9'
|
|
for (const day of visibleDays) {
|
|
if (day.ts < earliestTs) {
|
|
finalDayId = day.id
|
|
earliestTs = day.ts
|
|
}
|
|
}
|
|
|
|
agendaStore.$patch({ dayIntersectId: finalDayId.toString() })
|
|
}, {
|
|
root: null,
|
|
rootMargin: '0px',
|
|
threshold: [0.0, 1.0]
|
|
})
|
|
|
|
function reconnectScrollObservers () {
|
|
scrollObserver.disconnect()
|
|
visibleDays.length = 0
|
|
for (const mDay of agendaStore.meetingDays) {
|
|
const el = document.getElementById(`agenda-day-${mDay.slug}`)
|
|
el.dataset.dayId = mDay.slug.toString()
|
|
el.dataset.dayTs = mDay.ts
|
|
scrollObserver.observe(el)
|
|
}
|
|
}
|
|
|
|
// MOUNTED
|
|
|
|
onMounted(() => {
|
|
reconnectScrollObservers()
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
scrollObserver.disconnect()
|
|
})
|
|
|
|
// --------------------------------------------------------------------
|
|
|
|
// MOUNTED
|
|
|
|
onMounted(() => {
|
|
agendaStore.hideLoadingScreen()
|
|
})
|
|
|
|
// CREATED
|
|
|
|
// -> Handle loading tab directly based on URL
|
|
if (window.location.pathname.indexOf('-utc') >= 0) {
|
|
agendaStore.$patch({ timezone: 'UTC' })
|
|
} else if (window.location.pathname.indexOf('personalize') >= 0) {
|
|
// state.currentTab = 'personalize'
|
|
}
|
|
|
|
</script>
|
|
|
|
<style lang="scss">
|
|
@import "bootstrap/scss/functions";
|
|
@import "bootstrap/scss/variables";
|
|
@import "../shared/breakpoints";
|
|
|
|
.agenda {
|
|
min-height: 500px;
|
|
font-weight: 460;
|
|
|
|
&.bolder-text {
|
|
font-weight: 520;
|
|
}
|
|
|
|
&-topnav {
|
|
position: relative;
|
|
|
|
> button {
|
|
position: absolute;
|
|
top: 5px;
|
|
right: 0;
|
|
|
|
.bi {
|
|
transition: transform 1s ease;
|
|
}
|
|
|
|
&:hover {
|
|
.bi {
|
|
transform: rotate(180deg);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
&-tz-selector {
|
|
margin-right: .5rem;
|
|
|
|
@media screen and (max-width: $bs5-break-sm) {
|
|
margin-right: 0;
|
|
justify-content: stretch;
|
|
flex: 1;
|
|
|
|
> button {
|
|
flex: 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
&-timezone-ddn {
|
|
min-width: 350px;
|
|
}
|
|
|
|
&-infonote {
|
|
border: 1px solid $blue-400;
|
|
border-radius: .25rem;
|
|
background: linear-gradient(to top, lighten($blue-100, 2%), lighten($blue-100, 5%));
|
|
box-shadow: inset 0 0 0 1px #FFF;
|
|
padding: 16px 50px 16px 16px;
|
|
font-size: .9rem;
|
|
color: $blue-700;
|
|
position: relative;
|
|
|
|
> button {
|
|
position: absolute;
|
|
top: 15px;
|
|
right: 15px;
|
|
font-size: 1.2em;
|
|
color: $blue-400;
|
|
}
|
|
}
|
|
|
|
&-colorlegend {
|
|
display: grid;
|
|
gap: 10px;
|
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
border-radius: 5px;
|
|
font-size: .8rem;
|
|
font-weight: 600;
|
|
|
|
> div:first-child {
|
|
background-color: $gray-600;
|
|
background: linear-gradient(337deg, $gray-500 20%, $gray-600 70%);
|
|
padding: 5px 15px;
|
|
font-size: .7rem;
|
|
text-transform: uppercase;
|
|
color: #FFF;
|
|
border-radius: 5px;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
> div:not(:first-child) {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 5px 15px;
|
|
justify-content: center;
|
|
|
|
&::before {
|
|
content: '';
|
|
display: block;
|
|
width: 18px;
|
|
height: 18px;
|
|
border-radius: 9px;
|
|
border: 2px solid rgba(0,0,0,.1);
|
|
background-color: currentColor;
|
|
box-shadow: 0 0 10px 0 currentColor;
|
|
margin-right: 10px;
|
|
}
|
|
|
|
span {
|
|
color: currentColor;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.n-dropdown-option {
|
|
.text-red {
|
|
color: $red-500;
|
|
}
|
|
.text-brown {
|
|
color: $orange-700;
|
|
}
|
|
.text-blue {
|
|
color: $blue-600;
|
|
}
|
|
.text-green {
|
|
color: $green-500;
|
|
}
|
|
.text-purple {
|
|
color: $purple-500;
|
|
}
|
|
}
|
|
|
|
@keyframes spin {
|
|
from { transform:rotate(0deg); }
|
|
to { transform:rotate(360deg); }
|
|
}
|
|
|
|
@keyframes warningBorderFlash {
|
|
10% { color: #FFF; }
|
|
50% { color: $red-300; }
|
|
90% { color: #FFF; }
|
|
}
|
|
</style>
|