302 status code indicates that the requested resource is temporarily available at a different URL.
The user is automatically redirected to the new URL, but search engines continue to index the old address.
200 status code is a standard successful HTTP server response. It means that the client’s request (e.g., from a browser) was successfully processed, and the server is delivering the requested data.
The user receives content without errors, and the page or application functions properly. If Code 200 is accompanied by data, the browser or program processes and displays it to the user.
GET / HTTP/1.1 Host: dlyc.com Accept: */* User-Agent: Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; ClaudeBot/1.0; [email protected])
<!DOCTYPE html> <html lang="en"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <script src="https://js.stripe.com/v3/"></script> <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script> <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> <script src="/public/assets/js/jquery-ui.min.js"></script> <link href="/public/assets/css/jquery-ui.min.css" rel="stylesheet"> <script type="text/javascript" src="/parse-sdk-js"></script> <!-- start vue --> <!-- prod minified vue js --> <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.4.21/vue.global.prod.min.js"></script> <script src="/public/assets/js/catch-all-minified/vue.min.js"></script> <!-- end vue --> <!-- library for image orientation --> <script src="/public/assets/js/load-image-all.min.js"></script> <!-- moment.js --> <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.30.1/moment.min.js"></script> <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.30.1/moment-with-locales.min.js"></script> <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.45/moment-timezone-with-data.min.js"></script> <!-- library for charts --> <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.js"></script> <link rel="stylesheet" href="/public/assets/css/material-icons.css"> <!-- <link rel="stylesheet" href="https://kit.fontawesome.com/a59cae6e7b.css" crossorigin="anonymous"> --> <link rel="stylesheet" href="/public/assets/css/font-awesome.css" crossorigin="anonymous"> <!-- more js --> <script src="/public/assets/js/cookies.js"></script> <!-- web builder --> <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&family=Cormorant+Garamond&family=Lato&family=Lora:wght@400;500&family=Montserrat:wght@400;500;600&family=Boogaloo&family=PT+Sans:wght@400;700&family=Mukta:wght@400;700;800&family=Karla&family=Raleway:wght@400;500;700;800&family=Bree+Serif&family=Vollkorn&family=Nunito+Sans&family=Mrs+Saint+Delafield&family=Halant:wght@400;500;600;700&family=Carlito:ital,wght@0,400;0,700;1,400;1,700&family=Open+Sans:ital,wght@0,300;0,800&family=Cinzel:wght@400;900&display=swap" rel="stylesheet"> <!-- Juice browserified for inline css --> <script src="/public/assets/browserified/juice-browserified.js"></script> <!-- start calendar stuff (tui-calendar) --> <link rel="stylesheet" type="text/css" href="https://uicdn.toast.com/tui-calendar/v1.15.0/tui-calendar.css" /> <link rel="stylesheet" type="text/css" href="https://uicdn.toast.com/tui.date-picker/latest/tui-date-picker.css" /> <link rel="stylesheet" type="text/css" href="https://uicdn.toast.com/tui.time-picker/latest/tui-time-picker.css" /> <!-- <link rel="stylesheet" type="text/css" href="https://uicdn.toast.com/tui.date-picker/v1.15.0/tui-date-picker.css" /> --> <!-- <link rel="stylesheet" type="text/css" href="https://uicdn.toast.com/tui.time-picker/v1.15.0/tui-time-picker.css" /> --> <script src="https://uicdn.toast.com/tui.code-snippet/v1.5.2/tui-code-snippet.min.js"></script> <!-- <script src="https://uicdn.toast.com/tui.time-picker/v1.15.0/tui-time-picker.min.js"></script> --> <!-- <script src="https://uicdn.toast.com/tui.date-picker/v1.15.0/tui-date-picker.min.js"></script> --> <script src="https://uicdn.toast.com/tui-calendar/v1.15.0/tui-calendar.js"></script> <!-- end calendar stuff --> <script src="https://cdn.jsdelivr.net/npm/dayjs@1/dayjs.min.js"></script> <script src="/public/assets/js/browserify_src/bundle_browserify.js"></script> <script src="/public/assets/js/catch-all-minified/common-web-builder.min.js"></script> <!-- Cropper.js --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.6/cropper.min.css"> <script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.6/cropper.min.js"></script> <!-- unlayer (newer email builder, to replace tinymce) --> <script src="https://editor.unlayer.com/embed.js"></script> <link rel="stylesheet" href="/public/assets/css/ios.css"> <!-- weglot translations --> <!-- userway.org accessibility snippet --> <script src="/public/assets/js/catch-all-minified/hosted-sites.min.js"></script> <link href="/public/assets/css/hosted.css" rel="stylesheet"> <script src="https://cdn.jsdelivr.net/npm/dayjs@1/dayjs.min.js"></script> <!-- custom styles --> <style> /* set base fonts */ /* set base colors */ /* now write the styles */ /* NOTE: the wb-content class needs to prepend all the styles so they are ONLY applied to hosted website content, not other content on the page - eg. for the website builder, it shouldn't impact the builder menu on the left */ .wb-content .cs-header p, .wb-content .cs-header div, .cs-footer-sublink-label, .cs-header-sublink-label{ font-family: Lato !important; } .wb-content h1{ font-size: 64px; font-weight: normal; font-family: Cormorant Garamond; } .custom-font-wrap h1{ font-family: Cormorant Garamond; } .wb-content h2 { font-size: 42px; font-weight: normal; font-family: Cormorant Garamond; } .custom-font-wrap h2{ font-family: Cormorant Garamond; } .wb-content h3{ font-size: 28px; font-weight: normal; font-family: Cormorant Garamond; } .wb-content select{ font-size: 28px; font-weight: normal; font-family: Cormorant Garamond; } .custom-font-wrap h3{ font-family: Cormorant Garamond; } .wb-content p, .wb-content div{ font-size: 16px; font-weight: normal; font-family: Lato; } .custom-font-wrap p, .custom-font-wrap div{ font-family: Lato; } .wb-content button, .custom-font-wrap button { font-family: Cormorant Garamond } /*-- tab bars --*/ .cs-tab-bar-option.active, .cs-tab-bar-option:hover { color: #fff !important; background-color: #236651; } /*-- buttons --*/ .wb-content button{ color: #fff; background-color: #000000; } /* -- custom code for millbrook --*/ /*-- end custom code for millbrook --*/ /*-- color schemes --*/ .wb-content .color_scheme_white .background, .wb-content .color_scheme_white.background { background-color: #fff; color: #181b3e; } .wb-content .color_scheme_light .background, .wb-content .color_scheme_light.background { background-color: #eaefef; color: #181b3e; } .wb-content .color_scheme_dark .background, .wb-content .color_scheme_dark.background { background-color: #000000; color: #fff; } .wb-content .color_scheme_black .background, .wb-content .color_scheme_black.background { background-color: #000; color: #fff; } .wb-content .color_scheme_accent_light .background, .wb-content .color_scheme_accent_light.background { background-color: #fff; color: #236651; } .wb-content .color_scheme_accent_dark .background, .wb-content .color_scheme_accent_dark.background { background-color: #236651; color: #fff; } /*----- color schemes for header ------ */ .color_scheme_white .cs-header-link-wrap > p, .color_scheme_white .cs-burgee-wrap > p{ color: #000000 !important; } .color_scheme_white .cs-header-link-wrap::after{ background: #000000; } .color_scheme_light .cs-header-background{ background-color: #eaefef; } .color_scheme_light .cs-header-link-wrap > p, .color_scheme_light .cs-burgee-wrap > p{ color: #000000 !important; } .color_scheme_light .cs-header-link-wrap::after{ background: #000000; } .color_scheme_dark .cs-header-background{ background-color: #000000; } .color_scheme_dark .cs-header-link-wrap > p, .color_scheme_dark .cs-burgee-wrap > p{ color: #fff; } .color_scheme_dark .cs-header-link-wrap::after{ background: #fff; } .color_scheme_black .cs-header-background{ background-color: #000; } .color_scheme_black .cs-header-link-wrap > p, .color_scheme_black .cs-burgee-wrap > p{ color: #fff; } .color_scheme_black .cs-header-link-wrap::after{ background: #fff; } .color_scheme_accent_light .cs-header-background{ background-color: #fff; } .color_scheme_accent_light .cs-header-link-wrap > p, .color_scheme_accent_light .cs-burgee-wrap > p{ color: #236651; } .color_scheme_accent_light .cs-header-link-wrap::after{ background: #236651; } .color_scheme_accent_dark .cs-header-background{ background-color: #236651; } .color_scheme_accent_dark .cs-header-link-wrap > p, .color_scheme_accent_dark .cs-burgee-wrap > p{ color: #fff; } .color_scheme_accent_dark .cs-header-link-wrap::after{ background: #fff; } </style> <!-- favicon --> <!-- try to fall back to the burgee url, but google might not love this if it's not sized properly, so we really want favicon_url set --> <link rel="shortcut icon" href="https://myclubspot.s3-us-west-2.amazonaws.com/yVq6rz613F_burgee_1574269334439"> <link rel="icon" sizes="16x16 32x32 64x64" href="https://myclubspot.s3-us-west-2.amazonaws.com/yVq6rz613F_burgee_1574269334439"> <!-- touch icon (mobile web) --> <!-- web app title (mobile safari) --> <meta name="apple-mobile-web-app-title" content="Delavan Lake Yacht Club"> <!-- app_manifest (android) --> <title>Home - Delavan Lake Yacht Club</title> <!-- fb open graph tags --> <meta property="fb:app_id" content="314788812688609" /> <meta property="og:image" content="https://myclubspot.s3-us-west-2.amazonaws.com/yVq6rz613F_burgee_1574269334439"> <!-- Google Analytics for clubs --> <!-- END google analytics for clubs --> <!-- FB pixel for clubs --> <!-- FB pixel for clubs --> <!-- vue date picker --> <!-- see: https://vue3datepicker.com/ --> <script src="/public/assets/js/vue-date-picker-8.5.1.js"></script> <link rel="stylesheet" href="/public/assets/css/vue-date-picker.css"> </head> <body class='wb-content hosted-page' data-club-id='VI8DfNDE1X' data-club-name='Delavan Lake Yacht Club' data-timezone='America/Chicago' > <div class="globalMessageInsert flexNoWrap easeFast hideForPrinting"> <div class="global-message-icon-loading"> <div class="global-message-icon-spinner"></div> </div> <p> <i class="fa-solid fa-square-check global-message-icon-good"></i> <i class="fa-solid fa-square-xmark global-message-icon-bad"></i> <span></span> </p> </div> <!-- start standard content --> <div id="v-content" class="hidden relative leftText relative easeFast paddingTopHeader"> <!-- header --> <custom-page-header></custom-page-header> <!-- page components --> <!-- check the config --> <!-- clean them first, to weed out null values --> <!-- now generate markup --> <hero-banner-component :client_id="'Z6x0WhJKc8U_1652617443'" :formatting="'{"section_height":"70","section_width":"70","align_items":"flex-start","background_size":"inset","overlay_opacity":"0"}'" :class_string="' color_scheme_dark inset orientation-left number_of_columns-3 caption_style-show_on_hover'" :style_string="'min-height:70vh;align-items:flex-start;background-color:#fff'" :element_style_string="'max-width:70%;'" :component_type="'hero-banner'" :elements_string="'%5B%5D'" :images_string="'["https://d282wvk2qi4wzk.cloudfront.net/Z6x0WhJKc8U_1652617443_0_image_1711143758400","https://d282wvk2qi4wzk.cloudfront.net/Z6x0WhJKc8U_1652617443_1_image_1711143816607","https://d282wvk2qi4wzk.cloudfront.net/Z6x0WhJKc8U_1652617443_2_image_1711143888001","https://d282wvk2qi4wzk.cloudfront.net/1652617500888_6489","https://d282wvk2qi4wzk.cloudfront.net/Z6x0WhJKc8U_1652617443_4_image_1711144164838"]'" ></hero-banner-component> <!-- check the config --> <!-- clean them first, to weed out null values --> <!-- now generate markup --> <div class="cs-text background flexWrap relative color_scheme_white orientation-left number_of_columns-1 caption_style-show_on_hover" style="min-height:1vh;align-items:flex-start;"> <div class="element-wrapper flexNoWrap flexWrapOnMobile" class="'element-wrapper_' + text" style="max-width:100%;"> <text-component :number_of_columns="'1'" :column_prop="'%5B%7B%22type%22%3A%22h2-center%22%2C%22content%22%3A%22%3Ch1%20style%3D%5C%22font-weight%3A%20bold%3B%20text-align%3A%20center%3B%5C%22%3E%3Cspan%20class%3D%5C%22cs-font-size-46%5C%22%20style%3D%5C%22%5C%22%3E%3Cspan%20style%3D%5C%22color%3A%20rgb%2528251%2C%2087%2C%2031%2529%3B%5C%22%20class%3D%5C%22cs-color-PSFZCWTwx2g_1653515197%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23fdfcfc%5C%22%20class%3D%5C%22cs-color-BgJ63NvjRWE_1653515318%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23f5f5f5%5C%22%20class%3D%5C%22cs-color-S4tPwnRi7OR_1653515318%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23efecec%5C%22%20class%3D%5C%22cs-color-rCEy8uQOWbu_1653515318%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23ebe6e6%5C%22%20class%3D%5C%22cs-color-XHPI7k6Ugrk_1653515318%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23e2dfdf%5C%22%20class%3D%5C%22cs-color-YObsubA9opr_1653515318%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23dbd7d7%5C%22%20class%3D%5C%22cs-color-Fxhfdd1EySQ_1653515318%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23dcd6d6%5C%22%20class%3D%5C%22cs-color-AbMyCDWjX8Z_1653515318%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23d7d1d1%5C%22%20class%3D%5C%22cs-color-DqEvkLWyKIt_1653515318%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23d2cbcb%5C%22%20class%3D%5C%22cs-color-N242GdEeCJk_1653515318%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23cbc3c3%5C%22%20class%3D%5C%22cs-color-N5oh141j3cc_1653515318%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23c3bbbb%5C%22%20class%3D%5C%22cs-color-GIsT1aCUhrJ_1653515318%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23bdb2b2%5C%22%20class%3D%5C%22cs-color-vLS6EbkjBhD_1653515319%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23bab0b0%5C%22%20class%3D%5C%22cs-color-pu4RbQ0Tz3C_1653515319%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23aea3a3%5C%22%20class%3D%5C%22cs-color-LkmFCoT41iq_1653515319%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23a59c9c%5C%22%20class%3D%5C%22cs-color-oBatLDV1Xbj_1653515319%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23a09898%5C%22%20class%3D%5C%22cs-color-M9Sb8D7CLz8_1653515319%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23938a8a%5C%22%20class%3D%5C%22cs-color-pJ2gK4kPBym_1653515319%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%238b8484%5C%22%20class%3D%5C%22cs-color-xyg9inpk7C1_1653515319%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23888181%5C%22%20class%3D%5C%22cs-color-fAqgXG0kluV_1653515319%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23797272%5C%22%20class%3D%5C%22cs-color-pD4DVmYK8wk_1653515319%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23766f6f%5C%22%20class%3D%5C%22cs-color-xoGfS2x6Ql8_1653515319%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23696363%5C%22%20class%3D%5C%22cs-color-BwJI4E8yriA_1653515319%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23615c5c%5C%22%20class%3D%5C%22cs-color-FObOQcamV1N_1653515319%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23575252%5C%22%20class%3D%5C%22cs-color-irzaTJ33IpW_1653515319%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23535050%5C%22%20class%3D%5C%22cs-color-AxRSFv3hEnP_1653515319%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%234c4848%5C%22%20class%3D%5C%22cs-color-0qUQOJSoxuw_1653515319%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23444141%5C%22%20class%3D%5C%22cs-color-7SOWtggytVd_1653515319%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%233c3939%5C%22%20class%3D%5C%22cs-color-NTB78MZDWWN_1653515319%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23393737%5C%22%20class%3D%5C%22cs-color-LRYdv6T51qZ_1653515319%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23313030%5C%22%20class%3D%5C%22cs-color-tE0LOWRJqKF_1653515319%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23292828%5C%22%20class%3D%5C%22cs-color-O6cATuCuxr0_1653515319%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%231f1f1f%5C%22%20class%3D%5C%22cs-color-qBYeyszuLaH_1653515319%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23171717%5C%22%20class%3D%5C%22cs-color-5JiJkIaHZUq_1653515319%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23121212%5C%22%20class%3D%5C%22cs-color-FmfG81gJkOi_1653515319%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%230f0f0f%5C%22%20class%3D%5C%22cs-color-Xp7v51il2DJ_1653515319%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23080808%5C%22%20class%3D%5C%22cs-color-waXQPHN4lig_1653515319%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23050505%5C%22%20class%3D%5C%22cs-color-sZcjOG7vExd_1653515319%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23000000%5C%22%20class%3D%5C%22cs-color-jszi7v7a8Nm_1653515319%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23020000%5C%22%20class%3D%5C%22cs-color-lm1vtZzNst8_1653515428%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23190000%5C%22%20class%3D%5C%22cs-color-G8ANbMKAgde_1653515428%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23fb0000%5C%22%20class%3D%5C%22cs-color-htoH9sqLSKk_1653515428%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23fb0800%5C%22%20class%3D%5C%22cs-color-3AJAakS3FgZ_1653515432%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23fb5700%5C%22%20class%3D%5C%22cs-color-jIosIeH5awO_1653515433%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23fb5703%5C%22%20class%3D%5C%22cs-color-woewk1qr7I9_1653515440%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23fb571f%5C%22%20class%3D%5C%22cs-color-A46TpaWDfuP_1653515440%5C%22%3E%3Cspan%20style%3D%5C%22color%3A%2300571f%5C%22%20class%3D%5C%22cs-color-fxsv8lyQUUS_1653515452%5C%22%3E%3Cspan%20style%3D%5C%22color%3A%2300001f%5C%22%20class%3D%5C%22cs-color-yLi1NbGFXeA_1653515456%5C%22%3E%3Cspan%20style%3D%5C%22color%3A%23000000%5C%22%20class%3D%5C%22cs-color-AVhmSub0mpk_1653515460%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23ff4c2c%5C%22%20class%3D%5C%22cs-color-1IlqYwSFLM2_1656584533%5C%22%3ED%3C%2Fspan%3E%3Cspan%20style%3D%5C%22background-color%3A%23886d6d%5C%22%20class%3D%5C%22cs-color-PlhOf5V4n2N_1656584668%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23026d6d%5C%22%20class%3D%5C%22cs-color-NgEQbTaIlIP_1656584671%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23196d6d%5C%22%20class%3D%5C%22cs-color-qvPNuUqP8xz_1656584672%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23ff6d6d%5C%22%20class%3D%5C%22cs-color-JI6bjznv3gD_1656584672%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23ff026d%5C%22%20class%3D%5C%22cs-color-YsvMcZW3e8n_1656584673%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23ff196d%5C%22%20class%3D%5C%22cs-color-DpD1GmxCFtX_1656584674%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23ffff6d%5C%22%20class%3D%5C%22cs-color-N0N7wgYZB9a_1656584674%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23ffff02%5C%22%20class%3D%5C%22cs-color-mNy7u5ZWPhd_1656584677%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23ffff19%5C%22%20class%3D%5C%22cs-color-f2MtQgQ30RI_1656584678%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23ffffff%5C%22%20class%3D%5C%22cs-color-zfOLsBeeEcF_1656584678%5C%22%3Eelavan%20%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3EL%3Cspan%20style%3D%5C%22background-color%3A%23a06a6a%5C%22%20class%3D%5C%22cs-color-DYq98pI2ibH_1656584645%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23026a6a%5C%22%20class%3D%5C%22cs-color-F34JzTzcXoV_1656584649%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23196a6a%5C%22%20class%3D%5C%22cs-color-jvIsFBw1WaO_1656584650%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23ff6a6a%5C%22%20class%3D%5C%22cs-color-VcAwayOpMBO_1656584650%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23ff026a%5C%22%20class%3D%5C%22cs-color-urYDMi8zOYd_1656584652%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23ff196a%5C%22%20class%3D%5C%22cs-color-bAOebaqlFn5_1656584653%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23ffff6a%5C%22%20class%3D%5C%22cs-color-xCRYq5KHOgo_1656584653%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23ffff02%5C%22%20class%3D%5C%22cs-color-ci2iFFnqIHV_1656584655%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23ffff19%5C%22%20class%3D%5C%22cs-color-ax33cPNGM1d_1656584655%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23ffffff%5C%22%20class%3D%5C%22cs-color-7aDj3CkNpai_1656584655%5C%22%3Eake%20%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3EY%3Cspan%20style%3D%5C%22background-color%3A%2302fdfe%5C%22%20class%3D%5C%22cs-color-Xm6hbfiBW9s_1656584613%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%2319fdfe%5C%22%20class%3D%5C%22cs-color-HciMcHZkSM3_1656584613%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23fffdfe%5C%22%20class%3D%5C%22cs-color-akPih26URKP_1656584614%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23ff03fe%5C%22%20class%3D%5C%22cs-color-sKdgWQEQ1Mx_1656584617%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23ff23fe%5C%22%20class%3D%5C%22cs-color-Cl1qBJh5tdW_1656584617%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23ff02fe%5C%22%20class%3D%5C%22cs-color-yMOx5E5nzMw_1656584620%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23ff19fe%5C%22%20class%3D%5C%22cs-color-jHSBpjH7oSF_1656584621%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23fffffe%5C%22%20class%3D%5C%22cs-color-0hlNvLRnLRM_1656584621%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23ffff02%5C%22%20class%3D%5C%22cs-color-OrWpzyFWhR3_1656584624%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23ffff19%5C%22%20class%3D%5C%22cs-color-w0GPPQYBQiW_1656584624%5C%22%3E%3Cspan%20style%3D%5C%22background-color%3A%23ffffff%5C%22%20class%3D%5C%22cs-color-kOu14oJ9X1l_1656584625%5C%22%3Eacht%20%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3EC%3Cspan%20style%3D%5C%22background-color%3A%23fcfdfe%5C%22%20class%3D%5C%22cs-color-uAEa7Cjj56I_1656584574%5C%22%3Elub%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fspan%3E%3C%2Fh1%3E%22%7D%5D'" :column_index="'0'" ></text-component> <text-component :number_of_columns="'1'" :column_prop="'%5B%7B%22type%22%3A%22h2-center%22%2C%22content%22%3A%22%3Ch2%20style%3D%5C%22text-align%3A%20center%3B%5C%22%3EClick%20to%20edit%3C%2Fh2%3E%22%7D%2C%7B%22type%22%3A%22h3-center%22%2C%22content%22%3A%22%3Ch3%20style%3D%5C%22text-align%3A%20center%3B%5C%22%3EClick%20to%20edit%3C%2Fh3%3E%22%7D%5D'" :column_index="'1'" ></text-component> <text-component :number_of_columns="'1'" :column_prop="'%5B%7B%22type%22%3A%22h2-center%22%2C%22content%22%3A%22%3Ch2%20style%3D%5C%22text-align%3A%20center%3B%5C%22%3EClick%20to%20edit%3C%2Fh2%3E%22%7D%2C%7B%22type%22%3A%22h3-center%22%2C%22content%22%3A%22%3Ch3%20style%3D%5C%22text-align%3A%20center%3B%5C%22%3EClick%20to%20edit%3C%2Fh3%3E%22%7D%5D'" :column_index="'2'" ></text-component> </div> </div> <!-- check the config --> <!-- clean them first, to weed out null values --> <!-- now generate markup --> <image-grid-component :client_id="'ut3tyc3qhH6_1652619772'" :class_string="' color_scheme_white orientation-left number_of_columns-3 caption_style-show_on_hover'" :image_grid_layout="'grid'" :images_per_row="'5'" :caption_size="'24'" :subtext_size="'24'" :link_label_size="'24'" :text_position="'center'" :caption_font_weight="'400'" :image_grid_opacity="'40'" :image_grid_border_radius="'0'" :formatting="'{"caption_style":"show_on_hover","overlay_opacity":"40"}'" :images_string="'[{\"id\":\"CuSS50t2nYr_1652619784\",\"url\":\"https://d282wvk2qi4wzk.cloudfront.net/1652619783395_9459\"},{\"id\":\"zJud5oXcy4s_1652620007\",\"url\":\"https://d282wvk2qi4wzk.cloudfront.net/1652620006222_1064\"},{\"id\":\"Zz6S5w5vdj3_1652620015\",\"url\":\"https://d282wvk2qi4wzk.cloudfront.net/1652620014809_7139\"},{\"id\":\"YEWrgR32PGg_1652620035\",\"url\":\"https://d282wvk2qi4wzk.cloudfront.net/1652620034542_605\"},{\"id\":\"gS9EOJWvFvX_1711144036\",\"url\":\"https://d282wvk2qi4wzk.cloudfront.net/ut3tyc3qhH6_1652619772_grid_image_1711144027223\",\"is_video\":false},{\"id\":\"twG4mu5H213_1711144336\",\"url\":\"https://d282wvk2qi4wzk.cloudfront.net/ut3tyc3qhH6_1652619772_grid_image_1711144323423\",\"is_video\":false},{\"id\":\"gJSPaSJadJ9_1711144503\",\"url\":\"https://d282wvk2qi4wzk.cloudfront.net/ut3tyc3qhH6_1652619772_grid_image_1711144488327\",\"is_video\":false},{\"id\":\"f7aeiQgaPCs_1711144598\",\"url\":\"https://d282wvk2qi4wzk.cloudfront.net/ut3tyc3qhH6_1652619772_grid_image_1711144578992\",\"is_video\":false},{\"id\":\"QOxytAQMTaM_1711144538\",\"url\":\"https://d282wvk2qi4wzk.cloudfront.net/ut3tyc3qhH6_1652619772_grid_image_1711144527839\",\"is_video\":false},{\"id\":\"O8S2wyGBM5E_1711144670\",\"url\":\"https://d282wvk2qi4wzk.cloudfront.net/ut3tyc3qhH6_1652619772_grid_image_1711144663169\",\"is_video\":false}]'" ></image-grid-component> <!-- check the config --> <!-- clean them first, to weed out null values --> <!-- now generate markup --> <div class="cs-text background flexWrap relative color_scheme_white orientation-left number_of_columns-1 caption_style-show_on_hover" style="min-height:1vh;align-items:center;"> <div class="element-wrapper flexNoWrap flexWrapOnMobile" class="'element-wrapper_' + text" style="max-width:100%;"> <text-component :number_of_columns="'1'" :column_prop="'%5B%7B%22type%22%3A%22h2-center%22%2C%22content%22%3A%22%3Ch2%20style%3D%5C%22text-align%3Acenter%5C%22%3E%3Cp%20class%3D%5C%22MsoNormal%5C%22%3E%3C%2Fp%3E%3C%2Fh2%3E%3Ch3%20style%3D%5C%22text-align%3Acenter%5C%22%3E%3Cspan%20style%3D%5C%22font-size%3A18.0pt%3Bline-height%3A107%25%3Bfont-family%3A%26quot%3BArial%26quot%3B%2Csans-serif%5C%22%3E%3Cspan%20class%3D%5C%22cs-EDqs5I8qQ7O_1715257127_1715257127372%5C%22%20style%3D%5C%22background-color%3Argb%2528248%2C%20180%2C%2032%2529%5C%22%3E%3Cspan%20class%3D%5C%22cs-font-size-42%5C%22%20style%3D%5C%22background-color%3Argb%2528248%2C%20184%2C%2048%2529%5C%22%3ECLICK%20TO%20ACCESS%20RACE%20COMMITTEE%20VOLU%3Ca%20href%3D%5C%22https%3A%2F%2Fwww.signupgenius.com%2Fgo%2F10C054CA9AF29A2FACF8-56018278-2025%5C%22%20target%3D%5C%22_blank%5C%22%20class%3D%5C%22cs-link-MzNJaVUrr7U_1746536039%5C%22%3E%3C%2Fa%3ENTEER%20SIGNUP%3C%2Fspan%3E%3C%2Fspan%3E%3Co%3Ap%3E%3C%2Fo%3Ap%3E%3C%2Fspan%3E%3C%2Fh3%3E%3Cp%3E%3C%2Fp%3E%22%2C%22client_id%22%3A%221K0WltQICWh8OTt1%22%2C%22column_count%22%3A1%2C%22column_gap%22%3A0%7D%5D'" :column_index="'0'" ></text-component> <text-component :number_of_columns="'1'" :column_prop="'%5B%7B%22type%22%3A%22h2-center%22%2C%22content%22%3A%22%3Ch2%20style%3D%5C%22text-align%3Acenter%5C%22%3EClick%20to%20edit%3C%2Fh2%3E%22%2C%22client_id%22%3A%22DdGlNorG3yBmxS2S%22%2C%22column_count%22%3A1%2C%22column_gap%22%3A0%7D%2C%7B%22type%22%3A%22h3-center%22%2C%22content%22%3A%22%3Ch3%20style%3D%5C%22text-align%3Acenter%5C%22%3EClick%20to%20edit%3C%2Fh3%3E%22%2C%22client_id%22%3A%22iFxWOgG8hW0YC5sx%22%2C%22column_count%22%3A1%2C%22column_gap%22%3A0%7D%5D'" :column_index="'1'" ></text-component> <text-component :number_of_columns="'1'" :column_prop="'%5B%7B%22type%22%3A%22h2-center%22%2C%22content%22%3A%22%3Ch2%20style%3D%5C%22text-align%3Acenter%5C%22%3EClick%20to%20edit%3C%2Fh2%3E%22%2C%22client_id%22%3A%2264YC6qip4OfqlOOu%22%2C%22column_count%22%3A1%2C%22column_gap%22%3A0%7D%2C%7B%22type%22%3A%22h3-center%22%2C%22content%22%3A%22%3Ch3%20style%3D%5C%22text-align%3Acenter%5C%22%3EClick%20to%20edit%3C%2Fh3%3E%22%2C%22client_id%22%3A%22Mk9rP2TgPDIw8wqF%22%2C%22column_count%22%3A1%2C%22column_gap%22%3A0%7D%5D'" :column_index="'2'" ></text-component> </div> </div> <!-- footer --> <!-- check the config --> <div class="cs-footer relative color_scheme_white" style="" > <div class=""> <div class="cs-footer-flex-wrap-row"> <div class="cs-footer-logo-wrap background"> <div class="cs-burgee-wrap relative" style="height:70px;width:120px;"> <div class="cs-burgee" style="background-image:url(https://myclubspot.s3-us-west-2.amazonaws.com/yVq6rz613F_burgee_1574269334439)"></div> <a class="absoluteA" href="/"></a> </div> </div> <div> <div class="flexWrapLeftAlign"> <div class="cs-footer-link-wrap flexGrowOne noPaddingBottom"> <!-- component --> <div class="footer-link-section flexGrowOne"> <p class="footer-sublink-section-header smallMarginTop">262-725-1426</p> <!-- handle sub-components --> </div> <!-- component --> <div class="footer-link-section flexGrowOne"> <p class="footer-sublink-section-header smallMarginTop">P.O. BOX 482 DELAVAN, WI</p> <!-- handle sub-components --> </div> <!-- component --> <div class="footer-link-section flexGrowOne"> <p class="footer-sublink-section-header smallMarginTop">[email protected]</p> <!-- handle sub-components --> </div> <!-- component --> <div class="footer-link-section flexGrowOne"> <p class="footer-sublink-section-header smallMarginTop"> facebook <a class="absoluteA" href="https://www.facebook.com/DelavanLakeYachtClub"></a> </p> <!-- handle sub-components --> </div> <!-- component --> <div class="footer-link-section flexGrowOne"> <p class="footer-sublink-section-header smallMarginTop"> instagram <a class="absoluteA" href="https://www.instagram.com/delavanlakeyachtclub/"></a> </p> <!-- handle sub-components --> </div> <!-- social icons --> </div> </div> <div class="cs-footer-link-wrap noPaddingTop"> <div class="powered_by_wrap relative"> <div class="flexNoWrap align-items-center"> <p class="powered_by"><i class="fa-solid fa-shield-check" style="padding-right:3px;"></i> Powered by</p> <p class="modern pointer powered_by_text relative" style="font-weight:bold;">Clubspot <a class="absoluteA" href="https://theclubspot.com"></a></p> </div> <p style="display:flex;font-family:roboto !important;" class="cs-footer-sublink-label roboto copyright_text noPointer">© 2025 Delavan Lake Yacht Club, All rights reserved.</p> </div> </div> </div> </div> </div> </div> <!-- react native tab bar --> <native-tab-bar-component></native-tab-bar-component> </div> <div id="v-overlays" class="hidden"> <!-- common --> <template v-if="overlays.overflow_overlay"> <overflow-overlay></overflow-overlay> </template> <template v-if="overlays.view_all_events"> <view-all-events-overlay></view-all-events-overlay> </template> <template v-if="overlays.view_photo_carousel"> <view-photo-carousel></view-photo-carousel> </template> </div> <div id="overlay_membership-directory-popup" class="standardOverlay scrollable hugePaddingBottom"> <div class="standardOverlayCard centeredBlock eightyThenNinety overflowHidden relative"> <div class="xButton" onclick="fadeOut('#overlay_membership-directory-popup');"></div> <div class="profile-background overflowHidden"> <div class="profile-background-inner hundop" style="height:100%;"></div> </div> <div class="profile-picture color_scheme_accent_dark background capitalize"></div> <div class="paddingAllAround smallMarginBottom"> <p class="insert insert_name product-card-label"></p> <div class="section-wrap tinyMarginTop tinyMarginBottom"> <p class="flexGrowOne inline-value" data-db-key="street"></p> <p class="flexGrowOne inline-value" data-db-key="city"></p> <p class="flexGrowOne inline-value" data-db-key="state"></p> <p class="flexGrowOne inline-value" data-db-key="zip"></p> </div> <div class="section-wrap tinyMarginTop tinyMarginBottom"> <h4 class="smallBlockLetters leftText smallMarginTop">Contact</h4> <p class="flexGrowOne inline-value tinyPaddingTop" data-db-key="email"></p> <p class="flexGrowOne inline-value tinyPaddingTop" data-db-key="mobile"></p> </div> <div class="section-wrap tinyMarginTop tinyMarginBottom"> <h4 class="smallBlockLetters leftText smallMarginTop">Secondary address</h4> <p class="flexGrowOne inline-value dark tinyPaddingTop" data-db-key="street_secondary"></p> <p class="flexGrowOne inline-value dark" data-db-key="city_secondary"></p> <p class="flexGrowOne inline-value dark" data-db-key="state_secondary"></p> <p class="flexGrowOne inline-value dark" data-db-key="zip_secondary"></p> </div> <div class="section-wrap section-wrap_custom_fields tinyMarginTop tinyMarginBottom"> </div> <!-- boat fields --> <div class="section-wrap tinyMarginTop tinyMarginBottom"> <p class="flexGrowOne inline-value" data-db-key="boatName"></p> <p class="flexGrowOne inline-value" data-db-key="sailNumber" data-prepend="Sail #:"></p> <p class="flexGrowOne inline-value" data-db-key="hullNumber" data-prepend="Hull #:"></p> <p class="flexGrowOne inline-value" data-db-key="class" data-prepend="Class: "></p> <p class="flexGrowOne inline-value" data-db-key="registration" data-prepend="Registration #:"></p> </div> <div class="section-wrap section-wrap_documents hidden tinyMarginTop tinyMarginBottom"> <h4 class="smallBlockLetters leftText smallMarginTop">Documents</h4> <!-- documents inserted here --> </div> <div class="section-wrap section-wrap_family tinyMarginTop tinyMarginBottom"> <h4 class="smallBlockLetters leftText smallMarginTop version_member">Family</h4> <h4 class="smallBlockLetters leftText smallMarginTop version_boat hidden">Related Members</h4> <!-- family members inserted here --> </div> <div class="section-wrap section-wrap_boats hidden tinyMarginTop tinyMarginBottom"> <h4 class="smallBlockLetters leftText smallMarginTop version_member">Boats</h4> <h4 class="smallBlockLetters leftText smallMarginTop version_boat hidden">Related boats</h4> <!-- boats inserted here --> </div> </div> </div> </div> <div id="overlay_event-description-popup" class="standardOverlay scrollable hugePaddingBottom" style="display:none;opacity:0;"> <div class="standardOverlayCard centeredBlock eightyThenNinety"> <h3></h3> <div class="standardCardBody noPadding fff"> <div class="centeredBlock eightyThenNinety maxSixHundo bigPaddingBottom marginTop"> <p class="inline-label smallerMarginBottom">Date</p> <p class="metric marginBottom leftText tinyMarginTop metric_start_date" style="margin-bottom:20px;"></p> <p class="inline-label smallerMarginBottom">Time</p> <p class="metric marginBottom leftText tinyMarginTop metric_start_date_time" style="margin-bottom:20px;"></p> <p class="inline-label smallerMarginBottom">Event Details</p> <p class="metric marginBottom leftText tinyMarginTop metric_description" style="margin-bottom:20px;"></p> </div> </div> <div class="standardCardFooter flexNoWrap"> <div class="flexGrowOne"></div> <button class="cardFooterButton easeFast" onclick="fadeOut('#overlay_event-description-popup');">Done</button> </div> </div> </div> </body> <script> //add the timezone to the global store store.timezone = $('body').data('timezone'); //add to global store store.eventBus = ref({ listeners: {}, $on(event, callback) { if (!this.listeners[event]) { this.listeners[event] = []; } this.listeners[event].push(callback); }, $emit(event, ...args) { if (this.listeners[event]) { for (const callback of this.listeners[event]) { callback(...args); } } }, }); //create the app var app_content = createApp({ data() { return store }, mounted() { console.log("mounted vue app: main content"); $('#v-content').removeClass('hidden'); }, methods: { }, }); //create the app var app_overlays = createApp({ data() { return store }, mounted() { console.log("mounted vue app: overlays"); $('#v-overlays').removeClass('hidden'); } }); </script> <!-- define/register vue components --> <script type="text/html" id="template-custom-page-header"> <!-- globals --> <!-- check the config --> <!-- color scheme --> <!-- opacity --> <!-- font-size --> <!-- font-weight --> <!-- header style (eg stacked vs inline vs centered) --> <!-- header position (eg relative vs absolute) --> <!-- header background image, eg. for inverness yc --> <!-- burgee url --> <!-- ok, start header markup --> <div class="cs-header easeFast absolute-header color_scheme_white cs-font-size-16 header-400 header-style_inline header-position_relative" style=""> <div class="cs-header-background background" style="opacity: 100%;"></div> <div class="innerHeader align-items-middle flexNoWrap"> <div class="hamburgerWrap marketingMenu"> <div style="position: fixed; height: 1px; width: 1px; opacity: .01;"></div> <div class="hamburger hideOnDesktop"> <div id="headerLineTop" class="ease"></div> <div id="headerLineMiddle" class="ease"></div> <div id="headerLineBottom" class="ease"></div> </div> <p class="burger-menu-text hideOnDesktop hideOnMobile">Menu</p> </div> <div class="cs-burgee-wrap hideOnMobile" style="height:0px;width:0px;"> <div class="cs-burgee" style="background-image: url(https://myclubspot.s3-us-west-2.amazonaws.com/yVq6rz613F_burgee_1574269334439)"></div> <a href="/" class="absoluteA"></a> </div> <div class="cs-burgee-wrap-mobile"> <div class="cs-burgee" style="background-image:url(https://myclubspot.s3-us-west-2.amazonaws.com/yVq6rz613F_burgee_1574269334439);background-size:contain;"></div> <a :href="burgee_link" class="absoluteA"></a> </div> <div class="cs-desktop-menu-wrap flexGrowOne flexWrapRightAlign hideOnMobile header-style_inline"> <div class="cs-header-link-wrap header-style_inline"> <p class="button_link_p" style="">Regattas</p> <!-- link --> <!-- handle sub-components --> <div class="cs-header-sublink-outer easeFast"> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label">Regattas</p> <a class="absoluteA" href="/regattas"></a> </div> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label">Lodging</p> <a class="absoluteA" href="/lodging"></a> </div> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label">ILYA</p> <a class="absoluteA" href="https://ilya.org/"></a> </div> </div> </div> <div class="cs-header-link-wrap header-style_inline"> <p class="button_link_p" style="">Racing</p> <!-- link --> <a class="absoluteA" href="/race-schedule"></a> <!-- handle sub-components --> <div class="cs-header-sublink-outer easeFast"> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label">Official Notice Board</p> <a class="absoluteA" href="/official-notice-board"></a> </div> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label">X Boat</p> <a class="absoluteA" href="/fleet-x-boat"></a> </div> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label">MC Scow</p> <a class="absoluteA" href="/fleet-mc"></a> </div> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label">C Scow</p> <a class="absoluteA" href="/fleet-c"></a> </div> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label">E Scow</p> <a class="absoluteA" href="/fleet-e"></a> </div> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label">Flying Scot</p> <a class="absoluteA" href="/fleet-flying-scot"></a> </div> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label">Wallen All Sail</p> <a class="absoluteA" href="/wallen-all-sail"></a> </div> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label">Uncle Elmer's</p> <a class="absoluteA" href="/uncle-elmers"></a> </div> </div> </div> <div class="cs-header-link-wrap header-style_inline"> <p class="button_link_p" style="">Events</p> <!-- link --> <!-- handle sub-components --> <div class="cs-header-sublink-outer easeFast"> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label">Calendar</p> <a class="absoluteA" href="/event-calendar"></a> </div> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label">Club Rental</p> <a class="absoluteA" href="/club-rental"></a> </div> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label">Social Events</p> <a class="absoluteA" href="/dlyc-social-events-"></a> </div> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label">Reservations</p> <a class="absoluteA" href="/reservations-"></a> </div> </div> </div> <div class="cs-header-link-wrap header-style_inline"> <p class="button_link_p" style="">News</p> <!-- link --> <!-- handle sub-components --> <div class="cs-header-sublink-outer easeFast"> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label">Edmonds Videos</p> <a class="absoluteA" href="/edmonds"></a> </div> </div> </div> <div class="cs-header-link-wrap header-style_inline"> <p class="button_link_p" style="">Menus</p> <!-- link --> <!-- handle sub-components --> <div class="cs-header-sublink-outer easeFast"> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label">Menus</p> <a class="absoluteA" href="/menus"></a> </div> </div> </div> <div class="cs-header-link-wrap header-style_inline"> <p class="button_link_p" style="">Youth</p> <!-- link --> <!-- handle sub-components --> <div class="cs-header-sublink-outer easeFast"> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label">Day Camp</p> <a class="absoluteA" href="/day-camp"></a> </div> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label">DLSS</p> <a class="absoluteA" href="https://www.delavanlakesailingschool.com/"></a> </div> </div> </div> <div class="cs-header-link-wrap header-style_inline"> <p class="button_link_p" style="">About</p> <!-- link --> <!-- handle sub-components --> <div class="cs-header-sublink-outer easeFast"> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label">2024 early Advertisers</p> </div> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label">Advertisers</p> <a class="absoluteA" href="/advertisers"></a> </div> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label">FAQ</p> </div> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label">History</p> <a class="absoluteA" href="/history"></a> </div> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label">Contact Us</p> <a class="absoluteA" href="/contact-us-"></a> </div> </div> </div> <div class="cs-header-link-wrap header-style_inline"> <p class="button_link_p" style="">Merchandise</p> <!-- link --> <!-- handle sub-components --> <div class="cs-header-sublink-outer easeFast"> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label">DLYC Merchandise</p> <a class="absoluteA" href="https://dlycgear.itemorder.com/shop/home/"></a> </div> </div> </div> <div class="cs-header-link-wrap link-style-button header-style_inline"> <p class="button_link_p" style="background-color:#f77408 !important; color:#ffffff !important;">Member Login</p> <!-- link --> <a class="absoluteA" href="/login"></a> <!-- handle sub-components --> </div> </div> </div> </div> <!-- now the mobile menu --> <div class="mobileMenuOverlay hideOnDesktop ease"></div> <div class="mobileMenu hideOnDesktop easeFast scrollable"> <div class="innerMenu"> <div class="mobile-link-wrap"> <div class="cs-burgee-wrap" style="width:175px;height:60px;float:none;margin-top:-40px;margin-bottom:15px;"> <div class="cs-burgee" style="background-position:left;background-image:url(https://myclubspot.s3-us-west-2.amazonaws.com/yVq6rz613F_burgee_1574269334439);background-size:contain;margin-bottom:15px;"></div> <a :href="burgee_link" class="absoluteA"></a> </div> <div class="mobile-sublink-outer easeFast"> <div class="header-sublink-wrap relative"> <p class="header-sublink-label">My Account</p> <p class="header-sublink-explain">Click below to view your account</p> <button class="wb-button-primary easeFast boxShadowBottom smallMarginTop"> View my account <a class="absoluteA" href="/account"></a> </button> </div> <!-- component --> <p class="header-sublink-section-header smallMarginTop">Regattas</p> <!-- handle sub-components --> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label"> Regattas <a class="absoluteA" href="/regattas"></a> </p> </div> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label"> Lodging <a class="absoluteA" href="/lodging"></a> </p> </div> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label"> ILYA <a class="absoluteA" href="https://ilya.org/"></a> </p> </div> <!-- component --> <p class="header-sublink-section-header smallMarginTop"> Racing <a class="absoluteA" href="/race-schedule"></a> </p> <!-- handle sub-components --> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label"> Official Notice Board <a class="absoluteA" href="/official-notice-board"></a> </p> </div> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label"> X Boat <a class="absoluteA" href="/fleet-x-boat"></a> </p> </div> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label"> MC Scow <a class="absoluteA" href="/fleet-mc"></a> </p> </div> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label"> C Scow <a class="absoluteA" href="/fleet-c"></a> </p> </div> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label"> E Scow <a class="absoluteA" href="/fleet-e"></a> </p> </div> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label"> Flying Scot <a class="absoluteA" href="/fleet-flying-scot"></a> </p> </div> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label"> Wallen All Sail <a class="absoluteA" href="/wallen-all-sail"></a> </p> </div> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label"> Uncle Elmer's <a class="absoluteA" href="/uncle-elmers"></a> </p> </div> <!-- component --> <p class="header-sublink-section-header smallMarginTop">Events</p> <!-- handle sub-components --> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label"> Calendar <a class="absoluteA" href="/event-calendar"></a> </p> </div> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label"> Club Rental <a class="absoluteA" href="/club-rental"></a> </p> </div> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label"> Social Events <a class="absoluteA" href="/dlyc-social-events-"></a> </p> </div> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label"> Reservations <a class="absoluteA" href="/reservations-"></a> </p> </div> <!-- component --> <p class="header-sublink-section-header smallMarginTop">News</p> <!-- handle sub-components --> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label"> Edmonds Videos <a class="absoluteA" href="/edmonds"></a> </p> </div> <!-- component --> <p class="header-sublink-section-header smallMarginTop">Menus</p> <!-- handle sub-components --> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label"> Menus <a class="absoluteA" href="/menus"></a> </p> </div> <!-- component --> <p class="header-sublink-section-header smallMarginTop">Youth</p> <!-- handle sub-components --> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label"> Day Camp <a class="absoluteA" href="/day-camp"></a> </p> </div> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label"> DLSS <a class="absoluteA" href="https://www.delavanlakesailingschool.com/"></a> </p> </div> <!-- component --> <p class="header-sublink-section-header smallMarginTop">About</p> <!-- handle sub-components --> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label">2024 early Advertisers</p> </div> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label"> Advertisers <a class="absoluteA" href="/advertisers"></a> </p> </div> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label">FAQ</p> </div> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label"> History <a class="absoluteA" href="/history"></a> </p> </div> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label"> Contact Us <a class="absoluteA" href="/contact-us-"></a> </p> </div> <!-- component --> <p class="header-sublink-section-header smallMarginTop">Merchandise</p> <!-- handle sub-components --> <div class="cs-header-sublink-wrap relative"> <p class="cs-header-sublink-label"> DLYC Merchandise <a class="absoluteA" href="https://dlycgear.itemorder.com/shop/home/"></a> </p> </div> <!-- component --> <p class="header-sublink-section-header smallMarginTop"> Member Login <a class="absoluteA" href="/login"></a> </p> <!-- handle sub-components --> </div> </div> </div> </div> </script> <script> //define the component app_content.component("custom-page-header", { template: "#template-custom-page-header", data() { return {} }, props: {}, mounted(){ console.log("mounted hosted page header") }, methods: { }, computed: { burgee_link(){ if( this.is_react_native_member_app ){ return "/account" } return "/" }, current_user(){ return Parse.User.current() }, is_react_native_member_app(){ return store.is_react_native_member_app; } }, }); </script> <script type="text/html" id="template-image-grid"> <template v-if="!image_grid_layout || image_grid_layout !== 'carousel'"> <div class="image-grid-wrap centeredBlock" :class="'image-grid-wrap_' + client_id + ' images_per_row_' + images_per_row + ' ' + class_string" > <!-- draggable start for image-grid-image --> <div v-if="!loading && images?.length > 0" class="image-grid-insert flexWrapLeftAlign" :class=" is_admin ? 'draggable-container_image_grid_' + component.id : '' " > <template v-for="(image, index) in images" :key="image?.id"> <div v-if="image && image.url" class="image-grid-image relative" :class="'image-grid-image_' + image?.id" :id="image?.id" > <!-- video --> <div v-if="image.is_video" class="image-grid-image-inner" style="align-items:center;display:flex;" :style="{'border-radius': image_grid_border_radius + 'px' }" > <video class="cs-background-video" autoplay playsinline loop muted :src="image.url" style="display:block;"></video> <div v-if="is_admin" class="image-grid-drag-handle-wrap draggable-handle"> <i class="fa-regular fa-grip-dots image-grid-drag-handle"></i> </div> </div> <!-- image --> <div v-else class="image-grid-image-inner" :style="{'background-image': 'url(' + image.url + ')', 'border-radius': image_grid_border_radius + 'px' }" > <div v-if="is_admin" class="image-grid-drag-handle-wrap draggable-handle"> <i class="fa-regular fa-grip-dots image-grid-drag-handle"></i> </div> </div> <!-- overlay --> <div v-if="is_admin" class="image-grid-image-overlay flexWrap easeFast" :style="{ 'background-color': returnRGBAStringForImageGridImageOverlayOpacity( image_grid_opacity ), 'border-radius': image_grid_border_radius + 'px' }" @click.stop> <div class="image-toolbar-wrapper flexNoWrap centeredBlock" @click.stop> <div class="image-toolbar-option easeFast" data-option="update" style="border-right:1px solid #fff;" @click.stop="prepThenDisplayImageSettings(image)" >Update</div> <div class="image-toolbar-option easeFast" data-option="remove" @click.stop="tapped_remove_image(image)" ><i class="fa-regular fa-trash" style="font-size:14px;"></i></div> </div> <div class="image-text-wrapper" :style="{ alignItems: text_position === 'start' ? 'flex-start' : text_position === 'center' ? 'center' : 'flex-end' }"> <h2 v-if="image.caption" class="image-grid-image-caption" :style="{ 'font-size': caption_size + 'px', 'font-weight': caption_font_weight }" >{{ image.caption }}</h2> <p v-if="image.subtext" class="image-grid-image-caption marginTopFour" :style="{ 'font-size': subtext_size + 'px', 'font-weight': caption_font_weight }" >{{ image.subtext }}</p> <p v-if="image.link_label" class="image-grid-image-caption marginTopFour" style="text-decoration: underline;" :style="{ 'font-size': link_label_size + 'px', 'font-weight': caption_font_weight }" >{{ image.link_label }}</p> </div> </div> <div v-if="!is_admin" class="image-grid-image-overlay flexWrap easeFast" :style="{ 'background-color': returnRGBAStringForImageGridImageOverlayOpacity( image_grid_opacity ), 'border-radius': image_grid_border_radius + 'px' }"> <div class="image-text-wrapper" :style="{ alignItems: text_position === 'start' ? 'flex-start' : text_position === 'center' ? 'center' : 'flex-end' }"> <h2 v-if="image.caption" class="image-grid-image-caption" :style="{ 'font-size': caption_size + 'px', 'font-weight': caption_font_weight }" >{{ image.caption }}</h2> <p v-if="image.subtext" class="image-grid-image-caption marginTopFour" :style="{ 'font-size': subtext_size + 'px', 'font-weight': caption_font_weight }" >{{ image.subtext }}</p> <p v-if="image.link_label" class="image-grid-image-caption marginTopFour" style="text-decoration: underline;" :style="{ 'font-size': link_label_size + 'px', 'font-weight': caption_font_weight }" >{{ image.link_label }}</p> <a v-if="image.link_to" class="absoluteA" :href="image.link_to" rel="noopener noreferrer" ></a> </div> </div> </div> </template> <!-- upload image **for admin --> <template v-if="is_admin"> <div class="image-grid-image image-grid-image_upload non-draggable"> <div class="image-grid-image-inner"> <div class="uploadImageZone relative pointer"> <input @change="imageUploadChange($event)" class="image-grid-upload-input absoluteInput" type="file" accept="image/*,video/*" > <p class="centeredBlock" style="font-family:roboto !important;">+ Upload image/video</p> <div class="uploadImageOverlay"> <div class="uploadImageSpinner centeredBlock"></div> </div> </div> </div> </div> </template> </div> <!-- draggable end for image-grid-image --> <div v-if="!loading && images?.length === 0 && is_admin" class="image-grid-insert flexWrapLeftAlign"> <div class="image-grid-image image-grid-image_upload"> <div class="image-grid-image-inner"> <div class="uploadImageZone relative pointer"> <input @change="imageUploadChange($event)" class="image-grid-upload-input absoluteInput" type="file" accept="image/*,video/*" > <p class="centeredBlock" style="font-family:roboto !important;">+ Upload image/video</p> <div class="uploadImageOverlay"> <div class="uploadImageSpinner centeredBlock"></div> </div> </div> </div> </div> </div> <div v-if="loading" class="cardBodySpinnerWrap active relative"> <div class="cardBodySpinner centeredBlock"></div> </div> </div> </template> <template v-else> <div style="box-sizing: border-box; padding: 20px;" class="event-list-wrap centeredBlock centeredText" :class="'event-list-wrap_' + client_id"> <!-- <div class="flexNoWrap align-items-center event_list_carousel_default" style="padding: 0px 0px 15px 5px;" > <p>PLACEHOLDER</p> </div> --> <div class="image-grid-wrap centeredBlock" :class="'image-grid-wrap_' + client_id + class_string" style="padding: 20px;" > <div class="tableInsert event-list-card-wrapper" style="overflow: scroll !important; scrollbar-width: 5px !important; flex-wrap: nowrap;" :style="{ 'gap': formatting_obj.card_spacing ? Number(formatting_obj.card_spacing) + 'px' : '25px' }" > <template v-for="(image, index) in images" :key="image.id"> <div class="event-list-card noPadding noMargin" :class="{ 'small-card-image-grid': formatting_obj.card_size == 'small', 'medium-card-image-grid': !formatting_obj.card_size || formatting_obj.card_size == 'medium', 'large-card-image-grid': formatting_obj.card_size == 'large', 'box-shadow-light': formatting_obj.box_shadow == 'light', 'box-shadow-dark': formatting_obj.box_shadow == 'dark', }" :style="{'border-radius': image_grid_border_radius + 'px' ||'0px' }" > <div v-if="image && image.url" class="image-grid-image relative" :class="'image-grid-image_' + image?.id" :id="image?.id" > <!-- video --> <div v-if="image.is_video" class="image-grid-event-image" style="align-items:center;display:flex;" :style="{'border-radius': image_grid_border_radius + 'px' }" > <video class="cs-background-video" autoplay playsinline loop muted :src="image.url" style="display:block;"></video> </div> <!-- image --> <div v-else class="image-grid-event-image" :style="{'background-image': 'url(' + image.url + ')', 'border-radius': image_grid_border_radius + 'px' }" ></div> <!-- overlay --> <div @click.stop v-if="is_admin" class="image-grid-image-overlay flexWrap easeFast" style="left:0px;right:0px;bottom:0px;top:0px" :style="{ 'background-color': returnRGBAStringForImageGridImageOverlayOpacity( image_grid_opacity ), 'border-radius': image_grid_border_radius + 'px' }" > <div class="image-toolbar-wrapper flexNoWrap centeredBlock" @click.stop> <div class="image-toolbar-option easeFast" data-option="update" style="border-right:1px solid #fff;" @click.stop="prepThenDisplayImageSettings(image)" >Update</div> <div class="image-toolbar-option easeFast" data-option="remove" @click.stop="tapped_remove_image(image)" ><i class="fa-regular fa-trash" style="font-size:14px;"></i></div> </div> <div v-if="formatting_obj.caption_align !== 'under'" class="image-text-wrapper" :style="{ alignItems: text_position === 'start' ? 'flex-start' : text_position === 'center' ? 'center' : 'flex-end' }"> <h2 v-if="image.caption" class="image-grid-image-caption" :style="{ 'font-size': caption_size + 'px', 'font-weight': caption_font_weight }" >{{ image.caption }}</h2> <p v-if="image.subtext" class="image-grid-image-caption marginTopFour" :style="{ 'font-size': subtext_size + 'px', 'font-weight': caption_font_weight }" >{{ image.subtext }}</p> <p v-if="image.link_label" class="image-grid-image-caption marginTopFour" style="text-decoration: underline;" :style="{ 'font-size': link_label_size + 'px', 'font-weight': caption_font_weight }" >{{ image.link_label }}</p> </div> </div> <div v-if="!is_admin" class="image-grid-image-overlay flexWrap easeFast" style="left:0px;right:0px;bottom:0px;top:0px" :style="{ 'background-color': returnRGBAStringForImageGridImageOverlayOpacity( image_grid_opacity ), 'border-radius': image_grid_border_radius + 'px' }"> <div v-if="formatting_obj.caption_align !== 'under'" class="image-text-wrapper" :style="{ alignItems: text_position === 'start' ? 'flex-start' : text_position === 'center' ? 'center' : 'flex-end' }"> <h2 v-if="image.caption" class="image-grid-image-caption" :style="{ 'font-size': caption_size + 'px', 'font-weight': caption_font_weight }" >{{ image.caption }}</h2> <p v-if="image.subtext" class="image-grid-image-caption marginTopFour" :style="{ 'font-size': subtext_size + 'px', 'font-weight': caption_font_weight }" >{{ image.subtext }}</p> <p v-if="image.link_label" class="image-grid-image-caption marginTopFour" style="text-decoration: underline;" :style="{ 'font-size': link_label_size + 'px', 'font-weight': caption_font_weight }" > <a v-if="image.link_to" class="absoluteA" :href="image.link_to" rel="noopener noreferrer" >{{ image.link_label }}</a> </p> <a v-if="image.link_to" class="absoluteA" :href="image.link_to" rel="noopener noreferrer" ></a> </div> </div> </div> <div v-if="formatting_obj.caption_align === 'under'" class="image-grid-card-content background" :style="{'border-radius': image_grid_border_radius + 'px' ||'0px' }" > <div class="image-text-wrapper" :style="{ alignItems: text_position === 'start' ? 'flex-start' : text_position === 'center' ? 'center' : 'flex-end' }"> <h2 v-if="image.caption" class="image-grid-image-caption visible" :style="{ 'font-size': caption_size + 'px', 'font-weight': caption_font_weight }" >{{ image.caption }}</h2> <p v-if="image.subtext" class="image-grid-image-caption visible marginTopFour" :style="{ 'font-size': subtext_size + 'px', 'font-weight': caption_font_weight }" >{{ image.subtext }}</p> <p v-if="image.link_label" class="image-grid-image-caption visible marginTopFour" style="text-decoration: underline;" :style="{ 'font-size': link_label_size + 'px', 'font-weight': caption_font_weight }" >{{ image.link_label }}</p> </div> </div> </div> </template> <!-- upload image **for admin --> <template v-if="is_admin"> <div class="event-list-card pointer" :class="{ 'small-card-image-grid': formatting_obj.card_size == 'small', 'medium-card-image-grid': !formatting_obj.card_size || formatting_obj.card_size == 'medium', 'large-card-image-grid': formatting_obj.card_size == 'large', }" > <div class="image-grid-image relative upload-image-grid-image-zone"> <div class="image-grid-event-image"></div> <input @change="imageUploadChange($event)" class="image-grid-upload-input absoluteInput" type="file" accept="image/*,video/*" > <p class="centeredBlock" style="font-family:roboto !important;">+ Upload image/video</p> <!-- <div class="uploadImageOverlay"> <div class="uploadImageSpinner centeredBlock"></div> </div> --> </div> <div v-if="formatting_obj.caption_align === 'under'" class="image-grid-card-content" :style="{'border-radius': image_grid_border_radius + 'px' ||'0px' }" > <div class="image-text-wrapper" :style="{ alignItems: text_position === 'start' ? 'flex-start' : text_position === 'center' ? 'center' : 'flex-end' }"> <!-- PLACEHOLDER --> </div> </div> </div> </template> </div> </div> </div> </template> </script> <style> .image-grid-image.dragging{ overflow: hidden; border-radius: 10px; } .image-grid-image.dragging .image-grid-image-inner { transition: all 0.35s ease-in-out; border: 1.2px solid #fff; border-radius: 10px; z-index: 1000; transform: scale(1.1); cursor: grab; cursor: move; } .image-grid-image.dragging .image-grid-drag-handle-wrap { display: none; position: relative; } .event-list-card { overflow: hidden; background: white; font-family: 'Inter', sans-serif; display: flex; flex-direction: column; flex: 0 0 auto; transition: transform 0.1s ease; } .event-list-card.small-card-image-grid { width: 190px; } .event-list-card.medium-card-image-grid { width: 230px; } .event-list-card.large-card-image-grid { width: 300px; } .event-list-card.box-shadow-light { box-shadow: 2px 4px 8px rgba(0, 0, 0, 0.2); } .event-list-card.box-shadow-dark { box-shadow: 6px 6px 16px rgba(0, 0, 0, 0.2); } .event-list-card .image-grid-event-image { display: flex; text-align: center; align-items: center; width: 100%; height: 100%; background-size: cover; background-repeat: no-repeat; background-position: center; color: #fff; position: absolute; } .image-grid-card-content { height: 100px; padding: 12px 5px; } .upload-image-grid-image-zone{ color: #323d63; text-align: center; border-radius: 2px; background-color: #f1f5f9; position: relative; display: flex; align-items: center; } </style> <script> app_content.component("image-grid-component", { template: "#template-image-grid", data() { return { global: store, loading: false, }; }, props: { client_id: String, class_string: String, image_grid_layout: String, images_per_row: [String, Number], caption_size: String, subtext_size: String, link_label_size: String, text_position: String, caption_font_weight: String, image_grid_opacity: String, images_string: String, image_grid_border_radius: String, // carousel formatting: String, // admin is_admin: false, data_source_type: String, images_arr: Array, component: Object, last_update: Number, }, mounted() { this.$nextTick(() => { if (this.is_admin && this.images_arr){ this.makeElementsSortable_image_grid(); } }) }, methods: { makeElementsSortable_image_grid(){ //now create the new instance const containerSelector = `.draggable-container_image_grid_${this.component.id}`; const containers = document.querySelectorAll(containerSelector); if (containers?.length === 0) { return false; } const sortable = new Draggable.Sortable(containers, { draggable: '.image-grid-image:not(.non-draggable)', handle: '.draggable-handle', }); //now register listeners sortable.on('sortable:start', event => { var target = event.data.dragEvent.source; target.classList.add('dragging'); }); sortable.on('sortable:stop', event => { var target = event.data.dragEvent.source; target.classList.remove('dragging'); //now handle ui updates inside a timeout due to a draggable library issue (https://github.com/Shopify/draggable/issues/92) setTimeout(() => { this.handle_sortable_stop( event ); }, 0) }); }, handle_sortable_stop( event ){ let targetId = event.data.dragEvent.source.id; let dropElement = document.getElementById(targetId); var reordered_ids = Array.from(event.newContainer.children).map(event => event.id); reordered_ids.pop(); //pop off last value which is an empty string placeholder for image upload var reordered_images = []; reordered_ids.forEach(image_id => { //find the image var this_image = null; this.images.forEach(image => { if( image.id == image_id ){ this_image = image } }); if( this_image ){ reordered_images.push( this_image ); } }); //save changes save_changes_for_component( this.component, 'images', reordered_images ); }, returnRGBAStringForImageGridImageOverlayOpacity( opacity ){ if( ! opacity && opacity !== 0 ){ opacity = 40; } return "rgba(8,8,19,"+ opacity / 100 +")"; }, confirmed_remove_image( data ){ var image = data.image; var component = data.component; //find existing images var images = component.get('config_latest').images || []; //loop through to remove this one var new_images = []; for (var i = 0; i <= images?.length - 1; i++) { if( images[i].id !== image.id ){ new_images.push( images[i] ); } } //now save and update the preview save_changes_for_component( component, 'images', new_images ); store.overlays.are_you_sure = false; }, tapped_remove_image( image ){ //ask for confirmation var params = {}; params["fn"] = this.confirmed_remove_image; params["title"] = "Remove image"; params["subtext"] = "Are you sure you want to permanently remove this tile from the image grid?"; params["button_label"] = "Remove"; params["data"] = { "image": image, "component": this.component }; are_you_sure( params ); return }, prepThenDisplayImageSettings(imageObject){ store.components.image_settings_overlay.componentObject = this.component; store.components.image_settings_overlay.imageObject = imageObject; store.components.image_settings_overlay.settings = { link_input: imageObject.link_to || '', caption_input: imageObject.caption || '', subtext_input: imageObject.subtext || '', link_label_input: imageObject.link_label || '', } store.components.image_settings_overlay.image_grid_props = { client_id: this.client_id, caption_size: this.caption_size, caption_font_weight: this.caption_font_weight, }; this.global.overlays.image_settings_overlay = !this.global.overlays.image_settings_overlay; this.$nextTick(() => { this.updatePositionForImageSettings( imageObject ); }); }, updatePositionForImageSettings(imageObject){ var image_object = imageObject; if( ! image_object ){ return } var element = $(".image-grid-image_" + image_object.id); if( ! element ){ return } var offset = element.offset(); var top = offset.top; var bottom = top + element.height(); var correct_top = top - ( $('.image-settings').height() + 25 ); store.components.image_settings_overlay.position = { top: `${correct_top}px`, left: `${offset.left}px` }; }, imageUploadChange($event){ var file = event.target.files[0]; var componentObject = this.component; if(file && componentObject){ //update the ui $(this).parents('.uploadImageZone').addClass('loading'); //set attributes var type = 'image_grid'; var timestamp = Date.now(); //check the file type var is_video = this.returnIsVideo( file['name'] ) || false; if( is_video ){ //ok, it's a video //.. handle the video var file_name = componentObject.get('client_id')+"_grid_video_"+timestamp; return uploadFileToAWSV2( file, file_name ).then(( url )=>{ this.handleUploadedImageGridImage( url, componentObject, true ); }); }else{ //assume it's an image! var file_name = componentObject.get('client_id')+"_grid_image_"+timestamp; //handle the image loadCleanImage(file, this.handleCleanImage_image_grid, type, file_name, componentObject.get('client_id') ); } } //reset input value to detect change if same file is uploaded event.target.value = ''; }, returnIsVideo(filename) { var ext = this.getExtension(filename); switch (ext.toLowerCase()) { case 'm4v': case 'avi': case 'mpg': case 'mp4': case 'mp3': case 'mov': // etc return true; } return false; }, getExtension(filename) { var parts = filename.split('.'); return parts[parts?.length - 1]; }, completed_crop(params){ const { fileType, is_video, file_name, blob, componentObject } = params; //upload it return uploadFileToAWSV2( blob, file_name ).then(( url )=>{ this.handleUploadedImageGridImage( url, componentObject, is_video ); }); }, handleCleanImage_image_grid( canvas, type, file_name, componentID, image_index, is_video, fileType ){ $('body').data('can', canvas); let componentObject = this.component; if(!fileType) { fileType = 'image/jpeg' } var url = canvas.toDataURL(fileType); var params = {}; params["url"] = url; params["fn"] = this.completed_crop; params["componentID"] = componentID; params["fileType"] = fileType; params["is_video"] = is_video || false; params["file_name"] = file_name; params["componentObject"] = componentObject; launch_image_cropper( params ); }, handleUploadedImageGridImage(url, componentObject, is_video){ if( componentObject ){ //it's still in the dom //.. update it var images = componentObject.get('config_latest')?.images || []; images.push({ "id": generateClientID(), "url": url, "is_video": is_video }); save_changes_for_component( this.component, 'images', images ); //store.eventBus.$emit('saveChangesForComponent', this.component, option, value) //message if( is_video ){ displayMessage("Video uploaded. It may take a moment for your video to appear.", false, 5000); }else{ displayMessage("Image uploaded.", false, 3000); } } }, }, computed: { images(){ if (!this.is_admin && this.images_string){ try { const parsedObject = JSON.parse(this.images_string); return parsedObject } catch (error) { console.error('Error parsing JSON: ', error) } } else if (this.is_admin && this.images_arr){ return this.images_arr || []; } }, formatting_obj(){ if (this.formatting){ return JSON.parse(this.formatting); } return {}; }, }, }) </script> <script type="text/html" id="template-photo-gallery"> <div class="photo-gallery-wrap centeredBlock centeredText"> <!-- hosted site --> <div v-if="!loading && !is_admin"> <div v-if="photo_album_id && json_photos" class="photo-gallery" > <div v-for="(photo, index) in json_photos" class="photo-container" :id="photo.id" @click="clicked_gallery_photo( index )" :style="calculate_container_dimensions()" > <video v-if="photo && returnIsVideo(photo.file_name)" class="video-cover paddingTop" autoplay muted loop playsinline> <source :src="photo.url" type="video/mp4"> </video> <div v-else-if="photo && photo.url" class="photo-cover" :style="'background-image:url('+encodeURI(photo.url)+')'" ></div> <!-- overlay --> <div class="photo-overlay"></div> </div> </div> </div> <!-- admin --> <div v-if="!loading && is_admin"> <div v-if="!photo_album_id" @click="link_photo_album()" class="noObjectsMSG centeredBlock centeredText" style="padding: 5em;" > <p style="cursor: pointer; font-family: 'Roboto', 'Arial' !important;"><i class="fa-light fa-images smallMarginRight"></i>Click to link a photo album</p> </div> <div v-else-if="photo_album_id && json_photos" class="photo-gallery" > <div v-for="(photo, index) in json_photos" class="photo-container" :id="photo.id" @click="clicked_gallery_photo( index )" :style="calculate_container_dimensions()" > <video v-if="photo && returnIsVideo(photo.file_name)" class="video-cover paddingTop" autoplay muted loop playsinline> <source :src="photo.url" type="video/mp4"> </video> <div v-else-if="photo && photo.url" class="photo-cover" :style="'background-image:url('+encodeURI(photo.url)+')'" ></div> <!-- overlay --> <div class="photo-overlay"></div> </div> </div> </div> </div> </script> <style> .photo-gallery-wrap{ height: fit-content; width: 100%; } .photo-gallery{ display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-start; /* center for mobile */ } .photo-container { position: relative; box-sizing: border-box; margin: 0.3em; text-align: center; } .photo-container .video-cover { width: 100%; object-fit: cover; } .photo-container > .photo-cover { height: 100%; width: inherit; display: block; overflow: hidden; margin: 0 auto; background-size: cover; background-position: center; background-repeat: no-repeat; opacity: 1; } .photo-overlay{ position: absolute; display: flex; align-items: center; justify-content: center; top: 0; left: 0; height: 100%; width: 100%; opacity: 0; background-color: rgba(8,8,19,0.3); transition: opacity 0.2s ease-out; } .photo-container:hover .photo-overlay { cursor: pointer; opacity: 0.8 !important; } </style> <script> app_content.component("photo-gallery-component", { template: "#template-photo-gallery", data() { return { global: store, loading: false, json_photos: [], }; }, props: { client_id: String, is_admin: Boolean, component: Object, last_update: Number, photo_album: [String, Object], show_caption: Boolean, caption_size: Number, caption_font_weight: Number, photos_per_row: Number, }, mounted() { this.load_photo_albums( this.client_id ); }, methods: { calculate_container_dimensions(){ const percentage = 100 / this.photos_per_row; const isMobile = window.innerWidth <= 600; if ( isMobile ){ return { flex: `1 1 calc(${percentage}% - 0.6em)`, maxWidth: `calc(${percentage}% - 0.6em)`, height: `calc(${100}px * (${percentage} / 100) * 2.5)`, }; } return { flex: `1 1 calc(${percentage}% - 0.6em)`, maxWidth: `calc(${percentage}% - 0.6em)`, height: `calc(${500}px * (${percentage} / 100) * 2.5)`, }; }, load_photo_albums( client_id, overrides = {} ){ this.loading = true; var promise = Parse.Promise.as(); var thisPromise = promise.then(() => { var params = {}; params["client_id"] = client_id; params["photo_album_id"] = this.photo_album_id; params["overrides"] = overrides; params["request_url"] = window.location.href; return Parse.Cloud.run('retrieve_data_for_component', params); }).then(retrieved_photos => { this.json_photos = retrieved_photos; return Parse.Promise.as(); }).then(() => { this.loading = false; }, error => { checkParseError(error); this.loading = false; }); return thisPromise; }, clicked_gallery_photo( index ){ store.components.view_photo_carousel.json_photos = this.json_photos; store.components.view_photo_carousel.current_index = index; store.components.view_photo_carousel.json_config = { show_caption: this.show_caption || false, caption_size: this.caption_size || 16, caption_font_weight: this.caption_font_weight || 400, }; store.overlays.view_photo_carousel = true; }, link_photo_album(){ store.components.link_photo_album.component = this.component; store.components.link_photo_album.last_update = this.last_update; store.overlays.link_photo_album = true; }, returnIsVideo(filename) { var ext = this.getExtension(filename); switch (ext.toLowerCase()) { case 'm4v': case 'avi': case 'mpg': case 'mp4': case 'mp3': case 'mov': // etc return true; } return false; }, getExtension(filename) { var parts = filename.split('.'); return parts[parts.length - 1]; }, }, computed: { photo_album_obj(){ if (this.photo_album && typeof this.photo_album === 'string'){ return JSON.parse(unescape(decodeURIComponent(this.photo_album))); } else if (this.photo_album){ return this.photo_album; } return; }, photo_album_id(){ return this.photo_album_obj.id || null; }, photo_album_name(){ return this.photo_album_obj.name || null; }, }, watch: { last_update(newVal, oldVal){ if (newVal !== oldVal){ this.load_photo_albums( this.client_id ); } } } }) </script> <script type="text/html" id="template-membership-calendar"> <!-- calendar grid component --> <calendar-grid-component v-if="loadCalendarGrid" :client_id="client_id" :class_string="class_string" :style_string="style_string" :filterOptions="filterOptions" :schedules="schedules" :selectedFilterOptions="selectedFilterOptions" :is_membership_cal="is_membership_cal" @handleFilterSelection="handleFilterSelection" @load_cal="load_membership_cal" @createSchedule="createSchedule" @clickSchedule="clickSchedule" ref="calendarComponentRef" > </calendar-grid-component> <!-- spinner --> <div v-if="!loadCalendarGrid" class="cardBodySpinnerWrap relative active marginTop"> <div class="cardBodySpinner centeredBlock"></div> </div> </script> <script> /* ----- STORE ----- */ store.components['membership_cal'] = { clubId: null, // client_id: 'primary', filterOptions: [], events: [], recurringEvents: [], schedules: [], loadCalendarGrid: false, selectedFilterOptions: [ 'all' ], is_admin: false, is_membership_cal: true, config_obj: {}, }; /* ----- COMPONENT ----- */ app_content.component('membership-calendar-component', { template: '#template-membership-calendar', data() { return store.components['membership_cal']; }, props: { config: String, client_id: String, class_string: String, style_string: String, //admin component: Object, last_update: Number, }, mounted() { if (this.config){ this.config_obj = JSON.parse(this.config); } this.clubId = $('body').data('club-id'); this.load_membership_cal(this.client_id); this.is_admin = window.location.href.indexOf('website-builder') !== -1; this.checkThenHandleEventTagsForCalendar(this.client_id); //eventBus for overflow-overlay store.eventBus.$on('clickSchedule_membership_cal', (event) => { this.clickSchedule(event) }) }, created() { if(store.components.calendar_grid.view_preference !== 'Month') { this.loadCalendarGrid = false; this.setViewPreference(); } store.components.calendar_grid.currentDate = dayjs() || new Date(); }, methods: { async setViewPreference() { store.components.calendar_grid.view_preference = 'Month'; }, checkThenHandleEventTagsForCalendar(client_id) { //see if we should get tags, or if we already have them var event_tags = $(`.cs-calendar-wrap_${ client_id }`).data('event_tags'); if(! event_tags) { var club_id = $('body').data('club-id'); if(! club_id && $('body').data('clubObject')) { club_id = $('body').data('clubObject').id; } //ok, we should query for them var query = new Parse.Query('event_tags'); query.equalTo('clubObject', { __type: 'Pointer', objectId: club_id, className: 'clubs' }); query.equalTo('archived', false); query.ascending('name'); query.find().then(retrieved_tags => { $(`.cs-calendar-wrap_${ client_id }`).data('event_tags', retrieved_tags); // $('.cs-calendar-wrap_'+ client_id).find('.insert_event_tags').html(''); // $('.cs-calendar-wrap_'+ client_id).find('.insert_event_tags').append('<div class="cs-tab-bar-option cs-tab-bar-option_event_tags cs-tab-bar-option_event_tags_all mobile-order active" data-option="all">All events</div>'); //plug them in this.filterOptions = [{ id: 'all' }, ...retrieved_tags ].map((tag, i) => { if(i === 0) { return { name: 'All events', data_option: tag.id }; } else { return { name: tag.get('name'), data_option: tag.id }; } }); }); } }, load_membership_cal(client_id) { this.loadCalendarGrid = false; var promise = Parse.Promise.as(); var thisPromise = promise.then(() => { //get data var { startTimestamp, endTimestamp } = this.getRenderRangeForWebBuilderCalendar(); var params = {}; params['startTimestamp'] = startTimestamp; params['endTimestamp'] = endTimestamp; if(this.is_admin || $(`#componentPreview_${ this.client_id }`).data('componentObject')) { // var calendarComponentObject = $(`#componentPreview_${ this.client_id }`).data('componentObject'); // let event_types = this.component?.get('config_latest').event_types || []; /* returnEventTypesForComponent(calendarComponentObject); */ let event_types = this.event_types; params['filters'] = event_types.length > 0 ? event_types : null; } else { // let event_types = this.config_obj.event_types; let event_types = this.event_types; params['filters'] = event_types?.length ? [...event_types] : null; //set filters on server params['client_id'] = this.client_id; } params['club_id'] = this.clubId; // filtering logic var event_tag_id = this.selectedFilterOptions; if(this.selectedFilterOptions.indexOf('all') === -1) { params['event_tag_id'] = event_tag_id[0]; } return Parse.Cloud.run('retrieve_events_for_primary_calendar_v2', params); }).then((foundEventsJSON) => { events = foundEventsJSON.events; var socialEvents = events.filter(e => e.className == 'events'); var recurringEvents = this.formatRecurringCalendarEvents(foundEventsJSON.recurring_events, socialEvents); $('body').data('recurringCalendarEvents', recurringEvents); //vue this.events = events; this.recurringEvents = recurringEvents; return Parse.Promise.as(); }).then(() => { this.pluginEvents(); this.loadCalendarGrid = true; this.$nextTick(() => { this.$refs.calendarComponentRef.calculateTitleRange(); //ref on child component this.$refs.calendarComponentRef.generateCalendar(); //ref on child component }); //and return return Parse.Promise.as(); }); return thisPromise; }, getRenderRangeForWebBuilderCalendar() { // var client_id = $('.cs-calendar-wrap').data('client-id'); // var calendar = $('.cs-calendar-wrap_'+client_id).data('calendar'); var calendarDate = store.components.calendar_grid.currentDate ? new Date(new Date(store.components.calendar_grid.currentDate).setHours(0, 0, 0, 0)) : new Date(new Date().setHours(0, 0, 0, 0)); var calendarMonth = calendarDate.getMonth(); var startTimestamp = new Date(calendarDate).setMonth(calendarMonth - 1, 15); var endTimestamp = new Date(calendarDate).setMonth(calendarMonth + 1, 15); return { startTimestamp, endTimestamp }; }, formatRecurringCalendarEvents(recurringEvents, socialObjects) { var recurringCalendarEvents = []; for(var i = 0; i < recurringEvents.length; i++) { var thisEventObject = recurringEvents[i]; var socialObject = socialObjects.filter(s => s.id == thisEventObject.raw.eventObject)[0]; var correctedTs = thisEventObject.ts; //to handle daylight savings if(new Date(thisEventObject.ts).getHours() == 23) { correctedTs += 1000 * 60 * 60; } else if(new Date(thisEventObject.ts).getHours() == 1) { correctedTs -= 1000 * 60 * 60; } thisEventObject.ts = correctedTs; var startTs = new Date(thisEventObject.ts).setHours(thisEventObject.hours_mins_start.hours, thisEventObject.hours_mins_start.minutes, 0, 0); var endTs = new Date(thisEventObject.ts).setHours(thisEventObject.hours_mins_end.hours, thisEventObject.hours_mins_end.minutes, 0, 0); thisEventObject['start'] = new Date(startTs).toISOString(); thisEventObject['end'] = new Date(endTs).toISOString(); var color = '#e8edf1'; //default var dot_color = null; var event_tags = socialObject.get('event_tags'); if(event_tags) { for(var t = 0; t <= event_tags.length - 1; t++) { if(! event_tags[t].get('archived')) { if(event_tags[t].get('hex_value')) { color = event_tags[t].get('hex_value'); thisEventObject['color'] = '#ffffff'; dot_color = event_tags[t].get('hex_value'); } } } } thisEventObject['bgColor'] = color; thisEventObject['recurring'] = true; thisEventObject['external_event_url'] = socialObject.get('external_event_url'); thisEventObject['description'] = socialObject.get('description'); // //add // thisEventObject["className"] = socialObject.className; if(dot_color && thisEventObject.raw) { thisEventObject.raw.dot_color = dot_color; } else if(dot_color && !thisEventObject.raw) { thisEventObject.raw = { dot_color }; } var isAllDay = socialObject.get('all_day'); if(isAllDay) { thisEventObject.title = socialObject.get('name'); } thisEventObject.isAllDay = isAllDay; recurringCalendarEvents.push(thisEventObject); } return recurringCalendarEvents; }, pluginEvents() { var schedules = this.recurringEvents || []; for(var i = 0; i <= this.events.length - 1; i++) { var this_event = this.events[i]; var this_start_date = this_event.get('startDate'); if(this_start_date) { var this_end_date = this_event.get('endDate'); if(this_end_date) { if(! this_event.get('endTime')) { this_end_date.setDate(this_end_date.getDate() + 1); } } else { this_end_date = this_start_date; } //convert start/end dates from utc to browser date this_start_date = returnOffsetDateForUTCDate(this_start_date); this_end_date = returnOffsetDateForUTCDate(this_end_date); //handle start/end times set explicitly on the event if(this_event.get('startTime') && this_event.get('endTime')) { var start_data = returnHoursAndMinutesForMilitaryTime(this_event.get('startTime')); this_start_date.setHours(this_start_date.getHours() + start_data.hours); this_start_date.setMinutes(this_start_date.getMinutes() + start_data.minutes); var end_data = returnHoursAndMinutesForMilitaryTime(this_event.get('endTime')); this_end_date.setHours(this_end_date.getHours() + end_data.hours); this_end_date.setMinutes(this_end_date.getMinutes() + end_data.minutes); } var seconds = (this_end_date.getTime() - this_start_date.getTime()) / 1000; //continue var this_object = {}; this_object['id'] = this_event.id; this_object['calendarId'] = this.client_id; //add this_object['className'] = this_event.className; this_object['calendarOnly'] = this_event.get('calendarOnly'); this_object["raw"] = {}; //the code below is to account for all day events (only single day) on dates where there is a dls change //START DLS LOGIC var start_offset = this_start_date.getTimezoneOffset(); var end_offset = this_end_date.getTimezoneOffset(); var hasEndTime = this_event.get('endTime'); var is_dls_all_day = false; if(start_offset != end_offset && seconds <= 93600) { //ok its an all day event for just a particular day where the offset is changing (dls) //dont need to change the endDate for objects with an endTime as those will be correctly set already if(start_offset > end_offset && !hasEndTime) { this_end_date = new Date(new Date(this_end_date).setHours(this_end_date.getHours() + 1, 0, 0, 0)); } else if(start_offset < end_offset && !hasEndTime) { this_end_date = new Date(new Date(this_end_date).setHours(this_end_date.getHours() - 1, 0, 0, 0)); } is_dls_all_day = true; } //END DLS LOGIC if( this_start_date.getTime() == this_end_date.getTime() || seconds == 86400 || is_dls_all_day){ this_object["category"] = "allday"; this_object["isAllDay"] = true; this_object["title"] = this_event.get('name'); }else{ this_object["category"] = "time"; this_object["isAllDay"] = false; this_object["title"] = this_event.get('name'); if( seconds < 86400 ){ var nice_time = formatHoursAndMinutesForDate( this_start_date ); this_object.raw.nice_time = nice_time; } } //check event type if(this_event.className == 'regattas' || this_event.className == 'subevents') { /* this_object['bgColor'] = '#87d4f9'; this_object.raw.dot_color = '#87d4f9'; */ var bgColor = "#87d4f9"; //default var dot_color = "#87d4f9"; var event_tags; if ( this_event.className === "regattas" ){ event_tags = this_event.get('event_tags'); } else if ( this_event.className === "subevents" && this_event.get('regattaObject') ){ event_tags = this_event.get('regattaObject').get('event_tags'); } if( event_tags ){ event_tags.forEach(event_tag => { if ( ! event_tag.get('archived') ){ bgColor = event_tag.get('hex_value'); dot_color = event_tag.get('hex_value'); } }) } this_object["bgColor"] = bgColor; this_object.raw.dot_color = bgColor; } else if(this_event.className == 'camps') { /* this_object['bgColor'] = '#236651'; this_object.raw.dot_color = '#236651'; this_object['color'] = '#ffffff'; */ var bgColor = "#236651"; //default var dot_color = "#236651"; var event_tags = this_event.get('event_tags'); if( event_tags ){ event_tags.forEach(event_tag => { if ( ! event_tag.get('archived') ){ bgColor = event_tag.get('hex_value'); dot_color = event_tag.get('hex_value'); } }) } this_object["bgColor"] = bgColor; this_object.raw.dot_color = bgColor; this_object["color"] = "#ffffff"; } else { var color = '#e8edf1'; //default var dot_color = null; var event_tags = this_event.get('event_tags'); if(event_tags) { for(var t = 0; t <= event_tags.length - 1; t++) { if(! event_tags[t].get('archived')) { if(event_tags[t].get('hex_value')) { color = event_tags[t].get('hex_value'); this_object['color'] = '#ffffff'; dot_color = event_tags[t].get('hex_value'); } } } } this_object['bgColor'] = color; this_object.raw.dot_color = dot_color; } //set dates // this_object['start'] = this_start_date.toISOString(); // this_object['end'] = this_end_date.toISOString(); if(this_event.className == 'regattas' || this_event.className == "subevents" && this_event.get('startDate') && this_event.get('endDate')) { var clubObject = this_event.get('clubObject'); var start_date_params = {timezone: clubObject.get('timezone'), date: this_event.get('startDate')}; var end_date_params = {timezone: clubObject.get('timezone'), date: this_event.get('endDate')}; var start_time_components_at_club = this.return_date_components_v2(start_date_params); var end_time_components_at_club = this.return_date_components_v2(end_date_params); this_object["start"] = new Date(start_time_components_at_club.year, start_time_components_at_club.month, start_time_components_at_club.date).toISOString(); this_object["end"] = new Date(end_time_components_at_club.year, end_time_components_at_club.month, end_time_components_at_club.date).toISOString(); } else { this_object["start"] = this_start_date.toISOString(); this_object["end"] = this_end_date.toISOString(); } //push it if not recurring (handled already) if(!this_event.get('recurring_event')) { schedules.push(this_object); } } } // this.schedules = schedules; // this.schedules = schedules.sort((a, b) => { return dayjs(a.start).isBefore(dayjs(b.start)) ? -1 : 1 }); this.schedules = schedules.sort((a, b) => { if( !dayjs(a.start).isSame(dayjs(a.end), 'day') && !dayjs(b.start).isSame(dayjs(b.end), 'day') ) { //both schedules have different start and end dates, compare them return dayjs(a.start).isBefore(dayjs(b.start)) ? -1 : 1; } else if(!dayjs(a.start).isSame(dayjs(a.end), 'day')) { //only schedule a has different start and end dates, so it comes first return -1; } else if(!dayjs(b.start).isSame(dayjs(b.end), 'day')) { //only schedule b has different start and end dates, so it comes first return 1; } else { //both schedules have the same start and end dates, compare them return dayjs(a.start).isBefore(dayjs(b.start)) ? -1 : 1; } }); }, handleFilterSelection(optionObject) { store.components.calendar_grid.loading = true; this.selectedFilterOptions = [ optionObject.data_option ]; return this.load_membership_cal(); }, clickSchedule(event) { if(! this.is_admin) { var is_recurring_event = event.recurring; // open detail view var this_event = returnObjectByID(event.id, this.events); if(is_recurring_event) { var { external_event_url, description, title, start, end, ts } = event; if(external_event_url) { //its external, just pop open the window return window.open(external_event_url); } $('#overlay_event-description-popup h3').text(title); $('#overlay_event-description-popup .metric_start_date').text(formatUTCDate(new Date(ts), true, false, true)); var start_date_time_string = formatTimestampDate(new Date(start)).split(', ')[1]; if(end) { start_date_time_string += ` - ${ formatTimestampDate(new Date(end)).split(', ')[1] }`; } $('#overlay_event-description-popup .metric_start_date_time').text(start_date_time_string); if(event.description) { $('#overlay_event-description-popup .metric_description').text(event.description).removeClass('incomplete'); } else { $('#overlay_event-description-popup .metric_description').text('Not provided').addClass('incomplete'); } fadeIn('#overlay_event-description-popup'); } else if(this_event) { if(this_event.className == 'regattas') { if(this_event.get('regatta_external') && this_event.get('external_regatta_url')) { window.open(this_event.get('external_regatta_url')); } else { window.open(`/regatta/${ this_event.id }`); } } else if(this_event.className == 'subevents') { //go to microsite var regattaObject = this_event.get('regattaObject'); window.open(`/regatta/${ regattaObject.id }`); } else if(this_event.className == 'events') { if(this_event.get('external_event') && this_event.get('external_event_url')) { window.open(this_event.get('external_event_url')); } else if(! this_event.get('calendarOnly')) { window.open(`/event/${ this_event.id }`); } else { $('#overlay_event-description-popup h3').text(this_event.get('name')); if(is_recurring_event) { $('#overlay_event-description-popup .metric_start_date').text(formatUTCDate(new Date(this_event.start), true, false, true)); } else { $('#overlay_event-description-popup .metric_start_date').text(formatUTCDate(this_event.get('startDate'), true, false, true)); } var startTime = this_event.get('startTime'); var endTime = this_event.get('endTime'); var startTime_hours_mins = returnHoursAndMinutesForMilitaryTime(startTime); var endTime_hours_mins = returnHoursAndMinutesForMilitaryTime(endTime); var dateTime_start = new Date(new Date().setHours(startTime_hours_mins.hours, startTime_hours_mins.minutes)); var dateTime_end = new Date(new Date().setHours(endTime_hours_mins.hours, endTime_hours_mins.minutes)); var start_date_time_string = formatTimestampDate(dateTime_start).split(', ')[1]; if(this_event.get('endDate')) { start_date_time_string += ` - ${ formatTimestampDate(dateTime_end).split(', ')[1] }`; } $('#overlay_event-description-popup .metric_start_date_time').text(start_date_time_string); if(this_event.get('description')) { $('#overlay_event-description-popup .metric_description').text(this_event.get('description')).removeClass('incomplete'); } else { $('#overlay_event-description-popup .metric_description').text('Not provided').addClass('incomplete'); } fadeIn('#overlay_event-description-popup'); } } else if(this_event.className == 'camps') { //go to registration window.open(`/register/camp/${ this_event.id }/class`); } } } }, createSchedule(){ //placeholder prop }, return_date_components_v2( params ){ return return_date_components_v2( params ); } }, computed: { event_types(){ if(this.is_admin) { return this.component?.get('config_latest').event_types || []; } else { return this.config_obj.event_types; } }, }, watch: { last_update(newVal, oldVal){ if (newVal !== oldVal){ if (this.config){ this.config_obj = JSON.parse(this.config); } this.clubId = $('body').data('club-id'); this.load_membership_cal(this.client_id); this.is_admin = window.location.href.indexOf('website-builder') !== -1; this.checkThenHandleEventTagsForCalendar(this.client_id); } } }, }); </script> <script type="text/html" id="template-membership-directory"> <div class="cs-background-image relative" :class="class_string" :style="style_string"> <template v-if="formatting && formatting.overlay_opacity"> <div class="cs-image-overlay" :style="{ 'opacity': formatting.overlay_opacity.toString() }"></div> </template> <div class="element-wrapper" :class="'element-wrapper_' + component_type" :style="element_style_string"> <div class="membership-directory-wrap centeredBlock centeredText" :data-client-id="client_id"> <div class="ease flexNoWrap show_when_loaded"> <div class="inputWrap searchBarWrap flexGrowOne" style="min-width:100%;"> <input class="searchBar easeFast" placeholder="Search members..." v-model="search"> </div> <div class="flexGrowOne"></div> </div> <div v-if="!loading && member_tags?.length >= 1" class="scrollable posScrollbar lightBorderTop noPaddingBottom"> <div class="flexNoWrap insert_member_tags" style="padding:15px;"> <div :class="{ 'active' : selectedTag === 'all' }" class="cs-tab-bar-option cs-tab-bar-option_member_tags cs-tab-bar-option_member_tags_all mobile-order" data-option="all" @click="handleTagSelection( 'all' )">All members</div> <template v-for="tag in member_tags" :key="tag.id"> <div :class="{ 'active' : selectedTag === tag.id }" class="cs-tab-bar-option cs-tab-bar-option_member_tags mobile-order" :data-option="tag.id" @click="handleTagSelection( tag.id )">{{ tag.get('name') }}</div> </template> </div> </div> <!-- <div class="show_when_loaded table_markup_insert"> --> <div class="lightBorderTop scrollable fff noPadding"> <table v-if="!loading && filtered_members?.length >= 1 && configurable_columns.length >= 2" class="tableInsert hundop"> <tbody> <tr class="tableHeaderRow" :class="{ 'ascending' : sortObject.order == 'ascending' }"> <template v-for="column in configurable_columns"> <th v-if="!Array.isArray(column.headerName) && !column.headerClasses.includes('sortable')" :class="column.headerClasses" :style="column.headerStyle">{{ column.headerName }}</th> <th v-if="!Array.isArray(column.headerName) && column.headerClasses.includes('sortable')" :style="column.headerStyle" :data-db-key="column['data-db-key']" @click="clicked_sort(column['data-db-key'])" :class="[column.headerClasses, { 'sorted': sortObject.key === column['data-db-key'] }]" > {{ column.headerName }} <div class="sort-arrow"></div> </th> <template v-for="headerName in column.headerName"> <th v-if="Array.isArray(column.headerName)" :class="column.headerClasses">{{ headerName }}</th> </template> </template> </tr> <!-- member rows injected here --> <template v-for="member in filtered_members" :key="member.id"> <tr v-if="check_archive_status( member )" class="tableRow pointer" :class="{ 'member-row_' : member.id }" @click="clicked_member_row( member )" > <!-- photo column --> <td v-if="check_configurable_classes('column_photo')" class="lessPadding column_photo first-cell" style="padding-right:0px;"> <div v-if="member && member.get('profPicURL')" class="smallIcon noMargin noBorder" :style="{ backgroundImage: 'url(' + member.get('profPicURL') + ')' }"></div> <div v-else class="smallIcon noMargin noBorder flexWrap align-items-center" style="padding-right:0px; background-color: #f1f5fa; text-align: center; font-size: 16px;" > <i class="fa-regular fa-user"></i> </div> </td> <!-- firstName column --> <td v-if="check_configurable_classes('firstName')"> <p v-if="(member && member.get('firstName')) && !member.get('nickname')" class="capitalize">{{ member.get('firstName') }}</p> <p v-if="(member && member.get('firstName')) && member.get('nickname')" class="capitalize">{{ member.get('firstName') }} ({{ member.get('nickname') }})</p> <p v-if="member && !member.get('firstName')" class="incomplete">None</p> </td> <!-- lastName column --> <td v-if="check_configurable_classes('lastName')"> <p v-if="(member && member.get('lastName'))" class="capitalize">{{ member.get('lastName') }}</p> <p v-else class="incomplete">None</p> </td> <!-- member number column --> <td v-if="check_configurable_classes('column_member_number')" class="column_member_number"> <p v-if="(member && member.get('membershipObject')) && member.get('membershipObject').get('memberNumber')" class="uppercase">{{ member.get('membershipObject').get('memberNumber') }}</p> <p v-else class="incomplete">None</p> </td> <!-- email column --> <td v-if="check_configurable_classes('column_email')" class="column_email"> <p v-if="member && member.get('email')"><a @click.stop :href="'mailto:' + member.get('email')">{{ member.get('email')}}</a></p> <p v-else class="incomplete">None</p> </td> <!-- mobile column --> <td v-if="check_configurable_classes('column_mobile')" class="column_mobile"> <p v-if="member && member.get('mobile')"><a @click.stop :href="'tel:' + member.get('mobile')">{{ member.get('mobile')}}</a></p> <p v-else class="incomplete">None</p> </td> <!-- home phone column --> <td v-if="check_configurable_classes('column_phone_home')" class="column_phone_home"> <p v-if="member && member.get('phone_home')"><a @click.stop :href="'tel:' + member.get('phone_home')">{{ member.get('phone_home')}}</a></p> <p v-else class="incomplete">None</p> </td> <!-- member tags column --> <td v-if="check_configurable_classes('column_member_tags')" class="column_member_tags"> <div class="flexWrapLeftAlign" v-html="return_member_tags( member ) || ''"></div> </td> <!-- ghin number column --> <td v-if="check_configurable_classes('column_ghin_number')" class="column_ghin_number"> <p v-if="member && member.get('ghin_number')">{{ member.get('ghin_number') }}</p> <p v-else class="incomplete">None</p> </td> <!-- address ( street, city, state, zip) columns --> <template v-if="check_configurable_classes('column_address')"> <td class="column_address"> <p v-if="member && member.get('street')">{{ member.get('street') }}</p> <p v-else class="incomplete">None</p> </td> <td class="column_address"> <p v-if="member && member.get('city')">{{ member.get('city') }}</p> <p v-else class="incomplete">None</p> </td> <td class="column_address"> <p v-if="member && member.get('state')">{{ member.get('state') }}</p> <p v-else class="incomplete">None</p> </td> <td class="column_address"> <p v-if="member && member.get('zip')">{{ member.get('zip') }}</p> <p v-else class="incomplete">None</p> </td> </template> <!-- secondary address ( street, city, state, zip ) columns --> <template v-if="check_configurable_classes('column_address_secondary')"> <td class="column_address_secondary"> <p v-if="member && member.get('street_secondary')">{{ member.get('street_secondary') }}</p> <p v-else class="incomplete">None</p> </td> <td class="column_address_secondary"> <p v-if="member && member.get('city_secondary')">{{ member.get('city_secondary') }}</p> <p v-else class="incomplete">None</p> </td> <td class="column_address_secondary"> <p v-if="member && member.get('state_secondary')">{{ member.get('state_secondary') }}</p> <p v-else class="incomplete">None</p> </td> <td class="column_address_secondary"> <p v-if="member && member.get('zip_secondary')">{{ member.get('zip_secondary') }}</p> <p v-else class="incomplete">None</p> </td> </template> <!-- start date column --> <td v-if="check_configurable_classes('column_start_date')" class="column_start_date"> <p v-if="(member && member.get('membershipObject')) && member.get('membershipObject').get('startDate')">{{ format_utc_date( member.get('membershipObject').get('startDate') ) }}</p> <p v-else class="incomplete">None</p> </td> <!-- end date column --> <td v-if="check_configurable_classes('column_end_date')" class="column_end_date"> <p v-if="(member && member.get('membershipObject')) && member.get('membershipObject').get('endDate')">{{ format_utc_date( member.get('membershipObject').get('endDate')) }}</p> <p v-else class="incomplete">None</p> </td> <!-- renewal column --> <td v-if="check_configurable_classes('column_renewed')" class="column_renewed"> <p v-if="(member && member.get('membershipObject')) && member.get('membershipObject').get('startDate')">{{ format_utc_date( member.get('membershipObject').get('renewed') || member.get('membershipObject').get('startDate') ) }}</p> <p v-else class="incomplete">None</p> </td> <!-- category column --> <td v-if="check_configurable_classes('column_category')" class="column_category"> <p v-if="(member && member.get('membershipObject')) && member.get('membershipObject').get('tierObject')">{{ member.get('membershipObject').get('tierObject').get('name') }}</p> <p v-else class="incomplete">None</p> </td> <!-- plan column --> <td v-if="check_configurable_classes('column_plan')" class="column_plan"> <p v-if="(member && member.get('membershipObject')) && member.get('membershipObject').get('planObject')">{{ member.get('membershipObject').get('planObject').get('name') }}</p> <p v-else class="incomplete">None</p> </td> </tr> </template> </tbody> </table> </div> <!-- </div> --> <div v-if="loading" class="cardBodySpinnerWrap relative active"> <div class="cardBodySpinner centeredBlock"></div> </div> <p v-if="!loading && filtered_members.length == 0" class="noObjectsMSG withPadding">{{ return_no_results_msg() }}</p> </div> </div> </div> </script> <script> function prepThenDisplayMembershipDirectoryPopup( memberObject ){ //clear stuff $('#overlay_membership-directory-popup .insert').text(''); //name var name = ''; //check type if( memberObject.className == "boats" ){ //actually a boat - we re-use the same overlay $('#overlay_membership-directory-popup .version_boat').removeClass('hidden'); $('#overlay_membership-directory-popup .version_member').addClass('hidden'); name = returnNameStringForBoat( memberObject ); //handle documents $('.section-wrap_documents').find('.inline-value').remove(); // var documents = $('.membership-directory-wrap').data('documents'); var documents = store.components.membership_directory.documents; if( documents ){ for (var i = 0; i <= documents.length - 1; i++) { if( documents[i].get('boatObject') ){ if( documents[i].get('boatObject').id == memberObject.objectId ){ var this_name = documents[i].get('name'); if( this_name && this_name.length >= 1 ){ $('#overlay_membership-directory-popup .section-wrap_documents').append('<p class="flexGrowOne inline-value tinyPaddingTop"><a class="tappedDocument" data-document-id="'+ documents[i].id +'" target="_blank" href="'+ documents[i].get('URL') +'">'+ this_name +' ></a></p>'); } } } } } }else{ //ok, it's a member $('#overlay_membership-directory-popup .version_boat').addClass('hidden'); $('#overlay_membership-directory-popup .version_member').removeClass('hidden'); //name if( memberObject.get('firstName') ){ name += memberObject.get('firstName'); if( memberObject.get('nickname') ){ name += ' ('+ memberObject.get('nickname') + ')'; } } if( memberObject.get('lastName') ){ if( name.length >= 1 ){ name += ' '; } name += memberObject.get('lastName'); } } $('#overlay_membership-directory-popup .insert_name').text(name); //prof pic var profPicURL = memberObject.get('profPicURL'); if( profPicURL ){ $('#overlay_membership-directory-popup .profile-picture').text(""); $('#overlay_membership-directory-popup .profile-picture').css('background-image', 'url('+profPicURL+')' ); $('#overlay_membership-directory-popup .profile-background-inner').css('background-image', 'url('+profPicURL+')' ); }else{ $('#overlay_membership-directory-popup .profile-picture').css('background-image', '' ); $('#overlay_membership-directory-popup .profile-background-inner').css('background-image', ''); if( name.length >= 1 ){ var initial = name.substr(0,1).toLowerCase(); $('#overlay_membership-directory-popup .profile-picture').text( initial ); } } //plug in data $('#overlay_membership-directory-popup .inline-value').each(function(){ var key = $(this).data('db-key'); if( key ){ var value = memberObject.get(key); if( value ){ if( key.toLowerCase().indexOf('email') >= 0 ){ //it's an email $(this).html( '<a href="mailto:'+ value +'">'+ value +'</a>' ); $(this).removeClass( 'hidden' ); }else if( key.toLowerCase().includes('mobile') || key.toLowerCase().includes('phone') ){ //it's a mobile number $(this).html( '<a href="tel:'+ returnCleanMobileNumber( value ) +'">'+ formatMobileNumber( value ) +'</a>' ); $(this).removeClass( 'hidden' ); }else if( $(this).data('prepend') ){ $(this).text( $(this).data('prepend') + " " + value ); $(this).removeClass( 'hidden' ); }else{ //not an email, mobile number, or special boat field $(this).text( value ); $(this).removeClass( 'hidden' ); } }else{ $(this).text( "" ).addClass('hidden'); $(this).data( 'date', null ); } } }); //custom fields $('.section-wrap_custom_fields').html(''); // var custom_fields = $('.membership-directory-wrap').data('custom_fields'); var custom_fields = store.components.membership_directory.custom_fields; if( custom_fields ){ //plug them in for (var i = 0; i <= custom_fields.length - 1; i++) { var response = returnResponseForCustomField( memberObject.get('customFieldsArray'), custom_fields[i].id ); if( response ){ $('.section-wrap_custom_fields').append('<h4 class="smallBlockLetters leftText smallMarginTop">'+ custom_fields[i].get('name') +'</h4>'); $('.section-wrap_custom_fields').append('<p class="inline-value dark tinyPaddingTop">'+ response +'</p>'); } } } //family $('.section-wrap_family').find('.inline-value').remove(); var membershipObject = memberObject.get('membershipObject'); var membersArray = membershipObject.get('membersArray'); var family_members = []; if( membersArray ){ for (var i = 0; i <= membersArray.length - 1; i++) { if( membersArray[i].id != memberObject.id ){ var this_member = returnObjectByID( membersArray[i].id, store.components.membership_directory.allMembers ); if( this_member ){ family_members.push( this_member ); } } } } for (var i = 0; i <= family_members.length - 1; i++) { var this_name = ''; if( family_members[i].get('firstName') ){ this_name = family_members[i].get('firstName'); if( family_members[i].get('lastName') ){ this_name += ' '; this_name += family_members[i].get('lastName'); } } if( this_name.length >= 1 ){ $('#overlay_membership-directory-popup .section-wrap_family').append('<p class="flexGrowOne inline-value tinyPaddingTop"><a class="tappedFamilyMember" data-member-id="'+ family_members[i].id +'">'+ this_name +' ></a></p>'); } } //boats $('.section-wrap_boats').find('.inline-value').remove(); var boats_map = store.components.membership_directory.boats_map; if( boats_map ){ var these_boats = boats_map[ membershipObject.id ]; if( these_boats ){ for (var i = 0; i <= these_boats.length - 1; i++) { var this_name = returnNameStringForBoat( these_boats[i] ); if( this_name.length >= 1 ){ $('#overlay_membership-directory-popup .section-wrap_boats').append('<p class="flexGrowOne inline-value tinyPaddingTop"><a class="tappedBoat" data-boat-id="'+ these_boats[i].id +'" data-membership-id="'+ membershipObject.id +'">'+ this_name +' ></a></p>'); } } } } //continue $('#overlay_membership-directory-popup .section-wrap').each(function(){ if( $(this).find('.inline-value').not( $('.hidden') ).length >= 1 ){ $(this).removeClass('hidden'); }else{ $(this).addClass('hidden'); } }); //fade it in fadeIn('#overlay_membership-directory-popup'); } function returnNameStringForBoat( boatObject ){ var this_name = ''; if( boatObject.get('boatName') ){ this_name += boatObject.get('boatName'); } if( boatObject.get('sailNumber') ){ if( this_name.length >= 1 ){ this_name += " - "; } this_name += boatObject.get('sailNumber'); } if( boatObject.get('class') ){ if( this_name.length >= 1 ){ this_name += " - "; } this_name += boatObject.get('class'); } if( boatObject.get('hullNumber') ){ if( this_name.length >= 1 ){ this_name += " - "; } this_name += boatObject.get('hullNumber'); } return this_name; } function returnResponseForCustomField( customFieldsArray, custom_field_id ){ if( customFieldsArray ){ for (var i = 0; i <= customFieldsArray.length - 1; i++) { if( customFieldsArray[i].customFieldID == custom_field_id ){ if( customFieldsArray[i].response ){ return customFieldsArray[i].response; }else if( customFieldsArray[i].optionsStringsArray ){ var string = ""; for (var s = 0; s <= customFieldsArray[i].optionsStringsArray.length - 1; s++) { if( string.length >= 1 ){ string += ', '; } string += customFieldsArray[i].optionsStringsArray[s]; } return string; } break; } } } return null; } /* ----- STORE ----- */ store.components["membership_directory"] = { clubId: null, clubIds: ['8pB9LjQKoE', '2oWU1chg3P'], //for mantoloking and cambridge yc allMembers: [], member_tags: [], json: [], boats_map: [], custom_fields: [], documents: [], //for scira selectedTag: 'all', // configurable_columns: [], count: 0, search: '', loading: true, sortObject: { order: 'ascending', key: 'lastName' }, }; /* ----- COMPONENT ----- */ app_content.component("membership-directory-component", { template: "#template-membership-directory", data() { return store.components['membership_directory'] }, props: { formatting: Object, client_id: String, class_string: String, style_string: String, element_style_string: String, component_type: String, //admin last_update: Number, }, mounted() { this.clubId = $('body').data('club-id'); this.load_membership_directory(); // this.fillColumnClasses(); }, methods: { sortByFirstName_ascending( a, b ){ var aFirstName = ''; if( a.get('firstName') ){ aFirstName += a.get('firstName').trim().toLowerCase(); } var bFirstName = ''; if( b.get('firstName') ){ bFirstName += b.get('firstName').trim().toLowerCase(); } if(aFirstName < bFirstName) { return -1; } if(aFirstName > bFirstName) { return 1; } //they had the same name return 0; }, sortByFirstName_descending( a, b ){ var aFirstName = ''; if( a.get('firstName') ){ aFirstName += a.get('firstName').trim().toLowerCase(); } var bFirstName = ''; if( b.get('firstName') ){ bFirstName += b.get('firstName').trim().toLowerCase(); } if(aFirstName > bFirstName) { return -1; } if(aFirstName < bFirstName) { return 1; } return 0; }, sortByLastName_ascending( a, b ){ var aLastName = ''; if( a.get('lastName') ){ aLastName += a.get('lastName').trim().toLowerCase(); } var bLastName = ''; if( b.get('lastName') ){ bLastName += b.get('lastName').trim().toLowerCase(); } if(aLastName < bLastName) { return -1; } if(aLastName > bLastName) { return 1; } return 0; }, sortByLastName_descending( a, b ){ var aLastName = ''; if( a.get('lastName') ){ aLastName += a.get('lastName').trim().toLowerCase(); } var bLastName = ''; if( b.get('lastName') ){ bLastName += b.get('lastName').trim().toLowerCase(); } if(aLastName > bLastName) { return -1; } if(aLastName < bLastName) { return 1; } return 0; }, sortByMemberNumber_ascending( a, b ){ var aMemberNumber = ''; if( a.get('membershipObject') && a.get('membershipObject').get('memberNumber') ){ aMemberNumber += a.get('membershipObject').get('memberNumber').trim().toLowerCase(); } var bMemberNumber = ''; if( b.get('membershipObject') && b.get('membershipObject').get('memberNumber') ){ bMemberNumber += b.get('membershipObject').get('memberNumber').trim().toLowerCase(); } if(aMemberNumber < bMemberNumber) { return -1; } if(aMemberNumber > bMemberNumber) { return 1; } return 0; }, sortByMemberNumber_descending( a, b ){ var aMemberNumber = ''; if( a.get('membershipObject') && a.get('membershipObject').get('memberNumber') ){ aMemberNumber += a.get('membershipObject').get('memberNumber').trim().toLowerCase(); } var bMemberNumber = ''; if( b.get('membershipObject') && b.get('membershipObject').get('memberNumber') ){ bMemberNumber += b.get('membershipObject').get('memberNumber').trim().toLowerCase(); } if(aMemberNumber > bMemberNumber) { return -1; } if(aMemberNumber < bMemberNumber) { return 1; } return 0; }, sortByStartDate_ascending( a, b ){ var aStartDate; if( a.get('membershipObject') && a.get('membershipObject').get('startDate') ){ aStartDate = a.get('membershipObject').get('startDate'); } var bStartDate; if( b.get('membershipObject') && b.get('membershipObject').get('startDate') ){ bStartDate = b.get('membershipObject').get('startDate'); } if(aStartDate < bStartDate) { return -1; } if(aStartDate > bStartDate) { return 1; } return 0; }, sortByStartDate_descending( a, b ){ var aStartDate; if( a.get('membershipObject') && a.get('membershipObject').get('startDate') ){ aStartDate = a.get('membershipObject').get('startDate'); } var bStartDate; if( b.get('membershipObject') && b.get('membershipObject').get('startDate') ){ bStartDate = b.get('membershipObject').get('startDate'); } if(aStartDate > bStartDate) { return -1; } if(aStartDate < bStartDate) { return 1; } return 0; }, sortByRenewed_ascending( a, b ){ var aRenewed; if( a.get('membershipObject') && a.get('membershipObject').get('renewed') ){ aRenewed = a.get('membershipObject').get('renewed'); }else if( a.get('membershipObject') && a.get('membershipObject').get('startDate') ){ aRenewed = a.get('membershipObject').get('startDate'); } var bRenewed; if( b.get('membershipObject') && b.get('membershipObject').get('renewed') ){ bRenewed = b.get('membershipObject').get('renewed'); }else if( b.get('membershipObject') && b.get('membershipObject').get('startDate') ){ bRenewed = b.get('membershipObject').get('startDate'); } if(aRenewed < bRenewed) { return -1; } if(aRenewed > bRenewed) { return 1; } return 0; }, sortByRenewed_descending( a, b ){ var aRenewed; if( a.get('membershipObject') && a.get('membershipObject').get('renewed') ){ aRenewed = a.get('membershipObject').get('renewed'); }else if( a.get('membershipObject') && a.get('membershipObject').get('startDate') ){ aRenewed = a.get('membershipObject').get('startDate'); } var bRenewed; if( b.get('membershipObject') && b.get('membershipObject').get('renewed') ){ bRenewed = b.get('membershipObject').get('renewed'); }else if( b.get('membershipObject') && b.get('membershipObject').get('startDate') ){ bRenewed = b.get('membershipObject').get('startDate'); } if(aRenewed > bRenewed) { return -1; } if(aRenewed < bRenewed) { return 1; } return 0; }, sortByEndDate_ascending( a, b ){ var aEndDate; if( a.get('membershipObject') && a.get('membershipObject').get('endDate') ){ aEndDate = a.get('membershipObject').get('endDate'); } var bEndDate; if( b.get('membershipObject') && b.get('membershipObject').get('endDate') ){ bEndDate = b.get('membershipObject').get('endDate'); } if(aEndDate < bEndDate) { return -1; } if(aEndDate > bEndDate) { return 1; } return 0; }, sortByEndDate_descending( a, b ){ var aEndDate; if( a.get('membershipObject') && a.get('membershipObject').get('endDate') ){ aEndDate = a.get('membershipObject').get('endDate'); } var bEndDate; if( b.get('membershipObject') && b.get('membershipObject').get('endDate') ){ bEndDate = b.get('membershipObject').get('endDate'); } if(aEndDate > bEndDate) { return -1; } if(aEndDate < bEndDate) { return 1; } return 0; }, load_membership_directory(){ //update the ui store.components.membership_directory.loading = true; var promise = Parse.Promise.as(); var thisPromise = promise.then(() => { //get new data var params = {}; params["client_id"] = $('.membership-directory-wrap').data('client-id'); params["request_url"] = window.location.href; return Parse.Cloud.run('retrieve_data_for_component', params); }).then(result => { //vue store.components.membership_directory.member_tags = result.member_tags; store.components.membership_directory.loading = false; var json = []; var retrieved_members = result.members; for (var i = 0; i <= retrieved_members.length - 1; i++) { var boats_string = ""; var number = ""; if( retrieved_members[i].get('membershipObject') ){ number = retrieved_members[i].get('membershipObject').get('memberNumber'); } var boats_map = result.boats_map; if( boats_map && retrieved_members[i].get('membershipObject') ){ var these_boats = boats_map[ retrieved_members[i].get('membershipObject').id ]; if( these_boats ){ for (var b = these_boats.length - 1; b >= 0; b--) { if( boats_string.length >= 1 ){ boats_string += " "; } boats_string += returnNameStringForBoat( these_boats[b] ); } } } retrieved_members[i].boats_string = boats_string json.push({ "objectId": retrieved_members[i].id, "name": retrieved_members[i].get('firstName') + " " + retrieved_members[i].get('lastName'), "boats_string": boats_string, "number": number }) } //refactor for search store.components.membership_directory.allMembers = retrieved_members; //vue store.components.membership_directory.json = json; store.components.membership_directory.boats_map = result.boats_map; store.components.membership_directory.custom_fields = result.custom_fields; store.components.membership_directory.documents = result.documents; //for scira }, function(error){ checkParseError( error ); }); return thisPromise; }, /* ----- common methods ----- */ clicked_sort( key ){ if( this.sortObject.key == key ){ //already sorting by this one - flip it if( this.sortObject.order == "descending" ){ this.sortObject.order = "ascending"; }else{ this.sortObject.order = "descending"; } }else{ //not sorting by this yet this.sortObject.key = key; this.sortObject.order = "descending"; } }, /* ----- table methods ----- */ handleTagSelection( tag_id ){ this.selectedTag = tag_id; }, fillColumnClasses(){ // let configureColumns = { // 'show_photo': { // order: 1, // headerClasses: 'column_photo lessPadding', // headerStyle: 'padding-left:10px;0px', // headerName: null, // }, // 'show_member_number': { // order: 4, // headerClasses: 'column_member_number sortable', // 'data-db-key': 'memberNumber', // headerName: 'Number' // }, // 'show_email': { // order: 5, // headerClasses: 'column_email', // headerName: 'Email' // }, // 'show_mobile': { // order: 6, // headerClasses: 'column_mobile', // headerName: 'Mobile' // }, // 'show_phone_home': { // order: 7, // headerClasses: 'column_phone_home', // headerName: 'Home Phone' // }, // 'show_member_tags': { // order: 8, // headerClasses: 'column_member_tags', // headerName: 'Tags' // }, // 'show_ghin_number': { // order: 9, // headerClasses: 'column_ghin_number', // headerName: 'GHIN Number' // }, // 'show_address': { // order: 10, // headerClasses: 'column_address', // headerName: ['Street', 'City', 'State', 'Postal Code'] // }, // 'show_address_secondary': { // order: 11, // headerClasses: 'column_address_secondary', // headerName: ['Street (Secondary)', 'City (Secondary)', 'State (Secondary)', 'Postal Code (Secondary)'] // }, // 'show_start_date': { // order: 12, // headerClasses: 'sortable column_start_date', // 'data-db-key': 'startDate', // headerName: 'Start Date' // }, // 'show_end_date': { // order: 13, // headerClasses: 'sortable column_end_date', // 'data-db-key': 'endDate', // headerName: 'End Date' // }, // 'show_renewed': { // order: 14, // headerClasses: 'sortable column_renewed', // 'data-db-key': 'renewed', // headerName: 'Renewed' // }, // 'show_category': { // order: 15, // headerClasses: 'column_category', // headerName: 'Category' // }, // 'show_plan': { // order: 16, // headerClasses: 'column_plan', // headerName: 'Plan' // }, // } // let classStringArr = this.class_string.split(' '); // this.configurable_columns = classStringArr.reduce((acc, currentClass) => { // if (configureColumns[currentClass] !== undefined){ // acc.push(configureColumns[currentClass]) // } // return acc; // }, [ // { // order: 2, // headerClasses: 'sortable firstName', // 'data-db-key': 'firstName', // required: true, // headerName: 'First Name' // }, // { // order: 3, // headerClasses: 'sortable lastName', // 'data-db-key': 'lastName', // required: true, // headerName: 'Last Name' // }, // ]).sort((a, b) => { // return a.order - b.order; // }) }, check_archive_status( memberObject ){ var membershipObject = memberObject.get('membershipObject'); if(membershipObject && ! membershipObject.get('archived') && membershipObject.get('status') != 'canceled' ){ return true; } return false; }, clicked_member_row( memberObject ){ $('#overlay_membership-directory-popup').data('members', this.allMembers); $('#overlay_membership-directory-popup').data('boats_map', this.boats_map); prepThenDisplayMembershipDirectoryPopup( memberObject ); }, check_configurable_classes( className ){ for (let i = 0; i < this.configurable_columns.length; i++){ if (this.configurable_columns[i].headerClasses.includes(className)){ return true; break; } } return false; }, return_member_tags( memberObject ){ var member_tags = memberObject.get('member_tags') || []; var tag_string = ""; var tagsSituation = ''; if( member_tags.length >= 1 ){ for (var i = 0; i <= member_tags.length - 1; i++) { if( member_tags[i].className == "member_tags" ){ if( ! member_tags[i].get('archived') && ! member_tags[i].get('hidden') ){ if( tag_string.length >= 1 ){ tag_string += ','; } tag_string += member_tags[i].id; if( member_tags[i].get('hex_value') ){ //custom color tagsSituation += '<p class="table-row-tag" style="background-color:#'+ member_tags[i].get('hex_value') +';">' + member_tags[i].get('name') + '</p>'; }else{ //default color tagsSituation += '<p class="table-row-tag grey">' + member_tags[i].get('name') + '</p>'; } } } } } if( tagsSituation.length >= 1 ){ return tagsSituation; }else{ return '<p class="incomplete">None</p>' } }, format_utc_date( memberObject ){ return formatUTCDate( memberObject, true, false, true ) }, return_no_results_msg(){ if (this.clubIds.includes(this.clubId)){ return 'Search a member to get started.' //for mantoloking, cambridge yc, northport yc } else { return 'No members found.'; } }, sortMembershipDirectory( members ){ var sortObject = store.components.membership_directory.sortObject; if( ! sortObject ){ sortObject = {} } if( ! sortObject.key ){ sortObject.key = "startDate"; } if( ! sortObject.order ){ sortObject.order = "descending" } if( sortObject.key == "firstName" ){ if( sortObject.order == "ascending" ){ members = members.sort( this.sortByFirstName_ascending ); }else{ members = members.sort( this.sortByFirstName_descending ); } }else if( sortObject.key == "lastName" ){ if( sortObject.order == "ascending" ){ members = members.sort( this.sortByLastName_ascending ); }else{ members = members.sort( this.sortByLastName_descending ); } }else if( sortObject.key == "memberNumber" ){ if( sortObject.order == "ascending" ){ members = members.sort( this.sortByMemberNumber_ascending ); }else{ members = members.sort( this.sortByMemberNumber_descending ); } }else if( sortObject.key == "startDate" ){ if( sortObject.order == "ascending" ){ members = members.sort( this.sortByStartDate_ascending ); }else{ members = members.sort( this.sortByStartDate_descending ); } }else if( sortObject.key == "endDate" ){ if( sortObject.order == "ascending" ){ members = members.sort( this.sortByEndDate_ascending ); }else{ members = members.sort( this.sortByEndDate_descending ); } }else if( sortObject.key == "renewed" ){ if( sortObject.order == "ascending" ){ members = members.sort( this.sortByRenewed_ascending ); }else{ members = members.sort( this.sortByRenewed_descending ); } } return members; } }, computed: { filtered_members(){ let val = this.search.toLowerCase(); let filteredMembers; if (this.clubIds.includes(this.clubId)){ filteredMembers = []; //for mantoloking, cambridge yc, northport yc } else { filteredMembers = this.allMembers; } // handle search results using all members if (this.search.length > 0){ filteredMembers = this.allMembers.filter(member => { let firstName = member.get('firstName').toLowerCase() || ''; let lastName = member.get('lastName').toLowerCase() || ''; let boats_string = member['boats_string'].toLowerCase() || ''; let number = member?.get('membershipObject')?.get('memberNumber').toLowerCase() || ''; // change this to includes if ( firstName.includes(val) || lastName.includes(val) || boats_string.includes(val) || number.includes(val) ){ return member; } }) } // handle member tag selection using filtered search results if (this.selectedTag !== 'all'){ filteredMembers = filteredMembers.filter(member => { let memberTags = member.get('member_tags') || []; for (let i = 0; i < memberTags.length; i++){ if (memberTags[i].id === this.selectedTag){ return member; break; } } }) } if (this.clubIds.includes(this.clubId) && filteredMembers.length > 20){ return this.sortMembershipDirectory( filteredMembers.slice(0, 20) ); //for mantoloking, cambridge yc, northport yc } else { return this.sortMembershipDirectory( filteredMembers ); } }, configurable_columns(){ let configureColumns = { 'show_photo': { order: 1, headerClasses: 'column_photo lessPadding', headerStyle: 'padding-left:10px;0px', headerName: null, }, 'show_member_number': { order: 4, headerClasses: 'column_member_number sortable', 'data-db-key': 'memberNumber', headerName: 'Number' }, 'show_email': { order: 5, headerClasses: 'column_email', headerName: 'Email' }, 'show_mobile': { order: 6, headerClasses: 'column_mobile', headerName: 'Mobile' }, 'show_phone_home': { order: 7, headerClasses: 'column_phone_home', headerName: 'Home Phone' }, 'show_member_tags': { order: 8, headerClasses: 'column_member_tags', headerName: 'Tags' }, 'show_ghin_number': { order: 9, headerClasses: 'column_ghin_number', headerName: 'GHIN Number' }, 'show_address': { order: 10, headerClasses: 'column_address', headerName: ['Street', 'City', 'State', 'Postal Code'] }, 'show_address_secondary': { order: 11, headerClasses: 'column_address_secondary', headerName: ['Street (Secondary)', 'City (Secondary)', 'State (Secondary)', 'Postal Code (Secondary)'] }, 'show_start_date': { order: 12, headerClasses: 'sortable column_start_date', 'data-db-key': 'startDate', headerName: 'Start Date' }, 'show_end_date': { order: 13, headerClasses: 'sortable column_end_date', 'data-db-key': 'endDate', headerName: 'End Date' }, 'show_renewed': { order: 14, headerClasses: 'sortable column_renewed', 'data-db-key': 'renewed', headerName: 'Renewed' }, 'show_category': { order: 15, headerClasses: 'column_category', headerName: 'Category' }, 'show_plan': { order: 16, headerClasses: 'column_plan', headerName: 'Plan' }, } let classStringArr = this.class_string.split(' '); return classStringArr.reduce((acc, currentClass) => { if (configureColumns[currentClass] !== undefined){ acc.push(configureColumns[currentClass]) } return acc; }, [ { order: 2, headerClasses: 'sortable firstName', 'data-db-key': 'firstName', required: true, headerName: 'First Name' }, { order: 3, headerClasses: 'sortable lastName', 'data-db-key': 'lastName', required: true, headerName: 'Last Name' }, ]).sort((a, b) => { return a.order - b.order; }) }, } }) </script> <script type="text/html" id="template-event-list-card"> <div class="event-row overlay-event-row flexEventOne noBorderBottom" :class="'event-row_' + event.id" :data-event-id="event.id" > <div class="event-list-card" :class="{ 'small-card': formatting.card_size == 'small', 'large-card': formatting.card_size == 'large', 'box-shadow-light': formatting.box_shadow == 'light', 'box-shadow-dark': formatting.box_shadow == 'dark', }" :style="{ 'border-radius': formatting.border_radius + 'px' || '0px' }" > <div v-if="event_image" class="event-image" :class="{ 'small-card': formatting.card_size == 'small', 'large-card': formatting.card_size == 'large' }" :style="{ backgroundImage: `url(${event_image})` }" > <div class="date-badge"> <p class="event-list-card-month">{{ formatted_date('month') }}</p> <p class="event-list-card-date">{{ formatted_date('date') }}</p> </div> </div> <div v-else class="event-image" :class="{ 'small-card': formatting.card_size == 'small', 'large-card': formatting.card_size == 'large' }" > <div class="date-badge"> <p class="event-list-card-month">{{ formatted_date('month') }}</p> <p class="event-list-card-date">{{ formatted_date('date') }}</p> </div> <p class="no-background">{{ event.get('name').charAt(0).toUpperCase() }}</p> </div> <div class="card-content" :style="{ 'border-radius': formatting.border_radius + 'px' || '0px' }" > <p class="event-type">{{ event_type }}</p> <p class="event-title">{{ event_title }}</p> <div class="event-time"> <i class="fa-regular fa-clock" style="font-size: 0.8em;"></i> <span>{{ event_time }}</span> </div> <button v-if="button_label" class="event-button relative background" v-html="button_label"></button> </div> </div> </div> </script> <style> .event-list-card { width: 190px; overflow: hidden; background: white; font-family: 'Inter', sans-serif; display: flex; flex-direction: column; transition: transform 0.1s ease; } .event-list-card.small-card { width: 190px; } .event-list-card.large-card { width: 230px; } .event-list-card.box-shadow-light { box-shadow: 2px 4px 8px rgba(0, 0, 0, 0.2); } .event-list-card.box-shadow-dark { box-shadow: 6px 6px 16px rgba(0, 0, 0, 0.2); } .event-list-card .event-image { display: flex; text-align: center; align-items: center; width: 100%; height: 170px; background-size: cover; /*contain*/ background-repeat: no-repeat; background-position: center; background-color: #77818f; color: #fff; position: relative; } .event-list-card .event-image.small-card{ height: 170px; } .event-list-card .event-image.large-card{ height: 220px; } .event-list-card .event-image .no-background { width: 100%; font-weight: 500; font-size: 5em; } .date-badge { position: absolute; top: 5px; left: 5px; /* background-color: rgba(0,0,0,0.5); */ background-color: transparent; color: white; border-radius: 4px; padding: 4px 6px; display: flex; flex-direction: column; align-items: flex-start; justify-content: center; font-weight: bold; min-width: 45px; max-width: 50%; min-height: 45px; } .date-badge .event-list-card-month { font-size: 12px; letter-spacing: 1.2px; text-transform: uppercase; } .date-badge .event-list-card-date { font-size: 26px; letter-spacing: 1.2px; font-family: 'Times New Roman', Times, serif; } .card-content { height: 120px; padding: 14px 12px; background-color: #f9f9f9; } .event-list-card .event-type { font-size: 12px; text-transform: uppercase; color: #000; margin-bottom: 2px; } .event-list-card .event-title { font-size: 18px; text-transform: uppercase; margin-bottom: 8px; color: #000; font-family: serif; letter-spacing: 0.2px; } .event-list-card .event-time { display: flex; align-items: center; font-size: 14px; color: #000; margin-bottom: 12px; } .event-time i { margin-right: 6px; } .event-list-card .event-button { width: 100%; display: flex; align-items: center; justify-content: center; position: relative; z-index: 2; letter-spacing: 1px; line-height: 16px; cursor: pointer; font-size: 14px; font-weight: normal; padding: 6px 12px; text-transform: uppercase; transition: all 0.2s ease; border: 1.5px solid #000; border-radius: 2px; } .event-list-card .event-button:hover{ background-color: #000; color: white; } .res-callout{ display: flex; align-items: center; white-space: nowrap; height: fit-content; width: fit-content; padding: 3.5px; background-color: #fff; border: 1px solid #e4ebf1; border-radius: 5px; font-size: 10px; text-transform: uppercase; color: #5c6d7a; } </style> <script> var config = defineComponent({ template: "#template-event-list-card", data() { return {} }, props: { event: Object, formatting: Object, }, methods: { formatted_date( type ){ var start_offset, end_offset; var start_offset_time, end_offset_time; if( this.event.get('startDate') ){ start_offset = returnOffsetDateForUTCDate( this.event.get('startDate') ); //handle start/end times set explicitly on the event (for socials) if( this.event.get('startTime') ){ var start_data = returnHoursAndMinutesForMilitaryTime( this.event.get('startTime') ); start_offset.setHours( start_offset.getHours() + start_data.hours ); start_offset.setMinutes( start_offset.getMinutes() + start_data.minutes ); } start_offset_time = start_offset.getTime(); } var endDate = this.event.get('endDate'); if( ! endDate && this.event.get('startDate') ){ endDate = this.event.get('startDate'); } if( endDate ){ end_offset = returnOffsetDateForUTCDate( endDate ); //handle start/end times set explicitly on the event (for socials) if( this.event.get('endTime') ){ var end_data = returnHoursAndMinutesForMilitaryTime( this.event.get('endTime') ); end_offset.setHours( end_offset.getHours() + end_data.hours ); end_offset.setMinutes( end_offset.getMinutes() + end_data.minutes ); } end_offset_time = end_offset.getTime(); } let month = ''; let date = ''; if( start_offset ){ date = start_offset.getDate().toString(); if( end_offset && ! this.event.get('all_day') ){ if( end_offset.getDate() != start_offset.getDate() ){ date += '-' + end_offset.getDate(); } } if( date.length == 1 ){ date = '0' + date; } month = returnShortMonth( start_offset.getMonth() ); } if( type == 'month' ){ return month; }else if( type == 'date' ){ return date; } }, return_image_data( eventObject ){ if( eventObject.className == "subevents" && eventObject.get('regattaObject') ){ eventObject = eventObject.get('regattaObject'); } return { url: eventObject.get('imageURL'), crop: eventObject.get('imageCrop') } }, returnShowRegisterButtonForEvent( eventObject ){ //set defaults var now = new Date(); var ended = false; var registration_closed = eventObject.get('registration_closed'); var startDate = eventObject.get('startDate'); var lastChanceDate = eventObject.get('lastChanceDate'); var endDate = eventObject.get('endDate'); if( ! endDate && startDate ){ endDate = startDate; } var endDate_plus_one; if( endDate ){ endDate_plus_one = new Date( endDate.getTime() ); endDate_plus_one.setDate( endDate.getDate() + 1 ); } if( now > endDate_plus_one && endDate_plus_one ){ ended = true; } if( now > lastChanceDate && lastChanceDate ){ registration_closed = true; } if( ended || registration_closed || eventObject.get('calendarOnly') ){ return false; } else { return true; } }, }, computed: { event_image() { return this.return_image_data(this.event).url; }, event_title() { return this.event.get('name') || ''; }, event_type(){ if( this.event.className == 'camps' ){ return 'camp'; }else if( this.event.className == 'regattas' ){ return 'regatta'; }else{ return 'event'; } }, event_time() { if( this.event.get('startDate') ){ if( this.event.className == "regattas" || this.event.className == "subevents" ){ //these are stored in the club's time return format_utc_date_v2({ "utcDate": this.event.get('startDate'), "timezone": store.timezone, }); }else{ //these are stored as 12am UTC let date_components = return_date_components_v2({ "date": this.event.get('startDate'), "timezone": "UTC" }); date_components.amPm = null; if( this.event.get('startTime') ){ let time_data = returnHoursAndMinutesForMilitaryTime( this.event.get('startTime') ); date_components.hour = time_data.hours; date_components.minute = time_data.minutes; } let offset_date = return_utc_date_v2({ "dateComponents": date_components, "timezone": store.timezone }); return format_utc_date_v2({ "utcDate": offset_date, "timezone": store.timezone, "formatString": "h:mm A" }); } } return null; }, button_label(){ let eventObject = this.event; var href_base = ''; var clubObject = eventObject.get('clubObject'); var web_settings = clubObject.get('web_settings'); if( web_settings ){ if( web_settings.get('event_links') == "use_clubspot_regatta_links" && ( eventObject.className == "regattas" || eventObject.className == "subevents" ) ){ //force clubspot urls for regattas href_base += 'https://theclubspot.com'; } } var content; var show_register_button = this.returnShowRegisterButtonForEvent( eventObject ); if( eventObject.className == "regattas" || eventObject.className == "subevents" ){ if( eventObject.className == "subevents" ){ eventObject = eventObject.get('regattaObject'); } //check for external events if( eventObject.get('regatta_external') && eventObject.get('external_regatta_url') ){ //it's an external event content = 'View event<a class="absoluteA" target="_blank" href="'+ eventObject.get('external_regatta_url') +'"></a>'; }else{ //handle button status if( show_register_button ){ //register content = 'Register<a class="absoluteA" href="'+href_base+'/regatta/'+ eventObject.id +'"></a>'; }else{ //results content = 'View Results<a class="absoluteA" href="'+href_base+'/regatta/'+ eventObject.id +'/results"></a>'; } } }else if( eventObject.className == "events" ){ var registration_link; if( eventObject.get('events_v2') ){ registration_link = '/event/' + eventObject.id; }else if( eventObject.get('external_event') && eventObject.get('external_register_link') ){ registration_link = eventObject.get('external_register_link'); }else if( eventObject.get('event_product') ){ if( eventObject.get('event_product').get('availability') && eventObject.get('event_product').get('availability').startDate ){ registration_link = "/reserve/" + eventObject.get('event_product').id + "?date=" + eventObject.get('event_product').get('availability').startDate; } } if( registration_link && show_register_button ){ content = 'RSVP<a class="absoluteA" href="'+href_base+''+registration_link+'"></a>'; } }else if( eventObject.className == "camps" ){ //handle button status if( show_register_button ){ //register content = 'Register<a href="'+href_base+'/register/camp/'+ eventObject.id +'/class" class="absoluteA"></a>'; } } return content; } } }) //define the component app_overlays.component("event-list-card-component", config); app_content.component("event-list-card-component", config); </script> <script type="text/html" id="template-event-list-event"> <div class="event-row overlay-event-row flexEventOne" :class="'event-row_' + event.id" :data-event-id="event.id" > <!-- date and image wrap --> <div class="cs-signups-row-left flexNoWrapCenterAlign"> <div class="cs-signups-date-wrap relative" style="width: 55px;"> <div class="centeredBlock relative"> <div v-html="return_markup( event, 'date')"></div> </div> </div> <div v-if="return_image_data( event ).url" class="cs-signups-row-image flexWrap" :class="{ 'background-contain' : return_image_data( event ).crop === 'contain', 'background-cover' : return_image_data( event ).crop === 'cover'}" :style="{ backgroundImage: 'url(' + return_image_data( event ).url + ')' }" > <div v-if="return_markup( event, 'check-header-callout' )" class="header-callout">{{ return_markup( event, 'header-callout') }}</div> </div> <div v-else class="cs-signups-row-image flexWrap" > <div v-if="return_markup( event, 'check-header-callout' )" class="header-callout">{{ return_markup( event, 'header-callout') }}</div> {{ event.get('name').charAt(0).toUpperCase() }} </div> </div> <!-- event info, links, and host wrap --> <div class="cs-signups-row-inner flexWrapLeftAlign ellipsis align-items-center"> <!-- event info --> <div class="cs-signups-name-wrap ellipsis textLeft"> <span class="flexWrap" style="display: flex; align-items: center; justify-content: flex-start; margin-bottom: 5px;"> <div class="cs-signups-name-0 textLeft" style="flex-grow: 0; margin-right: 8px;">{{ event.get('name') }}</div> <!-- <div v-if="event.get('members_only')" class="res-callout" > <i class="fa-regular fa-user-check" style="padding-right:5px; font-size: 9px;"></i> <span style="font-size: 10px;">Members Only</span> </div> --> </span> <div class="event-date-wrap-v2">{{ niceDate( event ) }}</div> <div class="event-date-wrap-v2 tiniestPaddingTop"> <p>{{ return_markup( event, 'event-host-1') }}</p> <p>{{ return_markup( event, 'event-host-2' )}}</p> </div> </div> <!-- event links --> <div class="cs-signups-links-wrap flexGrowOne"> <div class="cs-signups-links-wrap-inner textLeft"> <div class="event-nav-1" v-html="return_markup( event, 'links' )"></div> <div class="event-nav-2" v-html="return_markup( event, 'button' )"></div> </div> </div> </div> </div> </script> <script> var config = defineComponent({ template: "#template-event-list-event", data() { return {} }, props: { event }, methods: { return_image_data( eventObject ){ if( eventObject.className == "subevents" && eventObject.get('regattaObject') ){ eventObject = eventObject.get('regattaObject'); } return { url: eventObject.get('imageURL'), crop: eventObject.get('imageCrop') } }, niceDate( eventObject ){ if( eventObject.get('startDate') ){ if( eventObject.className == "regattas" || eventObject.className == "subevents" ){ //these are stored in the club's time return format_utc_date_v2({ "utcDate": eventObject.get('startDate'), "timezone": store.timezone, }); }else{ //these are stored as 12am UTC let date_components = return_date_components_v2({ "date": eventObject.get('startDate'), "timezone": "UTC" }); date_components.amPm = null; if( eventObject.get('startTime') ){ let time_data = returnHoursAndMinutesForMilitaryTime( eventObject.get('startTime') ); date_components.hour = time_data.hours; date_components.minute = time_data.minutes; } let offset_date = return_utc_date_v2({ "dateComponents": date_components, "timezone": store.timezone }); return format_utc_date_v2({ "utcDate": offset_date, "timezone": store.timezone, "formatString": "MMM D, YYYY, h:mm A" }); } } return null; }, return_markup( eventObject, type ){ var href_base = ''; var clubObject = eventObject.get('clubObject'); var web_settings = clubObject.get('web_settings'); if( web_settings ){ if( web_settings.get('event_links') == "use_clubspot_regatta_links" && ( eventObject.className == "regattas" || eventObject.className == "subevents" ) ){ //force clubspot urls for regattas href_base += 'https://theclubspot.com'; } } var start_offset, end_offset; var start_offset_time, end_offset_time; if( eventObject.get('startDate') ){ start_offset = returnOffsetDateForUTCDate( eventObject.get('startDate') ); //handle start/end times set explicitly on the event (for socials) if( eventObject.get('startTime') ){ var start_data = returnHoursAndMinutesForMilitaryTime( eventObject.get('startTime') ); start_offset.setHours( start_offset.getHours() + start_data.hours ); start_offset.setMinutes( start_offset.getMinutes() + start_data.minutes ); } start_offset_time = start_offset.getTime(); } var endDate = eventObject.get('endDate'); if( ! endDate && eventObject.get('startDate') ){ endDate = eventObject.get('startDate'); } if( endDate ){ end_offset = returnOffsetDateForUTCDate( endDate ); //handle start/end times set explicitly on the event (for socials) if( eventObject.get('endTime') ){ var end_data = returnHoursAndMinutesForMilitaryTime( eventObject.get('endTime') ); end_offset.setHours( end_offset.getHours() + end_data.hours ); end_offset.setMinutes( end_offset.getMinutes() + end_data.minutes ); } end_offset_time = end_offset.getTime(); } // returns here if ( type === 'check-header-callout' ) { return start_offset && start_offset.getFullYear() !== new Date().getFullYear() } else if ( type === 'start_offset_time' ) { return start_offset_time; } else if ( type === 'end_offset_time' ) { return end_offset_time; } else if ( type === 'header-callout' ){ return start_offset.getFullYear(); } else if ( type === 'date' ){ //date var date = ''; if( start_offset ){ var style_string = ''; if( eventObject.get('imageURL') ){ //we always want white text for this style_string = ' style="color:#fff !important;" '; } date += '<p class="cs-signups-date-month">' + returnShortMonth( start_offset.getMonth() ) + '</p>'; var days = start_offset.getDate(); if( end_offset && ! eventObject.get('all_day') ){ if( end_offset.getDate() != start_offset.getDate() ){ days += ' - ' + end_offset.getDate(); } } date += '<p class="cs-signups-date-day">' + days + '</p>'; } return date; } else if ( type === 'event-host-1' ){ return $('body').data('club-name'); } else if ( type === 'event-host-2' ){ var club_string = ''; var location_params = { eventObject: eventObject, hideCountry: true, hideZip: true, hideStreet: true, } var club_location_params = { eventObject: clubObject, hideCountry: true, hideZip: true, hideStreet: true, } if(formatLocation(location_params) ){ if( club_string.length >= 1 ){ club_string += ', '; } club_string += formatLocation(location_params); }else if( clubObject && formatLocation(club_location_params) ){ if( club_string.length >= 1 ){ club_string += ', '; } club_string += formatLocation(club_location_params); } return club_string; } else if ( type === 'links' ){ var content; if( eventObject.className == "regattas" || eventObject.className == "subevents" ){ //check for external events if( eventObject.get('regatta_external') && eventObject.get('external_regatta_url') ){ //it's an external event content = '<p class="tiniestPaddingTop"><a target="_blank" href="'+ eventObject.get('external_regatta_url') +'">Event page</a></p>'; }else{ if( eventObject.className == "subevents" ){ eventObject = eventObject.get('regattaObject'); } //event page content = '<p class="tiniestPaddingTop"><a href="'+href_base+'/regatta/'+ eventObject.id +'">Event page</a></p>'; //entries content += '<p class="tiniestPaddingTop"><a href="'+href_base+'/regatta/'+ eventObject.id +'/#entry-list">Entries</a></p>'; //race docs content += '<p class="tiniestPaddingTop"><a href="'+href_base+'/regatta/'+ eventObject.id +'/#notice-board">Notice board</a></p>'; } }else if( eventObject.className == "events" ){ //check for external events if( ! eventObject.get('calendarOnly') ){ if( eventObject.get('external_event') && eventObject.get('external_event_url') ){ //it's an external event content = '<p class="tiniestPaddingTop"><a target="_blank" href="'+ eventObject.get('external_event_url') +'">Event page</a></p>'; }else{ //event page content = '<p class="tiniestPaddingTop"><a href="'+href_base+'/event/'+ eventObject.id +'">Event page</a></p>'; } } }else if( eventObject.className == "camps" ){ //it's a camp content = '<p class="tiniestPaddingTop"><a href="'+href_base+'/camp-registrations/'+ eventObject.id +'">Entry list</a></p>'; } return content; } else if ( type === 'button' ){ var content; var show_register_button = this.returnShowRegisterButtonForEvent( eventObject ); if( eventObject.className == "regattas" || eventObject.className == "subevents" ){ if( eventObject.className == "subevents" ){ eventObject = eventObject.get('regattaObject'); } //check for external events if( eventObject.get('regatta_external') && eventObject.get('external_regatta_url') ){ //it's an external event content = '<button class="relative background">View event<a class="absoluteA" target="_blank" href="'+ eventObject.get('external_regatta_url') +'"></a></button>'; }else{ //handle button status if( show_register_button ){ //register content = '<button class="relative background">Register<a class="absoluteA" href="'+href_base+'/register/regatta/'+ eventObject.id +'/class"></a></button>'; }else{ //results content = '<button class="relative background">View Results<a class="absoluteA" href="'+href_base+'/regatta/'+ eventObject.id +'/results"></a></button>'; } } }else if( eventObject.className == "events" ){ var registration_link; if( eventObject.get('events_v2') ){ registration_link = '/event/' + eventObject.id; }else if( eventObject.get('external_event') && eventObject.get('external_register_link') ){ registration_link = eventObject.get('external_register_link'); }else if( eventObject.get('event_product') ){ if( eventObject.get('event_product').get('availability') && eventObject.get('event_product').get('availability').startDate ){ registration_link = "/reserve/" + eventObject.get('event_product').id + "?date=" + eventObject.get('event_product').get('availability').startDate; } } if( registration_link && show_register_button ){ content = '<button class="relative background">RSVP<a class="absoluteA" href="'+href_base+''+registration_link+'"></a></button>'; } }else if( eventObject.className == "camps" ){ //handle button status if( show_register_button ){ //register content = '<button class="relative background">Register<a href="'+href_base+'/register/camp/'+ eventObject.id +'/class" class="absoluteA"></a></button>'; } } return content; } }, returnShowRegisterButtonForEvent( eventObject ){ //set defaults var now = new Date(); var ended = false; var registration_closed = eventObject.get('registration_closed'); var startDate = eventObject.get('startDate'); var lastChanceDate = eventObject.get('lastChanceDate'); var endDate = eventObject.get('endDate'); if( ! endDate && startDate ){ endDate = startDate; } var endDate_plus_one; if( endDate ){ endDate_plus_one = new Date( endDate.getTime() ); endDate_plus_one.setDate( endDate.getDate() + 1 ); } if( now > endDate_plus_one && endDate_plus_one ){ ended = true; } if( now > lastChanceDate && lastChanceDate ){ registration_closed = true; } if( ended || registration_closed || eventObject.get('calendarOnly') ){ return false; } else { return true; } }, } }) //define the component app_overlays.component("event-list-event-component", config); app_content.component("event-list-event-component", config); </script> <style> .res-callout{ display: flex; /* Add this to make the container a flexbox */ align-items: center; /* Center items vertically */ white-space: nowrap; /* Prevent wrapping */ height: fit-content; width: fit-content; padding: 3.5px; background-color: #fff; border: 1px solid #e4ebf1; border-radius: 5px; font-size: 10px; text-transform: uppercase; color: #5c6d7a; } </style> <script type="text/html" id="template-event-list"> <div class="cs-background-image relative" :class="class_string" :style="style_string"> <div class="element-wrapper" :class="'element-wrapper_' + component_type" :style="element_style_string" style="max-width:100% !important;min-width:90%;" > <!-- list view --> <template v-if="formatting_obj.layout === 'list' || !formatting_obj.layout"> <div class="event-list-wrap centeredBlock centeredText" :class="'event-list-wrap_' + client_id" :data-client_id="client_id"> <div class="flexNoWrap align-items-center" style="padding-bottom:5px;" > <h3 class="smallMarginRight" :style="{ 'font-size': '20px !important', 'font-weight': '600 !important', 'padding': '10px' }">{{ event_dates.charAt(0).toUpperCase() + event_dates.slice(1) || 'Upcoming' }} events</h3> <div class="view-all-events-wrapper tinyMarginLeft" @click="popViewAllEventsOverlay()" > <div :style="{ 'font-size': '14px !important', 'font-weight': '400' }">View all</div> <div class="smallDrilldownHeaderBackButton flip"></div> </div> </div> <div class="standardCardBody noBorderBottom fff noPadding overflowHidden"> <div v-if="!loading && filtered_events.length >= 1" class="tableInsert hundop flexWrapEvent"> <template v-for="(event, index) in filtered_events" :key="event.id"> <event-list-event-component :event="event" ></event-list-event-component> </template> </div> <div v-if="loading" class="cardBodySpinnerWrap active relative"> <div class="cardBodySpinner centeredBlock"></div> </div> <p v-if="!loading && filtered_events.length == 0" class="noObjectsMSG withPadding textLeft">No events found.</p> </div> </div> </template> <!-- carousel view --> <template v-if="formatting_obj.layout === 'carousel'"> <div class="event-list-wrap centeredBlock centeredText" :class="'event-list-wrap_' + client_id" :data-client_id="client_id"> <div class="flexNoWrap align-items-center event_list_carousel_default" style="padding: 0px 0px 15px 5px;" > <h3 class="event_list_carousel_header" >{{ event_dates || 'upcoming' }} events</h3> <div v-if="filter_types_arr && filter_types_arr.length > 0 && event_tags.length > 0" style="display: flex; align-items: center; justify-content: flex-start;" > <h3 class="tinyMarginLeft event_list_carousel_header" >in</h3> <select v-model="selected_event_tag_id" class="tinyMarginLeft event_list_carousel_header select_filter" > <option v-for="tag in event_tags" :key="tag.id" :value="tag.id" > {{ tag.get('name') }} </option> </select> <i class="fa-solid fa-sort-down tinyMarginLeft tinyMarginBottom pointer" style="color: #000;"></i> </div> </div> <span @mouseenter="start_auto_scroll('left')" @mouseleave="stop_auto_scroll" class="event-list-auto-scroll left" ></span> <div class="standardCardBody noBorderTop noBorderBottom fff noPadding"> <div ref="event_list_card_wrapper" v-if="!loading && filtered_events.length >= 1" class="tableInsert hundop event-list-card-wrapper" :style="{ gap: formatting_obj.card_spacing + 'px' || '25px' }" > <template v-for="(event, index) in filtered_events" :key="event.id"> <event-list-card-component :event="event" :formatting="formatting_obj" ></event-list-card-component> </template> </div> <div v-if="loading" class="cardBodySpinnerWrap active relative"> <div class="cardBodySpinner centeredBlock"></div> </div> <p v-if="!loading && filtered_events.length == 0" class="noObjectsMSG withPadding textLeft">No events found.</p> </div> <span @mouseenter="start_auto_scroll('right')" @mouseleave="stop_auto_scroll" class="event-list-auto-scroll right" ></span> </div> </template> </div> </div> </script> <style> .event_list_carousel_default { --event-list-carousel-font-family: 'Cormorant Garamond'; --event-list-carousel-font-size: 16px; --event-list-carousel-font-weight: 600; --event-list-carousel-text-transform: uppercase; --event-list-carousel-letter-spacing: 1.2px; --event-list-carousel-color: #000; } .event_list_carousel_header { /* font-family: var(--event-list-carousel-font-family) !important; */ font-size: var(--event-list-carousel-font-size) !important; font-weight: var(--event-list-carousel-font-weight) !important; text-transform: var(--event-list-carousel-text-transform) !important; letter-spacing: var(--event-list-carousel-letter-spacing) !important; color: var(--event-list-carousel-color) !important; } .event_list_carousel_header.select_filter { width: 100px !important; text-overflow: ellipsis !important; border: none !important; border-bottom: 0.5px solid black !important; appearance: none !important; -webkit-appearance: none !important; -moz-appearance: none !important; background: none !important; padding: 0px !important; box-shadow: none !important; display: inline-block !important; background-image: none !important; } .event-list-card-wrapper { position: relative; display: flex; justify-content: flex-start; align-items: center; padding: 10px; overflow: scroll !important; scrollbar-width: none !important; } .event-list-auto-scroll { position: absolute; top: 0; height: 100%; width: 20px; } .event-list-auto-scroll.left { left: 0; } .event-list-auto-scroll.right { right: 0; } </style> <script> app_content.component('event-list-component', { template: '#template-event-list', data() { return { allEvents: [], active_filters: { eventDates: 'upcoming', }, loading: true, sortObject: { order: 'ascending', key: 'startDate' }, event_tags: [], selected_event_tag_id: null, scroll_animation_frame: null, } }, props: { class_string: String, style_string: String, element_style_string: String, formatting: String, filter_types: String, event_dates: String, component_type: String, client_id: String, //admin component: Object, last_update: Number, }, mounted() { this.loadEventList(this.client_id); }, methods: { popViewAllEventsOverlay(){ store.overlays.view_all_events = true; store.components.view_all_events.allEvents = this.allEvents; //passed props store.components.view_all_events.class_string = this.class_string; store.components.view_all_events.style_string = this.style_string; store.components.view_all_events.element_style_string = this.element_style_string; store.components.view_all_events.component_type = this.component_type; store.components.view_all_events.client_id = this.client_id; }, start_auto_scroll(direction) { const wrapper = this.$refs.event_list_card_wrapper; const amount = direction === 'right' ? 10 : -10; //scroll speed const scrollStep = () => { wrapper.scrollBy({ left: amount, behavior: 'auto' }); this.scroll_animation_frame = requestAnimationFrame(scrollStep); }; scrollStep(); }, stop_auto_scroll() { cancelAnimationFrame(this.scroll_animation_frame); this.scroll_animation_frame = null; }, loadEventList( client_id, overrides = {} ){ var promise = Parse.Promise.as(); var thisPromise = promise.then(() => { var params = {}; params["client_id"] = client_id; params["overrides"] = overrides; params["request_url"] = window.location.href; return Parse.Cloud.run('retrieve_data_for_component', params); }).then(retrieved_events => { this.allEvents = retrieved_events; if( this.filter_types_arr && this.filter_types_arr.indexOf('tags') !== -1 ){ this.event_tags = this.retrieve_event_tags(); this.selected_event_tag_id = this.event_tags && this.event_tags.length > 0 ? this.event_tags[0].id : null; } if( this.event_dates ){ this.active_filters.eventDates = this.event_dates; } }).then(() => { this.loading = false; }, error => { checkParseError(error); this.loading = false; }); return thisPromise; }, retrieve_event_tags(){ let event_tags = []; this.filtered_events.forEach(event => { if (event.get('event_tags')) { event.get('event_tags').forEach(event_tag => { const exists = event_tags.some(tag => tag.id === event_tag.id); if (!exists) { event_tags.push(event_tag); } }); } }); return event_tags; }, sortEventList( events, sortObject ){ if( ! sortObject ){ sortObject = {} } if( ! sortObject.key ){ sortObject.key = "startDate"; } if( ! sortObject.order ){ sortObject.order = "ascending" } if( sortObject.key == "name" ){ if( sortObject.order == "ascending" ){ events = events.sort( this.sortByEventName_ascending ); }else{ events = events.sort( this.sortByEventName_descending ); } }else if( sortObject.key == "startDate" ){ if( sortObject.order == "ascending" ){ events = events.sort( this.sortByStartDate_event_ascending ); }else{ events = events.sort( this.sortByStartDate_event_descending ); } }else if( sortObject.key == "endDate" ){ if( sortObject.order == "ascending" ){ events = events.sort( this.sortByEndDate_event_ascending ); }else{ events = events.sort( this.sortByEndDate_event_descending ); } } return events; }, sortByEventName_ascending( a, b ){ var aName = ''; if( a.get('name') ){ aName += a.get('name').trim().toLowerCase(); } var bName = ''; if( b.get('name') ){ bName += b.get('name').trim().toLowerCase(); } if(aName < bName) { return -1; } if(aName > bName) { return 1; } //they had the same name return 0; }, sortByEventName_descending( a, b ){ var aName = ''; if( a.get('name') ){ aName += a.get('name').trim().toLowerCase(); } var bName = ''; if( b.get('name') ){ bName += b.get('name').trim().toLowerCase(); } if(aName > bName) { return -1; } if(aName < bName) { return 1; } //they had the same name return 0; }, sortByStartDate_event_ascending( a, b ){ var aStartDate = a.get('startDate') if( ! aStartDate ){ aStartDate = a.createdAt; } var bStartDate = b.get('startDate') if( ! bStartDate ){ bStartDate = b.createdAt; } if(aStartDate < bStartDate) { return -1; } if(aStartDate > bStartDate) { return 1; } return 0; }, sortByStartDate_event_descending( a, b ){ var aStartDate = a.get('startDate') if( ! aStartDate ){ aStartDate = a.createdAt; } var bStartDate = b.get('startDate') if( ! bStartDate ){ bStartDate = b.createdAt; } if(aStartDate > bStartDate) { return -1; } if(aStartDate < bStartDate) { return 1; } return 0; }, sortByEndDate_event_ascending( a, b ){ var aEndDate = a.get('endDate') if( ! aEndDate ){ aEndDate = a.createdAt; } var bEndDate = b.get('endDate') if( ! bEndDate ){ bEndDate = b.createdAt; } if(aEndDate < bEndDate) { return -1; } if(aEndDate > bEndDate) { return 1; } return 0; }, sortByEndDate_event_ascending( a, b ){ var aEndDate = a.get('endDate') if( ! aEndDate ){ aEndDate = a.createdAt; } var bEndDate = b.get('endDate') if( ! bEndDate ){ bEndDate = b.createdAt; } if(aEndDate > bEndDate) { return -1; } if(aEndDate < bEndDate) { return 1; } return 0; }, return_start_and_end_timestamps( eventObject ){ //timestamp defaults let start, end; //check the dates let startDate = eventObject.get('startDate'); var endDate = eventObject.get('endDate'); if( ! endDate && eventObject.get('startDate') ){ endDate = eventObject.get('startDate'); } if( startDate ){ //correct for UTC offset, based on event type if( eventObject.className == "events" ){ //these are stored as 12 am utc let start_components = return_date_components_v2({ "date": startDate, "timezone": "UTC", }); if( eventObject.get('startTime') ){ let time_data = returnHoursAndMinutesForMilitaryTime( eventObject.get('startTime') ); start_components.hour = time_data.hours; start_components.minute = time_data.minutes; } let offset_start = return_utc_date_v2({ "dateComponents": start_components, "timezone": store.timezone }); start = offset_start.getTime(); let end_components = return_date_components_v2({ "date": endDate, "timezone": "UTC", }); if( eventObject.get('endTime') ){ let time_data = returnHoursAndMinutesForMilitaryTime( eventObject.get('endTime') ); end_components.hour = time_data.hours; end_components.minute = time_data.minutes; } let offset_end = return_utc_date_v2({ "dateComponents": end_components, "timezone": store.timezone }); end = offset_end.getTime(); }else{ //regattas are already stored in the club's time start = startDate.getTime(); end = endDate.getTime(); } } //return them return { start, end } } /* return_start_and_end_timestamps( eventObject ){ var start_offset, end_offset; var start_offset_time, end_offset_time; if( eventObject.get('startDate') ){ start_offset = returnOffsetDateForUTCDate( eventObject.get('startDate') ); //handle start/end times set explicitly on the event (for socials) if( eventObject.get('startTime') ){ var start_data = returnHoursAndMinutesForMilitaryTime( eventObject.get('startTime') ); start_offset.setHours( start_offset.getHours() + start_data.hours ); start_offset.setMinutes( start_offset.getMinutes() + start_data.minutes ); } start_offset_time = start_offset.getTime(); } var endDate = eventObject.get('endDate'); if( ! endDate && eventObject.get('startDate') ){ endDate = eventObject.get('startDate'); } if( endDate ){ end_offset = returnOffsetDateForUTCDate( endDate ); //handle start/end times set explicitly on the event (for socials) if( eventObject.get('endTime') ){ var end_data = returnHoursAndMinutesForMilitaryTime( eventObject.get('endTime') ); end_offset.setHours( end_offset.getHours() + end_data.hours ); end_offset.setMinutes( end_offset.getMinutes() + end_data.minutes ); } end_offset_time = end_offset.getTime(); } return { start: start_offset_time, end: end_offset_time } } */ }, computed: { filtered_events(){ let filteredEvents = this.allEvents; // filter by upcoming events var timestamp = new Date().getTime(); if (this.active_filters.eventDates !== 'all'){ filteredEvents = filteredEvents.filter(event => { let start = this.return_start_and_end_timestamps( event ).start; let end = this.return_start_and_end_timestamps( event ).end; if (this.active_filters.eventDates === 'past'){ if( start < timestamp && ( end < timestamp || ! end || end == "undefined" ) ){ return event; } } else if (this.active_filters.eventDates === 'upcoming'){ if ( start > timestamp || end > timestamp ){ return event; } } }) } if(this.active_filters.eventDates === 'upcoming'){ this.sortObject = { order: 'ascending', key: 'startDate' } }else{ this.sortObject = { order: 'descending', key: 'startDate' } } if(!this.formatting_obj.layout || this.formatting_obj.layout === 'list'){ return this.sortEventList( filteredEvents, this.sortObject ).slice(0, 5); }else{ if(this.filter_types_arr.indexOf('tags') !== -1 && this.selected_event_tag_id){ return this.sortEventList( filteredEvents, this.sortObject ).filter(event => { if(event.get('event_tags')){ return event.get('event_tags').some(event_tag => event_tag.id == this.selected_event_tag_id); } }); }else{ return this.sortEventList( filteredEvents, this.sortObject ); } } }, formatting_obj(){ if (this.formatting){ return JSON.parse(this.formatting); } return {}; }, filter_types_arr(){ if (this.filter_types){ return JSON.parse(this.filter_types); } return []; }, }, watch: { last_update(newVal, oldVal){ if (newVal !== oldVal){ let event_types = this.component.get('config_latest')?.event_types || []; this.loadEventList( this.client_id, { event_types } ); } } }, }); </script> <script type="text/html" id="template-iframe"> <div v-if="!loading && !is_admin" class="cs-iframe-wrap" :class="class_string" :style="style_string"> <iframe class="cs-iframe" :src="iframe_src"></iframe> </div> <div v-if="!loading && is_admin" class="cs-iframe-wrap cs-min-section-width" :class="class_string" :style="[style_string, { 'max-width': formatting_obj.section_width + '%' }]"> <template v-if="iframe_src" > <iframe class="cs-iframe" sandbox="allow-forms allow-scripts" :src="iframe_src"></iframe> </template> <template v-if="!iframe_src"> <p class="noObjectsMSG centeredBlock centeredText">Click on edit to configure your iframe.</p> </template> </div> </script> <script> /* ----- COMPONENT ----- */ app_content.component('iframe-component', { template: '#template-iframe', data() { return { global: store, loading: true, is_admin: false, } }, props: { class_string: String, style_string: String, iframe_src: String, //admin component: Object, formatting_string: String, last_update: Number }, mounted() { this.is_admin = window.location.href.indexOf('website-builder') !== -1; this.loading = false; }, computed: { formatting_obj(){ if (this.formatting_string){ return JSON.parse(this.formatting_string); } return {}; }, }, }); </script> <script type="text/html" id="template-blog-list"> <div class="relative" :class="class_string" :style="style_string"> <div class="element-wrapper cs-min-section-width" :class="'element-wrapper_' + component_type" :style="[element_style_string, { 'min-width': `${formatting_obj.section_width}%`, 'max-width': `${formatting_obj.section_width}%` }]"> <div class="blog-list-wrap centeredBlock centeredText" :class="'blog-list-wrap_' + client_id" :data-client_id="client_id"> <div class="ease flexNoWrap show_when_loaded" style="justify-content: center; align-items: center;"> <!-- template type - list --> <template v-if="!formatting_obj.template || formatting_obj.template === 'list'"> <div class="inputWrap searchBarWrap flexGrowOne" style="min-width:100%;"> <input class="searchBar searchBar_blog-list easeFast" placeholder="Search posts..." v-model="search"> </div> <div class="flexGrowOne"></div> </template> <!-- template type - bulletin --> <template v-if="formatting_obj.template === 'bulletin'"> <div class="searchBarWrap_v2" style="width: 100%; display: flex; flex-direction: row; align-items: center; justify-content: center; margin-right: 8px; margin-left: 8px; margin-bottom: 10px;"> <input id="searchBar_blog-list_v2" type="text" placeholder="Search posts..." v-model="search" /> <button id="searchBar_blog-list_submit" type="submit"> <svg style="width:24px;height:24px" viewBox="0 0 24 24"><path fill="#666666" d="M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z" /></svg> </button> </div> </template> </div> <div class="fff noPadding"> <!-- template type - list --> <template v-if="!formatting_obj.template || formatting_obj.template === 'list'"> <div v-if="!loading && truncated_posts.length >= 1" class="tableInsert hundop"> <template v-for="(post, index) in truncated_posts" :key="post.id"> <div :style="{ 'background-color': formatting_obj.background_color || '#fff', 'border-radius': '4px' }" class="blog-row flexWrapLeftAlign relative pointer blog-row-border" :class="'blog-row_' + post.id" :data-post-id="post.id" > <div v-if="post && post.get('thumbnail')" class="blog-post-thumbnail" :style="'background-image:url(' + post.get('thumbnail') + ')'"></div> <div v-if="post && !post.get('thumbnail')" class="blog-post-thumbnail default-image-icon"></div> <div class="blog-row-inner flexGrowOne"> <div class="blog-name-wrap-column"> <p class="blog-title" :style="{ 'color': formatting_obj.title_color || '#000', 'font-weight': formatting_obj.title_styling && formatting_obj.title_styling.bold ? '600 !important' : '400 !important', 'font-style': formatting_obj.title_styling && formatting_obj.title_styling.italic ? 'italic' : 'normal', 'text-decoration': formatting_obj.title_styling && formatting_obj.title_styling.underline ? 'underline' : 'none', }" >{{ post.get('title') }}</p> <p v-if="post && post.get('subtitle')" class="blog-subtitle" :style="{ 'color': formatting_obj.subtitle_color || '#000', 'font-weight': formatting_obj.subtitle_styling && formatting_obj.subtitle_styling.bold ? '600 !important' : '400 !important', 'font-style': formatting_obj.subtitle_styling && formatting_obj.subtitle_styling.italic ? 'italic' : 'normal', 'text-decoration': formatting_obj.subtitle_styling && formatting_obj.subtitle_styling.underline ? 'underline' : 'none', }" >{{ post.get('subtitle') }}</p> <p class="blog-date" :style="{ 'color': formatting_obj.date_color || '#000' }">{{ niceDate(post) }}</p> <p><a class="absoluteA" :href="'/blog-post/' + post.id" style="text-transform: none !important; border-bottom: none !important;"></a> <a :style="{ 'font-variant': 'small-caps', 'opacity': '0.9', 'color': formatting_obj.date_color || '#000', 'border-bottom': 'none !important', 'text-decoration': 'underline !important', 'text-decoration-color': (formatting_obj.date_color || '#000') + ' !important' }" >Read more</a> </p> </div> </div> <div v-if="post && post.get('pinned') === true"> <i class="fa-solid fa-thumbtack thumbtack-icon" style="height: 1em; transform: rotate(45deg);"></i> </div> </div> </template> <div v-if="has_more_posts" class="view_more_button flexNoWrapCenter align-items-center" @click="view_all = true" style="margin-bottom:0px;margin-left:0px;margin-right:0px;" > <div> <p>View more</p> </div> <i class="fa-regular fa-arrow-right easeFast"></i> </div> </div> </template> <!-- template type - bulletin --> <template v-if="formatting_obj.template === 'bulletin'"> <div v-if="!loading && truncated_posts.length >= 1" :class="class_string" > <div class="blog-list-container"> <section class="blog-list-cards-v2" > <div v-for="(post, index) in truncated_posts" :key="post.id"> <article :style="{ 'background-color': formatting_obj.background_color || '#fff' }" style="display: flex; flex-direction: column; align-items: center; justify-content: flex-start; position: relative;" class="blog_post_card_v2" @click="go_to_post( post )" > <div class="blog-list-image-container"> <img v-if="post && post.get('thumbnail')" class="blog_post_thumbnail" :src="post.get('thumbnail')"> <div v-if="post && !post.get('thumbnail')" style="display: flex; align-items: center; justify-content: center;"> <i class="fa-duotone fa-solid fa-newspaper" style="font-size: 10em;"></i> </div> </div> <i v-if="post && post.get('pinned') === true" class="fa-solid fa-thumbtack" style="margin: 3px 0 0 2px; font-size: 1em; color: #fff; transform: rotate(45deg); position: absolute; top: 0; right: 5px;"></i> <div class="card__content"> <div v-if="post && post.get('title')" class="card__title_v2" :style="{ 'color': formatting_obj.title_color || '#000', 'font-weight': formatting_obj.title_styling && formatting_obj.title_styling.bold ? '600 !important' : '400 !important', 'font-style': formatting_obj.title_styling && formatting_obj.title_styling.italic ? 'italic' : 'normal', 'text-decoration': formatting_obj.title_styling && formatting_obj.title_styling.underline ? 'underline' : 'none', }" > {{ post.get('title') }} </div> <div v-if="post && post.get('subtitle')" class="card__subtitle tinyMarginTop" :style="{ 'color': formatting_obj.subtitle_color || '#000', 'font-weight': formatting_obj.subtitle_styling && formatting_obj.subtitle_styling.bold ? '600 !important' : '400 !important', 'font-style': formatting_obj.subtitle_styling && formatting_obj.subtitle_styling.italic ? 'italic' : 'normal', 'text-decoration': formatting_obj.subtitle_styling && formatting_obj.subtitle_styling.underline ? 'underline' : 'none', }" > {{ post.get('subtitle') }} </div> <div class="card__date" :style="{ 'color': formatting_obj.date_color || '#000' }">{{ niceDate(post) }}</div> <div class="card__excerpt"> <a @click.stop :href="'/blog-post/' + post.id" :style="{ 'font-variant': 'small-caps', 'opacity': '0.9', 'color': formatting_obj.date_color || '#000', 'border-bottom': 'none !important', 'text-decoration': 'underline !important', 'text-decoration-color': (formatting_obj.date_color || '#000') + ' !important' }" > read more </a> </div> </div> </article> </div> </section> </div> <div v-if="has_more_posts" class="view_more_button flexNoWrapCenter align-items-center" :class="{'larchmont': is_larchmont || is_larchmont === 'true'}" @click="view_all = true" style="margin-bottom:0px;" > <div> <p>View more</p> </div> <i class="fa-regular fa-arrow-right easeFast"></i> </div> </div> </template> <div v-if="!loading && filtered_posts.length == 0" class="tableInsert hundop blog-list-contain"> <p class="noObjectsMSG withPadding textLeft blog-row flexWrapLeftAlign relative pointer">No posts found.</p> </div> </div> <div v-if="loading" class="cardBodySpinnerWrap active relative"> <div class="cardBodySpinner centeredBlock"></div> </div> </div> </div> </div> </script> <style> /*------------------------ view more buttons --------------------- */ .view_more_button.card-layout{ margin-top:0px; } .view_more_button{ color: #6a7482; margin: 8px; border: 1px solid #e5eaec; border-top: 1px solid #e5eaec; border-radius: 5px; cursor: pointer; font-size: 16px; padding: 16px; margin-top: 8px; } .view_more_button:hover{ color: #081333; background-color: #f8f9fb; } .view_more_button.larchmont{ color: #fff; margin: 8px; border: 1px solid #fff; border-top: 1px solid #fff; border-radius: 5px; cursor: pointer; font-size: 16px; padding: 16px; margin-top: 8px; } .view_more_button.larchmont:hover{ background-color: #002D62; color: #fff; } .view_more_button i{ padding-left: 8px; } .view_more_button:hover i{ color: #4a56f8; transform: translateX(4px); } /* -- end view more button --*/ section.blog-list-cards-v2 { margin: 0 auto; display: grid; grid-template-columns: repeat(3, 1fr); grid-auto-rows: auto; grid-gap: 16px; align-content: center; justify-content: center; padding: 8px; width: 100%; box-sizing: border-box; } .blog-list-image-container { width: 100%; height: 300px; display: flex; align-items: center; justify-content: center; overflow: hidden; border-top-left-radius: 6px; border-top-right-radius: 6px; border-bottom-left-radius: 0px; border-bottom-right-radius: 0px; background-color: #f2f2f2; } .blog_post_thumbnail { width: 100%; height: 100%; object-fit: cover; display: block; } .default_image_icon { background-image: url('https://s3-us-west-2.amazonaws.com/myclubspot/upload-photo-icon.jpg'); background-size: contain; background-position: center; background-repeat: no-repeat; width: 100%; height: 100%; } @media (max-width: 768px) { section.blog-list-cards-v2 { grid-template-columns: repeat(1, 1fr); } } </style> <script> store.components["blog_list"] = { allPosts: [], search: '', loading: true, view_all: false, post_limit: 6 //how many to show on initial load (before clicking "see more") }; app_content.component("blog-list-component", { template: "#template-blog-list", data() { return store.components['blog_list'] }, props: { client_id: String, class_string: String, style_string: String, element_style_string: String, formatting: String, component_type: String, is_larchmont: Boolean, String, //admin component: Object, last_update: Number, }, mounted() { this.loadBlogList( this.client_id ) }, methods: { go_to_post( post ){ window.location.href = "blog-post/" + post.id; }, loadBlogList( client_id, overrides = {} ){ var promise = Parse.Promise.as(); var thisPromise = promise.then(() => { //get new data var params = {}; params["client_id"] = client_id; params["overrides"] = overrides; params["request_url"] = window.location.href; return Parse.Cloud.run('retrieve_data_for_component', params); }).then(retrieved_posts => { // sort with 'pinned' posts on top this.allPosts = retrieved_posts.sort((a, b) => (a.get('pinned') === b.get('pinned') ? 0 : a.get('pinned') ? -1 : 1)); this.loading = false; //and return return Parse.Promise.as(); }, error => { checkParseError( error ); }); return thisPromise; }, niceDate( postObject ){ return dayjs(postObject.get('post_date')).format('MMM DD, YYYY'); }, return_grid_column_css(){ if (window.innerWidth <= 990) { //for mobile return 'repeat(1, 256px)'; } else { return 'repeat(4, 256px)'; } }, }, computed: { has_more_posts(){ if( this.view_all ){ return false; } if( this.filtered_posts.length > this.post_limit ){ return true; } return false; }, truncated_posts(){ if( this.view_all ){ return this.filtered_posts; }else{ return this.filtered_posts.slice(0, this.post_limit); } }, filtered_posts(){ let val = this.search.toLowerCase(); let filteredPosts = this.allPosts; // handle search results using all posts if (this.search.length > 0){ filteredPosts = this.allPosts.filter(post => { let title = post.get('title').toLowerCase(); if ( title.includes(val) ){ return post; } }) } return filteredPosts; }, formatting_obj(){ if (this.formatting){ return JSON.parse(this.formatting); } return {}; }, } }) </script> <script type="text/html" id="template-custom-code"> <template v-if="preview_element"> <div class="cs-custom-code-wrap" :class="class_string"> <textarea @input="debounce(() => customCodeInputChanged($event))" @paste="debounce(() => customCodeInputChanged($event))" class="custom-code-textarea" placeholder="Enter custom code here..." :value="custom_code_value" ></textarea> </div> </template> <template v-if="!preview_element"> <span v-if="!loading" v-html="custom_code"></span> </template> </script> <script> function createDebounce() { let timeout = null; return function (fnc, delayMs) { clearTimeout(timeout); timeout = setTimeout(() => { fnc(); }, delayMs || 800); }; } app_content.component('custom-code-component', { template: '#template-custom-code', data() { return { loading: true, debounce: createDebounce(), custom_code_value: '', } }, props: { custom_code: String, //admin class_string: String, preview_element: { type: Boolean, default: false, }, component: { type: Object, default: null, }, last_update: Number, }, mounted() { this.loading = false; if (this.preview_element){ this.custom_code_value = this.component.get('config_latest').custom_code; } }, methods: { customCodeInputChanged(event){ let value = event.target.value; this.custom_code_value = value; save_changes_for_component( this.component, 'custom_code', value ); //store.eventBus.$emit('saveChangesForComponent', this.component, 'custom_code', value, false) } }, }); </script> <script type="text/html" id="template-text"> <div v-memo="[last_element_update]" :class="[{'hidden': (column_index + 1) > number_of_columns }, 'text-column_' + column_index]" class="cs-text_text-wrap-column" :data-column="column_index" > <template v-for="(element, element_index) in nice_elements"> <template v-if="preview_element"> <preview-element-component :element="element" :element_type="element.type" :element_content="element.content" :element_column_count="element.column_count" :element_column_gap="element.column_gap" :column_index="column_index" :element_index="element_index" :componentObject="component" ></preview-element-component> </template> <template v-if="!preview_element"> <element-component :element_type="element.type" :element_content="element.content" :element_column_count="element.column_count" :element_column_gap="element.column_gap" ></element-component> </template> </template> <!-- handle case where there are no elements (admin only) --> <template v-if="preview_element && elements && elements.length == 0"> <div class="no-elements-wrap"> <div class="tapped-insert-new-element-relative" @click="tapped_add_element( $event )" >+</div> </div> </template> </div> </script> <script> app_content.component('text-component', { template: '#template-text', data() { return {} }, props: { number_of_columns: [String, Number], preview_element: Boolean, component: Object, last_update: Number, //last component update last_element_update: Number, //last element update elements: Array, column_prop: String, column_index: Number, }, methods:{ tapped_add_element(){ var params = {}; params["component"] = this.component; params["insert_after"] = 0; params["column_index"] = this.column_index; handle_add_element( params ); } }, computed: { nice_elements(){ var nice_elements = []; if (this.elements){ nice_elements = this.elements; } else if (this.column_prop && typeof this.column_prop === 'string'){ nice_elements = JSON.parse(unescape(decodeURIComponent(this.column_prop))); } return nice_elements; } }, }); </script> <script type="text/html" id="template-image-with-text"> <div ref="animationTarget" class="cs-image-with-text flexWrap relative delay5s" :class="[class_string, { 'scroll-fade': !is_admin && !tab_element }, { 'view_hidden': ! is_admin && ( !formatting_obj.animation || formatting_obj.animation !== 'none' ) }, { 'background': image_mat === 'hide' }]" :style="style_string"> <template v-if="!loading && left_right_orientation === 'right'"> <!-- text first (image on the right) --> <div class="cs-image-with-text_text-wrap flexGrowOne"> <div :style="element_style_string"> <div v-memo="Object.keys(elements_obj)" class="element-wrapper"> <template v-for="(element, element_index) in elements_obj"> <template v-if="preview_element"> <preview-element-component :element="element" :element_type="element.type" :element_content="element.content" :element_column_count="element.column_count" :element_column_gap="element.column_gap" :element_index="element_index" :componentObject="component" ></preview-element-component> </template> <template v-if="!preview_element"> <element-component :element_type="element.type" :element_content="element.content" :element_column_count="element.column_count" :element_column_gap="element.column_gap" ></element-component> </template> </template> <!-- handle case where there are no elements (admin only) --> <template v-if="preview_element && elements_obj && elements_obj.length == 0"> <div class="no-elements-wrap"> <div class="tapped-insert-new-element-relative" @click="tapped_add_element( $event )" >+</div> </div> </template> </div> </div> </div> <div class="cs-image-with-text_image-wrap flexGrowOne"> <div class="cs-image-with-text_image-wrap-inner"> <div :style="check_full_background()" class="cs-image-with-text_image-slider-outer"> <div v-if="formatting_obj.background_size !== 'full'" :class="['cs-image-with-text-image-mat', { background: image_mat === 'show' }]" ></div> <image-slider-component :client_id="client_id" :formatting="formatting" :images_string="images_string" :preview_element="preview_element" :component="component" :last_update="last_update" ></image-slider-component> </div> </div> </div> </template> <template v-if="!loading && left_right_orientation === 'left'"> <!-- image first (text on the right) --> <div class="cs-image-with-text_image-wrap flexGrowOne"> <div class="cs-image-with-text_image-wrap-inner"> <div :style="check_full_background()" class="cs-image-with-text_image-slider-outer"> <div v-if="formatting_obj.background_size !== 'full'" :class="['cs-image-with-text-image-mat', { background: image_mat === 'show' }]" ></div> <image-slider-component :client_id="client_id" :formatting="formatting" :images_string="images_string" :preview_element="preview_element" :component="component" :last_update="last_update" ></image-slider-component> </div> </div> </div> <div class="cs-image-with-text_text-wrap flexGrowOne"> <div :style="element_style_string"> <div v-memo="[last_element_update]" class="element-wrapper" > <template v-for="(element, element_index) in elements_obj"> <template v-if="preview_element"> <preview-element-component :element="element" :element_type="element.type" :element_content="element.content" :element_column_count="element.column_count" :element_column_gap="element.column_gap" :element_index="element_index" :componentObject="component" ></preview-element-component> </template> <template v-if="!preview_element"> <element-component :element_type="element.type" :element_content="element.content" :element_column_count="element.column_count" :element_column_gap="element.column_gap" ></element-component> </template> </template> <!-- handle case where there are no elements (admin only) --> <template v-if="preview_element && elements_obj && elements_obj.length == 0"> <div class="no-elements-wrap"> <div class="tapped-insert-new-element-relative" @click="tapped_add_element( $event )" >+</div> </div> </template> </div> </div> </div> </template> </div> </script> <style> .delay5s{ animation-delay: 0.5s; } .view_hidden{ opacity: 0; } @keyframes fade-left-right { 0% { opacity: 0; transform: translateX(-100%) scaleX(0.5); } 100% { opacity: 1; transform: translateX(0) scaleX(1); } } @keyframes fade-right-left { 0% { opacity: 0; transform: translateX(100%) scaleX(0.5); } 100% { opacity: 1; transform: translateX(0) scaleX(1); } } .fade-left-right { animation: fade-left-right 1s ease-in-out forwards; } .fade-right-left { animation: fade-right-left 1s ease-in-out backwards; } </style> <script> app_content.component('image-with-text-component', { template: '#template-image-with-text', data() { return { is_admin: false, loading: true, } }, props: { class_string: String, style_string: String, element_style_string: String, elements_string: [String, Object], left_right_orientation: String, image_mat: String, //image slider client_id: String, formatting: String, images_string: String, //admin preview_element: Boolean, component: Object, last_update: Number, //last component update last_element_update: Number, //last element update tab_element: Boolean, }, mounted() { this.loading = false; this.is_admin = window.location.href.indexOf('website-builder') !== -1; }, updated(){ this.createIntersectionObserver(); }, methods: { check_full_background(){ if ( this.formatting_obj && this.formatting_obj.background_size === 'full' ){ return { top: 0, bottom: 0, left: 0, right: 0, } } }, tapped_add_element(){ var params = {}; params["component"] = this.component; params["insert_after"] = 0; params["column_index"] = 0; handle_add_element( params ); }, createIntersectionObserver(){ const options = { root: null, rootMargin: '0px', threshold: 0.5 //trigger animation when at least 50% of the target is visible }; const observer = new IntersectionObserver(this.handleIntersection, options); //observe the target element observer.observe(this.$refs.animationTarget); }, handleIntersection(entries, observer) { entries.forEach(entry => { //check if target is intersecting if (entry.isIntersecting) { //apply animation class entry.target.classList.add(this.formatting_obj.animation); entry.target.classList.remove('view_hidden'); //stop observing to prevent re-triggering observer.unobserve(entry.target); } }); }, }, computed: { elements_obj(){ if (this.elements_string && typeof this.elements_string === 'string'){ return JSON.parse(unescape(decodeURIComponent(this.elements_string))); } else if (this.elements_string){ return this.elements_string; } else { return {}; } }, formatting_obj(){ if (this.formatting){ return JSON.parse(this.formatting); } else { return {}; } }, }, }); </script> <script type="text/html" id="template-image-slider"> <div class="cs-image-slider-wrap background"> <div class="cs-image-slider" :class="[ 'cs-image-slider-' + filtered_images_length, 'cs-image-slider_' + client_id ]" :data-client-id="client_id" data-interval="8000" data-slider-index="0" > <!-- now add the images --> <template v-if="!loading && filtered_images_length > 0" v-for="(image, index) in filtered_images"> <template v-if="image && typeof image !== 'object'"> <template v-if="image && image.indexOf('video') >= 1"> <div class="cs-image-slider-card" :class="[ 'cs-image-slider-card-' + index, { 'active': index === display_index } ]" style="display:flex;align-items:center;justify-content:center;" > <video class="cs-background-video" autoplay playsinline loop muted :src="image" style="display:initial;"></video> </div> </template> <template v-if="image && typeof image === 'string'"> <div class="cs-image-slider-card" :class="[ 'cs-image-slider-card-' + index, { 'active': index === display_index } ]" > <div class="cs-background-image-photo" :style="`background-image: url(${image})`"></div> </div> </template> </template> <template v-if="(component && component.get('type') === 'image-with-text') && !image"> <div class="cs-image-slider-card" :class="[ 'cs-image-slider-card-' + index, { 'active': index === 0 } ]" > <div class="cs-background-image-photo default-image-icon" style="background-size:30%!important;"></div> </div> </template> </template> <template v-if="formatting_obj.overlay_opacity"> <div class="cs-image-overlay" :style="{ 'opacity': formatting_obj.overlay_opacity / 100 }"></div> </template> </div> </div> </script> <script> /* ----- COMPONENT ----- */ app_content.component('image-slider-component', { template: '#template-image-slider', data() { return { loading: true, display_index: 0, } }, props: { client_id: String, formatting: String, images_string: String, //for preview element preview_element: { type: Boolean, default: false, }, component: { type: Object, default: null, }, last_update: { type: Number, default: null, }, }, mounted() { this.loading = false; }, created(){ setInterval(() => { if (!this.preview_element){ if (this.display_index < this.filtered_images_length - 1){ this.display_index++; } else { this.display_index = 0; } } }, 8000); }, computed: { formatting_obj(){ if (this.formatting){ return JSON.parse(this.formatting); } }, filtered_images(){ if (!this.preview_element){ return JSON.parse(this.images_string).filter(image => { if ( image !== null ) return image; }); } else { return JSON.parse(this.images_string); } return []; }, filtered_images_length(){ if (!this.preview_element){ return JSON.parse(this.images_string).filter(image => { if ( image !== null ) return image; }).length; } else { return JSON.parse(this.images_string).length; } return 0; }, }, watch: { last_update(newVal, oldVal){ if (newVal !== oldVal){ let display_index = this.component.get('config_latest')?.display_index || 0; this.display_index = display_index; } }, } }); </script> <script type="text/html" id="template-hero-banner"> <div class="cs-background-image background relative" :class="[class_string, { 'scroll-fade': !is_admin && !tab_element }]" :style="style_string"> <image-slider-component :client_id="client_id" :formatting="formatting" :images_string="images_string" :preview_element="preview_element" :component="component" :last_update="last_update" ></image-slider-component> <div class="element-wrapper" :class="'element-wrapper_' + component_type" :style="element_style_string" > <div v-memo="[last_element_update]"> <!-- loop through elements --> <template v-for="(element, element_index) in elements_obj"> <template v-if="preview_element"> <preview-element-component :element="element" :element_type="element.type" :element_content="element.content" :element_column_count="element.column_count" :element_column_gap="element.column_gap" :element_index="element_index" :componentObject="component" ></preview-element-component> </template> <template v-if="!preview_element"> <element-component :element_type="element.type" :element_content="element.content" :element_column_count="element.column_count" :element_column_gap="element.column_gap" ></element-component> </template> </template> <!-- handle case where there are no elements (admin only) --> <template v-if="preview_element && elements_obj.length == 0"> <div class="no-elements-wrap"> <div class="tapped-insert-new-element-relative" @click="tapped_add_element( $event )" >+</div> </div> </template> </div> </div> </div> </script> <script> app_content.component('hero-banner-component', { template: '#template-hero-banner', data() { return { is_admin: false, loading: true, } }, props: { class_string: String, style_string: String, element_style_string: String, component_type: String, elements_string: [String, Object], //image slider client_id: String, formatting: String, images_string: String, //admin preview_element: Boolean, component: Object, last_update: Number, //last component update last_element_update: Number, //last element update tab_element: Boolean, }, mounted() { this.loading = false; this.is_admin = window.location.href.indexOf('website-builder') !== -1; }, methods:{ tapped_add_element(){ var params = {}; params["component"] = this.component; params["insert_after"] = 0; params["column_index"] = 0; handle_add_element( params ); } }, computed: { elements_obj(){ if (this.elements_string && typeof this.elements_string === 'string'){ return JSON.parse(unescape(decodeURIComponent(this.elements_string))) } else { return this.elements_string } return []; }, }, }); </script> <script type="text/html" id="template-element"> <template v-if="element_type === 'accordion_header'"> <div class="cs-accordion-header"> <div class="cs-accordion-header-toggle-wrap flexWrap"> <div class="cs-accordion-header-toggle">+</div> </div> <span v-html="element_content"></span> </div> </template> <template v-if="element_type === 'accordion_body'"> <div class="cs-accordion-body" v-html="element_content" :style="`column-count: ${element_column_count} !important; column-gap: ${element_column_gap}em;`" ></div> </template> <template v-else-if="element_type !== 'accordion_header' && element_type !== 'accordion_body'"> <div :style="`column-count: ${element_column_count} !important; column-gap: ${element_column_gap}em;`"> <span v-html="element_content"></span> </div> </template> </script> <script> app_content.component('element-component', { template: '#template-element', data() { return { loading: true, } }, props: { element_type: String, element_content: String, element_column_count: Number, element_column_gap: Number, }, mounted() { this.loading = false; }, }); </script> <script type="text/html" id="template-unlayer-component"> <template v-if="preview_element"> <div class="cs-unlayer-component-wrap unlayer-content" :class="class_string"> <!-- hover button to open unlayer builder --> <div class="darken-overlay easeFast"> <div class="hover-toolbar"> <div class="hover-button flexNoWrap centeredBlock pointer" @click="openUnlayerBuilder()"> <div class="hover-button-text"> Update <i class="fa-regular fa-pen-to-square" style="padding-left:3px;"></i> </div> </div> </div> </div> <!-- if there is content --> <div v-if="!loading && unlayerHtml" class="relative" > <div :id="'freeform_id_' + component.id" v-html="return_html_with_css()" ></div> </div> <!-- if there is no content --> <div v-else-if="!loading"> <div class="addFreeformContentZone" @click="openUnlayerBuilder()"> <div class="quick-action-wrap noMaxWidth centeredBlock" > <div class="quick-action-icon-wrap"> <i class="fa-light fa-scribble"></i> </div> <p class="quick-action-title" style="font-family:roboto;">Freeform section</p> </div> </div> </div> </div> </template> <template v-if="!preview_element"> <div class="cs-unlayer-component-wrap"> <div class="relative"></div> <div v-if="!loading" v-html="return_html_with_css()"></div> </div> </template> </script> <script> function createDebounce() { let timeout = null; return function (fnc, delayMs) { clearTimeout(timeout); timeout = setTimeout(() => { fnc(); }, delayMs || 800); }; } app_content.component('unlayer-component', { template: '#template-unlayer-component', data() { return { unlayerHtml: null, loading: true, debounce: createDebounce(), template: null, unlayer: '', } }, props: { unlayer_chunks: Object, class_string: String, preview_element: { type: Boolean, default: false, }, component: { type: Object, default: null, }, }, mounted() { if(!this.unlayer_chunks){ // find template for component id this.return_unlayer_data() } this.loading = false; }, methods: { return_unlayer_data(){ if(this.component.get('config_latest') && this.component.get('config_latest').unlayer_component.chunks){ this.unlayerHtml = this.component.get('config_latest').unlayer_component.chunks.body this.unlayerHtml = this.unlayerHtml.replace('style="min-height: 100vh;', 'style="min-height: auto;'); } }, openUnlayerBuilder(){ var jsonContent if(this.component.get('config_latest') && this.component.get('config_latest').unlayer_component){ jsonContent = this.component.get('config_latest').unlayer_component.design } var params = { jsonContent: jsonContent, fn: this.saveUnlayerContent } launch_unlayer_builder(params) }, saveUnlayerContent(content){ var promise = Promise.resolve().then(() => { var htmlContent = content.chunks.body htmlContent = htmlContent.replace('style="min-height: 100vh;', 'style="min-height: auto;'); this.unlayerHtml = htmlContent save_changes_for_component( this.component, 'unlayer_component', content ); }) return promise; }, return_html_with_css(){ let data if(this.preview_element){ data = this.component.get('config_latest').unlayer_component.chunks }else{ data = this.unlayer_chunks } var css = data.css var body = data.body let html_with_css = juice.inlineContent(body, css); html_with_css = html_with_css.replace('100vh', 'auto') return html_with_css } }, computed: { } }); </script> <style scoped> @media screen and (max-width: 768px) { .u-row { flex-wrap: wrap !important; /* Allow columns to wrap */ } .u-col { flex: 0 0 50% !important; /* Two columns per row */ max-width: 50% !important; } } @media screen and (max-width: 480px) { .u-col { flex: 0 0 100% !important; /* One column per row */ max-width: 100% !important; } } </style> <script type="text/html" id="template-form"> <div class="form-wrapper centeredBlock centeredText"> <template v-if="!loading && is_admin"> <div v-if="!form_id" @click="link_form()" class="noObjectsMSG centeredBlock centeredText" style="padding: 5em;" > <p style="cursor: pointer;"><i class="fa-light fa-list-dropdown smallMarginRight"></i>Click to link a form</p> </div> </template> <template v-if="!loading"> <div v-if="form_id && json_form" id="form-template" class="cs-min-section-width" :style="{ 'max-width': formatting_obj.section_width + '%' }" > <div class="form-color-theme" :style="{ 'background-color': color_theme }"> </div> <div class="form-header-wrapper"> <!-- name --> <div v-if="json_form && json_form.name" class="form-name flex-row-start"> <h3 class="default-font-css" style="font-size: 32px; padding: 5px; text-align: left;">{{ json_form.name }}</h3> </div> <!-- description --> <div v-if="json_form && json_form.description" class="form-description flex-row-start tinyMarginTop"> <p class="default-font-css" style="padding: 5px; text-align: left;">{{ json_form.description }}</p> </div> <template v-if="!valid_membership"> <div @click="tapped_log_in()" class="noObjectsMSG leftText" style="padding: 5px; min-width: fit-content;" > <p style="cursor: pointer;"><i class="fa-light fa-circle-exclamation smallMarginRight"></i>This form is restricted to members only. Please <a :href="login_url">click here to log in</a> and view this form.</p> </div> </template> </div> <template v-if="valid_membership"> <!-- default fields --> <template v-if="collect_default_fields && selected_fields.length > 0"> <default-field-component-wb :form="json_form" :is_admin="is_admin" :default_fields="selected_fields" :rerender_key="rerender_key" ></default-field-component-wb> </template> <!-- custom fields --> <template v-if="json_form.custom_fields" v-for="(custom_field, index) in custom_fields"> <custom-field-component-wb :id="custom_field.id" :form="json_form" :is_admin="is_admin" :custom_field="custom_field" :last_update="last_update" ref="custom_field_components" @field-updated="handle_field_update" ></custom-field-component-wb> </template> <!-- submit --> <div style="width: 100%; display: flex; justify-content: space-between; align-items: center;" class="tinyMarginTop"> <button v-if="!loading_submission" @click="tapped_submit_form()" :style="{ 'background-color': button_disabled ? 'var(--custom-field-grey-2)' : color_theme }" :class="[ 'default-font-css', 'submit-button', { 'inactive': button_disabled } ]" :disabled="is_admin" >Submit</button> <i v-if="loading_submission" class="fa-sharp-duotone fa-solid fa-spinner-third fa-spin smallMarginLeft" :style="{ 'color': color_theme, 'font-size': '1.2em', 'animation-duration': '1.2s' }" ></i> <p @click="!loading_submission && tapped_clear_form()" :style="{ 'color': loading_submission ? 'var(--custom-field-grey-2)': color_theme }" style="font-size: 14px; font-weight: 600; cursor: pointer;" >Clear form</p> </div> </template> </div> <div v-if="form_id && !json_form" class="noObjectsMSG centeredBlock centeredText" style="padding: 5em;" > <p style="cursor: pointer;"><i class="fa-light fa-list-dropdown smallMarginRight"></i>No form found.</p> </div> </template> </div> </script> <style> #form-template{ display: flex; flex-direction: column; align-items: center; justify-content: flex-start; margin: 0.5em 0.5em 1em 0.5em; padding: 0 0 1.5em 0; width: 100%; max-width: 700px; height: 100%; } #form-template .form-color-theme{ color: #fff; height: 10px; border-top-left-radius: 8px; border-top-right-radius: 8px; width: 100%; padding: 0 1.5em 0 1.5em; box-sizing: border-box; } #form-template .default-font-css{ font-family: Roboto, Arial, Helvetica, sans-serif; font-size: 16px; font-weight: 400; } #form-template .flex-row-start{ display: flex; flex-direction: row; align-items: center; justify-content: flex-start; } #form-template .submit-button{ color: #fff; border-radius: 6px; font-size: 14px; padding: 8px 24px; cursor: pointer; border: none; } #form-template .submit-button.inactive{ color: #fff; border-radius: 6px; font-size: 14px; padding: 8px 24px; cursor: auto; border: none; } .form-wrapper{ display: flex; align-items: center; justify-content: center; height: fit-content; width: 100%; } .form-header-wrapper{ border-width: 0px 1px 1px 1px; border-color: rgba(195, 195, 195, 0.7); border-style: solid; width: 100%; border-bottom-left-radius: 8px; border-bottom-right-radius: 8px; padding: 1.45em; box-sizing: border-box; } .form-name{ color: rgb(32, 33, 36); box-sizing: border-box; font-weight: 400; font-size: 32px; width: 100%; min-height: 21px; border-bottom: 1.5px solid transparent; transition: border-bottom 0.1s; } .form-description{ color: rgb(32, 33, 36); box-sizing: border-box; font-weight: 400; font-size: 14px; width: 100%; line-height: 1.5; letter-spacing: 0; min-height: 21px; border-bottom: 1.5px solid transparent; transition: border-bottom 0.1s; } .form-name.input-is-focused, .form-description.input-is-focused{ border-bottom: 1.5px solid rgba(195, 195, 195, 0.7); } </style> <script> app_content.component("form-component", { template: "#template-form", data() { return { global: store, loading: false, loading_submission: false, json_form: null, invalid_membership: false, selected_fields: [ { label: 'First name', value: 'firstName', selected: false, input_value: null, }, { label: 'Last name', value: 'lastName', selected: false, input_value: null, }, { label: 'Email', value: 'email', selected: false, input_value: null, }, { label: 'Mobile', value: 'mobile', selected: false, input_value: null, }, ], login_url: null, }; }, props: { client_id: String, is_admin: Boolean, component: Object, form: Object, formatting: String, last_update: String, }, mounted() { this.load_forms( this.client_id ); }, methods: { load_forms( client_id, overrides = {} ){ this.loading = true; var promise = Parse.Promise.as(); var thisPromise = promise.then(() => { var params = {}; params["client_id"] = client_id; params["form_id"] = this.form_id; params["overrides"] = overrides; params["request_url"] = window.location.href; return Parse.Cloud.run('retrieve_data_for_component', params); }).then(retrieved_form => { this.json_form = retrieved_form; if(this.json_form.members_only){ //check if theres a logged in member and redirect them if needed return this.check_valid_membership().then(is_member => { if(is_member){ this.invalid_membership = false; this.set_default_fields(); return Parse.Promise.as(); } }).catch(error => { if(error === "invalid_membership"){ this.invalid_membership = true; this.loading = false; this.set_login_url(); // return the error return Parse.Promise.error("invalid_membership"); } }); }else{ this.set_default_fields(); return Parse.Promise.as(); } }).then(() => { this.loading = false; }, error => { checkParseError(error); this.loading = false; }); return thisPromise; }, check_valid_membership(){ var club_id = $('body').data('clubId'); var user = Parse.User.current(); var memberObject; var promise = Parse.Promise.as(); var thisPromise = promise.then(() => { if( user && user.get('memberObject') ){ //ok, get the member and membership var query = new Parse.Query("members"); query.select('membershipObject.status'); query.select('membershipObject.memberNumber'); query.select('membershipObject.tierObject'); query.select('clubObject.web_settings.custom_domain'); query.include('membershipObject'); query.include('clubObject.web_settings'); return query.get( user.get('memberObject').id ); }else{ //redirect to login return Promise.reject("invalid_membership"); } }).then(( retrievedMember )=>{ if( retrievedMember && retrievedMember.get('clubObject')?.id == club_id ){ //ok, they're legit memberObject = retrievedMember; } if( ! memberObject ){ //redirect to login return Promise.reject("invalid_membership"); } //check membership let membershipObject = memberObject.get('membershipObject'); if( ! membershipObject ){ return Promise.reject("invalid_membership"); } if( membershipObject.get('status') == "canceled" ){ return Promise.reject("invalid_membership"); } var valid_membership = true; //ok, they're a legit member return Parse.Promise.as(valid_membership); }); return thisPromise; }, set_login_url(){ const clubId = $('body').data('clubId'); // or wherever you're storing it const query = new Parse.Query("clubs"); query.include("web_settings"); query.get(clubId).then(club => { const webSettings = club.get('web_settings'); const customDomain = webSettings?.get('custom_domain'); const currentUrl = window.location.href; let redirectBase = customDomain ? `https://${customDomain}` : "https://theclubspot.com"; const loginUrl = `${redirectBase}/login?next=${encodeURIComponent(currentUrl)}&msg=${encodeURIComponent("Please log in to view and complete this form.")}`; this.login_url = loginUrl; }); }, tapped_clear_form(){ if ( this.collect_default_fields && this.selected_fields.length > 0 ){ this.selected_fields.flat().forEach(default_field => { default_field.input_value = null; }) } if ( this.custom_fields ){ this.custom_fields.forEach(custom_field => { custom_field.input_value = null; if ( custom_field.custom_field_options ){ custom_field.custom_field_options.forEach(custom_field_option => { custom_field_option.selected = false; }) } if ( custom_field.type === 'select' ){ let target_custom_field_component = this.$refs.custom_field_components.find(ref => { if ( ref.custom_field.id === custom_field.id ){ ref.clear_dropdown_index(); } }) } }) } }, async tapped_submit_form(){ if ( this.button_disabled ){ return displayMessage("Required fields are missing.", true, 4000); } let club_id = $('body').data('clubId'); if ( !club_id || !this.form_id ){ return displayMessage("There was an issue submitting your form. Please try again, or contact support if the problem persists.", true, 4000); } //update ui this.loading_submission = true; //create new instance const formResponseInstance = Parse.Object.extend('form_responses'); let new_response = new formResponseInstance(); new_response.set('clubObject', { "__type": "Pointer", "className": "clubs", "objectId": club_id }); new_response.set('formObject', { "__type": "Pointer", "objectId": this.form_id, "className": "forms" }); new_response.set('archived', false); if ( this.collect_default_fields && this.selected_fields.length > 0 ){ this.selected_fields.flat().forEach(field => { new_response.set(field.value, field.input_value || '') }) } //save return new_response.save().then(async saved_form_response => { let form_response_id = saved_form_response.id; if ( this.custom_fields ){ let customFieldsArray = await this.create_custom_field_responses( this.custom_fields, club_id, saved_form_response ); saved_form_response.set('customFieldsArray', customFieldsArray); //set the acl var acl = new Parse.ACL(); acl.setPublicWriteAccess(false); acl.setPublicReadAccess(false); acl.setRoleReadAccess(`club_basic_${club_id}`, true); acl.setRoleWriteAccess(`club_basic_${club_id}`, true); saved_form_response.setACL( acl ); //and save it saved_form_response.save().then(saved_form_response_with_custom_fields_arr => { if (saved_form_response_with_custom_fields_arr){ var params = {}; params["form_response_id"] = saved_form_response_with_custom_fields_arr.id; return Parse.Cloud.run('send_notification_for_form_response', params); } }).catch(error => { checkParseError(error); displayMessage(`There was an error with your form submission: ${error}`, true, 6000); }); } //update ui this.loading_submission = false; displayMessage('Your response has been successfully submitted', false, 6000); //clear all inputs to avoid resubmission this.tapped_clear_form(); }).catch(error => { this.loading_submission = false; checkParseError(error); displayMessage(error, true, 4000); }) }, async create_custom_field_responses( custom_fields, club_id, form_response ){ if ( custom_fields && custom_fields.length > 0 ){ let custom_field_responses = []; for ( let i = 0; i < custom_fields.length; i++ ){ let custom_field = custom_fields[i]; //create new instance const customFieldResponseInstance = Parse.Object.extend('customFieldResponses'); let new_custom_field_response = new customFieldResponseInstance(); //set the acl var acl = new Parse.ACL(); acl.setPublicWriteAccess(false); acl.setPublicReadAccess(false); acl.setRoleReadAccess(`club_basic_${club_id}`, true); acl.setRoleWriteAccess(`club_basic_${club_id}`, true); new_custom_field_response.setACL( acl ); //continue new_custom_field_response.set('clubObject', { "__type": "Pointer", "objectId": club_id, "className": "clubs" }); new_custom_field_response.set('form', { "__type": "Pointer", "objectId": this.form_id, "className": "forms" }); new_custom_field_response.set('form_response', form_response); new_custom_field_response.set('custom_field', { "__type": "Pointer", "objectId": custom_field.id, "className": "customFields" }); new_custom_field_response.set('field_name', custom_field.name || ''); new_custom_field_response.set('field_type', custom_field.type); new_custom_field_response.set('field_is_required', custom_field.required); if ( custom_field.type === 'text' || custom_field.type === 'textarea' || custom_field.type === 'number' ){ new_custom_field_response.set('response', custom_field.input_value ? custom_field.input_value.toString() : ''); }else if ( custom_field.type === 'radio' || custom_field.type === 'select' || custom_field.type === 'checkbox' ){ if ( custom_field.custom_field_options ){ let selected_options = []; for ( let i = 0; i < custom_field.custom_field_options.length; i++ ){ let option = custom_field.custom_field_options[i]; if ( option.selected === true ){ selected_options.push({ option_id: option.id, option_name: option.name || '', }); } } new_custom_field_response.set('selected_options', selected_options); } }else if ( custom_field.type === 'file_upload' ){ let file = custom_field.input_value; if ( file && file.name ){ try { const document = await this.process_file_upload( file, form_response, acl ); new_custom_field_response.set('document', document); new_custom_field_response.set('field_url', 'https://theclubspot.com/document/' + document.id || null); } catch(error) { displayMessage("Sorry, something went wrong uploading your file. Please try again, or contact support if the problem persists.", true, 3000); throw error; } } } await new_custom_field_response.save().then(saved_response => { custom_field_responses.push(saved_response); }).catch(error => { checkParseError(error); displayMessage(error, true, 4000); }) } return await custom_field_responses; } }, process_file_upload( file, form_response, acl ){ return upload_private_file_v2({ file: file, fileName: file.name }).then((result)=>{ let { url, file_key } = result; var docsInstance = Parse.Object.extend("documents"); var newDoc = new docsInstance(); newDoc.set('form_response', form_response); newDoc.set('name', file.name); newDoc.set('active', true); newDoc.set('archived', false); newDoc.set('file_key', file_key); newDoc.set('private', true); if( Parse.User.current() ){ newDoc.set('created_by', Parse.User.current() ) } newDoc.set('type', 'custom_field_response'); newDoc.setACL( acl ); newDoc.set('URL', url); newDoc.set('clubObject', form_response.get('clubObject')); return newDoc.save(); }); }, returnIsVideo(filename) { var ext = this.getExtension(filename); switch (ext.toLowerCase()) { case 'm4v': case 'avi': case 'mpg': case 'mp4': case 'mp3': case 'mov': // etc return true; } return false; }, getExtension(filename) { var parts = filename.split('.'); return parts[parts.length - 1]; }, load_clean_image( file, type, newFileName ){ return new Promise((resolve, reject) => { setTimeout(() => { loadImage(file, canvas => { if (canvas) { this.clean_image_callback(canvas, type, newFileName, file) .then(url => resolve(url)) .catch(err => reject(err)); } else { reject(new Error('Failed to load image')); } }, { orientation: 1, canvas: true } ); }, 500); }); }, clean_image_callback( canvas, type, newFileName, file ){ return new Promise((resolve, reject) => { if (canvas) { var url = canvas.toDataURL(type); canvas.toBlob(blob => { uploadFileToAWSV2( blob, newFileName ) .then(uploadUrl => resolve(uploadUrl)) .catch(err => reject(err)); }, type); } else { reject(new Error('canvas is null')); } }); }, link_form(){ store.components.link_form.component = this.component; store.overlays.link_form = true; }, set_default_fields(){ if ( this.json_form && this.default_fields.length > 0 ){ this.selected_fields = this.selected_fields.map(default_field => { if ( this.default_fields.indexOf(default_field.value ) !== -1 ){ return { ...default_field, selected: true, } } else { return { ...default_field, selected: false, } } }); } if ( this.default_configuration === 'list' ){ //list this.selected_fields = this.selected_fields.filter(field => field.selected); } else if ( this.default_configuration === 'grid' ){ //grid this.selected_fields = this.selected_fields.filter(field => field.selected).reduce((acc, field) => { if ( field.value === 'firstName' || field.value === 'lastName' ){ acc[0].push(field); } else { acc[1].push(field); } return acc; }, [[], []]) } }, handle_field_update({ id, value }){ const field = this.custom_fields.find(f => f.id === id); if (field) { field.input_value = value; } }, }, computed: { form_id(){ if ( typeof( this.form ) === 'string' ){ return JSON.parse(this.form).id; } return this.form.id || null; }, color_theme(){ if ( this.json_form && this.json_form.color_theme ){ return this.json_form.color_theme; } return '#5280ff'; }, custom_fields(){ if ( this.json_form && this.json_form.custom_fields ){ return this.json_form.custom_fields; } return []; }, default_fields(){ if ( this.json_form && this.json_form.default_fields ){ return this.json_form.default_fields; } return []; }, collect_default_fields(){ if ( this.json_form && this.json_form.collect_default_fields ){ return this.json_form.collect_default_fields; } return false; }, default_configuration(){ if ( this.json_form && this.json_form.default_configuration ){ return this.json_form.default_configuration; } return 'list'; }, missing_default_field(){ if ( this.json_form && this.collect_default_fields && this.selected_fields.length > 0 ){ let default_values = this.default_configuration === 'list' ? this.selected_fields.map(field => field.input_value) : this.selected_fields.flatMap(arr => arr.map(field => field.input_value)); return default_values.includes(null) || default_values.includes(''); } return false; }, missing_custom_field(){ if ( this.json_form && this.custom_fields ){ for ( let i = 0; i < this.custom_fields.length; i++ ){ let custom_field = this.custom_fields[i]; let option_values = custom_field.custom_field_options && custom_field.custom_field_options.length > 0 ? custom_field.custom_field_options.map(option => option.selected) : []; if ( custom_field.required && ( !custom_field.input_value && option_values.indexOf(true) === -1 ) ){ return true; break; } } return false; } }, button_disabled(){ if ( this.is_admin ){ return true; }else{ return this.missing_custom_field || this.missing_default_field; } return false; }, formatting_obj(){ if ( this.formatting ){ return JSON.parse(this.formatting); } return; }, valid_membership(){ if(this.json_form && this.json_form.members_only){ return !this.invalid_membership; } //not members only, doesn't matter return true; }, }, watch: { form_id(newVal, oldVal){ if (newVal !== oldVal){ this.load_forms( this.client_id ); } } } }) </script> <script type="text/html" id="template-tab-bar"> <template v-if="is_web_builder()"> <template v-if="config_latest()['tabs']?.length > 0"> <div class="tab-bar-wrapper" :style="{ gap: (config_latest().formatting.section_width || 50) + 'px', height: (config_latest().formatting.section_height || 50) + 'px', }" > <div v-for="tab in config_latest()['tabs']" style="width: 200px; display: flex; align-items: center; justify-content: center;" > <div @click="selected_tab_id = tab.id" style="width: 100%; text-align: center;" class="tab-bar-label" :class="{ 'active': selected_tab_id === tab.id }" > <span> <preview-element-component v-if="tab.elements" :element="tab.elements" :element_type="tab.elements.type" :element_content="tab.elements.content" :componentObject="component" :disable_remove="true" :disable_column="true" :disable_new_element="true" ></preview-element-component> </span> </div> </div> </div> <web-builder-components v-if="selected_tab_id" :component="return_page_component(selected_tab_id)" :component_index="0" :preview_element="true" ></web-builder-components> </template> <template v-else> <div class="noObjectsMSG centeredBlock centeredText" style="padding: 5em;" > <p style="cursor: pointer;"><i class="fa-light fa-table-columns smallMarginRight"></i>Use the left side menu to start adding tabs</p> </div> </template> </template> <template v-if="!is_web_builder() && !loading"> <template v-if="hosted_config()['tabs']?.length > 0"> <div class="tab-bar-wrapper" :style="{ gap: (hosted_config().formatting.section_width || 50) + 'px', height: (hosted_config().formatting.section_height || 50) + 'px', }" > <div v-for="tab in hosted_config()['tabs']" :key="tab.id" style="width: 200px; display: flex; align-items: center; justify-content: center;" > <div @click="selected_tab_id = tab.id" style="width: 100%; text-align: center;" class="tab-bar-label" :class="{ 'active': selected_tab_id === tab.id }" > <element-component v-if="tab.elements" :element="tab.elements" :element_type="tab.elements.type" :element_content="tab.elements.content" :componentObject="component" :disable_remove="true" :disable_column="true" :disable_new_element="true" ></element-component> </div> </div> </div> <web-builder-components v-if="selected_tab_id" :component="return_hosted_page_component(selected_tab_id)" :component_index="0" :tab_element="true" :preview_element="false" ></web-builder-components> </template> </template> </script> <style> .tab-bar-wrapper{ display: flex; flex-direction: row; align-items: center; justify-content: center; border-top: 1.5px solid rgb(204, 203, 203); border-bottom: 1.5px solid rgb(204, 203, 203); box-sizing: border-box; } .tab-bar-label span { position: relative; display: inline-block; padding-bottom: 4px; border-bottom: none !important; text-decoration: none !important; } .tab-bar-label span::after { content: ""; position: absolute; bottom: 0; left: 50%; width: 0; height: 1px; background-color: #000; transform: translateX(-50%); transition: width 0.3s ease; } .tab-bar-label.active span::after { width: 100%; } </style> <script> app_content.component('tab-bar-component', { template: '#template-tab-bar', data() { return { loading: true, selected_tab_id: null, hosted_tab_component: null, hosted_page_components: [], } }, props: { client_id: String, component: { type: Object, default: null, }, last_update: Number, }, mounted() { if(this.is_web_builder()){ this.selected_tab_id = this.config_latest()?.tabs?.[0]?.id || null; }else{ this.load_tab_bar( this.client_id ); } }, methods: { load_tab_bar( client_id, overrides = {} ){ this.loading = true; var promise = Parse.Promise.as(); var thisPromise = promise.then(() => { var params = {}; params["client_id"] = client_id; params["overrides"] = overrides; params["request_url"] = window.location.href; return Parse.Cloud.run('retrieve_data_for_component', params); }).then(response => { this.hosted_tab_component = response.tab_component || null; this.hosted_page_components = response.page_components || []; this.selected_tab_id = this.hosted_config()?.tabs?.[0]?.id || null; }).catch(error => { checkParseError(error); }).finally(() => { this.loading = false; }); return thisPromise; }, return_page_component(id){ return store.page_components_latest.find(c => c.id === id) }, return_hosted_page_component(id){ return this.hosted_page_components.find(c => c.id === id) }, config_latest(){ if(this.component && this.component.get('config_latest')){ return this.component.get('config_latest'); } }, hosted_config(){ if(this.hosted_tab_component && this.hosted_tab_component.get('config')){ return this.hosted_tab_component.get('config'); } }, is_web_builder(){ return window.location.href.includes('website-builder'); }, }, watch: { last_update(newVal) { if (this.is_web_builder()) { this.selected_tab_id = this.config_latest()?.tabs?.[0]?.id || null; } } } }); </script> <script type="text/html" id="template-web-builder-components"> <div :id="'componentPreview_' + component.get('client_id')" class="componentPreview" :data-client-id="component.get('client_id')" :style="return_preview_style(component)" > <template v-if="component.get('type') === 'iframe'"> <iframe-component :class_string="return_attributes(component, 'classString')" :style_string="return_attributes(component, 'style_string')" :iframe_src="return_attributes(component, 'config_latest').iframe_src" :component="component" :formatting_string="return_attributes(component, 'formatting')" :last_update="return_last_update(component)" ></iframe-component> <toolbar-option-component @click.stop :component="component" :is_first="component_index === 0" :disable_options="disable_options(component)" ></toolbar-option-component> </template> <template v-if="component.get('type') === 'blog-list'"> <blog-list-component :client_id="return_attributes(component, 'client_id')" :class_string="return_attributes(component, 'classString')" :style_string="return_attributes(component, 'style_string')" :element_style_string="return_attributes(component, 'element_style')" :formatting="return_attributes(component, 'formatting')" :component_type="return_attributes(component, 'component_type')" :is_larchmont="global.is_larchmont" :component="component" :last_update="return_last_update(component)" ></blog-list-component> <toolbar-option-component @click.stop :component="component" :is_first="component_index === 0" :disable_options="disable_options(component)" ></toolbar-option-component> </template> <template v-if="component.get('type') === 'calendar'"> <membership-calendar-component :class_string="return_attributes(component, 'classString')" :client_id="return_attributes(component, 'client_id')" :style_string="return_attributes(component, 'style_string')" :component="component" :last_update="return_last_update(component)" ></membership-calendar-component> <toolbar-option-component @click.stop :component="component" :is_first="component_index === 0" :disable_options="disable_options(component)" ></toolbar-option-component> </template> <template v-if="component.get('type') === 'membership-directory'"> <membership-directory-component :formatting="JSON.parse(return_attributes(component, 'formatting'))" :class_string="return_attributes(component, 'classString')" :client_id="return_attributes(component, 'client_id')" :style_string="return_attributes(component, 'style_string')" :element_style_string="return_attributes(component, 'element_style')" :component_type="return_attributes(component, 'component_type')" :last_update="return_last_update(component)" ></membership-directory-component> <toolbar-option-component @click.stop :component="component" :is_first="component_index === 0" :disable_options="disable_options(component)" ></toolbar-option-component> </template> <template v-if="component.get('type') === 'event-list'"> <event-list-component :class_string="return_attributes(component, 'classString')" :style_string="return_attributes(component, 'style_string')" :element_style_string="return_attributes(component, 'element_style')" :formatting="return_attributes(component, 'formatting')" :filter_types="return_attributes(component, 'filter_types')" :event_dates="return_attributes(component, 'event_dates')" :component_type="return_attributes(component, 'component_type')" :client_id="return_attributes(component, 'client_id')" :component="component" :last_update="return_last_update(component)" ></event-list-component> <toolbar-option-component @click.stop :component="component" :is_first="component_index === 0" :disable_options="disable_options(component)" ></toolbar-option-component> </template> <template v-if="component.get('type') === 'image-grid'"> <image-grid-component :client_id="return_attributes(component, 'client_id')" :class_string="return_attributes(component, 'classString')" :image_grid_layout="return_attributes(component, 'image_grid_layout')" :images_per_row="return_attributes(component, 'images_per_row')" :caption_size="return_attributes(component, 'caption_size')" :subtext_size="return_attributes(component, 'subtext_size')" :link_label_size="return_attributes(component, 'link_label_size')" :text_position="return_attributes(component, 'text_position')" :caption_font_weight="return_attributes(component, 'caption_font_weight')" :image_grid_opacity="return_attributes(component, 'image_grid_overlay_opacity')" :image_grid_border_radius="return_attributes(component, 'image_grid_border_radius')" :formatting="return_attributes(component, 'formatting')" :is_admin="preview_element" :data_source_type="return_attributes(component, 'data_source')['type']" :images_arr="return_attributes(component, 'images')" :images_string="JSON.stringify(return_attributes(component, 'images'))" :component="component" :last_update="return_last_update(component)" ></image-grid-component> <toolbar-option-component @click.stop :component="component" :is_first="component_index === 0" :disable_options="disable_options(component)" ></toolbar-option-component> </template> <template v-if="component.get('type') === 'photo-gallery'"> <photo-gallery-component :client_id="return_attributes(component, 'client_id')" :component="component" :photo_album="return_attributes(component, 'photo_album')" :show_caption="return_attributes(component, 'show_caption')" :caption_size="return_attributes(component, 'caption_size')" :caption_font_weight="return_attributes(component, 'caption_font_weight')" :photos_per_row="return_attributes(component, 'photos_per_row')" :is_admin="preview_element" :last_update="return_last_update(component)" ></photo-gallery-component> <toolbar-option-component @click.stop :component="component" :is_first="component_index === 0" :disable_options="disable_options(component)" ></toolbar-option-component> </template> <template v-if="component.get('type') === 'form'"> <form-component :client_id="return_attributes(component, 'client_id')" :component="component" :form="return_attributes(component, 'form')" :formatting="return_attributes(component, 'formatting')" :is_admin="preview_element" :last_update="return_last_update(component)" ></form-component> <toolbar-option-component @click.stop :component="component" :is_first="component_index === 0" :disable_options="disable_options(component)" ></toolbar-option-component> </template> <template v-if="component.get('type') === 'image-with-text'"> <image-with-text-component :client_id="return_attributes(component, 'client_id')" :formatting="return_attributes(component, 'formatting')" :images_string="JSON.stringify(return_attributes(component, 'images'))" :class_string="return_attributes(component, 'classString')" :style_string="return_attributes(component, 'style_string')" :element_style_string="return_attributes(component, 'element_style')" :image_mat="return_attributes(component, 'image_mat')" :elements_string="return_attributes(component, 'elements')" :left_right_orientation="return_attributes(component, 'left_right_orientation')" :preview_element="preview_element" :component="component" :last_update="return_last_update(component)" :last_element_update="return_last_element_update( component )" :tab_element="tab_element" ></image-with-text-component> <toolbar-option-component @click.stop :component="component" :is_first="component_index === 0" :disable_options="disable_options(component)" ></toolbar-option-component> </template> <template v-if="component.get('type') === 'text' || component.get('type') === 'table'"> <div class="cs-text background flexWrap relative" :class="return_attributes(component, 'classString')" :style="return_attributes(component, 'style_string')" > <div v-if="return_attributes(component, 'columns').length > 0" class="element-wrapper flexNoWrap flexWrapOnMobile" :class="'element-wrapper_' + return_attributes(component, 'component_type')" :style="return_attributes(component, 'element_style')" > <template v-for="(elements, column_index) in return_attributes(component, 'columns')"> <text-component :number_of_columns="return_attributes(component, 'number_of_columns')" :preview_element="preview_element" :component="component" :last_update="return_last_update(component)" :last_element_update="return_last_element_update( component )" :elements="elements" :column_index="column_index" ></text-component> </template> </div> </div> <toolbar-option-component @click.stop :component="component" :is_first="component_index === 0" :disable_options="disable_options(component)" ></toolbar-option-component> </template> <template v-if="component.get('type') === 'hero-banner'"> <hero-banner-component :client_id="return_attributes(component, 'client_id')" :formatting="return_attributes(component, 'formatting')" :class_string="return_attributes(component, 'classString')" :style_string="return_attributes(component, 'style_string')" :element_style_string="return_attributes(component, 'element_style')" :component_type="return_attributes(component, 'component_type')" :elements_string="return_attributes(component, 'elements')" :images_string="JSON.stringify(return_attributes(component, 'images'))" :preview_element="preview_element" :component="component" :last_update="return_last_update(component)" :last_element_update="return_last_element_update( component )" :tab_element="tab_element" ></hero-banner-component> <toolbar-option-component @click.stop :component="component" :is_first="component_index === 0" :disable_options="disable_options(component)" ></toolbar-option-component> </template> <template v-if="component.get('type') === 'custom-code'"> <custom-code-component :class_string="return_attributes(component, 'classString')" :preview_element="preview_element" :component="component" :last_update="return_last_update(component)" ></custom-code-component> <toolbar-option-component @click.stop :component="component" :is_first="component_index === 0" :disable_options="disable_options(component)" ></toolbar-option-component> </template> <template v-if="component.get('type') === 'unlayer-component'"> <unlayer-component :class_string="return_attributes(component, 'classString')" :preview_element="preview_element" :component="component" :last_update="return_last_update(component)" ></unlayer-component> <toolbar-option-component @click.stop :component="component" :is_first="component_index === 0" :disable_options="disable_options(component)" ></toolbar-option-component> </template> <template v-if="component.get('type') === 'tab-bar'"> <tab-bar-component :client_id="return_attributes(component, 'client_id')" :preview_element="preview_element" :component="component" :last_update="return_last_update(component)" ></tab-bar-component> <toolbar-option-component @click.stop :component="component" :is_first="component_index === 0" :disable_options="disable_options(component)" ></toolbar-option-component> </template> </div> </script> <script> app_content.component('web-builder-components', { template: '#template-web-builder-components', data() { return { global: store, is_admin: false, } }, props: { component: Object, component_index: Number, tab_element: Boolean, preview_element: Boolean, last_update: Number, }, methods: { disable_options( componentObject ){ const component_is_under_tab_bar = store.page_components_latest?.some(c => { return c.get('type') === 'tab-bar' && (c.get('config_latest')?.tabs || []).some(tab => tab.id === componentObject?.id); }); return component_is_under_tab_bar; }, return_last_element_update( componentObject ){ return store.element_updates ? store.element_updates[ componentObject.id ] : 0; }, return_last_update(componentObject){ return store.component_updates ? store.component_updates[ componentObject.id ] : 0; }, return_preview_style(componentObject){ let type = componentObject.get('type'); if ( type === 'hero-banner' || type === 'image-with-text' ){ return this.return_attributes(componentObject, 'background_image_style'); } }, return_attributes(componentObject, attribute){ let attributes = {}; if(componentObject){ var type = componentObject.get('type'); var config_latest = componentObject.get('config_latest') || returnDefaultConfigForComponent( type ); var images = config_latest.images; if(!images){ // images = []; images = [null, null, null, null, null] } var formatting = config_latest.formatting; if( ! formatting ){ formatting = returnDefaultFormattingForComponent( type ); } var color_scheme = config_latest.color_scheme; if (store.is_larchmont && store.pageObject.id === 'QAcpSXHP5G'){ color_scheme = 'larchmont_secondary'; } //start the class string up here var classString = ''; classString += ' color_scheme_' + color_scheme; //now generate markup var background_image_style = ""; var style_string = ""; if( formatting.section_height ){ style_string += "min-height:" + ( formatting.section_height ).toString() + "vh;"; } if( formatting.align_items ){ style_string += "align-items:" + formatting.align_items + ";" ; } if ( formatting.background_size === 'inset' ){ background_image_style += "padding: 30px;" ; } else if ( formatting.background_size === 'cover' ){ background_image_style += "padding: 0px;" ; } //element wrapper var element_style = ""; if( formatting.section_width ){ element_style += "max-width:" + (formatting.section_width).toString() + "%;"; } //overlay opacity var opacity = 0; if( formatting.overlay_opacity ){ opacity = formatting.overlay_opacity; } //extra_columns var extra_columns = config_latest.extra_columns; if( extra_columns ){ for (var c = extra_columns.length - 1; c >= 0; c--) { classString += ' show_' + extra_columns[c]; } } //images per row (image grid) var images_per_row = config_latest.images_per_row; if( ! images_per_row ){ images_per_row = 3; } //photos per row (photo gallery) var photos_per_row = config_latest.photos_per_row; if( ! photos_per_row ){ photos_per_row = 4; } //left/right orientation (image with text) var left_right_orientation = config_latest.left_right_orientation; if( ! left_right_orientation ){ left_right_orientation = "left"; } classString += " orientation-" + left_right_orientation; //number of columns (text) var number_of_columns = config_latest.number_of_columns; if( ! number_of_columns ){ number_of_columns = 3; } classString += " number_of_columns-" + number_of_columns; //photo album id var photo_album = config_latest.photo_album; if( ! photo_album ){ photo_album = { id: null, name: null, }; } //form id var form = config_latest.form; if( ! form ){ form = { id: null, name: null, }; } //image with text var image_mat = config_latest.image_mat ? config_latest.image_mat : 'show'; //filter types (event list) var filter_types = config_latest.filter_types ? JSON.stringify(config_latest.filter_types) : null; //event_dates (event list) var event_dates = config_latest.event_dates ? config_latest.event_dates : 'upcoming'; //caption style if( formatting.caption_style ){ classString += " caption_style-" + formatting.caption_style; } //caption alignment if( formatting.caption_align && ( formatting.caption_align == "top" || formatting.caption_align == "bottom" || formatting.caption_align == "under" ) ){ classString += ' ' + formatting.caption_align + '-align-image-caption'; } var show_caption = formatting.show_caption; if( ! show_caption ){ show_caption = false; } var caption_size = formatting.caption_size; if( ! caption_size ){ caption_size = "24"; } var subtext_size = formatting.subtext_size; if( ! subtext_size ){ subtext_size = "24"; } var link_label_size = formatting.link_label_size; if( ! link_label_size ){ link_label_size = "24"; } var text_position = formatting.text_position; if( ! text_position ){ text_position = "center"; } var caption_font_weight = formatting.caption_font_weight; if( ! caption_font_weight ){ caption_font_weight = "400"; } var image_grid_overlay_opacity = formatting.image_grid_overlay_opacity; if( ! image_grid_overlay_opacity ){ image_grid_overlay_opacity = "40"; } var image_grid_border_radius = formatting.image_grid_border_radius; if ( ! image_grid_border_radius ){ image_grid_border_radius = "0" } var image_grid_layout = formatting.layout; if ( ! image_grid_layout ){ image_grid_layout = "grid" } //add all variables to attribute object attributes['client_id'] = componentObject.get('client_id'); attributes['component_type'] = componentObject.get('type'); if( componentObject.get('config') && componentObject.get('config').event_types ){ attributes['event_types'] = componentObject.get('config').event_types; } // attributes['images'] = componentObject.get('images'); attributes['type'] = type; attributes['config_latest'] = config_latest; attributes['data_source'] = config_latest['data_source']; attributes['images'] = images; attributes['formatting'] = JSON.stringify(formatting); attributes['color_scheme'] = color_scheme; attributes['classString'] = classString; attributes['style_string'] = style_string; attributes['elements'] = config_latest['elements']; attributes['element_style'] = element_style; attributes['opacity'] = opacity; attributes['extra_columns'] = extra_columns; attributes['images_per_row'] = images_per_row; attributes['photos_per_row'] = photos_per_row; attributes['left_right_orientation'] = left_right_orientation; attributes['columns'] = config_latest.columns; attributes['number_of_columns'] = number_of_columns; attributes['show_caption'] = show_caption; attributes['caption_size'] = caption_size; attributes['subtext_size'] = subtext_size; attributes['link_label_size'] = link_label_size; attributes['text_position'] = text_position; attributes['caption_font_weight'] = caption_font_weight; attributes['image_grid_overlay_opacity'] = image_grid_overlay_opacity; attributes['image_grid_border_radius'] = image_grid_border_radius; attributes['image_grid_layout'] = image_grid_layout; attributes['background_image_style'] = background_image_style; attributes['photo_album'] = photo_album; attributes['form'] = form; attributes['filter_types'] = filter_types; attributes['event_dates'] = event_dates; attributes['image_mat'] = image_mat; //return requested attribute return attributes[attribute]; } }, }, computed: { }, }); </script> <!-- overlays --> <script type="text/html" id="template-view-all-events"> <bottom-sheet-overlay id="overlay_view_all_events" @dismiss_overlay="dismiss_overlay()" > <template #header> <div class="sticky-wrap overflowHidden"> <div class="posScrollbar table-tab-wrap noPaddingBottom"> <div class="flexNoWrap table-tab-inner"> <div class="table-tab-option" :class="{'active': this.me.active_filters.eventDates === 'all'}" @click="handleDatesFilter('all')" style="margin-left:22px;">All</div> <div class="table-tab-option noMarginLeft" :class="{'active': this.me.active_filters.eventDates === 'upcoming'}" @click="handleDatesFilter('upcoming')">Upcoming</div> <div class="table-tab-option noMarginLeft" :class="{'active': this.me.active_filters.eventDates === 'past'}" @click="handleDatesFilter('past')">Past</div> </div> </div> </div> <div class="searchBarWrap inline-search relative noMargin overflowHidden"> <div class="inputWrap searchBar searchBar_event-list flexGrowOne boxShadowBottom maxFourHundo"> <input class="easeFast" placeholder="Search events..." v-model="this.me.search" > </div> </div> </template> <!-- body content --> <template #body> <div class="event-list-outer-contain lightBorderTop scrollable" :class="this.me.class_string" :style="this.me.style_string" > <div class="event-list-wrap centeredBlock centeredText event-list-inner-contain" :class="'event-list-wrap_' + client_id" :data-client_id="client_id" > <div class="fff noPadding overflowHidden"> <div v-if="!loading && filtered_events.length >= 1" class="tableInsert hundop flexWrapEvent eventListContain"> <template v-for="(event, index) in filtered_events" :key="event.id"> <event-list-event-component :event="event" ></event-list-event-component> </template> </div> <div v-if="!this.me.loading && filtered_events.length == 0" class="tableInsert hundop eventListContain"> <div class="eventList eventRow" :style="{ 'background-color': '#fff' }"> <p class="noObjectsMSG withPadding textLeft" :style="{ 'font-size': '20px' }">No events found.</p> </div> </div> </div> <div v-if="this.me.loading" class="cardBodySpinnerWrap active relative"> <div class="cardBodySpinner centeredBlock"></div> </div> </div> </div> </template> </bottom-sheet-overlay> </script> <script> //global store store.components["view_all_events"] = { allEvents: [], active_filters: { eventDates: 'upcoming', }, search: '', loading: true, sortObject: { order: 'ascending', key: 'startDate' }, //passed props class_string: '', style_string: '', element_style_string: '', component_type: '', client_id: '', }; //define the component app_overlays.component("view-all-events-overlay", { template: "#template-view-all-events", data() { return { global: store, me: store.components.view_all_events } }, mounted(){ if ( this.me.allEvents.length > 0 ){ this.me.loading = false; } }, methods: { niceDate( eventObject ){ let start_offset; let start_offset_time; if( eventObject.get('startDate') ){ start_offset = returnOffsetDateForUTCDate( eventObject.get('startDate') ); //handle start/end times set explicitly on the event (for socials) if( eventObject.get('startTime') ){ var start_data = returnHoursAndMinutesForMilitaryTime( eventObject.get('startTime') ); start_offset.setHours( start_offset.getHours() + start_data.hours ); start_offset.setMinutes( start_offset.getMinutes() + start_data.minutes ); } start_offset_time = start_offset.getTime(); } return `${dayjs(start_offset).format('MMM DD, YYYY')}, ${dayjs(start_offset_time).format('h:mm A')}` }, sortEventList( events, sortObject ){ if( ! sortObject ){ this.me.sortObject = {} } if( ! sortObject.key ){ this.me.sortObject.key = "startDate"; } if( ! sortObject.order ){ this.me.sortObject.order = "ascending" } if( sortObject.key == "name" ){ if( sortObject.order == "ascending" ){ events = events.sort( this.sortByEventName_ascending ); }else{ events = events.sort( this.sortByEventName_descending ); } }else if( sortObject.key == "startDate" ){ if( sortObject.order == "ascending" ){ events = events.sort( this.sortByStartDate_event_ascending ); }else{ events = events.sort( this.sortByStartDate_event_descending ); } }else if( sortObject.key == "endDate" ){ if( sortObject.order == "ascending" ){ events = events.sort( this.sortByEndDate_event_ascending ); }else{ events = events.sort( this.sortByEndDate_event_descending ); } } return events; }, sortByEventName_ascending( a, b ){ var aName = ''; if( a.get('name') ){ aName += a.get('name').trim().toLowerCase(); } var bName = ''; if( b.get('name') ){ bName += b.get('name').trim().toLowerCase(); } if(aName < bName) { return -1; } if(aName > bName) { return 1; } //they had the same name return 0; }, sortByEventName_descending( a, b ){ var aName = ''; if( a.get('name') ){ aName += a.get('name').trim().toLowerCase(); } var bName = ''; if( b.get('name') ){ bName += b.get('name').trim().toLowerCase(); } if(aName > bName) { return -1; } if(aName < bName) { return 1; } //they had the same name return 0; }, sortByStartDate_event_ascending( a, b ){ var aStartDate = a.get('startDate') if( ! aStartDate ){ aStartDate = a.createdAt; } var bStartDate = b.get('startDate') if( ! bStartDate ){ bStartDate = b.createdAt; } if(aStartDate < bStartDate) { return -1; } if(aStartDate > bStartDate) { return 1; } return 0; }, sortByStartDate_event_descending( a, b ){ var aStartDate = a.get('startDate') if( ! aStartDate ){ aStartDate = a.createdAt; } var bStartDate = b.get('startDate') if( ! bStartDate ){ bStartDate = b.createdAt; } if(aStartDate > bStartDate) { return -1; } if(aStartDate < bStartDate) { return 1; } return 0; }, sortByEndDate_event_ascending( a, b ){ var aEndDate = a.get('endDate') if( ! aEndDate ){ aEndDate = a.createdAt; } var bEndDate = b.get('endDate') if( ! bEndDate ){ bEndDate = b.createdAt; } if(aEndDate < bEndDate) { return -1; } if(aEndDate > bEndDate) { return 1; } return 0; }, sortByEndDate_event_ascending( a, b ){ var aEndDate = a.get('endDate') if( ! aEndDate ){ aEndDate = a.createdAt; } var bEndDate = b.get('endDate') if( ! bEndDate ){ bEndDate = b.createdAt; } if(aEndDate > bEndDate) { return -1; } if(aEndDate < bEndDate) { return 1; } return 0; }, return_markup( eventObject, type ){ var href_base = ''; var clubObject = eventObject.get('clubObject'); var web_settings = clubObject.get('web_settings'); if( web_settings ){ if( web_settings.get('event_links') == "use_clubspot_regatta_links" && ( eventObject.className == "regattas" || eventObject.className == "subevents" ) ){ //force clubspot urls for regattas href_base += 'https://theclubspot.com'; } } var start_offset, end_offset; var start_offset_time, end_offset_time; if( eventObject.get('startDate') ){ start_offset = returnOffsetDateForUTCDate( eventObject.get('startDate') ); //handle start/end times set explicitly on the event (for socials) if( eventObject.get('startTime') ){ var start_data = returnHoursAndMinutesForMilitaryTime( eventObject.get('startTime') ); start_offset.setHours( start_offset.getHours() + start_data.hours ); start_offset.setMinutes( start_offset.getMinutes() + start_data.minutes ); } start_offset_time = start_offset.getTime(); } var endDate = eventObject.get('endDate'); if( ! endDate && eventObject.get('startDate') ){ endDate = eventObject.get('startDate'); } if( endDate ){ end_offset = returnOffsetDateForUTCDate( endDate ); //handle start/end times set explicitly on the event (for socials) if( eventObject.get('endTime') ){ var end_data = returnHoursAndMinutesForMilitaryTime( eventObject.get('endTime') ); end_offset.setHours( end_offset.getHours() + end_data.hours ); end_offset.setMinutes( end_offset.getMinutes() + end_data.minutes ); } end_offset_time = end_offset.getTime(); } // returns here if ( type === 'check-header-callout' ) { return start_offset && start_offset.getFullYear() !== new Date().getFullYear() } else if ( type === 'start_offset_time' ) { return start_offset_time; } else if ( type === 'end_offset_time' ) { return end_offset_time; } else if ( type === 'header-callout' ){ return start_offset.getFullYear(); } else if ( type === 'date' ){ //date var date = ''; if( start_offset ){ var style_string = ''; if( eventObject.get('imageURL') ){ //we always want white text for this style_string = ' style="color:#fff !important;" '; } date += '<p class="cs-signups-date-month">' + returnShortMonth( start_offset.getMonth() ) + '</p>'; var days = start_offset.getDate(); if( end_offset && ! eventObject.get('all_day') ){ if( end_offset.getDate() != start_offset.getDate() ){ days += ' - ' + end_offset.getDate(); } } date += '<p class="cs-signups-date-day">' + days + '</p>'; } return date; } else if ( type === 'event-host-1' ){ return $('body').data('club-name'); } else if ( type === 'event-host-2' ){ var club_string = ''; var location_params = { eventObject: eventObject, hideCountry: true, hideZip: true, hideStreet: true, } var club_location_params = { eventObject: clubObject, hideCountry: true, hideZip: true, hideStreet: true, } if(formatLocation(location_params) ){ if( club_string.length >= 1 ){ club_string += ', '; } club_string += formatLocation(location_params); }else if( clubObject && formatLocation(club_location_params) ){ if( club_string.length >= 1 ){ club_string += ', '; } club_string += formatLocation(club_location_params); } return club_string; } else if ( type === 'links' ){ var content; if( eventObject.className == "regattas" || eventObject.className == "subevents" ){ //check for external events if( regattaObject.get('regatta_external') && regattaObject.get('external_regatta_url') ){ //it's an external event content = '<p><a target="_blank" href="'+ regattaObject.get('external_regatta_url') +'">Event page</a></p>'; }else{ //event page content = '<p><a href="'+href_base+'/regatta/'+ regattaObject.id +'">Event page</a></p>'; //entries content = '<p><a href="'+href_base+'/regatta/'+ regattaObject.id +'">Entries</a></p>'; //race docs content = '<p><a href="'+href_base+'/regatta/'+ regattaObject.id +'">Race docs</a></p>'; } }else if( eventObject.className == "events" ){ //check for external events if( ! eventObject.get('calendarOnly') ){ if( eventObject.get('external_event') && eventObject.get('external_event_url') ){ //it's an external event content = '<p><a target="_blank" href="'+ eventObject.get('external_event_url') +'">Event page</a></p>'; }else{ //event page content = '<p><a href="'+href_base+'/event/'+ eventObject.id +'">Event page</a></p>'; } } }else if( eventObject.className == "camps" ){ //it's a camp content = '<p><a href="'+href_base+'/camp-registrations/'+ eventObject.id +'">Entry list</a></p>'; } return content; } else if ( type === 'button' ){ var content; var show_register_button = this.returnShowRegisterButtonForEvent( eventObject ); if( eventObject.className == "regattas" || eventObject.className == "subevents" ){ //check for external events if( regattaObject.get('regatta_external') && regattaObject.get('external_regatta_url') ){ //it's an external event content = '<button class="relative background"><a class="absoluteA" target="_blank" href="'+ regattaObject.get('external_regatta_url') +'">View event</a></button>'; }else{ //handle button status if( show_register_button ){ //register content = '<button class="relative background"><a class="absoluteA" href="'+href_base+'/register/regatta/'+ regattaObject.id +'/class">Register</a></button>'; }else{ //results content = '<button class="relative background"><a class="absoluteA" href="'+href_base+'/regatta/'+ regattaObject.id +'/results">View Results</a></button>'; } } }else if( eventObject.className == "events" ){ var registration_link; if( eventObject.get('events_v2') ){ registration_link = '/register/event/' + eventObject.id; }else if( eventObject.get('external_event') && eventObject.get('external_register_link') ){ registration_link = eventObject.get('external_register_link'); }else if( eventObject.get('event_product') ){ if( eventObject.get('event_product').get('availability') && eventObject.get('event_product').get('availability').startDate ){ registration_link = "/reserve/" + eventObject.get('event_product').id + "?date=" + eventObject.get('event_product').get('availability').startDate; } } if( registration_link && show_register_button ){ content = '<button class="relative"><a href="'+href_base+''+registration_link+'">RSVP</a></button>'; } }else if( eventObject.className == "camps" ){ //handle button status if( show_register_button ){ //register content = '<button class="relative"><a href="'+href_base+'/register/camp/'+ eventObject.id +'/class">Register</a></button>'; } } return content; } }, returnShowRegisterButtonForEvent( eventObject ){ //set defaults var now = new Date(); var ended = false; var registration_closed = eventObject.get('registration_closed'); var startDate = eventObject.get('startDate'); var lastChanceDate = eventObject.get('lastChanceDate'); var endDate = eventObject.get('endDate'); if( ! endDate && startDate ){ endDate = startDate; } var endDate_plus_one; if( endDate ){ endDate_plus_one = new Date( endDate.getTime() ); endDate_plus_one.setDate( endDate.getDate() + 1 ); } if( now > endDate_plus_one && endDate_plus_one ){ ended = true; } if( now > lastChanceDate && lastChanceDate ){ registration_closed = true; } if( ended || registration_closed || eventObject.get('calendarOnly') ){ return false; } else { return true; } }, handleDatesFilter( option ){ this.me.active_filters.eventDates = option; // handle sortObject["order"] here instead of in loadEventList post-retrieval for better UX if ( option === 'upcoming' ){ this.me.sortObject["order"] = "ascending"; } else { this.me.sortObject["order"] = "descending"; } }, dismiss_overlay(){ store.overlays.view_all_events = false; } }, computed: { filtered_events(){ let val = this.me.search.toLowerCase(); let filteredEvents = this.me.allEvents; // handle search results using all events if (this.me.search.length > 0){ filteredEvents = this.me.allEvents.filter(event => { let name = event.get('name').toLowerCase(); if ( name.includes(val) ){ return event; } }) } // handle tab selection using filteredEvents var yesterday = new Date(); yesterday.setDate( yesterday.getDate() - 1 ); var timestamp = yesterday.getTime(); if (this.me.active_filters.eventDates !== 'all'){ filteredEvents = filteredEvents.filter(event => { let start = this.return_markup( event, 'start_offset_time' ); let end = this.return_markup( event, 'end_offset_time' ); if (this.me.active_filters.eventDates === 'past'){ if( start < timestamp && ( end < timestamp || ! end || end == "undefined" ) ){ return event; } } else if (this.me.active_filters.eventDates === 'upcoming'){ if ( start > timestamp || end > timestamp ){ return event; } } }) } return this.sortEventList( filteredEvents, this.me.sortObject ); }, } }); </script> <!-- HTML !--> <script type="text/html" id="template-view-photo-carousel"> <div id="overlay_view-photo-carousel" class="carousel-v-overlay" > <div class="carousel-container" @touchstart.self="touched_overlay()" @mousedown.self="touched_overlay()" @click.self="clicked_overlay()" > <div class="actions-container"> <!-- <div class="event-page-callout" style="color: #fff;" @click.stop="tapped_download()"> <template v-if="! downloading"> <i class="fa-regular fa-arrow-down-to-line" style="color: #fff;"></i> Download </template> <template v-else> <i class="fa-regular fa-check" style="color: #fff;"></i> Downloaded </template> </div> --> <div class="event-page-callout" style="color: #fff;" @click.stop="tapped_share()"> <template v-if="! sharing"> <i class="fa-regular fa-arrow-up-from-bracket" style="color: #fff;"></i> Share </template> <template v-else> <i class="fa-regular fa-check" style="color: #fff;"></i> Link copied </template> </div> </div> <i class="fa-light fa-chevron-left nav-arrow" @click.stop="prev()"></i> <div class="carousel-photo-container"> <video v-if="returnIsVideo(json_photos[current_index].file_name) && json_photos[current_index].url" class="carousel-photo" autoplay muted loop playsinline :key="current_index" > <source :src="json_photos[current_index].url" type="video/mp4"> </video> <div class="carousel-photo-wrapper"> <div class="carousel-photo" :style="'background-image:url('+json_photos[current_index].url+')'"></div> </div> <template v-if="json_config.show_caption"> <p class="carousel-caption" v-if="json_photos[current_index].caption" :style="{ 'font-size': json_config.caption_size + 'px', 'font-weight': json_config.caption_font_weight }">{{ json_photos[current_index].caption }}</p> </template> </div> <i class="fa-light fa-chevron-right nav-arrow" @click.stop="next()"></i> </div> </div> </script> <style> .carousel-v-overlay{ display: flex; align-items: center; justify-content: center; cursor: auto; background-color: rgba(8,8,19,0.84); position: fixed; z-index: 1000 !important; left: 0px; top: 0px; width: 100vw; animation: overlay-animation 0.3s forwards; } @keyframes overlay-animation { 0% { transform: scale(0.8); } 100% { transform: scale(1); } } .carousel-container{ display: flex; justify-content: space-between; align-items: center; height: 100vh; width: 98vw; position: relative; overflow: hidden; cursor: auto; } .carousel-container > .nav-arrow { font-size: 28px; padding: 1em; color: #fff; z-index: 1; opacity: 0.6; transition: opacity 0.3s ease; cursor: pointer; } .carousel-container > .nav-arrow:hover { opacity: 1; } .carousel-photo-container { display: flex; flex-direction: column; justify-content: center; align-items: center; width: 100%; height: 70%; min-width: 200px; min-height: 200px; } .carousel-photo-wrapper { display: flex; justify-content: center; align-items: center; width: 100%; height: 100%; } .carousel-photo { width: 100%; height: 100%; object-fit: contain; background-size: contain; background-repeat: no-repeat; background-position: center; } .carousel-caption{ margin: 10px 0 0 0; text-transform: uppercase; letter-spacing: 2px; color: #fff; padding: 5px; border-radius: 2px; background-color: rgba(0, 0 ,0, 0.1); } .actions-container{ position: absolute; top: 5px; right: 5px; display: flex; align-items: center; justify-content: center; } .actions-container > .event-page-callout{ padding-left: 10px; padding-right: 10px; border-radius: 5px; border: 1px solid #e4ebf1; font-size: 13px; margin: 6px; color: #6a7482; padding-bottom: 8px; padding-top: 8px; cursor: pointer; } .actions-container > .event-page-callout i{ padding-right:3px; } .actions-container > .event-page-callout:hover{ color: #081333; } </style> <script> function downloadImage( url, filename ) { return fetch(url, { mode: 'cors' }) .then(response => { if (!response.ok) { throw new Error(`Network response was not ok: ${response.statusText}`); } return response.blob(); }) .then(blob => { const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.style.display = 'none'; a.href = url; a.download = filename; // Append the anchor to the body document.body.appendChild(a); a.click(); // Remove the anchor from the body document.body.removeChild(a); window.URL.revokeObjectURL(url); }) .catch(err => console.error('Error downloading the image:', err)); } store.components.view_photo_carousel = { json_photos: [], current_index: 0, downloading: false, sharing: false, json_config: null, } app_overlays.component("view-photo-carousel", { template: "#template-view-photo-carousel", data() { return store.components.view_photo_carousel; }, methods: { async tapped_download(){ let url = this.json_photos[this.current_index].url; let file_name = this.json_photos[this.current_index].file_name; this.downloading = true; try { downloadImage( url, file_name ) .then(() => { displayMessage("File downloaded.", false, 6000); this.downloading = false; }) .catch(() => { displayMessage("Sorry, something went wrong with your download. Please try again, or contact support if the problem persists.", true, 8000); this.downloading = false; }) } catch (error) { displayMessage("Sorry, something went wrong with your download. Please try again, or contact support if the problem persists.", true, 8000); } }, returnIsVideo(filename) { var ext = this.getExtension(filename); switch (ext.toLowerCase()) { case 'm4v': case 'avi': case 'mpg': case 'mp4': case 'mp3': case 'mov': // etc return true; } return false; }, getExtension(filename) { var parts = filename.split('.'); return parts[parts.length - 1]; }, tapped_share(){ copyToClipboard( window.location.href ); displayMessage("Link copied.", false, 6000); this.sharing = true; setTimeout(() => { this.sharing = false; }, 3000); }, prev(){ if ( this.current_index === 0 ){ this.current_index = this.json_photos.length - 1; } else { this.current_index -= 1; } }, next(){ if ( this.current_index === this.json_photos.length - 1 ){ this.current_index = 0; } else { this.current_index += 1; } }, dismiss_overlay(){ store.overlays.view_photo_carousel = false; }, clicked_overlay(){ if( ! should_close_overlay( this.last_touch ) ){ return } this.dismiss_overlay(); }, touched_overlay(){ this.last_touch = new Date().getTime(); }, }, computed: { } }); </script> <script type="text/html" id="template-bottom-sheet-overlay"> <div class="v-overlay scrollable hugePaddingBottom active" @touchstart.self="touched_overlay()" @mousedown.self="touched_overlay()" @click.self="clicked_overlay()" > <div class="standardOverlayCard centeredBlock bottom-sheet" :class="{'short': height == 'short'}" > <!-- generic header content --> <div class="bottom-sheet-overlay-header"> <div class="relative" > <div class="overlay-header-handle-wrap centeredBlock" @click.stop="toggle_height()" > <div class="overlay-header-handle"></div> </div> <i class="overlay-x-button fa-regular fa-xmark pointer" @click.stop="dismiss_overlay()" ></i> </div> <!-- header content injected here --> <slot name="header"></slot> </div> <!-- body content injected here --> <slot name="body"></slot> <!-- footer content injected here --> <slot name="footer"></slot> </div> </div> </script> <style> .standardOverlayCard.bottom-sheet{ position: fixed; bottom: 0px; top: 50px; left: 0px; right: 0px; z-index: 1001; /* background-color: rgba(249, 251, 251, 1); */ background-color: #fff; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); transition: bottom 0.3s cubic-bezier(0.25, 0.1, 0.25, 1); border-top-left-radius: 10px; border-top-right-radius: 10px; padding-bottom: 10px; padding-top: 10px; max-width: 1100px; margin-top: 0px; border-bottom-left-radius: 0px; border-bottom-right-radius: 0px; } .standardOverlayCard.bottom-sheet.short{ top: 50%; } @media (max-height: 600px) { .standardOverlayCard.bottom-sheet.short{ top: 150px; } } .standardOverlayCard.bottom-sheet h3{ padding-top:0px; } .standardOverlayCard.bottom-sheet{ display: flex; flex-direction: column; padding-bottom:0px; padding-top: 0px; } .standardOverlayCard.bottom-sheet .bottom-sheet-overlay-card-header { flex: 0 0 auto; /* Fixed height, doesn't grow or shrink */ } .standardOverlayCard.bottom-sheet .standardCardFooter{ position: absolute; bottom: 0px; left: 0px; right: 0px; background-color: #fff; border-top: 1px solid #e4ebf1; z-index: 10; } .standardOverlayCard.bottom-sheet .standardCardBody{ flex: 1 1 auto; /* Flexible height, can grow and shrink */ padding-bottom: 90px !important; } .overlay-header-handle-wrap{ padding: 10px; cursor: pointer; max-width: 120px; } .overlay-header-handle{ width: 70px; height: 3px; border-radius: 10px; background-color: #acb3ba; margin-left: auto; margin-right: auto; margin-top: 8px; margin-bottom: 8px; } .overlay-x-button{ position: absolute; right: 14px; color: #535e6c; font-size: 20px; top: 14px; } </style> <script> //define the component app_overlays.component("bottom-sheet-overlay", { template: "#template-bottom-sheet-overlay", data() { return { height: "tall" } }, mounted(){ const universalEditOverlay = document.getElementById('overlay_universal-edit'); if (universalEditOverlay) { universalEditOverlay.style.zIndex = "1000000"; } const membershipValidationOverlay = document.getElementById('overlay_verifyMembership'); if (membershipValidationOverlay) { membershipValidationOverlay.style.zIndex = "1000000"; } }, beforeUnmount() { //reset the z-index const universalEditOverlay = document.getElementById('overlay_universal-edit'); if (universalEditOverlay) { universalEditOverlay.style.zIndex = ""; // Revert to default } const membershipValidationOverlay = document.getElementById('overlay_verifyMembership'); if (membershipValidationOverlay) { membershipValidationOverlay.style.zIndex = ""; // Revert to default } }, methods: { dismiss_overlay(){ this.$emit('dismiss_overlay'); }, toggle_height(){ if( this.height == "tall" ){ this.height = "short"; }else{ this.height = "tall"; } }, clicked_overlay(){ if (should_close_overlay(this.last_touch)) { this.dismiss_overlay(); } }, touched_overlay(){ this.last_touch = new Date().getTime(); }, }, }); </script> <!-- define/register shared vue components --> <script type="text/html" id="template-calendar-date-picker"> <vue-date-picker v-model="selected_date" :mode="mode" :modes="['year', 'month']" :monthPicker="true" :yearPicker="true" @update:modelValue="date_changed()" :clearable="false" format="MMM-d-yyyy" auto-apply :teleport="true" menu-class-name="cs-calendar-date-picker-menu" input-class-name="cs-calendar-date-picker-input" > <template #input-icon> <i class="fa-regular fa-calendar-arrow-down"></i> </template> <template #calendar-icon> <i class="fa-regular fa-arrow-turn-down-left"></i> </template> <template #arrow-left> <i class="fa-regular fa-chevron-left"></i> </template> <template #arrow-right> <i class="fa-regular fa-chevron-right"></i> </template> </vue-date-picker> </script> <style> .cs-calendar-date-picker-menu .dp__btn{ color: #000000 !important; background: none !important; font: inherit !important; line-height: normal !important; } .cs-calendar-date-picker-input{ width: 0px; padding-left: 26px; padding-right: 22px; text-align: center; margin-right: 8px; overflow: hidden; padding-top: 7px; padding-bottom: 7px; border: 1px solid #e4ebf1; } .cs-calendar-menu-wrap .dp__input_icon i{ font-size: 16px !important; transform: none; } </style> <script> app_content.component("calendar-date-picker-component", { template: "#template-calendar-date-picker", data() { return { selected_date: new Date(), mode: 'month', } }, props: { handle_button_click: Function, }, methods: { date_changed(){ this.handle_button_click( 'date-picker', this.selected_date ); }, }, }); </script> <script> app_content.component('VueDatePicker', VueDatePicker); </script> <script type="text/html" id="template-calendar-grid"> <div class="centeredOnMobile smallMarginTop" :class="{'ninetyPercent': !is_membership_cal, 'standardCard': !is_membership_cal}"> <!-- main options header // stays consistent across all views --> <div class="standardCardBody noBorderTop calendar noPadding fff" style="overflow: none;"> <!-- options for non-membership cal --> <div v-if="filterOptions?.length >= 1 && !is_membership_cal" class="cal-filter-option-wrap scrollable paddingLeftRight tinyPaddingTop smallPaddingBottom"> <div class="flexNoWrap"> <template v-for="option in filterOptions"> <div class="cs-cal-filter-option" :class="[ 'cs-cal-filter-option_' + option.data_option, { 'active': selectedFilterOptions.indexOf(option.data_option) !== -1 } ]" :data-option="option.data_option" @click="$emit('handleFilterSelection', option)" > {{ option.name }} </div> </template> </div> </div> <!-- main calendar header // stays consistent across all views --> <!-- options for membership cal --> <div class="cs-calendar-wrap overflowHidden" :class="[{ 'wrapBorderBottom': is_membership_cal }, 'cs-calendar-wrap_' + client_id, class_string]" :style="generateWrapStyle('cs-calendar-wrap')" :data-client-id="client_id"> <div v-if="filterOptions?.length >= 1 && is_membership_cal" class="scrollable posScrollbar lightBorderTop noPaddingBottom"> <div class="flexNoWrap" :style="generateWrapStyle('options')"> <template v-for="option in filterOptions"> <div class="cs-cal-filter-option" :class="{ ['cs-cal-filter-option_' + option.data_option]: true, 'active': selectedFilterOptions.indexOf(option.data_option) !== -1 }" :data-option="option.data_option" @click="$emit('handleFilterSelection', option)" > {{ option.name }} </div> </template> </div> </div> <div class="cs-calendar-menu-wrap flexNoWrap" :style="generateWrapStyle('cs-calendar-menu-wrap')"> <div class="flexWrapLeftAlign"> <div class="flexNoWrap"> <div class="cs-calendar-button roboto hideOnMobile" data-option="today" @click="handleButtonClick('today')">Today</div> <div class="cs-calendar-button cs-calendar-button-previous roboto" data-option="previous" @click="handleButtonClick('previous')"></div> <div class="cs-calendar-button cs-calendar-button-next roboto" data-option="next" @click="handleButtonClick('next')"></div> </div> <p v-if="calendarTitle" class="cs-calendar-date-range-title roboto">{{ calendarTitle }}</p> </div> <div class="flexGrowOne roboto"></div> <!-- calendar date picker --> <calendar-date-picker-component :handle_button_click="handleButtonClick" ></calendar-date-picker-component> <div class="vue-cs-calendar-select-wrap onWhite roboto"> <select class="vue-cs-calendar-view-select roboto" v-model="view_preference" @change="changeViewPreference" > <option value="Month">Month</option> <option value="Week">Week</option> <option value="Day">Day</option> </select> </div> </div> <!-- inject month view --> <div v-if="view_preference === 'Month' && !loading" class="flexGrid"> <!-- header --> <div class="daysOfWeek-grid"> <template v-for="day in daysOfWeek"> <calendar-cell-component class="grid-cell dayOfWeek" :class="{ 'borderClose': day === 'Sat' }" :day="day.toUpperCase()" cellType="dayOfWeek" :view_preference="view_preference" ></calendar-cell-component> </template> </div> <!-- events --> <div class="calendar-grid"> <template v-for="(calendarObj, i) in calendar"> <calendar-cell-component class="grid-cell" :client_id="client_id" :date="calendarObj.date" :class="{ 'notCurrentMonth': calendarObj.class === 'prevMonthDate' || calendarObj.class === 'nextMonthDate', 'currentMonth': calendarObj.class === 'currentMonthDate', 'borderClose': calendarObj.date.day() === 6, 'borderTopRightBottomClose': i >= 35 && i <= 40, 'borderTopBottomClose': i === calendar.length - 1 }" :month_class="calendarObj.class" :day="calendarObj.day" :events="calendarObj.events" cellType="calendarObj" :view_preference="view_preference" :defaultDate="calendarObj.defaultDate" :monthIndex="i" @createSchedule_emitted="createSchedule_emitted" @clickSchedule_emitted="clickSchedule_emitted" :is_membership_cal="is_membership_cal" ></calendar-cell-component> </template> </div> </div> <!-- inject week view --> <div v-if="view_preference === 'Week' && !loading" class="flexGrid"> <!-- header --> <div class="placeholder-daysOfWeek-grid"> <template v-for="day in placeholder_daysOfWeek"> <calendar-cell-component v-if="day.dayOfWeek === null" class="grid-cell-week dayOfWeek hide" :day="day" cellType="dayOfWeek" :view_preference="view_preference" ></calendar-cell-component> <calendar-cell-component v-if="day.dayOfWeek !== null" class="grid-cell-week dayOfWeek" :day="day" cellType="dayOfWeek" :view_preference="view_preference" :defaultDate="day.defaultDate" ></calendar-cell-component> </template> <!-- all day events --> <template v-for="allDay in placeholder_daysOfWeek"> <calendar-cell-component v-if="allDay.dayOfWeek === null" class="grid-cell-week allDay" :allDay="allDay" cellType="allDay_events" :view_preference="view_preference" :is_membership_cal="is_membership_cal" ></calendar-cell-component> <calendar-cell-component v-if="allDay.dayOfWeek !== null" class="grid-cell-week allDay" :allDay="allDay" cellType="allDay_events" :view_preference="view_preference" @createSchedule_emitted="createSchedule_emitted" @clickSchedule_emitted="clickSchedule_emitted" :is_membership_cal="is_membership_cal" ></calendar-cell-component> </template> </div> <!-- timestamps and events --> <div class="placeholder-calendar-grid"> <template v-for="(timestamp, i) in timestamps"> <template v-for="(calendarObj, j) in calendar"> <calendar-cell-component v-if="j === 0" class="grid-cell-week timestamp" :date="calendarObj.date" :timestamp="i % 4 === 0 ? formatShortTime( timestamp.customFormat ) : null" :events="[]" cellType="timestamp" :view_preference="view_preference" :is_membership_cal="is_membership_cal" :class="{ 'borderBottomClose': i === timestamps.length - 1 }" ></calendar-cell-component> <calendar-cell-component v-else class="grid-cell-week" :style="calendarObj.defaultDate ? { 'background-color': 'rgba( 175, 213, 240, 0.1)' } : null" :class="{ 'showBorder': i % 4 === 0, 'hideBorder': i === 0 || i % 4 !== 0 }" :date="calendarObj.date" :events="generateEventForTimestamp( timestamp.customFormat, calendarObj.events, calendarObj.date )" cellType="calendarObj" :view_preference="view_preference" @createSchedule_emitted="createSchedule_emitted" @clickSchedule_emitted="clickSchedule_emitted" ></calendar-cell-component> </template> </template> </div> </div> <!-- inject day view --> <div v-if="view_preference === 'Day' && !loading" class="flexGrid"> <!-- header --> <div class="day-daysOfWeek-grid"> <template v-for="day in today_daysOfWeek"> <calendar-cell-component v-if="day.dayOfWeek === null" class="grid-cell-week dayOfWeek hide" :day="day" cellType="dayOfWeek" :view_preference="view_preference" ></calendar-cell-component> <calendar-cell-component v-if="day.dayOfWeek !== null" class="grid-cell-week day" :day="day" cellType="dayOfWeek" :view_preference="view_preference" :defaultDate="day.defaultDate" ></calendar-cell-component> </template> <!-- all day events --> <template v-for="allDay in today_daysOfWeek"> <calendar-cell-component v-if="allDay.dayOfWeek === null" class="grid-cell-week allDay" :allDay="allDay" cellType="allDay_events" :view_preference="view_preference" :is_membership_cal="is_membership_cal" ></calendar-cell-component> <calendar-cell-component v-if="allDay.dayOfWeek !== null" class="grid-cell-week allDay" :allDay="allDay" cellType="allDay_events" :view_preference="view_preference" @createSchedule_emitted="createSchedule_emitted" @clickSchedule_emitted="clickSchedule_emitted" :is_membership_cal="is_membership_cal" ></calendar-cell-component> </template> </div> <!-- timestamps and events --> <div class="day-calendar-grid"> <template v-for="(timestamp, i) in timestamps"> <template v-for="(calendarObj, j) in calendar"> <calendar-cell-component v-if="j === 0" class="grid-cell-week timestamp" :date="calendarObj.date" :timestamp="i % 4 === 0 ? formatShortTime( timestamp.customFormat ) : null" :events="[]" cellType="timestamp" :view_preference="view_preference" :is_membership_cal="is_membership_cal" :class="{ 'borderBottomClose': i === timestamps.length - 1 }" ></calendar-cell-component> <calendar-cell-component v-if="j !== 0" class="grid-cell-week" :style="calendarObj.defaultDate ? { 'background-color': 'rgba( 175, 213, 240, 0.1)' } : null" :class="{ 'showBorder': i % 4 === 0, 'hideBorder': i === 0 || i % 4 !== 0 }" :date="calendarObj.date" :events="generateEventForTimestamp( timestamp.customFormat, calendarObj.events, calendarObj.date )" cellType="calendarObj" :view_preference="view_preference" @createSchedule_emitted="createSchedule_emitted" @clickSchedule_emitted="clickSchedule_emitted" ></calendar-cell-component> </template> </template> </div> </div> <!-- spinner while loading --> <div v-if="loading" class="cardBodySpinnerWrap relative active"> <div class="cardBodySpinner centeredBlock"></div> </div> </div> </div> </div> </script> <script> /* ----- STORE ----- */ store.components["calendar_grid"] = { calendar: [], loading: true, view_preference: 'Month', dropdownOpen: false, daysOfWeek: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], placeholder_daysOfWeek: [], today_daysOfWeek: [], view_options: [ 'Month', 'Week', 'Day' ], calendarTitle: null, //dates and times currentDate: dayjs() || new Date(), defaultDate: dayjs() || new Date(), currentMonth: dayjs().format('MMMM'), startOfWeek: dayjs().startOf('week'), endOfWeek: dayjs().endOf('week'), currentYear: dayjs().year(), timestamps: [], currentTime: dayjs().format('HH:mm:ss'), }; /* ----- COMPONENT ----- */ app_content.component("calendar-grid-component", { template: "#template-calendar-grid", data() { return store.components['calendar_grid'] }, created(){ if (this.timestamps.length === 0){ this.generateTimestamps(); } }, props: { client_id: String, class_string: String, style_string: String, filterOptions: Array, selectedFilterOptions: Array, schedules: Array, is_membership_cal: Boolean, load_cal: Function, handleFilterSelection: Function, createSchedule: Function, clickSchedule: Function, }, methods: { createSchedule_emitted( date ){ this.$emit('createSchedule', date) }, clickSchedule_emitted( event ){ this.$emit('clickSchedule', event ) }, formatShortTime( timeString ){ if (!timeString){ return '' // placeholder for 11:45-12:00 block } // if (timeString === '12:00 AM'){ // return Intl.DateTimeFormat().resolvedOptions().timeZone; // } const [hours, minutes] = timeString.split(':'); const AM_PM = minutes.split(' ')[1] return `${hours} ${AM_PM}`; }, generateWrapStyle( type ){ if (type === 'cs-calendar-wrap'){ return this.style_string === undefined ? {'width': '100%'} : this.style_string; } else if (type === 'options' && this.is_membership_cal){ return { 'padding-top': '15px', 'padding-right': '15px', 'padding-left': '15px', 'padding-bottom': '5px', 'border-width': '0 1.5px 0 1.5px', 'border-color': '#e2e8e8', 'border-style': 'solid', 'overflow': 'auto', } } else if (type === 'cs-calendar-menu-wrap' && this.is_membership_cal){ return { 'border-width': '0 1.5px 0 1.5px', 'border-color': '#e2e8e8', 'border-style': 'solid' } } }, generateEventForTimestamp( timestamp, events, date ){ // do not want to display all day or multiple day events -- being handled separately let eventsArr = []; if (events.length > 0){ for (let i = 0; i < events.length; i++){ let event = events[i]; // normal event if ((event.hours_mins_start && event.hours_mins_end) && !event.isAllDay && event.calendarOnly) { let startTime = dayjs().set('hour', event.hours_mins_start.hours).set('minute', event.hours_mins_start.minutes); let endTime = dayjs().set('hour', event.hours_mins_end.hours).set('minute', event.hours_mins_end.minutes).subtract(15, 'minutes'); let formattedStartTime = startTime.format('h:mm A'); let formattedEndTime = endTime.format('h:mm A'); let timesArray = [formattedStartTime]; while (startTime.isBefore(endTime)) { startTime = startTime.add(15, 'minutes'); timesArray.push(startTime.format('h:mm A')); } if ( formattedStartTime === timestamp ){ eventsArr.push({ event, numOfTimeBlocks: timesArray.length }); } // recurring event } else if ( (event.start && event.end) && (event.start !== event.end) && !event.isAllDay && !event.isMultipleDay && event.calendarOnly ){ let startTime = dayjs(event.start); let endTime = dayjs(event.end).subtract(15, 'minutes'); let formattedStartTime = startTime.format('h:mm A'); let formattedEndTime = endTime.format('h:mm A'); let timesArray = [formattedStartTime]; while (startTime.isBefore(endTime)) { startTime = startTime.add(15, 'minutes'); timesArray.push(startTime.format('h:mm A')); } if ( formattedStartTime === timestamp ){ eventsArr.push({ event, numOfTimeBlocks: timesArray.length }); } // non-calendarOnly events } else if ( (event.start && event.end) && (event.start !== event.end) && !event.calendarOnly ){ let startTime; let endTime; if (dayjs(event.start).startOf('day').$d.toString() === dayjs(date).startOf('day').$d.toString()){ startTime = dayjs(event.start); endTime = dayjs(event.end).subtract(15, 'minutes'); } else if (dayjs(event.end).startOf('day').$d.toString() === dayjs(date).startOf('day').$d.toString()) { startTime = dayjs(event.end).startOf('day'); endTime = dayjs(event.end).subtract(15, 'minutes'); } else { startTime = dayjs().startOf('day'); endTime = dayjs().endOf('day'); } let formattedStartTime = startTime.format('h:mm A'); let formattedEndTime = endTime.format('h:mm A'); let timesArray = [formattedStartTime]; while (startTime.isBefore(endTime)) { startTime = startTime.add(15, 'minutes'); timesArray.push(startTime.format('h:mm A')); } if ( formattedStartTime === timestamp ){ eventsArr.push({ event, numOfTimeBlocks: timesArray.length }); } } } } if (eventsArr.length > 0){ return eventsArr; } else { return []; } }, changeViewPreference(){ this.loading = true; //and re-load data this.$emit('load_cal', this.client_id); }, calculateTitleRange(){ if ( this.view_preference === 'Month' ){ this.calendarTitle = `${this.currentMonth} ${this.currentYear.toString()}`; } else if ( this.view_preference === 'Week' ){ this.calendarTitle = `${this.startOfWeek.format('MMMM')} ${this.startOfWeek.$D} - ${this.endOfWeek.format('MMMM')} ${this.endOfWeek.$D}`; } else if ( this.view_preference === 'Day' ){ this.calendarTitle = `${this.currentMonth} ${this.currentDate.$D}, ${this.currentYear}`; } }, handleButtonClick( option, selectedDate ){ this.loading = true; if (this.view_preference === 'Month'){ if( option === "today" ){ this.currentDate = dayjs(); this.currentMonth = this.currentDate.format('MMMM') this.currentYear = this.currentDate.$y this.startOfWeek = this.currentDate.startOf('week'); this.endOfWeek = this.currentDate.endOf('week'); }else if( option === "next" ){ this.currentDate = this.currentDate.add(1, 'month').startOf('month'); this.currentMonth = this.currentDate.format('MMMM') this.currentYear = this.currentDate.$y this.startOfWeek = this.currentDate.startOf('week'); this.endOfWeek = this.currentDate.endOf('week'); }else if( option === "previous" ){ this.currentDate = this.currentDate.subtract(1, 'month').startOf('month'); this.currentMonth = this.currentDate.format('MMMM') this.currentYear = this.currentDate.$y this.startOfWeek = this.currentDate.startOf('week'); this.endOfWeek = this.currentDate.endOf('week'); }else if ( option === "date-picker" ){ let { month, year } = selectedDate; this.currentDate = dayjs(new Date(year, month, 1)); this.currentMonth = this.currentDate.format('MMMM') this.currentYear = this.currentDate.$y this.startOfWeek = this.currentDate.startOf('week'); this.endOfWeek = this.currentDate.endOf('week'); } } else if (this.view_preference === 'Week'){ if( option === "today" ){ this.currentDate = dayjs(); this.currentMonth = this.currentDate.format('MMMM') this.currentYear = this.currentDate.$y this.startOfWeek = this.currentDate.startOf('week'); this.endOfWeek = this.currentDate.endOf('week'); }else if( option === "next" ){ this.currentDate = this.currentDate.add(1, 'week'); this.currentMonth = this.currentDate.format('MMMM') this.currentYear = this.currentDate.$y this.startOfWeek = this.currentDate.startOf('week'); this.endOfWeek = this.currentDate.endOf('week'); }else if( option === "previous" ){ this.currentDate = this.currentDate.subtract(1, 'week'); this.currentMonth = this.currentDate.format('MMMM') this.currentYear = this.currentDate.$y this.startOfWeek = this.currentDate.startOf('week'); this.endOfWeek = this.currentDate.endOf('week'); }else if ( option === "date-picker" ){ let { month, year } = selectedDate; this.currentDate = dayjs(new Date(year, month, 1)); this.currentMonth = this.currentDate.format('MMMM') this.currentYear = this.currentDate.$y this.startOfWeek = this.currentDate.startOf('week'); this.endOfWeek = this.currentDate.endOf('week'); } } else if (this.view_preference === 'Day'){ if( option === "today" ){ this.currentDate = dayjs(); this.currentMonth = this.currentDate.format('MMMM') this.currentYear = this.currentDate.$y this.startOfWeek = this.currentDate.startOf('week'); this.endOfWeek = this.currentDate.endOf('week'); }else if( option === "next" ){ this.currentDate = this.currentDate.add(1, 'day'); this.currentMonth = this.currentDate.format('MMMM') this.currentYear = this.currentDate.$y this.startOfWeek = this.currentDate.startOf('week'); this.endOfWeek = this.currentDate.endOf('week'); }else if( option === "previous" ){ this.currentDate = this.currentDate.subtract(1, 'day'); this.currentMonth = this.currentDate.format('MMMM') this.currentYear = this.currentDate.$y this.startOfWeek = this.currentDate.startOf('week'); this.endOfWeek = this.currentDate.endOf('week'); }else if ( option === "date-picker" ){ let { month, year } = selectedDate; this.currentDate = dayjs(new Date(year, month, 1)); this.currentMonth = this.currentDate.format('MMMM') this.currentYear = this.currentDate.$y this.startOfWeek = this.currentDate.startOf('week'); this.endOfWeek = this.currentDate.endOf('week'); } } //and re-load data this.$emit('load_cal', this.client_id); }, /* ----- date methods ----- */ generateTimestamps(){ const startTime = dayjs().startOf('day'); const endTime = dayjs().endOf('day'); let currentTime = startTime; while (currentTime.isBefore(endTime)) { this.timestamps.push({ customFormat: currentTime.format('h:mm A'), militaryFormat: currentTime.format('HH:mm:ss'), }); currentTime = currentTime.add(15, 'minutes'); } this.timestamps.push({ customFormat: null, militaryFormat: null, }) }, generateCalendar( ){ console.log("generate calendar") this.loading = true; if ( this.view_preference === 'Month'){ // current month start date, end date, total days calculation const start = this.currentDate.startOf('month'); const end = start.endOf('month'); const totalDays = end.diff(start, 'day') + 1; // number of calendar days to add before and after current month days const spaceBefore = start.day(); const spaceAfter = 42 - (spaceBefore + totalDays); // generate number of days for grid cell of 42 this.calendar = [...Array(spaceBefore)].map((_, i) => { const prevMonthDate = start.subtract(spaceBefore - i, 'day'); return { date: prevMonthDate, day: prevMonthDate.$D, class: 'prevMonthDate', events: [], defaultDate: false, } }).concat([...Array(totalDays)].map((_, i) => { const currentMonthDate = start.add(i, 'day'); if (this.defaultDate.startOf('day').$d.toString() === currentMonthDate.$d.toString()){ return { date: currentMonthDate, day: currentMonthDate.$D === 1 ? `${currentMonthDate.format('MMM')} ${currentMonthDate.$D}` : currentMonthDate.$D, class: 'currentMonthDate', events: [], defaultDate: true, } }else { return { date: currentMonthDate, day: currentMonthDate.$D === 1 ? `${currentMonthDate.format('MMM')} ${currentMonthDate.$D}` : currentMonthDate.$D, class: 'currentMonthDate', events: [], defaultDate: false, } } })).concat([...Array(spaceAfter)].map((_, i) => { const nextMonthDate = end.add(i + 1, 'day'); return { date: nextMonthDate, day: nextMonthDate.$D === 1 ? `${nextMonthDate.format('MMM')} ${nextMonthDate.$D}` : nextMonthDate.$D, class: 'nextMonthDate', events: [], defaultDate: false, } })) } else if ( this.view_preference === 'Week' ){ console.log("generate week A") // start and end of current week const startOfWeek = this.currentDate.startOf('week'); const endOfWeek = this.currentDate.endOf('week'); const weekDays = [{ date: null, day: null, events: [] }]; let currentDay = startOfWeek; while (currentDay.isBefore(endOfWeek) || currentDay.isSame(endOfWeek, 'day')) { weekDays.push(currentDay); currentDay = currentDay.add(1, 'day'); } this.placeholder_daysOfWeek = [null, 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], this.placeholder_daysOfWeek = this.placeholder_daysOfWeek.map((dayOfTheWeek, i) => { if (JSON.stringify(this.defaultDate.startOf('day').$d) === JSON.stringify(weekDays[i].$d)){ return { dayOfWeek: dayOfTheWeek, day: weekDays[i].$D, date: weekDays[i], events: [], defaultDate: true, } } else { return { dayOfWeek: dayOfTheWeek, day: weekDays[i].$D, date: weekDays[i], events: [], defaultDate: false, } } }) console.log("generate week B") this.mapAllDayEvents(); this.calendar = weekDays.map(date => { if(JSON.stringify(this.defaultDate.startOf('day').$d) === JSON.stringify(date.$d)){ return { date: date, events: [], defaultDate: true, } } else { return { date: date, events: [], defaultDate: false, } } }) console.log("generate week C") } else if ( this.view_preference === 'Day' ){ this.today_daysOfWeek = [null, this.daysOfWeek[this.currentDate.day()]] this.today_daysOfWeek = this.today_daysOfWeek.map((dayOfTheWeek, i) => { if (i === 1 && this.defaultDate.startOf('day').$d.toString() === this.currentDate.startOf('day').$d.toString()){ return { dayOfWeek: dayOfTheWeek, day: dayOfTheWeek !== null ? this.currentDate.$D : null, date: dayOfTheWeek !== null ? this.currentDate : null, events: [], defaultDate: true, } } else { return { dayOfWeek: dayOfTheWeek, day: dayOfTheWeek !== null ? this.currentDate.$D : null, date: dayOfTheWeek !== null ? this.currentDate : null, events: [], defaultDate: false, } } }) this.mapAllDayEvents(); this.calendar = [{ date: null, day: null, events: [] }, this.currentDate ].map((date, i) => { if(i === 1 && this.defaultDate.startOf('day').$d.toString() === date.startOf('day').$d.toString()){ return { date: date, events: [], defaultDate: true, } } else { return { date: date, events: [], defaultDate: false, } } }) } // map events onto calendar days this.mapCalendarEvents_v2(); }, // mapCalendarEvents(){ // let multipleDaySchedules = []; // this.calendar.map(day => { // let calendarDay; // if ( day.date !== null ){ // calendarDay = dayjs(day.date.$d).format('MMMM DD, YYYY'); // } // this.schedules.forEach(schedule => { // let scheduleStart = dayjs( schedule.start ).format('MMMM DD, YYYY'); // let scheduleEnd = dayjs( schedule.end ).format('MMMM DD, YYYY'); // let scheduleOnDay = scheduleStart === calendarDay && scheduleStart === scheduleEnd; // let scheduleOnDay_isAllDay = scheduleStart === calendarDay && schedule.isAllDay; // // check to see if there are any multiple event days // if ( multipleDaySchedules.length > 0 ){ // multipleDaySchedules.forEach((multipleDaySchedule, i) => { // if ( multipleDaySchedule.daysRemaining > 0 && calendarDay === multipleDaySchedule.scheduleStart.format('MMMM DD, YYYY')){ // day.events.push(multipleDaySchedule.schedule) // multipleDaySchedule.scheduleStart = multipleDaySchedule.scheduleStart.add(1, 'day') // multipleDaySchedule.daysRemaining--; // } else if ( multipleDaySchedule.daysRemaining === 0 ){ // multipleDaySchedules.splice(i, 1) // } // }) // } // // there is an event or all day event on this day // if ( scheduleOnDay || scheduleOnDay_isAllDay ){ // day.events.push(schedule) // // there is a multiple day event that starts on this day // } else if ( scheduleStart === calendarDay && scheduleStart !== scheduleEnd && !schedule.isAllDay ){ // multipleDaySchedules.push({ // schedule: schedule, // scheduleStart: dayjs(scheduleStart, { format: 'MMMM DD, YYYY' }).add(1, 'day'), // daysRemaining: dayjs(scheduleEnd, { format: 'MMMM DD, YYYY' }).diff(dayjs(scheduleStart, { format: 'MMMM DD, YYYY' }), 'day') - 1 // }) // schedule.isMultipleDay = true; // day.events.push(schedule); // } // }) // return day; // }) // console.log('this is final mapping: ', this.calendar) // this.loading = false; // }, mapCalendarEvents_v2(){ console.log("map calendar events v2"); let previousDayEventOrder = []; this.calendar.map((day, dayIndex) => { let calendarDay; if ( day.date !== null ){ calendarDay = dayjs(day.date.$d) } this.schedules.forEach(schedule => { let scheduleStart = dayjs( schedule.start ) let scheduleEnd = dayjs( schedule.end ) let scheduleOnDay = scheduleStart.format('MMMM DD, YYYY') === calendarDay.format('MMMM DD, YYYY') && scheduleStart.format('MMMM DD, YYYY') === scheduleEnd.format('MMMM DD, YYYY'); let scheduleOnDay_isAllDay = scheduleStart.format('MMMM DD, YYYY') === calendarDay.format('MMMM DD, YYYY') && schedule.isAllDay; // event falls on current day OR it's an all day event // added scheduleOnDay && scheduleOnDay_isAllDay 1/25/24 for regatta subevents start start and end on the same day if ( scheduleOnDay && !scheduleOnDay_isAllDay || scheduleOnDay && scheduleOnDay_isAllDay ){ day.events.push(schedule) } // it's the first day of a multiple day event // these checks are not thorough enough else if (scheduleStart.format('MMMM DD, YYYY') === calendarDay.format('MMMM DD, YYYY') && scheduleStart.format('MMMM DD, YYYY') !== scheduleEnd.format('MMMM DD, YYYY')){ schedule.isMultipleDay = true; day.events.push(schedule) } // it's a multiple day event and the current date falls between the start and end date else if ( scheduleStart.format('MMMM DD, YYYY') !== calendarDay.format('MMMM DD, YYYY') && (calendarDay.isAfter(scheduleStart) && calendarDay.isBefore(scheduleEnd)) ){ schedule.isMultipleDay = true; day.events.push(schedule) } }) /* ui stacking logic for month view */ if (this.view_preference === 'Month'){ if ( dayIndex !== 0 ){ let currentDayEventOrder; let currentDayIndexOrder = {}; let notMatchingEvents = []; let matchingEvents = []; for (let i = 0; i < day.events.length; i++){ let currentDayEventId = day.events[i].id; let previousDayIndex = previousDayEventOrder?.indexOf(currentDayEventId); if (previousDayIndex === -1){ notMatchingEvents.push(day.events[i]) } else if (previousDayIndex !== -1) { currentDayIndexOrder[previousDayIndex] = day.events[i]; matchingEvents.push(day.events[i]); } } let missingIndexes = []; for (let j = 0; j < 3; j++ ){ if (Object.keys(currentDayIndexOrder).indexOf(j.toString()) === -1 ){ missingIndexes.push(j) } } let placeholder = { bgColor: '', calendarId: '', calendarOnly: '', category: '', className: '', color: '', end: '', id: 'placeholder', raw: {dot_color: ''}, start: '', title: 'placeholder', }; for (let k = 0; k < missingIndexes.length; k++){ let missingIndexValue = missingIndexes[k]; currentDayIndexOrder[missingIndexValue] = notMatchingEvents[k] || placeholder; } let remainingIndexes = day.events.length - 3; let calculateRemainingEvents = matchingEvents.length > 3 ? matchingEvents.concat(notMatchingEvents).splice(3) : [...notMatchingEvents].splice(missingIndexes.length); if (remainingIndexes > 0){ for (let l = 3; l < 3 + remainingIndexes; l++){ let targetIndex = matchingEvents.length > 3 ? l - 3 : l - 3; //added placeholder for white lake yacht club bugs currentDayIndexOrder[l] = calculateRemainingEvents[targetIndex] || placeholder; } } const sortedObj = Object.fromEntries( Object.entries(currentDayIndexOrder).sort(([keyA], [keyB]) => parseInt(keyA) - parseInt(keyB)) ); currentDayEventOrder = Object.values(sortedObj); //set order of previous day events using event ids previousDayEventOrder = currentDayEventOrder.map(event => { return event.id }); //set current day events to newly sorted order day.events = currentDayEventOrder.filter((event, i, self) => event.id === 'placeholder' || (event.id !== 'placeholder' && i === self.findIndex(e => e.id === event.id))); } } return day; }) this.loading = false; }, mapAllDayEvents(){ let daysOfWeekArr; if (this.view_preference === 'Week'){ daysOfWeekArr = this.placeholder_daysOfWeek; } else if (this.view_preference === 'Day'){ daysOfWeekArr = this.today_daysOfWeek; } let multipleDaySchedules = []; daysOfWeekArr = daysOfWeekArr.map((day, i) => { this.schedules.forEach((schedule, i) => { if ( (i !== 0 && this.view_preference === 'Day' || i !== 0 && this.view_preference === 'Week') && ( schedule.calendarOnly || schedule.category === 'allday') ){ let calendarDay = dayjs(day.date) let scheduleStart = dayjs( schedule.start ) let scheduleEnd = dayjs( schedule.end ) let scheduleOnDay_isAllDay = scheduleStart.format('MMMM DD, YYYY') === calendarDay.format('MMMM DD, YYYY') && schedule.isAllDay; // it's an all day event if ( scheduleOnDay_isAllDay ){ day.events.push(schedule) } // it's the first day of a multiple day event else if (scheduleStart.format('MMMM DD, YYYY') === calendarDay.format('MMMM DD, YYYY') && scheduleStart.format('MMMM DD, YYYY') !== scheduleEnd.format('MMMM DD, YYYY')){ schedule.isMultipleDay = true; day.events.push(schedule) } // it's a multiple day event and the current date falls between the start and end date else if ( scheduleStart.format('MMMM DD, YYYY') !== calendarDay.format('MMMM DD, YYYY') && (calendarDay.isAfter(scheduleStart) && calendarDay.isBefore(scheduleEnd)) ){ schedule.isMultipleDay = true; day.events.push(schedule) } } }) return day; }) if (this.view_preference === 'Week'){ this.placeholder_daysOfWeek = daysOfWeekArr; } else if (this.view_preference === 'Day'){ this.today_daysOfWeek = daysOfWeekArr; } }, }, }) </script> <script type="text/html" id="template-calendar-cell"> <div @click="$emit('createSchedule_emitted', date)"> <!-- cell for month view --> <template v-if="view_preference === 'Month'"> <div class="cell-content" :class="cellType"> <div v-if="day" class="cell-day" :class="cellType" :style="checkDefaultDate( day, defaultDate, 'month' )">{{ day }}</div> <!-- all events --> <div v-if="cellType === 'calendarObj' && events.length > 0" class="cell-events-month" ref="cell" @click.stop> <template v-for="(event, index) in events"> <span v-if="index <= 2" :class="{ 'cell-event-month': event.id !== 'placeholder', 'transparent-event': event.id === 'placeholder' }" :style="generateEventStyleForMonth( event, 'event_body', date, monthIndex )" @click="handleScheduleClickForMonth( event )" ><div :style="generateEventStyleForMonth( event, 'status_dot', '', '', month_class )"></div><div class="event-title" :style="generateFontSize('eventTitle', event)">{{ generateEventTitle( event ) }}</div></span> <span v-if="index >= 3 && index === events.length - 1" class="overflow-cell-events" ref="overflow-cell" @click="toggleOverflowView( date, events, $event )"> <div :style="[ { display: 'flex' }, generateFontSize('eventTitle') ]" class="event-title">{{ index - 2 }} more</div> </span> </template> </div> </div> </template> <!-- cell for week view --> <template v-if="view_preference === 'Week'"> <div class="cell-content-week" :class="cellType"> <div v-if="day" :class="{ 'week-cellDay-container' : day.dayOfWeek !== null }"> <div class="cell-day numberHeader" :class="cellType" :style="[{ 'font-size': '25px', 'font-weight': '400' }, checkDefaultDate(day, defaultDate, 'week_header')]">{{ day.day }}</div> <div class="cell-day dayHeader" :class="cellType">{{ day.dayOfWeek !== null ? day.dayOfWeek.toUpperCase() : null }}</div> </div> <div v-if="timestamp" class="cell-day" :class="cellType"> <div class="timestamp" :style="generateFontSize('timestamp')">{{ timestamp }}</div> </div> <!-- all day events --> <div v-if="allDay"> <div v-if="allDay.dayOfWeek === null" class="allDay_events" :class="cellType" :style="generateFontSize('allDay')">All Day</div> <div class="cell-events-month"> <template v-if="allDay.dayOfWeek !== null" v-for="event in allDay.events"> <span :style="generateEventStyleForWeek( event, 'allDay_event_body' )" class="allDay-cell-event" @click="$emit('clickSchedule_emitted', event)" >{{ generateEventTitle( event ) }}</span> </template> </div> </div> <!-- all events --> <div v-if="cellType === 'calendarObj' && events.length > 0" class="cell-events-week" @click.stop> <template v-for="(event, index) in events"> <span class="cell-event-week" :style="generateEventStyleForWeek( event, 'event_body' )" @click="$emit('clickSchedule_emitted', event.event)" ><div></div>{{ generateEventTitle( event.event ) }}</span> </template> </div> </div> </template> <!-- cell for day view --> <template v-if="view_preference === 'Day'"> <div class="cell-content-week" :class="cellType"> <div v-if="day" :class="{ 'day-cellDay-container' : day.dayOfWeek !== null }"> <div class="cell-day numberHeader" :class="cellType" :style="[{ 'font-size': '25px', 'font-weight': '400' }, checkDefaultDate(day, defaultDate, 'day_header')]">{{ day.day }}</div> <div class="cell-day dayHeader" :class="cellType">{{ day.dayOfWeek !== null ? day.dayOfWeek.toUpperCase() : null }}</div> </div> <div v-if="timestamp" class="cell-day" :class="cellType"> <div class="timestamp" :style="generateFontSize('timestamp')">{{ timestamp }}</div> </div> <!-- all day events --> <div v-if="allDay"> <div v-if="allDay.dayOfWeek === null" class="allDay_events" :class="cellType" :style="generateFontSize('allDay')">All Day</div> <div class="cell-events-month"> <template v-if="allDay.dayOfWeek !== null" v-for="event in allDay.events"> <span :style="generateEventStyleForWeek( event, 'allDay_event_body' )" class="allDay-cell-event" @click="$emit('clickSchedule_emitted', event)" >{{ generateEventTitle( event ) }}</span> </template> </div> </div> <!-- all events --> <div v-if="cellType === 'calendarObj' && events.length > 0" class="cell-events-week" @click.stop> <template v-for="(event, index) in events"> <span class="cell-event-week" :style="generateEventStyleForWeek( event, 'event_body' )" @click="$emit('clickSchedule_emitted', event.event )" ><div></div>{{ generateEventTitle( event.event ) }}</span> </template> </div> </div> </template> </div> </script> <script> /* ----- STORE ----- */ store.components["calendar_cell"] = { cellWidth: this.getCellWidth, foo: null, }; /* ----- COMPONENT ----- */ app_content.component("calendar-cell-component", { template: "#template-calendar-cell", data() { return store.components['calendar_cell'] }, mounted(){ window.addEventListener('resize', this.handleResize); this.$nextTick(() => { this.handleResize(); }); }, beforeDestroy(){ window.removeEventListener('resize', this.handleResize); }, props: { client_id: String, date: Object, defaultDate: Boolean, day: { type: [String, Number, Object], required: false, }, allDay: Object, timestamp: String, currentTime: String, events: Array, cellType: String, view_preference: String, monthIndex: Number, month_class: String, is_membership_cal: Boolean, cell_emitted: Function, createSchedule_emitted: Function, clickSchedule_emitted: Function, }, methods: { handleResize(){ this.cellWidth = this.getCellWidth(); }, getCellWidth(){ const cell = this.$refs.cell; // Get the computed width of the cell if (cell){ this.cellWidth = cell.getBoundingClientRect().width; return cell.getBoundingClientRect().width; } }, checkDefaultDate( day, defaultDate, type ){ if( defaultDate ){ if ( type === 'month' ){ if ( day.length > 2){ return { 'padding': '2.5px', 'background-color': '#2d59d5', 'color': '#fff', 'min-width': 'fit-content', 'max-width': 'fit-content', 'min-height': 'fit-content', 'max-height': 'fit-content', } } else { return { 'background-color': '#2d59d5', 'color': '#fff', 'min-width': '22px', 'max-width': '22px', 'min-height': '22px', 'max-height': '22px', } } } else if ( type === 'week_header' || type === 'day_header' ){ return { 'font-weight': '575', // 'color': '#2d59d5', } } } else if ( !defaultDate ){ if ( type === 'month' ){ if ( day.length > 2){ return { 'padding': '2.5px', 'min-width': 'fit-content', 'max-width': 'fit-content', 'min-height': 'fit-content', 'max-height': 'fit-content', } } else { return { 'min-width': '22px', 'max-width': '22px', 'min-height': '22px', 'max-height': '22px', } } } } }, generateEventTitle( eventObject ){ if ( !eventObject.isAllDay && !eventObject.isMultipleDay ) { return eventObject.raw.nice_time !== undefined ? `${eventObject.raw.nice_time} ${eventObject.title}` : `${eventObject.title}`; } else if ( eventObject.isAllDay || eventObject.isMultipleDay ) { return eventObject.title || 'Not Provided'; } }, generateFontSize( type, eventObject ){ if (this.is_membership_cal){ if (type === 'timestamp') { return { 'font-size': '12px', 'font-weight': '300', } } else if (type === 'allDay'){ return { 'font-size': '13px', } } else if (type === 'eventTitle'){ if (eventObject?.id === 'placeholder'){ return { 'color': 'transparent', 'font-size': '13px', } } return { 'font-size': '13px', } } } else { if (type === 'timestamp') { return { 'font-size': '10px', 'font-weight': '300', } } else if (type === 'allDay'){ return { 'font-size': '11px', } }else if (type === 'eventTitle'){ if (eventObject?.id === 'placeholder'){ return { 'color': 'transparent', 'font-size': '12.5px', } } return { 'font-size': '12.5px', } } } }, generateEventStyleForMonth( eventObject, type, date = null, monthIndex = null, month_class = null ){ let styleObject = {}; const dotColor = eventObject.raw.dot_color; const bgColor = eventObject.bgColor || '#e8edf1'; const color = /* eventObject.color || */ this.get_contrast_color( bgColor ) || 'inherit'; if ( type === 'status_dot' ){ if ( eventObject.id === 'placeholder' ){ styleObject = { 'color': 'trasparent', } } else if ( !eventObject.isAllDay && !eventObject.isMultipleDay ) { if ( month_class === 'currentMonthDate' ){ styleObject = { 'min-width': '2.2px', 'max-width': '2.2px', 'min-height': '2.2px', 'max-height': '2.2px', 'border': `2.2px solid ${dotColor || '#333'}`, 'border-radius': '50%', 'background-color': `${dotColor || '#333'}`, 'margin': '0 5px 0 5px', } } else if ( month_class === 'prevMonthDate' || month_class === 'nextMonthDate' ){ styleObject = { 'min-width': '2.2px', 'max-width': '2.2px', 'min-height': '2.2px', 'max-height': '2.2px', 'border': `2.2px solid rgb(232, 237, 241)`, 'border-radius': '50%', 'background-color': `rgb(232, 237, 241)`, 'margin': '0 5px 0 5px', } } } else if ( eventObject.isAllDay || eventObject.isMultipleDay ){ styleObject = { 'margin-right': '3.5px', } } } else if ( type === 'event_body' ){ if ( eventObject.isAllDay && !eventObject.isMultipleDay ) { styleObject = { 'background-color': bgColor, 'color': color, }; } else if ( eventObject.isMultipleDay ){ let calendarDay = dayjs(date.$d).format('MMMM DD, YYYY'); let scheduleStart = dayjs( eventObject.start ).format('MMMM DD, YYYY'); let scheduleEnd = dayjs( eventObject.end ).format('MMMM DD, YYYY'); let isOneDay = ((dayjs( eventObject.start ).format('HH:mm:ss') === dayjs( eventObject.end ).format('HH:mm:ss')) && (dayjs( eventObject.start ).format('HH:mm:ss') === dayjs( eventObject.start ).startOf('day').format('HH:mm:ss'))) && dayjs( eventObject.start ).add(1, 'day').format('MMMM DD, YYYY') === scheduleEnd; let endsAtMidnight = dayjs( eventObject.end ).format('HH:mm:ss') === dayjs().startOf('day').format('HH:mm:ss'); if (calendarDay === scheduleStart || monthIndex % 7 === 0 ){ let remainingDays = dayjs(scheduleEnd, { format: 'MMMM DD, YYYY' }).diff(dayjs(calendarDay, { format: 'MMMM DD, YYYY' }), 'day'); //handle non-calendarOnly events since end time is different than calendarOnly events if (!eventObject.calendarOnly && !isOneDay && !endsAtMidnight ){ remainingDays += 1; } if (this.cellWidth === undefined){ styleObject = { 'background-color': bgColor, 'color': color, 'z-index': '5', 'width' : `${remainingDays*this.getCellWidth()}px` }; } else { styleObject = { 'background-color': bgColor, 'color': color, 'z-index': '5', 'width': `${remainingDays*this.cellWidth}px` }; } } } } return styleObject }, generateEventStyleForWeek( eventObject, type ){ const event = eventObject.event || eventObject; const numOfTimeBlocks = eventObject.numOfTimeBlocks; let styleObject = {}; const dotColor = event?.raw.dot_color; const bgColor = event?.bgColor || '#e8edf1'; const color = /* event?.color */ this.get_contrast_color( bgColor ) || 'inherit'; if ( type === 'event_body' ){ if ( event ) { styleObject = { 'background-color': dotColor || bgColor, 'color': color, 'width': '50%', 'border-width': '1px 1px 1px 1.5px', 'border-left-color': 'black', 'border-top-color':'white', 'border-right-color':'transparent', 'border-bottom-color': 'white', 'border-style': 'solid', 'border-radius': '2px', 'opacity': '85%', 'z-index': '5', 'height': `${(numOfTimeBlocks*12) - 5}px`, /* (number of 15 min increments * .placeholder-calendar-grid > grid-auto-rows) - .cell-event-week > padding(top) */ }; } } else if ( type === 'allDay_event_body' ){ if ( event ) { styleObject = { 'background-color': dotColor || bgColor, 'color': color, }; } } return styleObject }, toggleOverflowView( date, eventsArr, event ){ if (store.overlays.overflow_overlay){ store.overlays.overflow_overlay = false; } else { store.components.overflow_overlay.cellPosition = { top: (event.clientY + window.scrollY)-(230/2), //230 overlay height left: (event.clientX + window.scrollX)-(230/2), //230 overlay width } if (this.is_membership_cal){ store.components.overflow_overlay.is_membership_cal = true; } store.components.overflow_overlay.client_id = this.client_id; store.components.overflow_overlay.date = date; store.components.overflow_overlay.events = eventsArr; store.overlays.overflow_overlay = true; } }, handleScheduleClickForMonth( event ){ if (event.id !== 'placeholder') { this.$emit('clickSchedule_emitted', event); } else { this.$emit('createSchedule_emitted', this.date); } }, //methods to determine font color in contrast to bg color calculate_luminance(color) { const hex = color.substring(1); //remove # const rgb = parseInt(hex, 16); //convert to RGB const r = (rgb >> 16) & 0xff; //extract the red channel const g = (rgb >> 8) & 0xff; //extract the green channel const b = (rgb >> 0) & 0xff; //extract the blue channel //convert RGB to linear values const sR = r / 255; const sG = g / 255; const sB = b / 255; //calculate luminance const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; return luminance; }, get_contrast_color(background) { const backgroundLuminance = this.calculate_luminance(background); const fontColor = backgroundLuminance > 0.5 ? '#000000' : '#ffffff'; return fontColor; } }, }) </script> <script type="text/html" id="template-overflow-overlay"> <div v-if="global.overlays.overflow_overlay" id="overlay_overflow_overlay" :style="generateStyle()" class="calendar-overlay active" @click.self="global.overlays.overflow_overlay=false;"> <div class="calendarOverlayCard"> <div class="overlay-events-contain"> <div v-if="me.date" class="overflow-date-contain"> <span class="overflow-dateOfWeek">{{ me.date.format('ddd') }}</span> <span class="overflow-day">{{ me.date.$D }}</span> </div> </div> <template v-for="event in me.events"> <template v-if="event.id !== 'placeholder'"> <span class="cell-event-overlay" :style="generateEventStyleForMonth( event, 'event_body' )" @click="clickSchedule( event )" ><div :style="generateEventStyleForMonth( event, 'status_dot' )"></div><div class="event-title">{{ generateEventTitle( event ) }}</div></span> </template> </template> </div> <div class="xButton-calendarOverlay" @click.self="global.overlays.overflow_overlay=false;"></div> </div> </div> </script> <script> //global store store.components["overflow_overlay"] = { client_id: null, date: null, events: [], cellPosition: {}, loading: false, is_membership_cal: false, }; //define the component app_overlays.component("overflow-overlay", { template: "#template-overflow-overlay", data() { return { global: store, me: store.components.overflow_overlay } }, methods: { generateStyle(){ const cellPosition = this.me.cellPosition; const viewportHeight = window.innerHeight; const viewportWidth = window.innerWidth; const overlayHeight = 230; const overlayWidth = 230; let top = cellPosition.top; let left = cellPosition.left; // Adjust top position if overlay goes off-screen // if ( top + overlayHeight > viewportHeight) { // } // Adjust left position if overlay goes off-screen if ( left + overlayWidth > viewportWidth) { let difference = left + overlayWidth; left -= difference - viewportWidth; // 20px margin from right } return { 'top': `${top}px`, 'left': `${left}px` }; }, generateEventTitle( eventObject ){ if ( !eventObject.isAllDay && !eventObject.isMultipleDay ) { return eventObject.raw.nice_time !== undefined ? `${eventObject.raw.nice_time} ${eventObject.title}` : `${eventObject.title}`; } else if ( eventObject.isAllDay || eventObject.isMultipleDay ) { return eventObject.title || 'Not Provided'; } }, generateEventStyleForMonth( eventObject, type ){ let styleObject = {}; const dotColor = eventObject.raw.dot_color; const bgColor = eventObject.bgColor || '#e8edf1'; const color = /* eventObject.color */ this.get_contrast_color( bgColor ) || 'inherit'; if ( type === 'status_dot' ){ if ( !eventObject.isAllDay && !eventObject.isMultipleDay ) { styleObject = { 'min-width': '2.2px', 'max-width': '2.2px', 'min-height': '2.2px', 'max-height': '2.2px', 'border': `2.2px solid ${dotColor || '#333'}`, 'border-radius': '50%', 'background-color': `${dotColor || '#333'}`, 'margin': '0 5px 0 5px', } } else if ( eventObject.isAllDay || eventObject.isMultipleDay ){ styleObject = { 'margin-right': '3.5px', } } } else if ( type === 'event_body' ){ if ( eventObject.isAllDay || eventObject.isMultipleDay ) { styleObject = { 'background-color': bgColor, 'color': color, 'height': '22px', }; } } return styleObject }, clickSchedule(event){ if ( this.me.client_id === 'primary' ){ store.eventBus.$emit('clickSchedule_events_cal', event) store.overlays.overflow_overlay = false; } else if ( this.me.client_id === 'banquets' ){ store.eventBus.$emit('clickSchedule_banquets_cal', event) store.overlays.overflow_overlay = false; } else if ( this.me.client_id === 'bookings' ){ store.eventBus.$emit('clickSchedule_reservations_cal', event) store.overlays.overflow_overlay = false; } else if ( this.me.client_id === 'time_blocks' ){ store.eventBus.$emit('clickSchedule_time_blocks_cal', event) store.overlays.overflow_overlay = false; } else if (this.me.is_membership_cal ){ store.eventBus.$emit('clickSchedule_membership_cal', event) store.overlays.overflow_overlay = false; } }, //methods to determine font color in contrast to bg color calculate_luminance(color) { const hex = color.substring(1); //remove # const rgb = parseInt(hex, 16); //convert to RGB const r = (rgb >> 16) & 0xff; //extract the red channel const g = (rgb >> 8) & 0xff; //extract the green channel const b = (rgb >> 0) & 0xff; //extract the blue channel //convert RGB to linear values const sR = r / 255; const sG = g / 255; const sB = b / 255; //calculate luminance const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; return luminance; }, get_contrast_color(background) { const backgroundLuminance = this.calculate_luminance(background); const fontColor = backgroundLuminance > 0.5 ? '#000000' : '#ffffff'; return fontColor; } }, }); </script> <script type="text/html" id="template-custom-field-wb"> <div class="custom-field-wrapper tinyMarginTop" :id="custom_field.id"> <div class="flex-row-start" style="width: 100%;"> <!-- question --> <div class="flex-column-start tinyMarginRight" style="width: 100%;"> <div class="custom-field flex-row-start"> <p v-if="custom_field && custom_field.name" class="default-font-body">{{ custom_field.name }}</p> <span v-if="custom_field && custom_field.required" class="flex-row-start"> <p style="color: rgba(255, 0, 0, 0.885); margin: 0 5.5px 0 3px;">*</p> <p class="default-font-body" style="font-size: 14px; color: rgb(150, 152, 154); font-style: italic;">(required)</p> </span> </div> </div> </div> <!-- response options --> <div class="tinyMarginTop" style="width: 100%;"> <!-- short response --> <template v-if="custom_field && custom_field.type === 'text'"> <span class="flex-row-start tinyMarginBottom"> <input type="text" placeholder="Your answer" class="border-bottom-grey-2 default-font-body" style="width: 50%; line-height: 24px;" v-model="input_value" :disabled="is_admin" > </span> </template> <!-- paragraph --> <template v-if="custom_field && custom_field.type === 'textarea'"> <span class="flex-row-start tinyMarginBottom"> <input type="text" placeholder="Your answer" class="border-bottom-grey-2 default-font-body" style="width: 100%; line-height: 24px;" v-model="input_value" :disabled="is_admin" > </span> </template> <!-- multiple choice, checkbox --> <div v-if="custom_field.type === 'radio' || custom_field.type === 'checkbox'" class="flex-column-start"> <template v-for="(option, index) in custom_field_options" :id="index"> <span class="flex-row-start" style="position: relative; width: 100%; left: -5px;"> <!-- multiple choice --> <template v-if="custom_field.type === 'radio'"> <span v-if="!option.selected" @click="select_mc_option( true, option )" class="flex-row-start"> <i class="fa-light fa-circle options-icon"></i> <p v-if="option && option.name" class="default-font-body">{{ option.name }}</p> </span> <span v-if="option.selected" @click="select_mc_option( false, option )" class="flex-row-start"> <i class="fa-solid fa-circle-dot options-icon"></i> <p v-if="option && option.name" class="default-font-body">{{ option.name }}</p> </span> </template> <!-- checkbox --> <template v-if="custom_field.type === 'checkbox'"> <span v-if="!option.selected" @click="option.selected = true;" class="flex-row-start"> <i class="fa-light fa-square options-icon"></i> <p v-if="option && option.name" class="default-font-body">{{ option.name }}</p> </span> <span v-if="option.selected" @click="option.selected = false;" class="flex-row-start"> <i class="fa-solid fa-square-check options-icon"></i> <p v-if="option && option.name" class="default-font-body">{{ option.name }}</p> </span> </template> </span> </template> </div> <!-- dropdown --> <template v-if="custom_field.type === 'select' && custom_field_options"> <div id="response-dropdown" :style="{ borderBottomLeftRadius: dropdown_open ? '0' : '4px', borderBottomRightRadius: dropdown_open ? '0' : '4px' }"> <!-- dropdown closed --> <div id="selected-response" @click="dropdown_open = !dropdown_open;"> <p v-if="selected_dropdown_index < 0" class="response-option default-font-body">Choose</p> <p v-if="selected_dropdown_index >= 0 && custom_field_options && custom_field_options[selected_dropdown_index]" class="response-option default-font-body"> {{ custom_field_options[selected_dropdown_index].name }} </p> <i v-if="!dropdown_open" class="fa-solid fa-sort-down" style="color: rgb(95, 95, 95)"></i> <i v-if="dropdown_open" class="fa-solid fa-sort-up" style="color: rgba(95, 95, 95)"></i> </div> <!-- dropdown open --> <div v-if="dropdown_open" id="response-options"> <div class="response-option border-bottom-grey-2" @click="select_dropdown_option( null, -1 )" :style="[ return_selected_style( -1 ) ]" > <p class="default-font-body" style="color: rgb(150, 152, 154);">Choose</p> </div> <div v-for="(option, index) in custom_field_options" class="response-option" :key="option.value" @click="select_dropdown_option( option, index )" :style="[ return_selected_style( index ) ]" > <p class="default-font-body">{{ option.name }}</p> </div> </div> </div> </template> <!-- numeric value --> <template v-if="custom_field && custom_field.type === 'number'"> <span class="flex-row-start tinyMarginBottom"> <input type="text" placeholder="Number" class="border-bottom-grey-2 inherit-font-css" style="width: 20%; line-height: 24px;" v-model="input_value" @input="check_input_is_num()" :disabled="is_admin" > <span v-if="display_num_warning" class="flex-row-start" @mouseover="display_popup = true;" @mouseleave="display_popup = false;"> <i class="fa-solid fa-triangle-exclamation" style="color: rgba(255, 0, 0, 0.885); font-size: 1em; cursor: pointer;"></i> </span> <div v-if="display_popup" class="display-popup" style="font-size: 12px;">This value must be a number.</div> </span> </template> <!-- file upload --> <template v-if="custom_field && custom_field.type === 'file_upload'"> <div class="file-upload-wrapper flex-column-center" :style="{ 'color': 'var(--custom-field-grey-3)' }"> <label for="file-upload-input" style="display: flex; flex-direction: column; align-items: center; cursor: pointer;"> <i class="fa-light fa-folder-arrow-up" style="font-size: 1.5em; color: inherit;"></i> <span class="tinyMarginTop" :style="{ 'color': 'var(--custom-field-primary)', 'font-size': '15px' }">Upload File</span> </label> <input :id="file-upload-input + custom_field.id" type="file" style="opacity: 0; position: absolute; width: 30%; height: 90px; overflow: hidden;" @change="handle_file_upload($event)" :disabled="is_admin" > </div> <span v-if="input_value" class="flex-row-start tinyMarginTop" :style="{ 'color': 'var(--custom-field-grey-3)' }"> <i class="fa-regular fa-check" style="font-size: 1em; color: inherit; margin-right: 8px;"></i> <p class="default-font-body">{{ input_value ? input_value.name : '' }}</p> </span> </template> </div> </div> </script> <style scoped> :root { --custom-field-grey-1: rgba(180, 180, 180, 0.8); --custom-field-grey-2: rgba(195, 195, 195, 0.7); --custom-field-grey-3: rgb(154, 160, 166); --custom-field-primary: #202124; } .custom-field-wrapper{ width: 100%; border: 1px solid rgba(195, 195, 195, 0.7); border-radius: 8px; padding: 1.45em; background-color: #fff; box-sizing: border-box; } .custom-field-wrapper .options-icon{ color: var(--custom-field-grey-3); margin-right: 8px; padding: 7.5px; cursor: pointer; border-radius: 50%; font-size: 1.1em; } .custom-field-wrapper .options-icon:hover{ background-color: rgba(150, 150, 150, 0.1); } .custom-field-wrapper .default-font-body{ font-family: Roboto, Arial, Helvetica, sans-serif; font-size: 15px; font-weight: 400; text-align: left; } .custom-field-wrapper .file-upload-wrapper{ height: 90px; width: 50%; border: 1px dashed var(--custom-field-grey-1); } .custom-field-wrapper .inherit-font-css{ font-family: inherit; font-size: inherit; font-weight: inherit; } .custom-field-wrapper .flex-row-center{ display: flex; flex-direction: row; align-items: center; justify-content: center; } .custom-field-wrapper .flex-column-center{ display: flex; flex-direction: column; align-items: center; justify-content: center; } .custom-field-wrapper .flex-row-start{ display: flex; flex-direction: row; align-items: center; justify-content: flex-start; } .custom-field-wrapper .flex-column-start{ display: flex; flex-direction: column; align-items: flex-start; justify-content: center; } .custom-field-wrapper .custom-field{ color: rgb(32, 33, 36); box-sizing: border-box; font-weight: 400; font-size: 14px; width: 100%; line-height: 2.5; letter-spacing: 0; } .custom-field-wrapper .custom-field > .custom-field-input{ font-family: inherit; font-size: inherit; font-weight: inherit; width: inherit; line-height: inherit; background-color: rgb(248, 249, 250); padding: 5px 0 5px 15px; } .custom-field-wrapper .border-bottom-grey-2, .custom-field-wrapper .input-is-focused{ border-bottom: 1.25px solid var(--custom-field-grey-2); } .custom-field-wrapper input { transition: border-color 0.2s; } #response-dropdown{ position: relative; display: flex; flex-direction: column; align-items: flex-start; justify-content: center; font-size: 14px; color: var(--custom-field-primary); background-color: #fff; width: 33%; padding: 12px; border: 1px solid var(--custom-field-grey-1); border-top-left-radius: 4px; border-top-right-radius: 4px; } #response-dropdown > #selected-response{ display: flex; flex-direction: row; align-items: center; justify-content: space-between; width: 100%; cursor: pointer; } #response-options{ position: absolute; top: 100%; left: -1px; z-index: 10; background-color: #fff; border: 1px solid var(--custom-field-grey-1); border-bottom-left-radius: 4px; border-bottom-right-radius: 4px; width: 100%; } #response-options > .response-option{ display: flex; flex-direction: row; align-items: center; justify-content: flex-start; padding: 12px; font-family: Roboto; cursor: pointer; } #response-options > .response-option:hover{ background-color: rgba(150, 150, 150, 0.2); } .display-popup{ background-color: rgb(245, 245, 245); padding: 5px; border-radius: 6px; position: relative; left: -100px; bottom: 30px; z-index: 10; width: fit-content; } </style> <script> app_content.component("custom-field-component-wb", { template: "#template-custom-field-wb", data() { return { selected_dropdown_index: -1, //default dropdown_open: false, custom_field_name: '', custom_field_required: false, display_num_warning: false, display_popup: false, input_value: '', } }, props: { form: Object, is_admin: Boolean, custom_field: Object, last_update: Number, }, methods: { return_selected_style( index ){ if ( index === this.selected_dropdown_index ){ return { 'background-color': 'rgba(26, 115, 232, .09)', } } }, clear_dropdown_index(){ this.selected_dropdown_index = -1; }, select_dropdown_option( option, index ){ this.selected_dropdown_index = index; this.dropdown_open = false; if ( option ){ option.selected = true; this.unselect_other_options( option, false ); } else if ( !option ){ this.unselect_other_options( option, true ); } }, select_mc_option( select, option ){ if ( select ){ option.selected = true; this.unselect_other_options( option, false ); } else if ( !select ){ option.selected = false; } }, unselect_other_options( option, clear_all = false ){ if ( clear_all ){ this.custom_field_options.forEach( custom_field_option => { custom_field_option.selected = false; }) } else if ( !clear_all ){ this.custom_field_options.forEach( custom_field_option => { if ( custom_field_option.id !== option.id ){ custom_field_option.selected = false; } }) } }, check_input_is_num(){ let number = Number(this.input_value); if ( isNaN(number) || !this.input_value ){ this.display_num_warning = true; } else { this.input_value = number; this.display_num_warning = false; } }, handle_file_upload( event ){ let file = event.target.files[0]; if ( file ){ this.input_value = file; } else { displayMessage('There was an issue uploading your file. Please try again or contact support.', true, 6000); } }, }, computed: { custom_field_options(){ if ( this.custom_field && this.custom_field.custom_field_options ){ return this.custom_field.custom_field_options; } return []; }, }, watch: { input_value( newValue ){ this.$emit('field-updated', { id: this.custom_field.id, value: newValue }); } } }) </script> <script type="text/html" id="template-default-field-wb"> <div class="default-field-wrapper tinyMarginTop"> <span class="flex-row-start"> <p style="color: rgba(255, 0, 0, 0.885); margin: 0 5.5px 0 3px;">*</p> <p class="default-font-body" style="font-size: 14px; color: rgb(150, 152, 154); font-style: italic;">(required)</p> </span> <!-- default fields --> <div class="flex-row-start" style="width: 100%;"> <div class="flex-column-start" style="width: 100%;"> <template v-if="default_configuration === 'list'" v-for="(field, index) in default_fields" :id="index"> <div class="flex-row-start smallMarginTop" style="width: 100%;"> <input type="text" :placeholder="field.label" class="default-field-input border-bottom-grey-2 default-font-body" style="width: 100%;" v-model="field.input_value" :disabled="is_admin" > </div> </template> <template v-if="default_configuration === 'grid'" v-for="(row, i) in default_fields" :id="i"> <div class="flex-row-start smallMarginTop" style="width: 100%;"> <template v-for="(field, j) in row" :id="j"> <input type="text" :placeholder="field.label" class="default-field-input border-bottom-grey-2 default-font-body marginRight" style="width: 100%;" v-model="field.input_value" :disabled="is_admin" > </template> </div> </template> </div> </div> </div> </script> <style scoped> :root { --default-field-grey-1: rgba(180, 180, 180, 0.8); --default-field-grey-2: rgba(195, 195, 195, 0.7); --default-field-grey-3: rgb(154, 160, 166); --default-field-primary: #202124; } .default-field-wrapper{ width: 100%; border: 1px solid rgba(195, 195, 195, 0.7); border-radius: 8px; padding: 1.45em; background-color: #fff; box-sizing: border-box; } .default-field-wrapper .default-font-body{ font-family: Roboto, Arial, Helvetica, sans-serif; font-size: 15px; font-weight: 400; } .default-field-wrapper .configuration-icon{ font-size: 1em; cursor: pointer; padding: 5px; border-radius: 50%; color: rgba(95, 95, 95); background-color: #fff; transition: color 0.2s, background-color 0.2s; } .default-field-wrapper .configuration-icon:hover{ color: var(--default-field-primary); background-color: rgba(180, 180, 180, 0.15); } .default-field-wrapper .file-upload-wrapper{ height: 90px; width: 50%; border: 1px dashed var(--default-field-grey-1); } .default-field-wrapper .inherit-font-css{ font-family: inherit; font-size: inherit; font-weight: inherit; } .default-field-wrapper .flex-row-center{ display: flex; flex-direction: row; align-items: center; justify-content: center; } .default-field-wrapper .flex-column-center{ display: flex; flex-direction: column; align-items: center; justify-content: center; } .default-field-wrapper .flex-row-start{ display: flex; flex-direction: row; align-items: center; justify-content: flex-start; } .default-field-wrapper .flex-row-end{ display: flex; flex-direction: row; align-items: center; justify-content: flex-end; } .default-field-wrapper .flex-column-start{ display: flex; flex-direction: column; align-items: flex-start; justify-content: center; } .default-field-wrapper .default-field{ color: rgb(32, 33, 36); box-sizing: border-box; font-weight: 400; font-size: 14px; width: 100%; line-height: 2.5; letter-spacing: 0; } .default-field-wrapper .default-field > .default-field-input{ font-family: inherit; font-size: inherit; font-weight: inherit; width: inherit; line-height: inherit; background-color: rgb(248, 249, 250); padding: 5px 0 5px 15px; } .default-field-wrapper .border-bottom-grey-2, .default-field-wrapper .input-is-focused{ border-bottom: 1.25px solid var(--default-field-grey-2); } .default-field-wrapper input { transition: border-color 0.2s; } </style> <script> app_content.component("default-field-component-wb", { template: "#template-default-field-wb", data() { return { } }, props: { form: Object, //json is_admin: Boolean, default_fields: Array, rerender_key: String, }, mounted() { }, methods: { }, computed: { default_configuration(){ if ( this.form && this.form.default_configuration ){ return this.form.default_configuration; } return 'list'; }, } }) </script> <!-- native tab bar --> <script type="text/html" id="template-native-tab-bar"> <!-- first, make sure we're in the react native member app --> <template v-if="is_react_native_member_app"> <div v-if="show_tab_bar" class="native-tab-bar-outer flexNoWrap" > <!-- tab bar (only for the member portal) --> <template v-if="visible_tabs"> <div v-for="tab in visible_tabs" :key="tab.key" class="native-tab-bar-item" :class="{ active : tab.key == primary_view }" @touchstart.prevent="tapped_tab_bar_item( tab )" @click="tapped_tab_bar_item( tab )" > <div class="centeredText centeredBlock"> <p><i class="native-tab-bar-icon" :class="tab.icon_class"></i></p> <p class="native-tab-bar-label">{{ tab.label }}</p> </div> </div> <div class="native-tab-bar-item" @touchstart.prevent="tapped_more()" @click="tapped_more()" > <div class="centeredText centeredBlock"> <p><i class="fa-regular fa-grid native-tab-bar-icon"></i></p> <p class="native-tab-bar-label">More</p> </div> </div> </template> </div> <!-- back button (global) --> <div v-else-if="! loading && show_back_button"> <!-- overlay --> <div class="native-tab-bar-back-button-overlay pointer" @touchstart.self.prevent="back_button_expanded = false" @click.self="back_button_expanded = false" :class="{ 'active': back_button_expanded }" ></div> <!-- home button --> <div class="native-tab-bar-floating-home-button boxShadowBottom moreBlur pointer easeFast" @click="tapped_home()" :class="{ 'active': back_button_expanded }" > <div class="centeredText centeredBlock"> <p><i class="fa-regular fa-house native-tab-bar-icon" style="font-size:20px;"></i><span v-if="back_button_expanded" style="padding-left:6px;font-size:18px;">Home</span></p> </div> </div> <!-- back button --> <div class="native-tab-bar-floating-back-button boxShadowBottom moreBlur pointer easeFast" @click="tapped_back()" :class="{ 'expanded': back_button_expanded }" > <div class="centeredText centeredBlock"> <p><i class="fa-regular fa-chevron-left native-tab-bar-icon" style="font-size:20px;"></i><span v-if="back_button_expanded" style="padding-left:6px;font-size:18px;">Back</span></p> </div> </div> <!-- <div class="native-tab-bar-item" @click="tapped_home()" style="max-width:60px;" > <div class="centeredText centeredBlock"> <p><i class="fa-regular fa-house native-tab-bar-icon"></i></p> <p class="native-tab-bar-label">Home</p> </div> </div> --> </div> <!-- full menu sheet --> <div v-if="tabs" class="native-tab-bar-menu-sheet" :class="{'active': show_full_menu}" > <!-- overlay --> <div class="native-tab-bar-menu-sheet-overlay pointer" @touchstart.self.prevent="show_full_menu = false" @click.self="show_full_menu = false" ></div> <!-- actual menu sheet --> <div class="native-tab-bar-menu-sheet-inner scrollable"> <div class="flexWrap" style="padding-top: 10px" > <!-- loop through tabs--> <div v-for="tab in tabs" class="quick-action-wrap noMaxWidth relative" @click="selected_full_menu_option( tab )" > <div class="quick-action-icon-wrap overflowHidden" > <div> <i v-if="tab.icon_class" :class="tab.icon_class"></i> </div> </div> <p class="quick-action-title">{{ tab.label }}</p> </div> </div> </div> </div> </template> </script> <style> .native-tab-bar-outer{ position: fixed; bottom: 0px; left: 0px; right: 0px; background-color: rgba(249, 251, 251, 1); border-top: 1px solid #e4ebf1; z-index: 1000; transition: 0.15s cubic-bezier(0.25, 0.1, 0.25, 1); } .native-tab-bar-item{ padding: 11px; display: flex; flex: 1 1 0; /* flex-grow: 1; flex-shrink: 1; flex-basis: 0; */ color: #697684; cursor: pointer; } .native-tab-bar-item.active{ color: #4a56f8; } .native-tab-bar-label{ font-size: 12px; padding-top: 4px; white-space: nowrap; } .native-tab-bar-icon{ font-size: 22px; } /* ------ menu sheet (full menu) ------- */ .native-tab-bar-menu-sheet-overlay{ position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(8,8,19,0.64); opacity: 0; visibility: hidden; transition: opacity 0.15s ease; z-index: 1000; } .native-tab-bar-menu-sheet.active .native-tab-bar-menu-sheet-overlay{ opacity: 1; visibility: visible; } .native-tab-bar-menu-sheet.active .native-tab-bar-menu-sheet-inner{ bottom: 0px; } .native-tab-bar-menu-sheet-inner .quick-action-wrap{ padding-left: 10px; padding-right:10px; } .native-tab-bar-menu-sheet-inner{ position: fixed; bottom: -100%; max-height: 90%; left: 0px; right: 0px; z-index: 1001; background-color: rgba(249, 251, 251, 1); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); transition: bottom 0.15s cubic-bezier(0.25, 0.1, 0.25, 1); border-top-left-radius: 10px; border-top-right-radius: 10px; padding-bottom: 10px; padding-top: 10px; } /*------ back button -------*/ .native-tab-bar-back-button-overlay{ position: fixed; top: 0; left: 0; width: 100%; height: 100%; /* background-color: rgba(111,117,122,0.5); */ background-color: rgba(249, 251, 251, 0.8); opacity: 0; visibility: hidden; transition: opacity 0.15s ease; z-index: 999; } .native-tab-bar-back-button-overlay.active{ opacity: 1; visibility: visible; } .native-tab-bar-floating-back-button{ position: fixed; bottom: 15px; left: 22px; right: auto; background-color: rgba(249, 251, 251, 1); /* border-top: 1px solid #e4ebf1; */ z-index: 1000; transition: 0.15s cubic-bezier(0.25, 0.1, 0.25, 1); border-radius: 100%; width: 50px; height: 50px; background-color: #fff; line-height: 50px; font-size: 16px; white-space: nowrap; min-width: 50px; } .native-tab-bar-floating-back-button.expanded{ border-radius: 25px; padding-left: 16px; padding-right: 16px; width: auto; } /*------ home button -------*/ .native-tab-bar-floating-home-button.active{ border-radius: 25px; padding-left: 16px; padding-right: 16px; width: auto; visibility: visible; opacity: 1; animation: expand 0.15s cubic-bezier(0.25, 0.1, 0.25, 1) forwards; background-color: #4a56f8; color: #fff; } .native-tab-bar-floating-home-button{ position: fixed; bottom: 75px; left: 22px; right: auto; visibility: hidden; opacity: 0; background-color: rgba(249, 251, 251, 1); /* border-top: 1px solid #e4ebf1; */ z-index: 1000; transition: 0.15s cubic-bezier(0.25, 0.1, 0.25, 1); border-radius: 20px; width: auto; height: 50px; background-color: #fff; line-height: 50px; font-size: 16px; white-space: nowrap; /* Initial state for animation */ transform: scale(0); animation: none; } /* Keyframes for the expansion effect */ @keyframes expand { from { transform: scale(0); opacity: 0; } to { transform: scale(1); opacity: 1; } } </style> <script> app_content.component("native-tab-bar-component", { template: "#template-native-tab-bar", data() { return { loading: true, last_scroll_top: 0, slide_down: false, ticking: false, show_full_menu: false, back_button_expanded: false } }, props: { primary_view: String, tabs: Array, }, mounted(){ if( this.is_react_native_member_app ){ this.last_scroll_top = this.return_scroll_top(); window.addEventListener('scroll', this.handle_scroll); setTimeout(() => { this.loading = false; }, 100); } }, unmounted(){ if( this.is_react_native_member_app ){ window.removeEventListener('scroll', this.handle_scroll); } }, methods: { tapped_home(){ window.location.href = "/account/home"; }, tapped_more(){ this.show_full_menu = true; }, selected_full_menu_option( option ){ this.show_full_menu = false; this.tapped_tab_bar_item( option ); }, return_scroll_top(){ let scrollTop = window.pageYOffset || document.documentElement.scrollTop; return scrollTop <= 0 ? 0 : scrollTop; // For Mobile or negative scrolling }, handle_scroll(){ if ( ! this.ticking ) { window.requestAnimationFrame(() => { let scrollTop = this.return_scroll_top(); let windowHeight = window.innerHeight; let documentHeight = document.documentElement.scrollHeight; // Prevent action if bouncing past the bottom of the page if (scrollTop + windowHeight >= documentHeight) { // We're at or past the bottom, so we ignore this scroll event this.ticking = false; return; } if (scrollTop > this.last_scroll_top) { //hide tab bar this.slide_down = true; } else if (scrollTop < this.last_scroll_top && scrollTop >= 0) { //show tab bar this.slide_down = false; } this.last_scroll_top = scrollTop; this.ticking = false; }); this.ticking = true; } }, tapped_tab_bar_item( tab ){ updateURL( tab.key ); displayViewForUrlPath(); }, tapped_back(){ if( this.back_button_expanded ){ this.back_button_expanded = false; window.history.back(); } else { this.back_button_expanded = true; } }, }, computed: { //we only show it when we're on one of the relevant primary views (contained in the tab bar) //.. otherwise we should display the back button bar tab_bar_keys(){ var keys = []; if( this.tabs ){ this.tabs.forEach(tab => { keys.push( tab.key ); }); } return keys; }, show_tab_bar(){ for (let t = 0; t < this.tab_bar_keys.length; t++) { if( this.tab_bar_keys[t] == this.primary_view ){ return true } } return false; }, visible_tabs(){ if( this.tabs ){ return this.tabs.slice(0,4) } return []; }, show_back_button(){ if( ! this.show_tab_bar && window.history.length >= 1 ){ return true; } return false; }, is_react_native_member_app(){ return store.is_react_native_member_app; } }, }) </script> <script> //after we've included all the relevant views, mount the vue apps app_content.mount('#v-content'); app_overlays.mount('#v-overlays'); </script> </html>