diff --git a/network-api/networkapi/mozfest/factory.py b/network-api/networkapi/mozfest/factory.py index 86332a3f0af..790000222de 100644 --- a/network-api/networkapi/mozfest/factory.py +++ b/network-api/networkapi/mozfest/factory.py @@ -53,6 +53,9 @@ class Meta: banner_video_url = Faker('url') banner_heading_text = Faker('sentence', nb_words=6, variable_nb_words=True) + banner_carousel = Faker('streamfield', fields=['banner_carousel']) + banner_video = Faker('streamfield', fields=['banner_video']) + signup = SubFactory(SignupFactory) diff --git a/network-api/networkapi/mozfest/migrations/0022_mozfesthomepage_banner_carousel_banner_video.py b/network-api/networkapi/mozfest/migrations/0022_mozfesthomepage_banner_carousel_banner_video.py new file mode 100644 index 00000000000..74c571ec1ce --- /dev/null +++ b/network-api/networkapi/mozfest/migrations/0022_mozfesthomepage_banner_carousel_banner_video.py @@ -0,0 +1,27 @@ +# Generated by Django 3.1.11 on 2021-10-15 16:07 + +from django.db import migrations +import networkapi.wagtailpages.pagemodels.customblocks.video_block +import wagtail.core.blocks +import wagtail.core.fields +import wagtail.images.blocks + + +class Migration(migrations.Migration): + + dependencies = [ + ('mozfest', '0021_add_spaces_cards'), + ] + + operations = [ + migrations.AddField( + model_name='mozfesthomepage', + name='banner_carousel', + field=wagtail.core.fields.StreamField([('slide', wagtail.core.blocks.StructBlock([('image', wagtail.images.blocks.ImageChooserBlock()), ('heading', wagtail.core.blocks.CharBlock(required=False)), ('description', wagtail.core.blocks.CharBlock(required=False))]))], blank=True, help_text='The slides shown on the new Hero. Please ensure that there are exactly 3 slides. The old Hero will be shown if there are no slides present.', null=True), + ), + migrations.AddField( + model_name='mozfesthomepage', + name='banner_video', + field=wagtail.core.fields.StreamField([('CMS_video', networkapi.wagtailpages.pagemodels.customblocks.video_block.WagtailVideoChooserBlock()), ('external_video', wagtail.core.blocks.StructBlock([('video_url', wagtail.core.blocks.URLBlock(help_text='For YouTube: go to your YouTube video and click “Share,” then “Embed,” and then copy and paste the provided URL only. For example: https://www.youtube.com/embed/3FIVXBawyQw For Vimeo: follow similar steps to grab the embed URL. For example: https://player.vimeo.com/video/9004979')), ('thumbnail', wagtail.images.blocks.ImageChooserBlock(help_text='The image to show before the video is played.'))]))], blank=True, help_text='The video to play when users click "Watch Video". This is only shown on the new Hero.', null=True), + ), + ] diff --git a/network-api/networkapi/mozfest/models.py b/network-api/networkapi/mozfest/models.py index e91dcd4ad83..29dece1dd2b 100644 --- a/network-api/networkapi/mozfest/models.py +++ b/network-api/networkapi/mozfest/models.py @@ -6,7 +6,6 @@ from wagtail.snippets.edit_handlers import SnippetChooserPanel from wagtail_localize.fields import SynchronizedField, TranslatableField - from networkapi.wagtailpages.utils import ( set_main_site_nav_information, get_page_tree_information @@ -126,10 +125,15 @@ class MozfestHomepage(MozfestPrimaryPage): MozFest Homepage 'banner_video_type' determines what version of banner design the page should load - """ - # this tells the templates to load a hardcoded, pre-defined video in the banner background - banner_video_type = "hardcoded" + If the value of `banner_video_type` is `hardcoded`, it displays a hardcoded, + predefined video in the banner background. + + If the value of `banner_video_type` is `featured`, it displays a carousel of + cards with their associated headings and body content (`banner_carousel`), + and an embedded user-defined video (`banner_video`). + """ + banner_video_type = "featured" cta_button_label = models.CharField( max_length=250, @@ -156,12 +160,39 @@ class MozfestHomepage(MozfestPrimaryPage): help_text='A banner paragraph specific to the homepage' ) + # For banner_video_type == 'hardcoded' banner_video_url = models.URLField( max_length=2048, blank=True, help_text='The video to play when users click "watch video"' ) + # For banner_video_type == 'featured' + banner_carousel = StreamField( + [ + ('slide', customblocks.BannerCarouselSlideBlock()), + ], + max_num=3, + help_text='The slides shown on the new Hero. Please ensure that there ' + 'are exactly 3 slides. The old Hero will be shown if there ' + 'are no slides present.', + blank=True, + null=True, + ) + + # For banner_video_type == 'featured' + banner_video = StreamField( + [ + ('CMS_video', customblocks.WagtailVideoChooserBlock()), + ('external_video', customblocks.ExternalVideoBlock()), + ], + max_num=1, + help_text='The video to play when users click "Watch Video". This is ' + 'only shown on the new Hero.', + blank=True, + null=True, + ) + subpage_types = [ 'MozfestPrimaryPage', 'MozfestHomepage', @@ -176,16 +207,29 @@ class MozfestHomepage(MozfestPrimaryPage): FieldPanel('cta_button_label'), FieldPanel('cta_button_destination'), FieldPanel('banner_heading'), + StreamFieldPanel('banner_carousel'), FieldPanel('banner_guide_text'), FieldPanel('banner_video_url'), + StreamFieldPanel('banner_video'), ] + parent_panels[n:] if banner_video_type == "hardcoded": # Hide all the panels that aren't relevant for the video banner version of the MozFest Homepage content_panels = [ field for field in all_panels - if field.field_name not in - ['banner', 'header', 'intro', 'banner_guide_text', 'banner_video_url'] + if field.field_name not in [ + 'banner', 'header', 'intro', 'banner_carousel', 'banner_guide_text', + 'banner_video', 'banner_video_url', + ] + ] + elif banner_video_type == "featured": + # Hide all the panels that aren't relevant for the video banner version of the MozFest Homepage + content_panels = [ + field for field in all_panels + if field.field_name not in [ + 'banner', 'banner_guide_text', 'banner_video_url', 'cta_button_destination', + 'cta_button_label', 'header', 'hero_image', 'intro', + ] ] else: content_panels = all_panels diff --git a/network-api/networkapi/mozfest/templates/fragments/carousel_navigation.html b/network-api/networkapi/mozfest/templates/fragments/carousel_navigation.html new file mode 100644 index 00000000000..7c46877c03e --- /dev/null +++ b/network-api/networkapi/mozfest/templates/fragments/carousel_navigation.html @@ -0,0 +1,25 @@ +
+

