PluginProbe ʕ •ᴥ•ʔ
Booking for Appointments and Events Calendar – Amelia / trunk
Booking for Appointments and Events Calendar – Amelia vtrunk
2.4.4 2.4.3 2.4.2 2.4.1 2.4 trunk 1.2.1 1.2.10 1.2.11 1.2.12 1.2.13 1.2.14 1.2.15 1.2.16 1.2.17 1.2.18 1.2.19 1.2.2 1.2.20 1.2.21 1.2.22 1.2.23 1.2.24 1.2.25 1.2.26 1.2.27 1.2.28 1.2.29 1.2.3 1.2.30 1.2.31 1.2.32 1.2.33 1.2.34 1.2.35 1.2.36 1.2.37 1.2.38 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 2.0 2.0.1 2.0.2 2.1 2.1.1 2.1.2 2.1.3 2.2 2.2.1 2.3
ameliabooking / v3 / src / views / public / EventForm / Common / Parts / EventCard.vue
ameliabooking / v3 / src / views / public / EventForm / Common / Parts Last commit date
BringingAnyone.vue 6 days ago DescriptionItem.vue 6 days ago EmptyState.vue 6 days ago EventCard.vue 6 days ago EventEmployee.vue 6 days ago EventTicket.vue 6 days ago GalleryCarousel.vue 6 days ago
EventCard.vue
743 lines
1 <template>
2 <div
3 class="am-ec"
4 :class="[{ 'am-no-border': !props.borderVisibility }, responsiveClass]"
5 :style="cssVars"
6 role="listitem"
7 >
8 <!-- Period -->
9 <div class="am-ec__period" :class="responsiveClass">
10 <p
11 v-if="
12 multipleDaysEvent(props.event.periods) &&
13 (eventStatus === 'open' || eventStatus === 'upcoming')
14 "
15 class="am-ec__period-text"
16 :class="responsiveClass"
17 >
18 {{ props.labels.event_begins }}
19 </p>
20 <p class="am-ec__period-date" :class="responsiveClass">
21 <span
22 class="am-ec__period-date__day"
23 :class="[
24 { 'am-ghost': eventStatus === 'canceled' || eventStatus === 'closed' },
25 responsiveClass,
26 ]"
27 >
28 {{ getEventFrontedFormattedDateDay(props.event.periods[0].periodStart.split(' ')[0]) }}
29 </span>
30 <span
31 class="am-ec__period-date__month"
32 :class="[
33 { 'am-ghost': eventStatus === 'canceled' || eventStatus === 'closed' },
34 responsiveClass,
35 ]"
36 >
37 {{
38 monthsTranslationsShort(localLanguage)[
39 moment(props.event.periods[0].periodStart.split(' ')[0]).format('M')
40 ]
41 }}
42 </span>
43 </p>
44 <p
45 class="am-ec__period-time"
46 :class="[
47 { 'am-ghost': eventStatus === 'canceled' || eventStatus === 'closed' },
48 responsiveClass,
49 ]"
50 >
51 {{ getEventFrontedFormattedTime(props.event.periods[0].periodStart.split(' ')[1]) }}
52 </p>
53 </div>
54
55 <!-- Image -->
56 <div
57 v-if="
58 (props.event.gallery.length || props.event.pictureFullPath) &&
59 props.imageVisibility &&
60 props.customizedOptions.imgTab.visibility
61 "
62 class="am-ec__image"
63 :class="responsiveClass"
64 :style="{
65 backgroundImage: `url(${props.event.pictureFullPath ? props.event.pictureFullPath : props.event.gallery[0].pictureFullPath})`,
66 }"
67 role="img"
68 :aria-label="props.event.name"
69 ></div>
70
71 <!-- Info -->
72 <div class="am-ec__info">
73 <p
74 class="am-ec__info-name"
75 :class="{ 'am-ghost': eventStatus === 'canceled' || eventStatus === 'closed' }"
76 >
77 {{ props.event.name }}
78 </p>
79 <p
80 v-if="inHeader && componentWidth <= 500 && props.customizedOptions.price.visibility"
81 class="am-ec__info-price"
82 >
83 {{ eventCardPrice }}
84 <template v-if="useCheckIfEventNotFree(props.event) && props.taxVisible">
85 <template v-if="amSettings.payments.taxes.excluded">
86 {{ `+${props.labels.total_tax_colon}` }}
87 </template>
88 <template v-else>
89 {{ props.labels.incl_tax }}
90 </template>
91 </template>
92 </p>
93 <p
94 v-if="
95 props.customizedOptions.location.visibility && useEventLocation(props.event, locations)
96 "
97 class="am-ec__info-location"
98 :class="{ 'am-ghost': eventStatus === 'canceled' || eventStatus === 'closed' }"
99 >
100 {{ useEventLocation(props.event, locations) }}
101 </p>
102 <div class="am-ec__info-other">
103 <p
104 v-if="props.customizedOptions.status.visibility"
105 :class="`am-ec__info-availability ${eventStatus}`"
106 >
107 {{ props.labels[eventStatus] }}
108 </p>
109 <p
110 v-if="
111 props.customizedOptions.slots.visibility &&
112 useWaitingListAvailability(props.event) &&
113 useWaitingListOccupancy(props.event) > 0
114 "
115 class="am-ec__info-waiting-list"
116 >
117 {{ useWaitingListOccupancy(props.event) }}
118 {{
119 useWaitingListOccupancy(props.event) === 1
120 ? props.labels.person_waiting
121 : props.labels.people_waiting
122 }}
123 </p>
124 <p
125 v-if="props.customizedOptions.slots.visibility && showEventCapacity(eventStatus)"
126 class="am-ec__info-capacity"
127 >
128 <span class="am-ec__info-capacity__number">
129 {{ eventPlaces }}
130 </span>
131 <span class="am-ec__info-capacity__text">
132 {{
133 eventPlaces === 1
134 ? ` ${props.labels.event_slot_left}`
135 : ` ${props.labels.event_slots_left}`
136 }}
137 </span>
138 </p>
139 </div>
140 </div>
141
142 <!-- Price and Action Button -->
143 <div
144 v-if="!inHeader || componentWidth > 500"
145 class="am-ec__actions"
146 :class="[
147 { 'am-vertical-center': !props.customizedOptions.price.visibility },
148 responsiveClass,
149 ]"
150 >
151 <div
152 v-if="props.customizedOptions.price.visibility"
153 class="am-ec__actions-price"
154 :class="responsiveClass"
155 >
156 <p>
157 {{ eventCardPrice }}
158 </p>
159 <p v-if="useCheckIfEventNotFree(props.event) && props.taxVisible" class="am-tax">
160 <template v-if="amSettings.payments.taxes.excluded">
161 {{ `+${props.labels.total_tax_colon}` }}
162 </template>
163 <template v-else>
164 {{ props.labels.incl_tax }}
165 </template>
166 </p>
167 </div>
168 <p v-if="props.btnVisibility" class="am-ec__actions-btn">
169 <AmButton
170 :size="componentWidth > 500 ? 'small' : 'medium'"
171 :type="bookingBtnType"
172 :category="useWaitingListAvailability(props.event) ? 'waiting' : 'primary'"
173 :aria-label="`${useWaitingListAvailability(props.event) ? props.labels.join_waiting_list : eventStatus !== 'open' ? props.labels.event_learn_more : props.labels.event_read_more}: ${props.event.name}`"
174 @click="selectEvent(props.event.id)"
175 >
176 {{
177 useWaitingListAvailability(props.event)
178 ? props.labels.join_waiting_list
179 : eventStatus !== 'open'
180 ? props.labels.event_learn_more
181 : props.labels.event_read_more
182 }}
183 </AmButton>
184 </p>
185 </div>
186 </div>
187 </template>
188
189 <script setup>
190 // * _components
191 import AmButton from '../../../../_components/button/AmButton.vue'
192
193 // * Import from Vue
194 import { computed, inject } from 'vue'
195
196 // * Libraries
197 import moment from 'moment'
198
199 // * Composables
200 import {
201 useEventLocation,
202 useMinTicketPrice,
203 showEventCapacity,
204 useEventStatus,
205 useCheckIfEventNotFree,
206 useWaitingListAvailability,
207 useWaitingListOccupancy,
208 } from '../../../../../assets/js/public/events.js'
209 import {
210 getEventFrontedFormattedTime,
211 getEventFrontedFormattedDateDay,
212 } from '../../../../../assets/js/common/date.js'
213 import { useFormattedPrice } from '../../../../../assets/js/common/formatting'
214 import { useColorTransparency } from '../../../../../assets/js/common/colorManipulation'
215 import { useResponsiveClass } from '../../../../../assets/js/common/responsive'
216 import { monthsTranslationsShort } from '../../../../../assets/js/common/translationsElementPlus'
217
218 // * Component Properties
219 let props = defineProps({
220 event: {
221 type: [Object, Function, Array],
222 required: true,
223 },
224 labels: {
225 type: Object,
226 required: true,
227 },
228 customizedOptions: {
229 type: Object,
230 default: () => {
231 return {
232 bookingBtn: {
233 buttonType: 'filled',
234 },
235 infoBtn: {
236 buttonType: 'plain',
237 },
238 imgTab: {
239 visibility: true,
240 },
241 location: {
242 visibility: true,
243 },
244 price: {
245 visibility: true,
246 },
247 slots: {
248 visibility: true,
249 },
250 status: {
251 visibility: true,
252 },
253 }
254 },
255 },
256 tickets: {
257 type: Array,
258 default: () => [],
259 },
260 locations: {
261 type: Array,
262 required: true,
263 },
264 borderVisibility: {
265 type: Boolean,
266 default: true,
267 },
268 btnVisibility: {
269 type: Boolean,
270 default: true,
271 },
272 autoInfoHeight: {
273 type: Boolean,
274 default: false,
275 },
276 imageVisibility: {
277 type: Boolean,
278 default: true,
279 },
280 verticalOrientation: {
281 type: String,
282 default: 'center',
283 },
284 inHeader: {
285 type: Boolean,
286 default: false,
287 },
288 inDialog: {
289 type: Boolean,
290 default: false,
291 },
292 taxVisible: {
293 type: Boolean,
294 default: false,
295 },
296 })
297
298 // * Component Emits
299 const emits = defineEmits(['click'])
300
301 // * Root Settings
302 const amSettings = inject('settings')
303
304 // * Locale language long
305 const localLanguage = inject('localLanguage')
306
307 // * Container width
308 let cWidth = inject('containerWidth')
309 let dWidth = inject('dialogWidth')
310
311 let componentWidth = computed(() => {
312 return props.inDialog ? dWidth.value : cWidth.value
313 })
314
315 // * Responsive
316 let responsiveClass = computed(() => useResponsiveClass(componentWidth.value))
317
318 // * Event Status
319 let eventStatus = computed(() => {
320 return useEventStatus(props.event)
321 })
322
323 let bookingBtnType = computed(() => {
324 if (useWaitingListAvailability(props.event)) {
325 if ('waitingBtn' in props.customizedOptions) {
326 return props.customizedOptions.waitingBtn.buttonType
327 }
328
329 return 'filled'
330 }
331
332 if (eventStatus.value !== 'open') {
333 return props.customizedOptions.infoBtn.buttonType
334 }
335
336 return props.customizedOptions.bookingBtn.buttonType
337 })
338
339 let eventPlaces = computed(() => {
340 if (props.event.customPricing) {
341 let places = 0
342 let eventTickets = props.tickets.length ? props.tickets : props.event.customTickets
343
344 if (props.event.maxCustomCapacity) {
345 let sold = 0
346 eventTickets.forEach((t) => {
347 if ('enabled' in t ? t.enabled : true) {
348 sold += t.sold
349 }
350 })
351
352 places = props.event.maxCustomCapacity - sold
353 } else {
354 eventTickets.forEach((t) => {
355 if ('enabled' in t ? t.enabled : true) {
356 places += t.spots - t.sold - ('persons' in t ? t.persons : 0)
357 }
358 })
359 }
360
361 return places
362 }
363
364 return props.event.places
365 })
366
367 let eventCardPrice = computed(() => {
368 if (props.event.customPricing) {
369 if (useCheckIfEventNotFree(props.event)) {
370 return `${props.labels.from} ${useFormattedPrice(useMinTicketPrice(props.event))}`
371 } else {
372 return props.labels.event_free
373 }
374 }
375
376 return useCheckIfEventNotFree(props.event)
377 ? useFormattedPrice(props.event.price)
378 : props.labels.event_free
379 })
380
381 function selectEvent(id) {
382 emits('click', id)
383 }
384
385 function multipleDaysEvent(arr) {
386 let testArr = []
387 arr.forEach((period) => {
388 testArr.push(period.periodStart.split(' ')[0])
389 testArr.push(period.periodEnd.split(' ')[0])
390 })
391
392 return !testArr.every((val) => val === testArr[0])
393 }
394
395 // * Colors
396 let amColors = inject('amColors')
397
398 let cardBorderOrientation = computed(() => {
399 if (componentWidth.value <= 500) {
400 return `0px 2px 2px -1px ${amColors.value.colorCardBorder}, 0px 0px 11px ${useColorTransparency(amColors.value.colorCardBorder, 0.3)}, inset 0px -8px 0px ${props.event.color}`
401 }
402
403 return `0px 2px 2px -1px ${amColors.value.colorCardBorder}, 0px 0px 11px ${useColorTransparency(amColors.value.colorCardBorder, 0.3)}, inset 8px 0px 0px ${props.event.color}`
404 })
405
406 // * Css Vars
407 let cssVars = computed(() => {
408 let obj = {
409 boxShadow: cardBorderOrientation.value,
410 '--am-c-ec-success': amColors.value.colorSuccess,
411 '--am-c-ec-primary': amColors.value.colorPrimary,
412 '--am-c-ec-warning': amColors.value.colorWarning,
413 '--am-c-ec-bgr': props.inHeader ? amColors.value.colorMainBgr : amColors.value.colorCardBgr,
414 '--am-c-ec-text': props.inHeader ? amColors.value.colorMainText : amColors.value.colorCardText,
415 '--am-c-ec-text-op90': useColorTransparency(
416 props.inHeader ? amColors.value.colorMainText : amColors.value.colorCardText,
417 0.9,
418 ),
419 '--am-c-ec-text-op80': useColorTransparency(
420 props.inHeader ? amColors.value.colorMainText : amColors.value.colorCardText,
421 0.8,
422 ),
423 '--am-c-ec-text-op50': useColorTransparency(
424 props.inHeader ? amColors.value.colorMainText : amColors.value.colorCardText,
425 0.5,
426 ),
427 '--am-c-ec-error-op70': useColorTransparency(amColors.value.colorError, 0.7),
428 // ? Height of card content
429 '--am-h-ec-info': props.autoInfoHeight ? 'unset' : '84px',
430 '--am-vo-ec': props.verticalOrientation,
431 '--am-fs-ec-title': props.inHeader ? '18px' : '16px',
432 }
433
434 if (!props.borderVisibility) delete obj.boxShadow
435
436 return obj
437 })
438 </script>
439
440 <script>
441 export default {
442 name: 'EventCard',
443 }
444 </script>
445
446 <style lang="scss">
447 .amelia-v2-booking #amelia-container {
448 // ec - event card
449 .am-ec {
450 display: flex;
451 flex-direction: row;
452 align-items: var(--am-vo-ec);
453 width: 100%;
454 padding: 12px 16px 12px 24px;
455 background: var(--am-c-ec-bgr);
456 border-radius: 8px;
457 margin-bottom: 12px;
458
459 &.am-no-border {
460 padding: 0;
461 }
462
463 &.am-rw-500 {
464 --am-vo-ec: flex-start !important;
465 flex-direction: column;
466 }
467
468 &__period {
469 margin-right: 26px;
470
471 &.am-rw-500 {
472 display: flex;
473 align-items: baseline;
474 margin: 0 0 8px;
475 }
476
477 &-text {
478 font-size: 12px;
479 font-weight: 400;
480 line-height: 1.5;
481 padding: 0;
482 margin: 0;
483 color: var(--am-c-ec-text-op50);
484
485 &.am-rw {
486 &-500 {
487 font-size: 14px;
488 margin: 0 4px 0 0;
489 }
490 }
491 }
492
493 &-date {
494 padding: 0;
495 margin: 0;
496
497 &.am-rw-500 {
498 display: flex;
499 align-items: baseline;
500 margin-right: 8px;
501 }
502
503 * {
504 color: var(--am-c-ec-text); // $shade-1000
505 }
506
507 &__day {
508 font-size: 20px;
509 font-weight: 500;
510 line-height: 1;
511 margin-right: 2px;
512
513 &.am-ghost {
514 color: var(--am-c-ec-text-op50); // $shade-500
515 }
516
517 &.am-rw-500 {
518 font-size: 18px;
519 }
520 }
521
522 &__month {
523 font-size: 13px;
524 font-weight: 400;
525 line-height: 1.23077;
526
527 &.am-ghost {
528 color: var(--am-c-ec-text-op50); // $shade-500
529 }
530
531 &.am-rw-500 {
532 font-size: 14px;
533 }
534 }
535 }
536
537 &-time {
538 font-size: 12px;
539 font-weight: 400;
540 line-height: 1.5;
541 color: var(--am-c-ec-text-op80); // $shade-800
542 padding: 0;
543 margin: 0;
544
545 &.am-ghost {
546 color: var(--am-c-ec-text-op50); // $shade-500
547 }
548
549 &.am-rw-500 {
550 font-size: 14px;
551 }
552 }
553 }
554
555 &__image {
556 width: 88px;
557 height: 88px;
558 border-radius: 4px;
559 overflow: hidden;
560 margin: 0 16px 0 0;
561 background-repeat: no-repeat;
562 background-size: cover;
563 background-position: center;
564
565 &.am-rw-500 {
566 width: 100%;
567 padding-top: 70%;
568 margin: 0 0 8px;
569 }
570 }
571
572 &__info {
573 display: flex;
574 flex-direction: column;
575 justify-content: space-between;
576 flex: 1;
577 //min-height: var(--am-h-ec-info);
578
579 &-name {
580 font-size: var(--am-fs-ec-title);
581 font-weight: 500;
582 line-height: 1.7777777;
583 color: var(--am-c-ec-text-op90); // $shade-900
584 margin: 0 0 4px;
585
586 &.am-ghost {
587 color: var(--am-c-ec-text-op50); // $shade-500
588 }
589 }
590
591 &-price {
592 font-size: 15px;
593 font-weight: 500;
594 line-height: 1.6;
595 color: var(--am-c-ec-primary);
596 }
597
598 &-location {
599 font-size: 15px;
600 font-weight: 400;
601 line-height: 1.6;
602 color: var(--am-c-ec-text-op80); // $shade-800
603 margin: 0 0 4px;
604
605 &.am-ghost {
606 color: var(--am-c-ec-text-op50); // $shade-500
607 }
608 }
609
610 &-other {
611 display: flex;
612 align-items: center;
613 margin: 4px 0 0;
614 }
615
616 &-availability {
617 font-size: 15px;
618 font-weight: 500;
619 line-height: 1.6;
620 margin: 0 8px 0 0;
621
622 &.closed {
623 color: var(--am-c-ec-text-op50); // $shade-500
624 }
625
626 &.open {
627 color: var(--am-c-ec-success); // $green-1000
628 }
629
630 &.full {
631 color: var(--am-c-ec-primary); // $blue-700
632 }
633
634 &.upcoming {
635 color: var(--am-c-ec-warning);
636 }
637
638 &.canceled {
639 color: var(--am-c-ec-error-op70);
640 }
641 }
642
643 &-capacity {
644 display: flex;
645 align-items: center;
646 margin: 0;
647
648 &__number {
649 font-weight: 700;
650 font-size: 15px;
651 line-height: 24px;
652 color: var(--am-c-ec-text-op80);
653 }
654
655 &__text {
656 font-weight: 400;
657 font-size: 15px;
658 line-height: 1.6;
659 margin: 0 0 0 4px;
660 color: var(--am-c-ec-text-op80);
661 }
662 }
663
664 &-waiting-list {
665 color: var(--am-c-ec-warning);
666 font-size: 15px;
667 line-height: 1.6;
668 font-weight: 500;
669 margin: 0;
670 }
671 }
672
673 &__actions {
674 display: flex;
675 flex-direction: column;
676 align-self: stretch;
677 align-items: flex-end;
678 justify-content: space-between;
679
680 &.am-vertical-center {
681 justify-content: center;
682
683 .am-ec__actions-btn {
684 width: 100%;
685
686 .am-button {
687 width: 100%;
688 }
689 }
690 }
691
692 &.am-rw-500 {
693 flex-direction: row;
694 align-items: center;
695 justify-content: space-between;
696 width: 100%;
697 margin: 24px 0 8px;
698 }
699
700 &-price {
701 display: flex;
702 align-items: center;
703 flex-wrap: nowrap;
704 font-size: var(--am-fs-ec-title);
705 font-weight: 500;
706 line-height: 1.6;
707 color: var(--am-c-ec-primary);
708 text-align: center;
709
710 &.am-rw-500 {
711 --am-fs-ec-title: 15px !important;
712
713 p {
714 margin: 0;
715 }
716 }
717
718 &.am-rw-360 {
719 flex-wrap: wrap;
720 }
721
722 p {
723 font-size: inherit;
724 font-weight: inherit;
725 line-height: inherit;
726 text-align: right;
727 color: inherit;
728 margin: 0 0 8px;
729
730 &.am-tax {
731 margin-left: 8px;
732 }
733 }
734 }
735
736 &-btn {
737 margin: 0;
738 }
739 }
740 }
741 }
742 </style>
743