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