{{ title }}

+
+
+ + + + +
+
+ + + + +
+
+
diff --git a/network-api/networkapi/mozfest/templates/fragments/hero/carousel_hero.html b/network-api/networkapi/mozfest/templates/fragments/hero/carousel_hero.html new file mode 100644 index 00000000000..06a682bdcd1 --- /dev/null +++ b/network-api/networkapi/mozfest/templates/fragments/hero/carousel_hero.html @@ -0,0 +1,69 @@ +{% load i18n wagtailimages_tags %} +
+
+ + {# Background images slider #} +
+
+
+
+ + {# Background Images slider #} +
+ +
+ + {# Text slider #} +
+
+

{{ page.banner_heading }}

+ {% if page.banner_video %} + + {% endif %} +
+ +
+ + {# pagination progress bars on desktop #} +
+ + {# Text #} +
+ {% for slide_block in page.specific.banner_carousel %} + {% with slide=slide_block.value %} +
+ {{ slide.heading }} +

{{ slide.description }}

+
+ {% endwith %} + {% endfor %} +
+
+ + {# mobile slider #} + {% include 'fragments/hero/hero_mobile_slider.html' with items=page.hero_slides %} +
+ + {# Video block #} + {% if page.banner_video %} + {% include 'fragments/hero/featured_video.html' %} + {% endif %} + +
+
+ +{# Spacing for top of content section when there is no video #} +{% if not page.banner_video %} +
+{% endif %} + + diff --git a/network-api/networkapi/mozfest/templates/fragments/hero/featured_video.html b/network-api/networkapi/mozfest/templates/fragments/hero/featured_video.html new file mode 100644 index 00000000000..5c9c0a528ee --- /dev/null +++ b/network-api/networkapi/mozfest/templates/fragments/hero/featured_video.html @@ -0,0 +1,52 @@ +{% load i18n static wagtailcore_tags wagtailimages_tags %} +
+ + {# Overlay spacer #} +
+ + {# Video Container #} +
+ {% for block in page.banner_video %} +
+ + {# Thumbnail and overlay #} + + + {# Video #} + {% if block.block_type == 'external_video' %} + + {% else %} + {{ block }} + {% endif %} +
+ {% endfor %} +
+
diff --git a/network-api/networkapi/mozfest/templates/fragments/hero/hardcoded_video_hero.html b/network-api/networkapi/mozfest/templates/fragments/hero/hardcoded_video_hero.html new file mode 100644 index 00000000000..3ddf892f1eb --- /dev/null +++ b/network-api/networkapi/mozfest/templates/fragments/hero/hardcoded_video_hero.html @@ -0,0 +1,43 @@ +{% load i18n wagtailcore_tags %} + + +{# Bottom overlaping spacing bar #} +
+
+
+
+
+
diff --git a/network-api/networkapi/mozfest/templates/fragments/hero/hero_mobile_slider.html b/network-api/networkapi/mozfest/templates/fragments/hero/hero_mobile_slider.html new file mode 100644 index 00000000000..b96203bb1af --- /dev/null +++ b/network-api/networkapi/mozfest/templates/fragments/hero/hero_mobile_slider.html @@ -0,0 +1,16 @@ +
+ +
+ {% for slide_block in page.banner_carousel %} + {% with slide=slide_block.value %} +
+
+ {{ slide.heading }} +

{{ slide.description }}

+
+ {% endwith %} + {% endfor %} +
+
+ + diff --git a/network-api/networkapi/mozfest/templates/fragments/hero/large.html b/network-api/networkapi/mozfest/templates/fragments/hero/large.html deleted file mode 100644 index c31b6f12a20..00000000000 --- a/network-api/networkapi/mozfest/templates/fragments/hero/large.html +++ /dev/null @@ -1,50 +0,0 @@ -{% load wagtailcore_tags wagtailimages_tags i18n static %} - -
-
- {% if banner_video_type == "hardcoded" %} - - -
-
-
-
- - -
-
-
- -
- {% else %} - {% with banner=page.specific.get_banner %} - {% if banner %} - - {% image banner fill-4960x3000 as image_xl %} - {% image banner fill-2480x1500 as image_lg %} - {% image banner fill-1984x1200 as image_md %} - {% image banner fill-1536x929 as image_sm %} - - - - - {# Fallback Image #} - {% image banner fill-1536x929 alt="" %} - - {% else %} - - - - {% endif %} - {% endwith %} - {% endif %} -
diff --git a/network-api/networkapi/mozfest/templates/partials/homepage_banner.html b/network-api/networkapi/mozfest/templates/partials/homepage_banner.html deleted file mode 100644 index b1f148063dd..00000000000 --- a/network-api/networkapi/mozfest/templates/partials/homepage_banner.html +++ /dev/null @@ -1,15 +0,0 @@ -{% load i18n %} - -
-
-
-

{{ page.banner_heading }}

- {% if banner_video_type != "hardcoded" %} -

{{ page.banner_guide_text }}

- {% if page.banner_video_url %} - {% trans "Watch Video" %} - {% endif %} - {% endif %} -
-
-
diff --git a/network-api/networkapi/mozfest/templates/partials/intro_section.html b/network-api/networkapi/mozfest/templates/partials/intro_section.html new file mode 100644 index 00000000000..594820fca5a --- /dev/null +++ b/network-api/networkapi/mozfest/templates/partials/intro_section.html @@ -0,0 +1,12 @@ +{% load wagtailcore_tags %} + +
+
+
+

{% if root.title %}{{ root.title }}{% elif page.header %}{{ page.header }}{% else %}{{ page.title }}{% endif %}

+ {% if page.intro %} +
{{ page.intro | richtext }}
+ {% endif %} +
+
+
diff --git a/network-api/networkapi/mozfest/templates/partials/primary_heroguts.html b/network-api/networkapi/mozfest/templates/partials/primary_heroguts.html index f08bd471003..7cf2d14b593 100644 --- a/network-api/networkapi/mozfest/templates/partials/primary_heroguts.html +++ b/network-api/networkapi/mozfest/templates/partials/primary_heroguts.html @@ -1,42 +1,23 @@ {% load wagtailcore_tags wagtailimages_tags %}
- - - {% if homepage %} -
-
-
- {% if banner_video_type != "hardcoded" %} -

{% if page.header %}{{ page.header }}{% else %}{{ page.title }}{% endif %}

- {% if page.intro %} -
{{ page.intro | richtext }}
- {% endif %} - {% endif %} -
-
-
{% else %} -
-
-
-

{% if root.title %}{{ root.title }}{% elif page.header %}{{ page.header }}{% else %}{{ page.title }}{% endif %}

- {% if page.intro %} -
{{ page.intro | richtext }}
- {% endif %} -
-
+ + {% include "partials/intro_section.html" %} {% endif %} {% if singleton_page == True %} {% include "partials/intro_and_content_divider.html" with wrapper_class="d-md-none" %} {% endif %}
+ diff --git a/network-api/networkapi/utility/faker/streamfield_provider.py b/network-api/networkapi/utility/faker/streamfield_provider.py index eddf4d2edda..0d01eea576a 100644 --- a/network-api/networkapi/utility/faker/streamfield_provider.py +++ b/network-api/networkapi/utility/faker/streamfield_provider.py @@ -326,6 +326,21 @@ def generate_dear_internet_letter_field(): return generate_field('letter', attributes) +def generate_banner_carousel_field(): + return generate_field('slide', { + 'image': choice(Image.objects.all()).id, + 'heading': fake.sentence(nb_words=4, variable_nb_words=True), + 'description': fake.paragraph(nb_sentences=3, variable_nb_sentences=True), + }) + + +def generate_banner_video_field(): + return generate_field('external_video', { + 'video_url': 'https://www.youtube.com/embed/3FIVXBawyQw', + 'thumbnail': choice(Image.objects.all()).id, + }) + + class StreamfieldProvider(BaseProvider): """ A custom Faker Provider for relative image urls, for use with factory_boy @@ -368,7 +383,9 @@ def streamfield(self, fields=None): 'recent_blog_entries': generate_recent_blog_entries_field, 'blog_set': generate_blog_set_field, 'airtable': generate_airtable_field, - 'typeform': generate_typeform_field + 'typeform': generate_typeform_field, + 'banner_carousel': generate_banner_carousel_field, + 'banner_video': generate_banner_video_field, } streamfield_data = [] diff --git a/network-api/networkapi/wagtailpages/pagemodels/customblocks/__init__.py b/network-api/networkapi/wagtailpages/pagemodels/customblocks/__init__.py index eefdbdf9d6c..a6ded0a97d9 100644 --- a/network-api/networkapi/wagtailpages/pagemodels/customblocks/__init__.py +++ b/network-api/networkapi/wagtailpages/pagemodels/customblocks/__init__.py @@ -5,6 +5,7 @@ from .blog_set_block import BlogSetBlock from .bootstrap_spacer_block import BootstrapSpacerBlock from .card_grid import CardGrid, CardGridBlock +from .banner_carousel import BannerCarouselSlideBlock from .iframe_block import iFrameBlock from .image_block import ImageBlock from .image_grid import ImageGrid, ImageGridBlock @@ -21,7 +22,7 @@ from .quote_block import QuoteBlock from .single_quote_block import SingleQuoteBlock from .space_card_list_block import SpaceCardListBlock -from .video_block import VideoBlock +from .video_block import ExternalVideoBlock, VideoBlock, WagtailVideoChooserBlock from .youtube_regret_block import YoutubeRegretBlock from .articles import ArticleRichText, ArticleDoubleImageBlock, ArticleFullWidthImageBlock, ArticleImageBlock from .dear_internet_letter_block import DearInternetLetterBlock @@ -36,11 +37,13 @@ ArticleFullWidthImageBlock, ArticleRichText, AudioBlock, + BannerCarouselSlideBlock, BlogSetBlock, BootstrapSpacerBlock, CardGrid, CardGridBlock, DearInternetLetterBlock, + ExternalVideoBlock, iFrameBlock, ImageBlock, ImageGrid, @@ -59,5 +62,6 @@ RecentBlogEntries, TypeformBlock, VideoBlock, + WagtailVideoChooserBlock, YoutubeRegretBlock, ] diff --git a/network-api/networkapi/wagtailpages/pagemodels/customblocks/banner_carousel.py b/network-api/networkapi/wagtailpages/pagemodels/customblocks/banner_carousel.py new file mode 100644 index 00000000000..97bb1f74957 --- /dev/null +++ b/network-api/networkapi/wagtailpages/pagemodels/customblocks/banner_carousel.py @@ -0,0 +1,8 @@ +from wagtail.core import blocks +from wagtail.images.blocks import ImageChooserBlock + + +class BannerCarouselSlideBlock(blocks.StructBlock): + image = ImageChooserBlock() + heading = blocks.CharBlock(required=False) + description = blocks.CharBlock(required=False) diff --git a/network-api/networkapi/wagtailpages/pagemodels/customblocks/video_block.py b/network-api/networkapi/wagtailpages/pagemodels/customblocks/video_block.py index b9efc2d339d..6f23d7fb892 100644 --- a/network-api/networkapi/wagtailpages/pagemodels/customblocks/video_block.py +++ b/network-api/networkapi/wagtailpages/pagemodels/customblocks/video_block.py @@ -1,5 +1,8 @@ from django import forms from wagtail.core import blocks +from wagtail.images.blocks import ImageChooserBlock + +from wagtailmedia import blocks as wagtailmedia_blocks class RadioSelectBlock(blocks.ChoiceBlock): @@ -10,6 +13,22 @@ def __init__(self, *args, **kwargs): ) +class ExternalVideoBlock(blocks.StructBlock): + video_url = blocks.URLBlock( + help_text='For YouTube: go to your YouTube video and click “Share,” ' + 'then “Embed,” and then copy and paste the provided URL only. ' + 'For example: https://www.youtube.com/embed/3FIVXBawyQw ' + 'For Vimeo: follow similar steps to grab the embed URL. ' + 'For example: https://player.vimeo.com/video/9004979' + ) + thumbnail = ImageChooserBlock( + help_text='The image to show before the video is played.' + ) + + class Meta: + icon = 'media' + + class VideoBlock(blocks.StructBlock): url = blocks.CharBlock( help_text='For YouTube: go to your YouTube video and click “Share,” ' @@ -37,3 +56,9 @@ class VideoBlock(blocks.StructBlock): class Meta: template = 'wagtailpages/blocks/video_block.html' + + +class WagtailVideoChooserBlock(wagtailmedia_blocks.VideoChooserBlock): + class Meta: + icon = 'media' + template = 'wagtailpages/blocks/wagtail_video_block.html' diff --git a/network-api/networkapi/wagtailpages/templates/wagtailpages/blocks/wagtail_video_block.html b/network-api/networkapi/wagtailpages/templates/wagtailpages/blocks/wagtail_video_block.html new file mode 100644 index 00000000000..5090271cf44 --- /dev/null +++ b/network-api/networkapi/wagtailpages/templates/wagtailpages/blocks/wagtail_video_block.html @@ -0,0 +1,22 @@ +{% extends "./base_streamfield_block.html" %} +{% load wagtailcore_tags %} + +{% block block_row_classes %} +no-gutters +{% endblock %} + +{% block main_block_class %} +streamfield-content +{% endblock %} + +{% block block_content %} + +{% endblock %} diff --git a/package-lock.json b/package-lock.json index 4b96f070c39..2fbd1b086d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21830,9 +21830,9 @@ } }, "swiper": { - "version": "6.7.5", - "resolved": "https://registry.npmjs.org/swiper/-/swiper-6.7.5.tgz", - "integrity": "sha512-KaTjO93tZyMpxWHaey+T+H/JeePMZV/joZWhZaor76Xk+rPGmjOz1S8mXSyrRkaW0p0LOJYeWGB8d0gYxSSV/Q==", + "version": "6.8.4", + "resolved": "https://registry.npmjs.org/swiper/-/swiper-6.8.4.tgz", + "integrity": "sha512-O+buF9Q+sMA0H7luMS8R59hCaJKlpo8PXhQ6ZYu6Rn2v9OsFd4d1jmrv14QvxtQpKAvL/ZiovEeANI/uDGet7g==", "requires": { "dom7": "^3.0.0", "ssr-window": "^3.0.0" diff --git a/package.json b/package.json index fad0147b3d1..f762f9c9a2e 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,7 @@ "react-ga": "3.3.0", "sass": "^1.38.2", "shx": "^0.3.3", - "swiper": "^6.7.5", + "swiper": "^6.8.4", "tailwindcss": "^2.2.9", "uuid": "^8.3.2", "whatwg-fetch": "^3.6.2" diff --git a/source/images/mozfest/play-circle-grey.svg b/source/images/mozfest/play-circle-grey.svg new file mode 100644 index 00000000000..fa3b3a4c01c --- /dev/null +++ b/source/images/mozfest/play-circle-grey.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/source/js/components/mozfest-hero-carousel/mozfest-hero-carousel.js b/source/js/components/mozfest-hero-carousel/mozfest-hero-carousel.js new file mode 100644 index 00000000000..b1ee4c2e2ee --- /dev/null +++ b/source/js/components/mozfest-hero-carousel/mozfest-hero-carousel.js @@ -0,0 +1,86 @@ +import Swiper, { + A11y, + Autoplay, + Pagination, + Navigation, + Keyboard, + EffectFade, +} from "swiper"; + +Swiper.use([A11y, Autoplay, Pagination, Navigation, Keyboard, EffectFade]); + +class MozfestHeroCarousel { + constructor(node) { + this.node = node; + this.delay = 10000; + + // Initialize Carousels + this.initBackgroundImageCarousel(); + this.initMobileTexCarousel(); + + // Link transitions + this.linkSlideChanges(); + } + + initBackgroundImageCarousel() { + this.backGroundImagesSwiper = new Swiper(".swiper-hero-carousel", { + loop: true, + watchSlidesProgress: true, + autoplay: { + delay: this.delay, + disableOnInteraction: false, + }, + paginationClickable: false, + keyboard: { + enabled: true, + }, + pagination: { + el: ".swiper-hero-pagination", + clickable: true, + }, + effect: "fade", + fadeEffect: { + crossFade: false, + }, + }); + } + + initMobileTexCarousel() { + this.heroTextMobile = new Swiper(".swiper-hero-mobile", { + allowTouchMove: true, + loop: true, + keyboard: { + enabled: true, + }, + autoplay: { + delay: this.delay, + disableOnInteraction: false, + }, + slidesPerView: 1, + centeredSlides: true, + spaceBetween: 30, + }); + } + + // Ensure that the background image slider stays in sync with the mobile one + linkSlideChanges() { + this.heroTextMobile.on("slideChange", (event) => { + if (event.swipeDirection === "next") { + this.backGroundImagesSwiper.slideNext(); + } + if (event.swipeDirection === "prev") { + this.backGroundImagesSwiper.slidePrev(); + } + }); + } +} + +const MozfestHeroCarousels = { + init: function () { + document + .querySelectorAll(`[data-mozfest-hero-carousel]`) + .forEach((e) => new MozfestHeroCarousel(e)); + }, +}; + +export default MozfestHeroCarousels; diff --git a/source/js/foundation/pages/mozfest/template-js-handler/home-banner.js b/source/js/foundation/pages/mozfest/template-js-handler/home-banner.js index 984ffb869a3..c823d9fcf6e 100644 --- a/source/js/foundation/pages/mozfest/template-js-handler/home-banner.js +++ b/source/js/foundation/pages/mozfest/template-js-handler/home-banner.js @@ -1,22 +1,50 @@ import { ReactGA } from "../../../../common"; -const watchVideoButtonHandler = () => { - let homeWatchVideoButton = document.querySelector( - `#mozfest-home-watch-video-button` +// For the featured banner type on mozefest homepage +const watchFeaturedVideoHandler = () => { + const watchVideoButton = document.querySelector( + `#mozfest-home-watch-featured-video-button` ); + const externalVideo = document.querySelector(`#mozfest-hero-video iframe`); + const internalVideo = document.querySelector(`#mozfest-hero-video video`); - if (homeWatchVideoButton) { - homeWatchVideoButton.addEventListener(`click`, () => { - ReactGA.event({ - category: `CTA`, - action: `watch video tap`, - label: `watch video button tap`, - }); + // If no video exists then do nothing + if (!externalVideo && !internalVideo) { + return; + } + + if (watchVideoButton) { + watchVideoButton.addEventListener(`click`, () => { + trackWatchVideoClicks(); + + if (externalVideo) { + // Get video url from button + const videoUrl = watchVideoButton.dataset.videoUrl; + + // Add Src to video to play it + externalVideo.setAttribute("src", videoUrl); + fadeOutOverlay(watchVideoButton); + } + + if (internalVideo) { + fadeOutOverlay(watchVideoButton); + internalVideo.play(); + } }); } }; -const backgroundVideoHandler = () => { +const fadeOutOverlay = (overlay) => { + // Fade out overlay + overlay.classList.add("tw-opacity-0"); + + // After fading out remove from DOM Flow + setTimeout(() => { + overlay.classList.add("tw-hidden"); + }, 500); +}; + +const backgroundHardcodedVideoHandler = () => { let homepageBanner = document.querySelector( "#view-mozfest-home #hero .banner" ); @@ -49,6 +77,7 @@ const backgroundVideoHandler = () => { }); playButton.addEventListener(`click`, () => { + trackWatchVideoClicks(); video.play(); }); @@ -64,10 +93,39 @@ const backgroundVideoHandler = () => { } }; +// Track video watches in google analytics +const trackWatchVideoClicks = () => { + ReactGA.event({ + category: `CTA`, + action: `watch video tap`, + label: `watch video button tap`, + }); +} + +const scrollToVideoHandler = () => { + let element = document.getElementById('mozfest-hero-video'); + let button = document.getElementById('mozfest-hero-video-cta'); + + if (element && button) { + let headerOffset = 90; + let elementPosition = element.getBoundingClientRect().top; + let offsetPosition = elementPosition - headerOffset; + + button.addEventListener('click', () => { + window.scrollTo({ + top: offsetPosition, + behavior: "smooth" + }); + }) + } +} + + /** * Bind handlers to MozFest homepage banner */ export default () => { - watchVideoButtonHandler(); - backgroundVideoHandler(); + watchFeaturedVideoHandler(); + backgroundHardcodedVideoHandler(); + scrollToVideoHandler(); }; diff --git a/source/js/main.js b/source/js/main.js index 9bf2ec01e52..e1dc3d7eaa3 100644 --- a/source/js/main.js +++ b/source/js/main.js @@ -16,6 +16,7 @@ import { import primaryNav from "./primary-nav.js"; import EmbedTypeform from "./embed-typeform.js"; import Dropdowns from "./dropdowns.js"; +import MozfestHeroCarousels from "./components/mozfest-hero-carousel/mozfest-hero-carousel"; import initializeSentry from "./common/sentry-config.js"; import YouTubeRegretsTunnel from "./foundation/pages/youtube-regrets/intro-tunnel"; import RegretsReporterTimeline from "./foundation/pages/youtube-regrets/regrets-reporter/timeline"; @@ -138,6 +139,11 @@ let main = { if (document.querySelector("#view-dear-internet")) { bindDearInternetEventHandlers(); } + + // Mozfest pages + if (document.querySelector(`.mozfest`)) { + MozfestHeroCarousels.init(); + } }, }; diff --git a/source/sass/mozfest.scss b/source/sass/mozfest.scss index c767574e55a..0c2efe735e9 100644 --- a/source/sass/mozfest.scss +++ b/source/sass/mozfest.scss @@ -1,6 +1,12 @@ // MozFest-specific styling // https://www.mozillafestival.org +// Mozfest Carousel Modules +@import "../../node_modules/swiper/swiper-vars.scss"; +@import "../../node_modules/swiper/swiper.scss"; +@import "../../node_modules/swiper/components/pagination/pagination.scss"; +@import "../../node_modules/swiper/components/effect-fade/effect-fade.scss"; + body.mozfest { .primary-nav-container { // The following overrides are to make sure @@ -48,6 +54,7 @@ body.mozfest { height: $icon-size; background: center left/$icon-size $icon-size no-repeat transparent; padding-left: calc(#{$icon-size} + 0.5rem); + text-transform: uppercase; // due to a Safari bug, we have to remove transition for these buttons // so background SVGs don't get resized on hover transition: none; @@ -174,4 +181,101 @@ body.mozfest { } } } + +} + +.swiper-button-next, +.swiper-button-prev { + @apply tw-border-2 tw-text-blue tw-flex tw-flex-col tw-justify-center tw-items-center tw-w-[40px] tw-h-[40px] tw-transition; + + &.swiper-button-disabled { + @apply tw-border-gray-20; + } + + &::after { + content: ""; + } + + &:hover { + @apply tw-opacity-75; + } +} + +.swiper-button-icon { + @apply tw-text-blue tw-w-4 tw-h-4; +} + +.swiper-button-disabled { + .swiper-button-icon { + @apply tw-text-gray-20; + } +} + +.swiper-button-prev { + @apply tw-mr-5; +} + +.swiper-pagination-bullet { + @apply tw-w-3 tw-h-3; +} + +.swiper-pagination-bullet-active { + @apply tw-bg-festival-blue-100; +} + +@keyframes slide-progress-bar { + 0% { + transform: translateX(-100%); + } + + 100% { + transform: translateX(0); + } +} + +#view-mozfest-home { + .swiper-hero-pagination { + .swiper-pagination-bullet { + @apply tw-block tw-w-full tw-rounded-full tw-overflow-hidden tw-relative tw-bg-white; + height: 4px; + + &::before { + @apply tw-block tw-inset-0 tw-absolute; + background: $white; + content: ""; + } + } + + .swiper-pagination-bullet-active { + /* stylelint-disable */ + background: rgba($white, 0.2); + /* stylelint-enable */ + + &::before { + animation: slide-progress-bar 10s ease-in-out forwards; + } + } + } + + .swiper-mobile-progress-bar { + @apply tw-rounded-full tw-relative tw-overflow-hidden tw-w-full; + height: 4px; + /* stylelint-disable */ + background: rgba($white, 0.5); + /* stylelint-enable */ + + &::before { + @apply tw-block tw-inset-0 tw-absolute; + background: $white; + content: ""; + } + } + + .swiper-slide-active { + .swiper-mobile-progress-bar { + &::before { + animation: slide-progress-bar 10s ease-in-out forwards; + } + } + } } diff --git a/tailwind.config.js b/tailwind.config.js index 80ab675f422..183721fd340 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -41,6 +41,9 @@ module.exports = { screens: { print: { raw: "print" }, }, + opacity: { + 40: 0.4, + } }, // Overriding default spacing spacing: { @@ -104,8 +107,14 @@ module.exports = { purple: "#a66efd", }, festival: { - blue: "#0e11bf", - purple: "#8f14fb", + blue: { + DEFAULT: "#0e11bf", + 100: "#2e05ff" + }, + purple: { + DEFAULT: "#8f14fb", + 100: "#fa00ff", + }, }, "dear-internet": { lilac: "#d3d5fc",