* text-muted -> text-body-secondary * navbar-dark is deprecated * Remove FIXME block, not an issue anymore * Remove `navbar-light`
619 lines
15 KiB
Vue
619 lines
15 KiB
Vue
<template lang="pug">
|
|
.agenda(
|
|
v-if='agendaStore.isLoaded'
|
|
: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}}
|
|
h4
|
|
span {{agendaStore.meeting.city}}, {{ meetingDate }}
|
|
h6.float-end.d-none.d-lg-inline(v-if='meetingUpdated') #[span.text-body-secondary Updated:] {{ meetingUpdated }}
|
|
|
|
.agenda-topnav.my-3
|
|
meeting-navigation
|
|
.agenda-topnav-right.d-none.d-md-flex
|
|
n-button(
|
|
quaternary
|
|
@click='startTour'
|
|
)
|
|
template(#icon)
|
|
i.bi.bi-question-square
|
|
span Help
|
|
n-button(
|
|
quaternary
|
|
@click='toggleShare'
|
|
)
|
|
template(#icon)
|
|
i.bi.bi-share
|
|
span Share
|
|
n-button(
|
|
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-body-secondary
|
|
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='siteStore.viewport > 1250'
|
|
v-model:value='agendaStore.timezone'
|
|
:options='timezones'
|
|
placeholder='Select Time Zone'
|
|
filterable
|
|
@update:value='() => { agendaStore.persistMeetingPreferences() }'
|
|
)
|
|
|
|
.agenda-currentwarn.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
|
|
|
|
// -----------------------------------
|
|
// -> Anchored Day Quick Access Menu
|
|
// -----------------------------------
|
|
.col-auto.d-print-none(v-if='siteStore.viewport >= 990')
|
|
agenda-quick-access
|
|
|
|
agenda-mobile-bar
|
|
agenda-share-modal(v-model:shown='state.shareModalShown')
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
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 AgendaShareModal from './AgendaShareModal.vue'
|
|
import AgendaMobileBar from './AgendaMobileBar.vue'
|
|
import MeetingNavigation from './MeetingNavigation.vue'
|
|
|
|
import timezones from '../shared/timezones'
|
|
import { initTour } from './tour'
|
|
|
|
import { useAgendaStore } from './store'
|
|
import { useSiteStore } from '../shared/store'
|
|
|
|
import './agenda.scss'
|
|
|
|
// MESSAGE PROVIDER
|
|
|
|
const message = useMessage()
|
|
|
|
// STORES
|
|
|
|
const agendaStore = useAgendaStore()
|
|
const siteStore = useSiteStore()
|
|
|
|
// ROUTER
|
|
|
|
const router = useRouter()
|
|
const route = useRoute()
|
|
|
|
// DATA
|
|
|
|
const state = reactive({
|
|
searchText: '',
|
|
shareModalShown: false
|
|
})
|
|
|
|
// REFS
|
|
|
|
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)
|
|
})
|
|
})
|
|
|
|
watch(() => agendaStore.isLoaded, () => {
|
|
let resetQuery = false
|
|
if (route.query.filters) {
|
|
// Handle ?filters= parameter
|
|
const keywords = route.query.filters.split(',').map(k => k.trim()).filter(k => !!k)
|
|
if (keywords?.length > 0) {
|
|
agendaStore.$patch({
|
|
selectedCatSubs: keywords
|
|
})
|
|
}
|
|
resetQuery = true
|
|
}
|
|
if (route.query.show) {
|
|
// Handle ?show= parameter
|
|
const keywords = route.query.show.split(',').map(k => k.trim()).filter(k => !!k)
|
|
if (keywords?.length > 0) {
|
|
const pickedIds = []
|
|
for (const ev of agendaStore.scheduleAdjusted) {
|
|
if (keywords.includes(ev.sessionKeyword)) {
|
|
pickedIds.push(ev.id)
|
|
}
|
|
}
|
|
if (pickedIds.length > 0) {
|
|
agendaStore.$patch({
|
|
pickerMode: true,
|
|
pickerModeView: true,
|
|
pickedEvents: pickedIds
|
|
})
|
|
}
|
|
}
|
|
resetQuery = true
|
|
}
|
|
if (route.query.pick) {
|
|
// Handle legacy /personalize path (open picker mode)
|
|
agendaStore.$patch({ pickerMode: true })
|
|
resetQuery = true
|
|
}
|
|
if (route.query.tz) {
|
|
// Handle tz param
|
|
agendaStore.$patch({ timezone: route.query.tz })
|
|
resetQuery = true
|
|
}
|
|
|
|
if (resetQuery) {
|
|
agendaStore.persistMeetingPreferences()
|
|
router.replace({ query: null })
|
|
}
|
|
|
|
handleCurrentMeetingRedirect()
|
|
})
|
|
|
|
// COMPUTED
|
|
|
|
const titleExtra = computed(() => {
|
|
let title = ''
|
|
if (agendaStore.timezone === 'UTC') {
|
|
title = `${title} (UTC)`
|
|
}
|
|
return title
|
|
})
|
|
const meetingDate = computed(() => {
|
|
// Adjust to first meeting start time (to ensure proper start date when switching timezones)
|
|
const firstEventStartTime = { hour: 0, minute: 0 }
|
|
if (agendaStore.schedule.length > 0) {
|
|
const evStartObj = DateTime.fromISO(agendaStore.schedule[0].startDateTime, { zone: agendaStore.meeting.timezone }).toObject()
|
|
firstEventStartTime.hour = evStartObj.hour
|
|
firstEventStartTime.minute = evStartObj.minute
|
|
}
|
|
|
|
// Adjust start and end dates for current timezone
|
|
const start = DateTime.fromISO(agendaStore.meeting.startDate, { zone: agendaStore.meeting.timezone }).set(firstEventStartTime).setZone(agendaStore.timezone)
|
|
const end = DateTime.fromISO(agendaStore.meeting.endDate, { zone: agendaStore.meeting.timezone }).set({ hour: 23, minute: 59}).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(() => {
|
|
if (!agendaStore.meeting.updated) { return false }
|
|
|
|
const updatedDatetime = DateTime.fromISO(agendaStore.meeting.updated).setZone(agendaStore.timezone)
|
|
if (!updatedDatetime.isValid || updatedDatetime < DateTime.fromISO('1980-01-01')) {
|
|
return false
|
|
}
|
|
|
|
return updatedDatetime.toFormat(`DD 'at' T ZZZZ`)
|
|
})
|
|
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
|
|
}
|
|
agendaStore.persistMeetingPreferences()
|
|
}
|
|
|
|
function closeSearch () {
|
|
agendaStore.$patch({
|
|
searchText: '',
|
|
searchVisible: false
|
|
})
|
|
}
|
|
|
|
function toggleInfoNote () {
|
|
agendaStore.$patch({ infoNoteShown: !agendaStore.infoNoteShown })
|
|
agendaStore.persistMeetingPreferences()
|
|
}
|
|
|
|
function toggleSettings () {
|
|
agendaStore.$patch({
|
|
settingsShown: !agendaStore.settingsShown
|
|
})
|
|
}
|
|
|
|
function toggleShare () {
|
|
state.shareModalShown = !state.shareModalShown
|
|
}
|
|
|
|
function startTour () {
|
|
const tour = initTour({
|
|
mobileMode: siteStore.viewport < 990,
|
|
pickerMode: agendaStore.pickerMode
|
|
})
|
|
tour.start()
|
|
}
|
|
|
|
// -> Go to current meeting if not provided
|
|
function handleCurrentMeetingRedirect () {
|
|
if (!route.params.meetingNumber && agendaStore.meeting.number) {
|
|
router.replace({ params: { meetingNumber: agendaStore.meeting.number } })
|
|
}
|
|
}
|
|
|
|
// --------------------------------------------------------------------
|
|
// 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.fetch(route.params.meetingNumber)
|
|
|
|
handleCurrentMeetingRedirect()
|
|
|
|
// -> Hide Loading Screen
|
|
if (agendaStore.isLoaded) {
|
|
agendaStore.hideLoadingScreen()
|
|
}
|
|
})
|
|
|
|
</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;
|
|
|
|
&-right {
|
|
position: absolute;
|
|
top: 5px;
|
|
right: 0;
|
|
display: flex;
|
|
|
|
button + button {
|
|
margin-left: 5px;
|
|
}
|
|
|
|
> button:last-child {
|
|
.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>
|