@citizenplane/pimp 18.10.3 → 18.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/CpSeatMap.vue.d.ts +29 -0
- package/dist/components/CpSeatMap.vue.d.ts.map +1 -0
- package/dist/components/CpSeatMapEmergencyExitDialog.vue.d.ts +21 -0
- package/dist/components/CpSeatMapEmergencyExitDialog.vue.d.ts.map +1 -0
- package/dist/components/CpSeatMapGrid.vue.d.ts +17 -0
- package/dist/components/CpSeatMapGrid.vue.d.ts.map +1 -0
- package/dist/components/CpSeatMapPlane.vue.d.ts +24 -0
- package/dist/components/CpSeatMapPlane.vue.d.ts.map +1 -0
- package/dist/components/CpSeatMapRow.vue.d.ts +22 -0
- package/dist/components/CpSeatMapRow.vue.d.ts.map +1 -0
- package/dist/components/CpSeatMapRowFacility.vue.d.ts +16 -0
- package/dist/components/CpSeatMapRowFacility.vue.d.ts.map +1 -0
- package/dist/components/CpSeatMapSeat.vue.d.ts +21 -0
- package/dist/components/CpSeatMapSeat.vue.d.ts.map +1 -0
- package/dist/components/CpSeatMapSeatWrapper.vue.d.ts +23 -0
- package/dist/components/CpSeatMapSeatWrapper.vue.d.ts.map +1 -0
- package/dist/components/CpSeatMapZone.vue.d.ts +16 -0
- package/dist/components/CpSeatMapZone.vue.d.ts.map +1 -0
- package/dist/components/icons/IconPlaneNose.vue.d.ts +3 -0
- package/dist/components/icons/IconPlaneNose.vue.d.ts.map +1 -0
- package/dist/components/icons/IconPlaneWing.vue.d.ts +3 -0
- package/dist/components/icons/IconPlaneWing.vue.d.ts.map +1 -0
- package/dist/components/icons/IconSparkle.vue.d.ts +3 -0
- package/dist/components/icons/IconSparkle.vue.d.ts.map +1 -0
- package/dist/components/index.d.ts +14 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/constants/seatMap/CpSeatMapConfig.d.ts +22 -0
- package/dist/constants/seatMap/CpSeatMapConfig.d.ts.map +1 -0
- package/dist/constants/seatMap/CpSeatMapSeatSelection.d.ts +12 -0
- package/dist/constants/seatMap/CpSeatMapSeatSelection.d.ts.map +1 -0
- package/dist/constants/seatMap/CpSeatMapTransitionDirections.d.ts +5 -0
- package/dist/constants/seatMap/CpSeatMapTransitionDirections.d.ts.map +1 -0
- package/dist/constants/seatMap/IataTravelerTypes.d.ts +7 -0
- package/dist/constants/seatMap/IataTravelerTypes.d.ts.map +1 -0
- package/dist/constants/seatMap/Seat.d.ts +18 -0
- package/dist/constants/seatMap/Seat.d.ts.map +1 -0
- package/dist/constants/seatMap/SeatAncillary.d.ts +9 -0
- package/dist/constants/seatMap/SeatAncillary.d.ts.map +1 -0
- package/dist/constants/seatMap/SeatCharacteristic.d.ts +7 -0
- package/dist/constants/seatMap/SeatCharacteristic.d.ts.map +1 -0
- package/dist/constants/seatMap/SeatCharacteristicCodes.d.ts +10 -0
- package/dist/constants/seatMap/SeatCharacteristicCodes.d.ts.map +1 -0
- package/dist/constants/seatMap/SeatMap.d.ts +10 -0
- package/dist/constants/seatMap/SeatMap.d.ts.map +1 -0
- package/dist/constants/seatMap/SeatMapZone.d.ts +6 -0
- package/dist/constants/seatMap/SeatMapZone.d.ts.map +1 -0
- package/dist/constants/seatMap/SeatRow.d.ts +15 -0
- package/dist/constants/seatMap/SeatRow.d.ts.map +1 -0
- package/dist/constants/seatMap/SeatRowFacilities.d.ts +10 -0
- package/dist/constants/seatMap/SeatRowFacilities.d.ts.map +1 -0
- package/dist/constants/seatMap/SeatRowFacilityFlags.d.ts +8 -0
- package/dist/constants/seatMap/SeatRowFacilityFlags.d.ts.map +1 -0
- package/dist/constants/seatMap/SeatStatuses.d.ts +8 -0
- package/dist/constants/seatMap/SeatStatuses.d.ts.map +1 -0
- package/dist/constants/seatMap/index.d.ts +16 -0
- package/dist/constants/seatMap/index.d.ts.map +1 -0
- package/dist/helpers/seatMap.d.ts +28 -0
- package/dist/helpers/seatMap.d.ts.map +1 -0
- package/dist/pimp.es.js +4263 -3756
- package/dist/pimp.umd.js +40 -40
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/CpSeatMap.vue +226 -0
- package/src/components/CpSeatMapEmergencyExitDialog.vue +87 -0
- package/src/components/CpSeatMapGrid.vue +41 -0
- package/src/components/CpSeatMapPlane.vue +221 -0
- package/src/components/CpSeatMapRow.vue +183 -0
- package/src/components/CpSeatMapRowFacility.vue +88 -0
- package/src/components/CpSeatMapSeat.vue +138 -0
- package/src/components/CpSeatMapSeatWrapper.vue +189 -0
- package/src/components/CpSeatMapZone.vue +60 -0
- package/src/components/icons/IconPlaneNose.vue +39 -0
- package/src/components/icons/IconPlaneWing.vue +11 -0
- package/src/components/icons/IconSparkle.vue +8 -0
- package/src/components/index.ts +51 -0
- package/src/constants/seatMap/CpSeatMapConfig.ts +47 -0
- package/src/constants/seatMap/CpSeatMapSeatSelection.ts +12 -0
- package/src/constants/seatMap/CpSeatMapTransitionDirections.ts +4 -0
- package/src/constants/seatMap/IataTravelerTypes.ts +6 -0
- package/src/constants/seatMap/Seat.ts +18 -0
- package/src/constants/seatMap/SeatAncillary.ts +8 -0
- package/src/constants/seatMap/SeatCharacteristic.ts +7 -0
- package/src/constants/seatMap/SeatCharacteristicCodes.ts +9 -0
- package/src/constants/seatMap/SeatMap.ts +10 -0
- package/src/constants/seatMap/SeatMapZone.ts +5 -0
- package/src/constants/seatMap/SeatRow.ts +15 -0
- package/src/constants/seatMap/SeatRowFacilities.ts +10 -0
- package/src/constants/seatMap/SeatRowFacilityFlags.ts +7 -0
- package/src/constants/seatMap/SeatStatuses.ts +7 -0
- package/src/constants/seatMap/index.ts +16 -0
- package/src/helpers/seatMap.ts +76 -0
- package/src/stories/CpSeatMap.stories.ts +357 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="cpSeatMapRow">
|
|
3
|
+
<template v-if="hasExit">
|
|
4
|
+
<div
|
|
5
|
+
v-for="exit in activeExits"
|
|
6
|
+
:key="exit.direction"
|
|
7
|
+
class="cpSeatMapRow__exit"
|
|
8
|
+
:class="getExitDynamicClass(exit.direction)"
|
|
9
|
+
>
|
|
10
|
+
exit
|
|
11
|
+
</div>
|
|
12
|
+
</template>
|
|
13
|
+
<template v-if="hasTopFacilities">
|
|
14
|
+
<cp-seat-map-row-facility
|
|
15
|
+
v-for="(facility, index) in formattedTopFacilities"
|
|
16
|
+
:key="index"
|
|
17
|
+
:columns-count="columnsCount"
|
|
18
|
+
:config="config"
|
|
19
|
+
is-top
|
|
20
|
+
:position="facility.position"
|
|
21
|
+
:seat-block-size="seatBlockSize"
|
|
22
|
+
:type="facility.type"
|
|
23
|
+
/>
|
|
24
|
+
</template>
|
|
25
|
+
<slot />
|
|
26
|
+
<template v-if="hasBottomFacilities">
|
|
27
|
+
<cp-seat-map-row-facility
|
|
28
|
+
v-for="(facility, index) in formattedBottomFacilities"
|
|
29
|
+
:key="index"
|
|
30
|
+
:columns-count="columnsCount"
|
|
31
|
+
:config="config"
|
|
32
|
+
:position="facility.position"
|
|
33
|
+
:seat-block-size="seatBlockSize"
|
|
34
|
+
:type="facility.type"
|
|
35
|
+
/>
|
|
36
|
+
</template>
|
|
37
|
+
</div>
|
|
38
|
+
</template>
|
|
39
|
+
|
|
40
|
+
<script setup lang="ts">
|
|
41
|
+
import { computed } from 'vue'
|
|
42
|
+
|
|
43
|
+
import type { CpSeatMapConfig } from '@/constants/seatMap/CpSeatMapConfig'
|
|
44
|
+
import type { SeatRow } from '@/constants/seatMap/SeatRow'
|
|
45
|
+
import type { SeatRowFacilityFlags } from '@/constants/seatMap/SeatRowFacilityFlags'
|
|
46
|
+
|
|
47
|
+
import { DEFAULT_CP_SEAT_MAP_CONFIG } from '@/constants/seatMap/CpSeatMapConfig'
|
|
48
|
+
import { SeatRowFacilities, SeatRowFacilityPosition } from '@/constants/seatMap/SeatRowFacilities'
|
|
49
|
+
import { capitalizeFirstLetter } from '@/helpers'
|
|
50
|
+
import CpSeatMapRowFacility from './CpSeatMapRowFacility.vue'
|
|
51
|
+
|
|
52
|
+
interface Props {
|
|
53
|
+
config?: CpSeatMapConfig
|
|
54
|
+
row: SeatRow
|
|
55
|
+
seatBlockSize: number
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
59
|
+
config: () => DEFAULT_CP_SEAT_MAP_CONFIG,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
enum ExitDirections {
|
|
63
|
+
FRONT = 'front',
|
|
64
|
+
BACK = 'back',
|
|
65
|
+
LEFT = 'left',
|
|
66
|
+
RIGHT = 'right',
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface Exit {
|
|
70
|
+
active: boolean
|
|
71
|
+
direction: ExitDirections
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface FormattedFacility {
|
|
75
|
+
position: SeatRowFacilityPosition
|
|
76
|
+
type: SeatRowFacilities
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const allExits = computed<Exit[]>(() => [
|
|
80
|
+
{ active: props.row.has_exit_back, direction: ExitDirections.BACK },
|
|
81
|
+
{ active: props.row.has_exit_front, direction: ExitDirections.FRONT },
|
|
82
|
+
{ active: props.row.has_exit_left, direction: ExitDirections.LEFT },
|
|
83
|
+
{ active: props.row.has_exit_right, direction: ExitDirections.RIGHT },
|
|
84
|
+
])
|
|
85
|
+
|
|
86
|
+
const activeExits = computed(() => allExits.value.filter(({ active }) => active))
|
|
87
|
+
|
|
88
|
+
const columnsCount = computed(() => Object.keys(props.row.seats).length + 1) // +1 for the hallway
|
|
89
|
+
|
|
90
|
+
const hasExit = computed(() => !!allExits.value.length)
|
|
91
|
+
|
|
92
|
+
const getFacilityType = (facility: SeatRowFacilityFlags, position: SeatRowFacilityPosition) => {
|
|
93
|
+
if (position === SeatRowFacilityPosition.LEFT) {
|
|
94
|
+
if (facility.lavatory_left) return SeatRowFacilities.LAVATORY
|
|
95
|
+
if (facility.galley_left) return SeatRowFacilities.GALLEY
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (position === SeatRowFacilityPosition.RIGHT) {
|
|
99
|
+
if (facility.lavatory_right) return SeatRowFacilities.LAVATORY
|
|
100
|
+
if (facility.galley_right) return SeatRowFacilities.GALLEY
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return null
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const getFormattedFacilities = (facility: SeatRowFacilityFlags): FormattedFacility[] => {
|
|
107
|
+
const positions = [
|
|
108
|
+
{ position: SeatRowFacilityPosition.LEFT, hasFacility: facility.lavatory_left || facility.galley_left },
|
|
109
|
+
{ position: SeatRowFacilityPosition.RIGHT, hasFacility: facility.lavatory_right || facility.galley_right },
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
return positions.reduce<FormattedFacility[]>((facilities, { position, hasFacility }) => {
|
|
113
|
+
if (!hasFacility) return facilities
|
|
114
|
+
|
|
115
|
+
const type = getFacilityType(facility, position)
|
|
116
|
+
if (!type) return facilities
|
|
117
|
+
|
|
118
|
+
return [...facilities, { position, type }]
|
|
119
|
+
}, [])
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const formattedTopFacilities = computed<FormattedFacility[]>(() => {
|
|
123
|
+
if (!props.row.top) return []
|
|
124
|
+
return getFormattedFacilities(props.row.top)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
const formattedBottomFacilities = computed<FormattedFacility[]>(() => {
|
|
128
|
+
if (!props.row.bottom) return []
|
|
129
|
+
return getFormattedFacilities(props.row.bottom)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
const hasTopFacilities = computed(() => !!formattedTopFacilities.value.length)
|
|
133
|
+
const hasBottomFacilities = computed(() => !!formattedBottomFacilities.value.length)
|
|
134
|
+
|
|
135
|
+
const getExitDynamicClass = (direction: ExitDirections) => [
|
|
136
|
+
`cpSeatMapRow__exit--is${capitalizeFirstLetter(direction)}`,
|
|
137
|
+
]
|
|
138
|
+
</script>
|
|
139
|
+
|
|
140
|
+
<style lang="scss">
|
|
141
|
+
.cpSeatMapRow {
|
|
142
|
+
position: relative;
|
|
143
|
+
display: grid;
|
|
144
|
+
gap: var(--cp-spacing-md);
|
|
145
|
+
place-items: center center;
|
|
146
|
+
|
|
147
|
+
&__exit {
|
|
148
|
+
position: absolute;
|
|
149
|
+
z-index: 1;
|
|
150
|
+
color: var(--cp-text-error-primary);
|
|
151
|
+
font-size: fn.px-to-em(12);
|
|
152
|
+
font-weight: 700;
|
|
153
|
+
letter-spacing: fn.px-to-rem(2);
|
|
154
|
+
line-height: fn.px-to-rem(24);
|
|
155
|
+
place-self: start;
|
|
156
|
+
text-transform: uppercase;
|
|
157
|
+
|
|
158
|
+
&--isLeft,
|
|
159
|
+
&--isFront,
|
|
160
|
+
&--isBack {
|
|
161
|
+
left: fn.px-to-rem(-80);
|
|
162
|
+
text-orientation: sideways;
|
|
163
|
+
writing-mode: sideways-lr;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
&--isRight {
|
|
167
|
+
right: fn.px-to-rem(-80);
|
|
168
|
+
text-orientation: sideways;
|
|
169
|
+
writing-mode: sideways-rl;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
@include mx.media-query-max(551px) {
|
|
175
|
+
.cpSeatMapRow {
|
|
176
|
+
gap: var(--cp-spacing-sm);
|
|
177
|
+
|
|
178
|
+
&__exit {
|
|
179
|
+
display: none;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
</style>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
v-tooltip="facilityTooltip"
|
|
4
|
+
class="cpSeatMapRowFacility"
|
|
5
|
+
:class="facilityDynamicClass"
|
|
6
|
+
:style="facilityDynamicStyle"
|
|
7
|
+
>
|
|
8
|
+
<cp-icon class="cpSeatMapRowFacility__icon" size="16" :type="facilityIcon" />
|
|
9
|
+
</div>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<script setup lang="ts">
|
|
13
|
+
import { vTooltip } from 'floating-vue'
|
|
14
|
+
import { computed } from 'vue'
|
|
15
|
+
|
|
16
|
+
import type { CpSeatMapConfig } from '@/constants/seatMap/CpSeatMapConfig'
|
|
17
|
+
|
|
18
|
+
import { DEFAULT_CP_SEAT_MAP_CONFIG } from '@/constants/seatMap/CpSeatMapConfig'
|
|
19
|
+
import { SeatRowFacilities, SeatRowFacilityPosition } from '@/constants/seatMap/SeatRowFacilities'
|
|
20
|
+
import CpIcon from './CpIcon.vue'
|
|
21
|
+
|
|
22
|
+
interface Props {
|
|
23
|
+
columnsCount: number
|
|
24
|
+
config?: CpSeatMapConfig
|
|
25
|
+
isTop?: boolean
|
|
26
|
+
position: SeatRowFacilityPosition
|
|
27
|
+
seatBlockSize: number
|
|
28
|
+
type: SeatRowFacilities
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
32
|
+
config: () => DEFAULT_CP_SEAT_MAP_CONFIG,
|
|
33
|
+
isTop: false,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const isLavatory = computed(() => props.type === SeatRowFacilities.LAVATORY)
|
|
37
|
+
const isGalley = computed(() => props.type === SeatRowFacilities.GALLEY)
|
|
38
|
+
|
|
39
|
+
const facilityIcon = computed(() => {
|
|
40
|
+
if (isGalley.value) return 'coffee'
|
|
41
|
+
return 'handshake'
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const isLeftFacility = computed(() => props.position === SeatRowFacilityPosition.LEFT)
|
|
45
|
+
const isRightFacility = computed(() => props.position === SeatRowFacilityPosition.RIGHT)
|
|
46
|
+
|
|
47
|
+
const facilityDynamicStyle = computed(() => {
|
|
48
|
+
const columnStart = isLeftFacility.value ? 1 : props.seatBlockSize + 1
|
|
49
|
+
const columnEnd = isRightFacility.value ? props.columnsCount + 1 : props.seatBlockSize
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
gridColumn: `${columnStart} / ${columnEnd}`,
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const facilityDynamicClass = computed(() => {
|
|
57
|
+
return {
|
|
58
|
+
'cpSeatMapRowFacility--isTop': props.isTop,
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const facilityTooltip = computed(() => {
|
|
63
|
+
if (isLavatory.value) return props.config.facility.lavatory
|
|
64
|
+
if (isGalley.value) return props.config.facility.galley
|
|
65
|
+
|
|
66
|
+
return ''
|
|
67
|
+
})
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<style lang="scss">
|
|
71
|
+
.cpSeatMapRowFacility {
|
|
72
|
+
display: flex;
|
|
73
|
+
width: 100%;
|
|
74
|
+
align-items: center;
|
|
75
|
+
justify-content: center;
|
|
76
|
+
padding: var(--cp-spacing-xl);
|
|
77
|
+
border-radius: var(--cp-spacing-sm);
|
|
78
|
+
background-color: var(--cp-background-accent-secondary);
|
|
79
|
+
|
|
80
|
+
&:is(.v-popper--has-tooltip) {
|
|
81
|
+
cursor: help;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
&__icon {
|
|
85
|
+
color: var(--cp-foreground-accent-secondary);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
</style>
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="cpSeatMapSeat" :class="seatDynamicClass">
|
|
3
|
+
<transition name="fade">
|
|
4
|
+
<cp-icon v-if="isUnavailable" size="16" type="x" />
|
|
5
|
+
<div v-else>
|
|
6
|
+
<icon-sparkle v-if="isFree" />
|
|
7
|
+
<span v-else-if="isOccupiedByCurrentPax">
|
|
8
|
+
<slot />
|
|
9
|
+
</span>
|
|
10
|
+
<span v-else-if="isPayable">{{ seatAmount }}</span>
|
|
11
|
+
</div>
|
|
12
|
+
</transition>
|
|
13
|
+
</div>
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<script setup lang="ts">
|
|
17
|
+
import { computed } from 'vue'
|
|
18
|
+
|
|
19
|
+
import type { SeatAncillary } from '@/constants/seatMap/SeatAncillary'
|
|
20
|
+
|
|
21
|
+
import { SeatStatuses } from '@/constants/seatMap/SeatStatuses'
|
|
22
|
+
import CpIcon from './CpIcon.vue'
|
|
23
|
+
import IconSparkle from './icons/IconSparkle.vue'
|
|
24
|
+
|
|
25
|
+
interface Props {
|
|
26
|
+
seat: {
|
|
27
|
+
selected_ancillary?: SeatAncillary
|
|
28
|
+
status: SeatStatuses
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const props = defineProps<Props>()
|
|
33
|
+
|
|
34
|
+
const seatStatus = computed(() => props.seat.status)
|
|
35
|
+
|
|
36
|
+
const isOccupied = computed(() => [SeatStatuses.OCCUPIED, SeatStatuses.BLOCKED].includes(seatStatus.value))
|
|
37
|
+
const isOccupiedByCurrentPax = computed(() => seatStatus.value === SeatStatuses.OCCUPIED_BY_CURRENT_PAX)
|
|
38
|
+
const isUnavailable = computed(() => isOccupied.value)
|
|
39
|
+
const isPayable = computed(() => seatStatus.value === SeatStatuses.PAYABLE)
|
|
40
|
+
const isFree = computed(() => seatStatus.value === SeatStatuses.FREE)
|
|
41
|
+
|
|
42
|
+
const seatDynamicClass = computed(() => {
|
|
43
|
+
return {
|
|
44
|
+
'cpSeatMapSeat--isOccupied': isOccupied.value,
|
|
45
|
+
'cpSeatMapSeat--isOccupiedByCurrentPax': isOccupiedByCurrentPax.value,
|
|
46
|
+
'cpSeatMapSeat--isUnavailable': isUnavailable.value,
|
|
47
|
+
'cpSeatMapSeat--isPayable': isPayable.value,
|
|
48
|
+
'cpSeatMapSeat--isFree': isFree.value,
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
// The API already ships a localised, currency-aware string, so no formatting here.
|
|
53
|
+
const seatAmount = computed(() => {
|
|
54
|
+
if (!isPayable.value) return ''
|
|
55
|
+
return props.seat.selected_ancillary?.formatted_amount || '€'
|
|
56
|
+
})
|
|
57
|
+
</script>
|
|
58
|
+
|
|
59
|
+
<style lang="scss">
|
|
60
|
+
.cpSeatMapSeat {
|
|
61
|
+
position: relative;
|
|
62
|
+
display: flex;
|
|
63
|
+
width: var(--seat-width);
|
|
64
|
+
height: var(--seat-width);
|
|
65
|
+
align-items: center;
|
|
66
|
+
justify-content: center;
|
|
67
|
+
border-radius: fn.px-to-rem(4);
|
|
68
|
+
aspect-ratio: 1 / 1;
|
|
69
|
+
font-size: fn.px-to-em(10);
|
|
70
|
+
font-weight: 700;
|
|
71
|
+
line-height: fn.px-to-rem(24);
|
|
72
|
+
text-transform: uppercase;
|
|
73
|
+
transition:
|
|
74
|
+
background-color 200ms ease,
|
|
75
|
+
color 200ms ease;
|
|
76
|
+
|
|
77
|
+
&--isFree {
|
|
78
|
+
border: fn.px-to-rem(1) solid var(--cp-border-strong);
|
|
79
|
+
border-bottom-left-radius: 0;
|
|
80
|
+
border-bottom-right-radius: 0;
|
|
81
|
+
color: var(--cp-border-strong);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
&--isFree::after {
|
|
85
|
+
position: absolute;
|
|
86
|
+
bottom: 0;
|
|
87
|
+
left: 0;
|
|
88
|
+
width: 100%;
|
|
89
|
+
height: fn.px-to-rem(8);
|
|
90
|
+
border: fn.px-to-rem(1) solid var(--cp-border-strong);
|
|
91
|
+
border-radius: fn.px-to-rem(6) fn.px-to-rem(6) 0 0;
|
|
92
|
+
border-bottom: none;
|
|
93
|
+
content: '';
|
|
94
|
+
pointer-events: none;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
&--isOccupiedByCurrentPax {
|
|
98
|
+
background-color: var(--cp-background-accent-solid);
|
|
99
|
+
color: var(--cp-text-white);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
&--isUnavailable {
|
|
103
|
+
background-color: var(--cp-utility-neutral-100);
|
|
104
|
+
|
|
105
|
+
i {
|
|
106
|
+
color: var(--cp-foreground-disabled);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
&--isPayable {
|
|
111
|
+
border: fn.px-to-rem(1) solid var(--cp-border-warning-primary);
|
|
112
|
+
color: var(--cp-text-warning-primary);
|
|
113
|
+
font-weight: 500;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
&--isPayable:is(&--isOccupiedByCurrentPax) {
|
|
117
|
+
background-color: var(--cp-background-warning-solid);
|
|
118
|
+
color: var(--cp-text-white);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
&--isPayable::after {
|
|
122
|
+
position: absolute;
|
|
123
|
+
right: var(--cp-spacing-xs);
|
|
124
|
+
bottom: var(--cp-spacing-xs);
|
|
125
|
+
border-radius: var(--cp-radius-xs);
|
|
126
|
+
border-bottom: fn.px-to-rem(10) solid var(--cp-border-warning-solid);
|
|
127
|
+
border-left: fn.px-to-rem(10) solid transparent;
|
|
128
|
+
content: '';
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@include mx.media-query-max(551px) {
|
|
133
|
+
.cpSeatMapSeat {
|
|
134
|
+
width: var(--seat-mobile-width);
|
|
135
|
+
height: var(--seat-mobile-width);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
</style>
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<button
|
|
3
|
+
:id="seatId"
|
|
4
|
+
ref="seatWrapper"
|
|
5
|
+
v-tooltip="dynamicSeatTooltip"
|
|
6
|
+
class="cpSeatMapSeatWrapper"
|
|
7
|
+
:class="seatWrapperDynamicClass"
|
|
8
|
+
:disabled="isSeatDisabled"
|
|
9
|
+
type="button"
|
|
10
|
+
@click="handleSelectSeat"
|
|
11
|
+
>
|
|
12
|
+
<cp-seat-map-seat class="cpSeatMapSeatWrapper__seat" :seat="seatProperties">
|
|
13
|
+
{{ seatValue }}
|
|
14
|
+
<cp-badge
|
|
15
|
+
v-if="hasLinkedTraveler"
|
|
16
|
+
class="cpSeatMapSeatWrapper__additionalTraveler"
|
|
17
|
+
color="accent"
|
|
18
|
+
:leading-icon="linkedTravelerIcon"
|
|
19
|
+
size="xs"
|
|
20
|
+
variant="outline"
|
|
21
|
+
/>
|
|
22
|
+
</cp-seat-map-seat>
|
|
23
|
+
</button>
|
|
24
|
+
</template>
|
|
25
|
+
|
|
26
|
+
<script setup lang="ts">
|
|
27
|
+
import { vTooltip } from 'floating-vue'
|
|
28
|
+
import { computed, useTemplateRef, watch } from 'vue'
|
|
29
|
+
|
|
30
|
+
import type { CpSeatMapConfig } from '@/constants/seatMap/CpSeatMapConfig'
|
|
31
|
+
import type { CpSeatMapSeatSelection } from '@/constants/seatMap/CpSeatMapSeatSelection'
|
|
32
|
+
import type { Seat } from '@/constants/seatMap/Seat'
|
|
33
|
+
|
|
34
|
+
import { DEFAULT_CP_SEAT_MAP_CONFIG } from '@/constants/seatMap/CpSeatMapConfig'
|
|
35
|
+
import { SeatStatuses } from '@/constants/seatMap/SeatStatuses'
|
|
36
|
+
import { displaySeat, getTravelerIcon } from '@/helpers/seatMap'
|
|
37
|
+
import CpBadge from './CpBadge.vue'
|
|
38
|
+
import CpSeatMapSeat from './CpSeatMapSeat.vue'
|
|
39
|
+
|
|
40
|
+
interface Props {
|
|
41
|
+
config?: CpSeatMapConfig
|
|
42
|
+
isHighlighted?: boolean
|
|
43
|
+
isSelectionEnabled?: boolean
|
|
44
|
+
rowNumber: number
|
|
45
|
+
seat: Seat
|
|
46
|
+
selection?: CpSeatMapSeatSelection | null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
50
|
+
config: () => DEFAULT_CP_SEAT_MAP_CONFIG,
|
|
51
|
+
isHighlighted: false,
|
|
52
|
+
isSelectionEnabled: true,
|
|
53
|
+
selection: null,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const emit = defineEmits<{ select: [seat: Seat, rowNumber: number] }>()
|
|
57
|
+
|
|
58
|
+
const HIGHLIGHT_DURATION = 500
|
|
59
|
+
|
|
60
|
+
const seatWrapperRef = useTemplateRef<HTMLButtonElement>('seatWrapper')
|
|
61
|
+
|
|
62
|
+
const seatId = computed(() => `seat-${displaySeat({ row: props.rowNumber, column: props.seat.column })}`)
|
|
63
|
+
|
|
64
|
+
const isSeatSelected = computed(() => props.selection !== null)
|
|
65
|
+
|
|
66
|
+
const isSeatOccupied = computed(() => props.seat.status === SeatStatuses.OCCUPIED)
|
|
67
|
+
const isSeatBlocked = computed(() => props.seat.status === SeatStatuses.BLOCKED)
|
|
68
|
+
const isSeatUnavailable = computed(() => isSeatBlocked.value || isSeatOccupied.value)
|
|
69
|
+
const isSeatPayable = computed(() => props.seat.status === SeatStatuses.PAYABLE)
|
|
70
|
+
|
|
71
|
+
const highlightSeat = () => {
|
|
72
|
+
seatWrapperRef.value?.classList.add('cpSeatMapSeatWrapper--isHighlighted')
|
|
73
|
+
|
|
74
|
+
setTimeout(() => {
|
|
75
|
+
seatWrapperRef.value?.classList.remove('cpSeatMapSeatWrapper--isHighlighted')
|
|
76
|
+
}, HIGHLIGHT_DURATION)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
watch(
|
|
80
|
+
() => props.isHighlighted,
|
|
81
|
+
(isHighlighted) => {
|
|
82
|
+
if (isHighlighted) highlightSeat()
|
|
83
|
+
},
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
const seatWrapperDynamicClass = computed(() => {
|
|
87
|
+
return {
|
|
88
|
+
'cpSeatMapSeatWrapper--isPayable': isSeatPayable.value,
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const isSeatDisabled = computed(() => {
|
|
93
|
+
if (!props.isSelectionEnabled) return true
|
|
94
|
+
return isSeatUnavailable.value
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
const dynamicSeatStatus = computed(() => {
|
|
98
|
+
if (!isSeatSelected.value) {
|
|
99
|
+
if (isSeatUnavailable.value || isSeatPayable.value) return props.seat.status
|
|
100
|
+
return SeatStatuses.FREE
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return SeatStatuses.OCCUPIED_BY_CURRENT_PAX
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
const seatProperties = computed(() => ({
|
|
107
|
+
...props.seat,
|
|
108
|
+
status: dynamicSeatStatus.value,
|
|
109
|
+
}))
|
|
110
|
+
|
|
111
|
+
const hasLinkedTraveler = computed(() => !!props.selection?.hasLinkedTraveler)
|
|
112
|
+
|
|
113
|
+
const linkedTravelerIcon = computed(() => {
|
|
114
|
+
if (!hasLinkedTraveler.value) return ''
|
|
115
|
+
return getTravelerIcon(props.selection?.linkedTravelerType) || 'infant'
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
const dynamicSeatTooltip = computed(() => {
|
|
119
|
+
const { seatStatuses } = props.config
|
|
120
|
+
|
|
121
|
+
if (isSeatSelected.value) {
|
|
122
|
+
if (hasLinkedTraveler.value) return seatStatuses.selectedForLinkedTraveler
|
|
123
|
+
|
|
124
|
+
return seatStatuses.selected
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!props.isSelectionEnabled) return seatStatuses.selectTravelerFirst
|
|
128
|
+
|
|
129
|
+
const seatLabel = displaySeat({ row: props.rowNumber, column: props.seat.column })
|
|
130
|
+
|
|
131
|
+
return `${seatLabel} • ${seatStatuses[props.seat.status]}`
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
const seatValue = computed(() => props.selection?.initials || '')
|
|
135
|
+
|
|
136
|
+
const handleSelectSeat = () => {
|
|
137
|
+
if (isSeatDisabled.value) return
|
|
138
|
+
emit('select', props.seat, props.rowNumber)
|
|
139
|
+
}
|
|
140
|
+
</script>
|
|
141
|
+
|
|
142
|
+
<style lang="scss">
|
|
143
|
+
.cpSeatMapSeatWrapper {
|
|
144
|
+
position: relative;
|
|
145
|
+
display: flex;
|
|
146
|
+
align-items: center;
|
|
147
|
+
justify-content: center;
|
|
148
|
+
transition: transform 200ms ease;
|
|
149
|
+
|
|
150
|
+
&:focus-visible {
|
|
151
|
+
@extend %u-focus-outline;
|
|
152
|
+
|
|
153
|
+
border-radius: var(--cp-radius-sm);
|
|
154
|
+
outline-offset: fn.px-to-rem(1);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
&--isPayable:focus-visible {
|
|
158
|
+
outline-color: var(--cp-border-warning-primary);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
&:disabled {
|
|
162
|
+
cursor: not-allowed;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
&--isHighlighted {
|
|
166
|
+
transform: scale(1.15);
|
|
167
|
+
|
|
168
|
+
> * {
|
|
169
|
+
outline: fn.px-to-rem(1) solid var(--cp-background-accent-solid-hover);
|
|
170
|
+
outline-offset: fn.px-to-rem(1);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
&__additionalTraveler {
|
|
175
|
+
position: absolute;
|
|
176
|
+
|
|
177
|
+
// stylelint-disable-next-line custom-property-pattern
|
|
178
|
+
top: calc(var(--cp-dimensions-1_5) * -1);
|
|
179
|
+
|
|
180
|
+
// stylelint-disable-next-line custom-property-pattern
|
|
181
|
+
right: calc(var(--cp-dimensions-1_5) * -1);
|
|
182
|
+
padding: var(--cp-spacing-xs);
|
|
183
|
+
|
|
184
|
+
.cpBadge__icon {
|
|
185
|
+
width: fn.px-to-rem(12);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
</style>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="cpSeatMapZone">
|
|
3
|
+
<cp-heading class="cpSeatMapZone__title" heading-level="h6">{{ title }}</cp-heading>
|
|
4
|
+
<div class="cpSeatMapZone__layout">
|
|
5
|
+
<slot />
|
|
6
|
+
</div>
|
|
7
|
+
</div>
|
|
8
|
+
</template>
|
|
9
|
+
|
|
10
|
+
<script setup lang="ts">
|
|
11
|
+
import CpHeading from './CpHeading.vue'
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
title: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
defineProps<Props>()
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<style lang="scss">
|
|
21
|
+
.cpSeatMapZone {
|
|
22
|
+
display: flex;
|
|
23
|
+
flex-direction: column;
|
|
24
|
+
gap: var(--cp-spacing-md);
|
|
25
|
+
|
|
26
|
+
&__title {
|
|
27
|
+
display: flex;
|
|
28
|
+
align-items: center;
|
|
29
|
+
justify-content: space-between;
|
|
30
|
+
font-size: fn.px-to-em(12);
|
|
31
|
+
font-weight: 700;
|
|
32
|
+
gap: var(--cp-spacing-xl);
|
|
33
|
+
letter-spacing: fn.px-to-rem(2);
|
|
34
|
+
line-height: fn.px-to-rem(24);
|
|
35
|
+
text-transform: uppercase;
|
|
36
|
+
|
|
37
|
+
&::before,
|
|
38
|
+
&::after {
|
|
39
|
+
display: block;
|
|
40
|
+
width: 100%;
|
|
41
|
+
height: fn.px-to-rem(1);
|
|
42
|
+
flex: 1;
|
|
43
|
+
background-color: var(--cp-border-soft);
|
|
44
|
+
content: '';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
&__layout {
|
|
49
|
+
padding-inline: fn.px-to-rem(20);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@include mx.media-query-max(551px) {
|
|
54
|
+
.cpSeatMapZone {
|
|
55
|
+
&__layout {
|
|
56
|
+
padding-inline: 0;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
</style>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<svg fill="none" viewBox="0 0 504 433" xmlns="http://www.w3.org/2000/svg">
|
|
3
|
+
<g filter="url(#filter0_i_23228_86232)">
|
|
4
|
+
<path
|
|
5
|
+
d="M171.634 43.4901C209.521 -14.4961 294.478 -14.497 332.366 43.4891L462.309 242.364C489.513 283.999 504 332.657 504 382.391L504 432.666H0.00012207L0 382.391C5.19025e-05 332.657 14.4874 283.999 41.6914 242.364L171.634 43.4901Z"
|
|
6
|
+
fill="white"
|
|
7
|
+
/>
|
|
8
|
+
</g>
|
|
9
|
+
<path
|
|
10
|
+
d="M172.053 43.7637C209.743 -13.9203 294.257 -13.9212 331.947 43.7627L461.89 242.638C489.041 284.191 503.5 332.754 503.5 382.392V432.166H0.5V382.392C0.500052 332.754 14.9595 284.191 42.1104 242.638L172.053 43.7637Z"
|
|
11
|
+
stroke="#E9EAF6"
|
|
12
|
+
/>
|
|
13
|
+
<defs>
|
|
14
|
+
<filter
|
|
15
|
+
id="filter0_i_23228_86232"
|
|
16
|
+
color-interpolation-filters="sRGB"
|
|
17
|
+
filterUnits="userSpaceOnUse"
|
|
18
|
+
height="432.666"
|
|
19
|
+
width="504"
|
|
20
|
+
x="0"
|
|
21
|
+
y="0"
|
|
22
|
+
>
|
|
23
|
+
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
|
24
|
+
<feBlend in="SourceGraphic" in2="BackgroundImageFix" mode="normal" result="shape" />
|
|
25
|
+
<feColorMatrix
|
|
26
|
+
in="SourceAlpha"
|
|
27
|
+
result="hardAlpha"
|
|
28
|
+
type="matrix"
|
|
29
|
+
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
|
30
|
+
/>
|
|
31
|
+
<feOffset />
|
|
32
|
+
<feGaussianBlur stdDeviation="20" />
|
|
33
|
+
<feComposite in2="hardAlpha" k2="-1" k3="1" operator="arithmetic" />
|
|
34
|
+
<feColorMatrix type="matrix" values="0 0 0 0 0.917647 0 0 0 0 0.92549 0 0 0 0 1 0 0 0 1 0" />
|
|
35
|
+
<feBlend in2="shape" mode="normal" result="effect1_innerShadow_23228_86232" />
|
|
36
|
+
</filter>
|
|
37
|
+
</defs>
|
|
38
|
+
</svg>
|
|
39
|
+
</template>
|