* test: agenda-neue - separate timezone controls assertions to allow retries * test: agenda-neue - use dom query selectors instead of first() / eq() * test: agenda-neue - playwright * test: fix playwright setup for ci * test: playwright - remove safari + fix timezone local * test: upload playwright report * test: playwright - fix trace upload * test * test: playwright - agenda search * test: fix startdate timezone * test: playwright - agenda table events * test: playwright - remove only filter * test: remove exit early flag * test: allow longer tests * test: agenda materials dialog * test: agenda filter by area/group * test: agenda calendar view * test: agenda settings * test: jump to day * test: fix agenda jump to day timezone parse * test: increase test timeout * test: remove fail fast * test: test sharding + increase delay * test: fixes * test: use macos image * test: fixes * test: agenda color assign + future + live meeting tests * test: agenda mobile tests * test: remainder of tests for playwright + optimizations * test: red line intersection accept close value * test: add delay for agenda search tests * chore: cleanup old tests + adapt build workflow * ci: fix build workflow * ci: fix build workflow order * fix: point to playwright floor plan images + readme
380 lines
8.2 KiB
Vue
380 lines
8.2 KiB
Vue
<template lang="pug">
|
|
.floorplan
|
|
template(v-if='agendaStore.isLoaded')
|
|
h1
|
|
span #[strong IETF {{agendaStore.meeting.number}}] Floor Plan
|
|
.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 }}
|
|
|
|
.floorplan-topnav.my-3
|
|
meeting-navigation
|
|
|
|
nav.floorplan-floors.nav.nav-pills.nav-justified(v-if='agendaStore.isLoaded')
|
|
a.nav-link(
|
|
v-for='floor of agendaStore.floors'
|
|
:key='floor.id'
|
|
:name='floor.name'
|
|
:class='{ active: state.currentFloor === floor.id }'
|
|
@click='state.currentFloor = floor.id'
|
|
)
|
|
i.bi.bi-arrow-down-right-square.me-2
|
|
span {{floor.name}}
|
|
|
|
.row.mt-3
|
|
.col-12.col-md-auto(v-if='agendaStore.isLoaded')
|
|
.floorplan-rooms.list-group.shadow-sm
|
|
router-link.list-group-item.list-group-item-action(
|
|
v-for='room of floor.rooms'
|
|
:key='room.id'
|
|
:class='{ active: state.currentRoom === room.id }'
|
|
:aria-current='state.currentRoom === room.id'
|
|
@click='state.currentRoom = room.id'
|
|
:to='{ query: { room: xslugify(room.name) } }'
|
|
)
|
|
.badge.me-3 {{floor.short}}
|
|
span
|
|
strong {{room.name}}
|
|
small {{room.functionalName}}
|
|
.col
|
|
.card.floorplan-plan.shadow-sm
|
|
.floorplan-plan-pin(
|
|
v-if='state.currentRoom && state.isLoaded'
|
|
:style='pinPosition'
|
|
)
|
|
i.bi.bi-geo-alt-fill
|
|
img(
|
|
:src='floor.image'
|
|
ref='planImage'
|
|
@load='planImageLoaded'
|
|
)
|
|
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
|
import find from 'lodash/find'
|
|
import xslugify from '../shared/xslugify'
|
|
import { DateTime } from 'luxon'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { useAgendaStore } from './store'
|
|
|
|
import MeetingNavigation from './MeetingNavigation.vue'
|
|
|
|
// STORES
|
|
|
|
const agendaStore = useAgendaStore()
|
|
|
|
// ROUTER
|
|
|
|
const router = useRouter()
|
|
const route = useRoute()
|
|
|
|
// STATE
|
|
|
|
const state = reactive({
|
|
currentFloor: null,
|
|
currentRoom: null,
|
|
desiredRoom: null,
|
|
isLoaded: false,
|
|
awaitsPinDrop: false,
|
|
xRatio: 1,
|
|
yRatio: 1
|
|
})
|
|
|
|
// REFS
|
|
|
|
const planImage = ref(null)
|
|
|
|
// COMPUTED
|
|
|
|
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 floor = computed(() => {
|
|
return state.currentFloor ? find(agendaStore.floors, ['id', state.currentFloor]) : {}
|
|
})
|
|
|
|
const pinPosition = computed(() => {
|
|
if (!state.currentRoom || !floor.value.rooms?.some(r => r.id === state.currentRoom)) {
|
|
return {
|
|
display: 'none'
|
|
}
|
|
} else {
|
|
const room = find(floor.value.rooms, ['id', state.currentRoom])
|
|
const xPos = Math.round((room.left + (room.right - room.left) / 2) * state.xRatio) - 25
|
|
const yPos = Math.round((room.top + (room.bottom - room.top) / 2) * state.yRatio) - 40
|
|
return {
|
|
display: 'block',
|
|
top: `${yPos}px`,
|
|
left: `${xPos}px`
|
|
}
|
|
}
|
|
})
|
|
|
|
// WATCHERS
|
|
|
|
watch(() => agendaStore.floors, (newValue) => {
|
|
if (newValue && newValue.length > 0 && !state.currentFloor) {
|
|
state.currentFloor = newValue[0].id
|
|
}
|
|
handleDesiredRoom()
|
|
})
|
|
watch(() => state.currentFloor, () => {
|
|
state.isLoaded = false
|
|
})
|
|
watch(() => state.currentRoom, () => {
|
|
nextTick(() => {
|
|
computePlanSizeRatio()
|
|
setTimeout(() => {
|
|
if (state.isLoaded) {
|
|
document.querySelector('.floorplan-plan-pin')?.scrollIntoView({ behavior: 'smooth' })
|
|
} else {
|
|
state.awaitsPinDrop = true
|
|
}
|
|
}, 100)
|
|
})
|
|
})
|
|
watch(() => agendaStore.viewport, () => {
|
|
nextTick(() => {
|
|
computePlanSizeRatio()
|
|
})
|
|
})
|
|
|
|
watch(() => agendaStore.isLoaded, handleCurrentMeetingRedirect)
|
|
|
|
// METHODS
|
|
|
|
function computePlanSizeRatio () {
|
|
if (!planImage.value || !state.currentFloor) {
|
|
return
|
|
}
|
|
state.xRatio = planImage.value.width / floor.value.width
|
|
state.yRatio = planImage.value.height / floor.value.height
|
|
}
|
|
|
|
function planImageLoaded () {
|
|
setTimeout(() => {
|
|
state.isLoaded = true
|
|
nextTick(() => {
|
|
computePlanSizeRatio()
|
|
if (state.awaitsPinDrop) {
|
|
setTimeout(() => {
|
|
document.querySelector('.floorplan-plan-pin')?.scrollIntoView({ behavior: 'smooth' })
|
|
}, 100)
|
|
}
|
|
})
|
|
}, 1000)
|
|
}
|
|
|
|
function handleDesiredRoom () {
|
|
if (state.desiredRoom) {
|
|
for (const fl of agendaStore.floors) {
|
|
const rm = find(fl.rooms, ['slug', state.desiredRoom])
|
|
if (rm) {
|
|
state.currentFloor = fl.id
|
|
state.currentRoom = rm.id
|
|
break
|
|
}
|
|
}
|
|
state.desiredRoom = null
|
|
}
|
|
}
|
|
|
|
// -> Go to current meeting if not provided
|
|
function handleCurrentMeetingRedirect () {
|
|
if (!route.params.meetingNumber && agendaStore.meeting.number) {
|
|
router.replace({ params: { meetingNumber: agendaStore.meeting.number } })
|
|
}
|
|
}
|
|
|
|
// --------------------------------------------------------------------
|
|
// Handle browser resize
|
|
// --------------------------------------------------------------------
|
|
|
|
const resizeObserver = new ResizeObserver(entries => {
|
|
agendaStore.$patch({ viewport: Math.round(window.innerWidth) })
|
|
})
|
|
|
|
onMounted(() => {
|
|
resizeObserver.observe(planImage.value)
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
resizeObserver.unobserve(planImage.value)
|
|
})
|
|
|
|
// MOUNTED
|
|
|
|
onMounted(() => {
|
|
agendaStore.fetch(route.params.meetingNumber)
|
|
|
|
handleCurrentMeetingRedirect()
|
|
|
|
// -> Hide Loading Screen
|
|
if (agendaStore.isLoaded) {
|
|
agendaStore.hideLoadingScreen()
|
|
}
|
|
|
|
// -> Set Current Floor
|
|
if (agendaStore.floors?.length > 0) {
|
|
state.currentFloor = agendaStore.floors[0].id
|
|
}
|
|
|
|
// -> Go to requested room
|
|
if (route.query.room) {
|
|
state.desiredRoom = route.query.room
|
|
handleDesiredRoom()
|
|
}
|
|
})
|
|
|
|
</script>
|
|
|
|
<style lang="scss">
|
|
@import "bootstrap/scss/functions";
|
|
@import "bootstrap/scss/variables";
|
|
|
|
.floorplan {
|
|
min-height: 500px;
|
|
font-weight: 460;
|
|
|
|
nav.floorplan-floors {
|
|
padding: 5px;
|
|
background-color: #FFF;
|
|
border: 1px solid $gray-300;
|
|
border-radius: 5px;
|
|
font-weight: 500;
|
|
|
|
a {
|
|
cursor: pointer;
|
|
|
|
&:not(.active):hover {
|
|
background-color: rgba($blue-100, .25);
|
|
}
|
|
}
|
|
}
|
|
|
|
&-rooms {
|
|
width: 350px;
|
|
font-size: .9rem;
|
|
font-weight: 500;
|
|
|
|
@media screen and (max-width: 768px) {
|
|
width: 100%;
|
|
}
|
|
|
|
a {
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
|
|
.badge {
|
|
background-color: $blue-500;
|
|
color: #FFF;
|
|
}
|
|
|
|
span {
|
|
display: block;
|
|
}
|
|
|
|
strong {
|
|
font-weight: 600;
|
|
}
|
|
|
|
small {
|
|
display: block;
|
|
}
|
|
|
|
&.active {
|
|
.badge {
|
|
background-color: $blue-100;
|
|
color: $blue-500;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
&-plan {
|
|
position: relative;
|
|
|
|
img {
|
|
width: 100%;
|
|
animation: planInAnim 1s ease-out;
|
|
}
|
|
|
|
&-pin {
|
|
position: absolute;
|
|
top: 100px;
|
|
left: 100px;
|
|
width: 20px;
|
|
height: 20px;
|
|
color: $red-500;
|
|
font-size: 50px;
|
|
animation: pinDropAnim .6s ease-out;
|
|
|
|
> .bi {
|
|
animation: pinColorAnim 1.2s ease infinite;
|
|
text-shadow: 0 5px 10px #000;
|
|
}
|
|
|
|
@media screen and (max-width: 992px) {
|
|
font-size: 40px;
|
|
margin-left: 5px;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@keyframes planInAnim {
|
|
0% {
|
|
opacity: 0;
|
|
transform: scale(.9, .9);
|
|
}
|
|
100% {
|
|
opacity: 1;
|
|
transform: scale(1, 1);
|
|
}
|
|
}
|
|
|
|
@keyframes pinDropAnim {
|
|
0% {
|
|
opacity: 0;
|
|
transform: translateY(-200px);
|
|
}
|
|
60% {
|
|
opacity: 1;
|
|
transform: translateY(10px);
|
|
}
|
|
80% {
|
|
transform: translateY(-5px);
|
|
}
|
|
100% {
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
@keyframes pinColorAnim {
|
|
0% {
|
|
color: $red-500;
|
|
}
|
|
33% {
|
|
color: $yellow-500;
|
|
}
|
|
66% {
|
|
color: $blue-500;
|
|
}
|
|
100% {
|
|
color: $red-500;
|
|
}
|
|
}
|
|
</style>
|