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