@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.
Files changed (92) hide show
  1. package/dist/components/CpSeatMap.vue.d.ts +29 -0
  2. package/dist/components/CpSeatMap.vue.d.ts.map +1 -0
  3. package/dist/components/CpSeatMapEmergencyExitDialog.vue.d.ts +21 -0
  4. package/dist/components/CpSeatMapEmergencyExitDialog.vue.d.ts.map +1 -0
  5. package/dist/components/CpSeatMapGrid.vue.d.ts +17 -0
  6. package/dist/components/CpSeatMapGrid.vue.d.ts.map +1 -0
  7. package/dist/components/CpSeatMapPlane.vue.d.ts +24 -0
  8. package/dist/components/CpSeatMapPlane.vue.d.ts.map +1 -0
  9. package/dist/components/CpSeatMapRow.vue.d.ts +22 -0
  10. package/dist/components/CpSeatMapRow.vue.d.ts.map +1 -0
  11. package/dist/components/CpSeatMapRowFacility.vue.d.ts +16 -0
  12. package/dist/components/CpSeatMapRowFacility.vue.d.ts.map +1 -0
  13. package/dist/components/CpSeatMapSeat.vue.d.ts +21 -0
  14. package/dist/components/CpSeatMapSeat.vue.d.ts.map +1 -0
  15. package/dist/components/CpSeatMapSeatWrapper.vue.d.ts +23 -0
  16. package/dist/components/CpSeatMapSeatWrapper.vue.d.ts.map +1 -0
  17. package/dist/components/CpSeatMapZone.vue.d.ts +16 -0
  18. package/dist/components/CpSeatMapZone.vue.d.ts.map +1 -0
  19. package/dist/components/icons/IconPlaneNose.vue.d.ts +3 -0
  20. package/dist/components/icons/IconPlaneNose.vue.d.ts.map +1 -0
  21. package/dist/components/icons/IconPlaneWing.vue.d.ts +3 -0
  22. package/dist/components/icons/IconPlaneWing.vue.d.ts.map +1 -0
  23. package/dist/components/icons/IconSparkle.vue.d.ts +3 -0
  24. package/dist/components/icons/IconSparkle.vue.d.ts.map +1 -0
  25. package/dist/components/index.d.ts +14 -1
  26. package/dist/components/index.d.ts.map +1 -1
  27. package/dist/constants/seatMap/CpSeatMapConfig.d.ts +22 -0
  28. package/dist/constants/seatMap/CpSeatMapConfig.d.ts.map +1 -0
  29. package/dist/constants/seatMap/CpSeatMapSeatSelection.d.ts +12 -0
  30. package/dist/constants/seatMap/CpSeatMapSeatSelection.d.ts.map +1 -0
  31. package/dist/constants/seatMap/CpSeatMapTransitionDirections.d.ts +5 -0
  32. package/dist/constants/seatMap/CpSeatMapTransitionDirections.d.ts.map +1 -0
  33. package/dist/constants/seatMap/IataTravelerTypes.d.ts +7 -0
  34. package/dist/constants/seatMap/IataTravelerTypes.d.ts.map +1 -0
  35. package/dist/constants/seatMap/Seat.d.ts +18 -0
  36. package/dist/constants/seatMap/Seat.d.ts.map +1 -0
  37. package/dist/constants/seatMap/SeatAncillary.d.ts +9 -0
  38. package/dist/constants/seatMap/SeatAncillary.d.ts.map +1 -0
  39. package/dist/constants/seatMap/SeatCharacteristic.d.ts +7 -0
  40. package/dist/constants/seatMap/SeatCharacteristic.d.ts.map +1 -0
  41. package/dist/constants/seatMap/SeatCharacteristicCodes.d.ts +10 -0
  42. package/dist/constants/seatMap/SeatCharacteristicCodes.d.ts.map +1 -0
  43. package/dist/constants/seatMap/SeatMap.d.ts +10 -0
  44. package/dist/constants/seatMap/SeatMap.d.ts.map +1 -0
  45. package/dist/constants/seatMap/SeatMapZone.d.ts +6 -0
  46. package/dist/constants/seatMap/SeatMapZone.d.ts.map +1 -0
  47. package/dist/constants/seatMap/SeatRow.d.ts +15 -0
  48. package/dist/constants/seatMap/SeatRow.d.ts.map +1 -0
  49. package/dist/constants/seatMap/SeatRowFacilities.d.ts +10 -0
  50. package/dist/constants/seatMap/SeatRowFacilities.d.ts.map +1 -0
  51. package/dist/constants/seatMap/SeatRowFacilityFlags.d.ts +8 -0
  52. package/dist/constants/seatMap/SeatRowFacilityFlags.d.ts.map +1 -0
  53. package/dist/constants/seatMap/SeatStatuses.d.ts +8 -0
  54. package/dist/constants/seatMap/SeatStatuses.d.ts.map +1 -0
  55. package/dist/constants/seatMap/index.d.ts +16 -0
  56. package/dist/constants/seatMap/index.d.ts.map +1 -0
  57. package/dist/helpers/seatMap.d.ts +28 -0
  58. package/dist/helpers/seatMap.d.ts.map +1 -0
  59. package/dist/pimp.es.js +4263 -3756
  60. package/dist/pimp.umd.js +40 -40
  61. package/dist/style.css +1 -1
  62. package/package.json +1 -1
  63. package/src/components/CpSeatMap.vue +226 -0
  64. package/src/components/CpSeatMapEmergencyExitDialog.vue +87 -0
  65. package/src/components/CpSeatMapGrid.vue +41 -0
  66. package/src/components/CpSeatMapPlane.vue +221 -0
  67. package/src/components/CpSeatMapRow.vue +183 -0
  68. package/src/components/CpSeatMapRowFacility.vue +88 -0
  69. package/src/components/CpSeatMapSeat.vue +138 -0
  70. package/src/components/CpSeatMapSeatWrapper.vue +189 -0
  71. package/src/components/CpSeatMapZone.vue +60 -0
  72. package/src/components/icons/IconPlaneNose.vue +39 -0
  73. package/src/components/icons/IconPlaneWing.vue +11 -0
  74. package/src/components/icons/IconSparkle.vue +8 -0
  75. package/src/components/index.ts +51 -0
  76. package/src/constants/seatMap/CpSeatMapConfig.ts +47 -0
  77. package/src/constants/seatMap/CpSeatMapSeatSelection.ts +12 -0
  78. package/src/constants/seatMap/CpSeatMapTransitionDirections.ts +4 -0
  79. package/src/constants/seatMap/IataTravelerTypes.ts +6 -0
  80. package/src/constants/seatMap/Seat.ts +18 -0
  81. package/src/constants/seatMap/SeatAncillary.ts +8 -0
  82. package/src/constants/seatMap/SeatCharacteristic.ts +7 -0
  83. package/src/constants/seatMap/SeatCharacteristicCodes.ts +9 -0
  84. package/src/constants/seatMap/SeatMap.ts +10 -0
  85. package/src/constants/seatMap/SeatMapZone.ts +5 -0
  86. package/src/constants/seatMap/SeatRow.ts +15 -0
  87. package/src/constants/seatMap/SeatRowFacilities.ts +10 -0
  88. package/src/constants/seatMap/SeatRowFacilityFlags.ts +7 -0
  89. package/src/constants/seatMap/SeatStatuses.ts +7 -0
  90. package/src/constants/seatMap/index.ts +16 -0
  91. package/src/helpers/seatMap.ts +76 -0
  92. 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>