diff --git a/tbx/courses/__init__.py b/tbx/courses/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tbx/courses/blocks.py b/tbx/courses/blocks.py new file mode 100644 index 000000000..314894525 --- /dev/null +++ b/tbx/courses/blocks.py @@ -0,0 +1,33 @@ +from tbx.core import blocks as tbx_blocks +from wagtail import blocks + + +class CourseOutlineItemBlock(blocks.StructBlock): + title = blocks.CharBlock() + text = blocks.RichTextBlock() + + +class CourseOutlineBlock(blocks.StructBlock): + heading = blocks.CharBlock() + course_outline = blocks.ListBlock( + CourseOutlineItemBlock(), + ) + + class Meta: + template = ("patterns/molecules/streamfield/blocks/course_outline_block.html",) + + +class ExternalLinkCTABlock(blocks.StructBlock): + link_url = blocks.URLBlock(label="External Link") + heading = blocks.CharBlock() + text = blocks.CharBlock() + + class Meta: + template = ( + "patterns/molecules/streamfield/blocks/external_link_cta_block.html", + ) + + +class CourseDetailStoryBlock(tbx_blocks.StoryBlock): + course_outline = CourseOutlineBlock() + external_link_cta = ExternalLinkCTABlock() diff --git a/tbx/courses/migrations/0001_initial.py b/tbx/courses/migrations/0001_initial.py new file mode 100644 index 000000000..aa28343fe --- /dev/null +++ b/tbx/courses/migrations/0001_initial.py @@ -0,0 +1,337 @@ +# Generated by Django 4.2.8 on 2024-01-02 15:35 + +from django.db import migrations, models +import django.db.models.deletion +import tbx.core.blocks +import wagtail.blocks +import wagtail.embeds.blocks +import wagtail.fields +import wagtail.images.blocks +import wagtailmarkdown.blocks +import wagtailmedia.blocks + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("wagtailcore", "0089_log_entry_data_json_null_to_object"), + ("images", "0005_alter_rendition_file"), + ] + + operations = [ + migrations.CreateModel( + name="CourseLandingPage", + fields=[ + ( + "page_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="wagtailcore.page", + ), + ), + ("social_text", models.CharField(blank=True, max_length=255)), + ( + "social_image", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="images.customimage", + ), + ), + ], + options={ + "abstract": False, + }, + bases=("wagtailcore.page", models.Model), + ), + migrations.CreateModel( + name="CourseDetailPage", + fields=[ + ( + "page_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="wagtailcore.page", + ), + ), + ("social_text", models.CharField(blank=True, max_length=255)), + ( + "strapline", + models.CharField( + help_text="Words in tag will display in a contrasting colour.", + max_length=255, + ), + ), + ( + "sessions", + models.CharField( + blank=True, help_text="e.g. 4 sessions", max_length=25 + ), + ), + ( + "cost", + models.CharField( + blank=True, + help_text="e.g.£999 / $1,299 per person ", + max_length=255, + ), + ), + ("intro", wagtail.fields.RichTextField(blank=True)), + ( + "header_link", + models.URLField( + blank=True, help_text="e.g. https://www.example.com" + ), + ), + ( + "header_link_text", + models.CharField( + blank=True, + help_text="e.g. Visit example.com for dates", + max_length=255, + ), + ), + ( + "body", + wagtail.fields.StreamField( + [ + ( + "h2", + wagtail.blocks.CharBlock( + form_classname="title", + icon="title", + template="patterns/molecules/streamfield/blocks/heading2_block.html", + ), + ), + ( + "h3", + wagtail.blocks.CharBlock( + form_classname="title", + icon="title", + template="patterns/molecules/streamfield/blocks/heading3_block.html", + ), + ), + ( + "h4", + wagtail.blocks.CharBlock( + form_classname="title", + icon="title", + template="patterns/molecules/streamfield/blocks/heading4_block.html", + ), + ), + ( + "intro", + wagtail.blocks.RichTextBlock( + icon="pilcrow", + template="patterns/molecules/streamfield/blocks/intro_block.html", + ), + ), + ( + "paragraph", + wagtail.blocks.RichTextBlock( + icon="pilcrow", + template="patterns/molecules/streamfield/blocks/paragraph_block.html", + ), + ), + ( + "aligned_image", + wagtail.blocks.StructBlock( + [ + ( + "image", + wagtail.images.blocks.ImageChooserBlock(), + ), + ( + "alignment", + tbx.core.blocks.ImageFormatChoiceBlock(), + ), + ("caption", wagtail.blocks.CharBlock()), + ( + "attribution", + wagtail.blocks.CharBlock(required=False), + ), + ], + label="Aligned image", + template="patterns/molecules/streamfield/blocks/aligned_image_block.html", + ), + ), + ( + "wide_image", + wagtail.blocks.StructBlock( + [ + ( + "image", + wagtail.images.blocks.ImageChooserBlock(), + ) + ], + label="Wide image", + template="patterns/molecules/streamfield/blocks/wide_image_block.html", + ), + ), + ( + "bustout", + wagtail.blocks.StructBlock( + [ + ( + "image", + wagtail.images.blocks.ImageChooserBlock(), + ), + ("text", wagtail.blocks.RichTextBlock()), + ], + template="patterns/molecules/streamfield/blocks/bustout_block.html", + ), + ), + ( + "pullquote", + wagtail.blocks.StructBlock( + [ + ( + "quote", + wagtail.blocks.CharBlock( + form_classname="quote title" + ), + ), + ("attribution", wagtail.blocks.CharBlock()), + ], + template="patterns/molecules/streamfield/blocks/pullquote_block.html", + ), + ), + ( + "raw_html", + wagtail.blocks.RawHTMLBlock( + icon="code", + label="Raw HTML", + template="patterns/molecules/streamfield/blocks/raw_html_block.html", + ), + ), + ( + "mailchimp_form", + wagtail.blocks.RawHTMLBlock( + icon="code", + label="Mailchimp embedded form", + template="patterns/molecules/streamfield/blocks/mailchimp_form_block.html", + ), + ), + ( + "markdown", + wagtailmarkdown.blocks.MarkdownBlock( + icon="code", + template="patterns/molecules/streamfield/blocks/markdown_block.html", + ), + ), + ( + "embed", + wagtail.embeds.blocks.EmbedBlock( + group="Media", + icon="code", + template="patterns/molecules/streamfield/blocks/embed_block.html", + ), + ), + ( + "video_block", + wagtail.blocks.StructBlock( + [ + ( + "video", + wagtailmedia.blocks.VideoChooserBlock(), + ), + ( + "autoplay", + wagtail.blocks.BooleanBlock( + default=False, + help_text="Automatically start and loop the video. Please use sparingly.", + required=False, + ), + ), + ( + "use_original_width", + wagtail.blocks.BooleanBlock( + default=False, + help_text="Use the original width of the video instead of the default content width. Note that videos wider than the content width will be limited to the content width.", + required=False, + ), + ), + ], + group="Media", + ), + ), + ( + "story_embed", + tbx.core.blocks.ExternalStoryEmbedBlock( + icon="code", + template="patterns/molecules/streamfield/blocks/external_story_block.html", + ), + ), + ( + "course_outline", + wagtail.blocks.StructBlock( + [ + ("heading", wagtail.blocks.CharBlock()), + ( + "course_outline", + wagtail.blocks.ListBlock( + wagtail.blocks.StructBlock( + [ + ( + "title", + wagtail.blocks.CharBlock(), + ), + ( + "text", + wagtail.blocks.RichTextBlock(), + ), + ] + ) + ), + ), + ] + ), + ), + ( + "external_link_cta", + wagtail.blocks.StructBlock( + [ + ( + "link_url", + wagtail.blocks.URLBlock( + label="External Link" + ), + ), + ("heading", wagtail.blocks.CharBlock()), + ("text", wagtail.blocks.CharBlock()), + ] + ), + ), + ], + use_json_field=True, + ), + ), + ( + "social_image", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="images.customimage", + ), + ), + ], + options={ + "abstract": False, + }, + bases=("wagtailcore.page", models.Model), + ), + ] diff --git a/tbx/courses/migrations/0002_adds_course_landing_page_fields.py b/tbx/courses/migrations/0002_adds_course_landing_page_fields.py new file mode 100644 index 000000000..f7e8013eb --- /dev/null +++ b/tbx/courses/migrations/0002_adds_course_landing_page_fields.py @@ -0,0 +1,47 @@ +# Generated by Django 4.2.8 on 2024-01-02 19:08 + +from django.db import migrations, models +import wagtail.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ("courses", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="courselandingpage", + name="child_page_listing_heading", + field=models.CharField( + blank=True, + help_text="A heading shown above the child pages listed.", + max_length=255, + ), + ), + migrations.AddField( + model_name="courselandingpage", + name="intro", + field=wagtail.fields.RichTextField(blank=True), + ), + migrations.AddField( + model_name="courselandingpage", + name="strapline", + field=models.CharField( + default="", + help_text="Words in tag will display in a contrasting colour.", + max_length=255, + ), + preserve_default=False, + ), + migrations.AddField( + model_name="courselandingpage", + name="sub_title", + field=models.CharField( + blank=True, + help_text="Displayed just below the strapline.", + max_length=255, + ), + ), + ] diff --git a/tbx/courses/migrations/0003_adds_related_course_pages.py b/tbx/courses/migrations/0003_adds_related_course_pages.py new file mode 100644 index 000000000..989d5510c --- /dev/null +++ b/tbx/courses/migrations/0003_adds_related_course_pages.py @@ -0,0 +1,53 @@ +# Generated by Django 4.2.8 on 2024-01-03 10:18 + +from django.db import migrations, models +import django.db.models.deletion +import modelcluster.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ("wagtailcore", "0089_log_entry_data_json_null_to_object"), + ("courses", "0002_adds_course_landing_page_fields"), + ] + + operations = [ + migrations.CreateModel( + name="RelatedCoursePage", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "sort_order", + models.IntegerField(blank=True, editable=False, null=True), + ), + ( + "page", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="courses.coursedetailpage", + ), + ), + ( + "source_page", + modelcluster.fields.ParentalKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="related_course_pages", + to="wagtailcore.page", + ), + ), + ], + options={ + "ordering": ["sort_order"], + "abstract": False, + }, + ), + ] diff --git a/tbx/courses/migrations/__init__.py b/tbx/courses/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tbx/courses/models.py b/tbx/courses/models.py new file mode 100644 index 000000000..2cbaec3d7 --- /dev/null +++ b/tbx/courses/models.py @@ -0,0 +1,156 @@ +from django.core import exceptions as django_exceptions +from django.db import models + +from modelcluster import fields as modelcluster_fields +from tbx.core.utils import models as utils_models +from tbx.courses import blocks as tbx_course_blocks +from wagtail import fields as wagtail_fields +from wagtail import models as wagtail_models +from wagtail.admin import panels +from wagtail.search import index + +INTRO_RICHTEXT_FEATURES = ["bold", "italic", "link", "document-link", "strikethrough"] + + +class CourseLandingPage(utils_models.SocialFields, wagtail_models.Page): + # Don't offer a theme style, just set to dark + theme = "dark" + template = "patterns/pages/courses/course_landing_page.html" + + strapline = models.CharField( + max_length=255, + help_text="Words in tag will display in a contrasting colour.", + ) + sub_title = models.CharField( + max_length=255, + help_text="Displayed just below the strapline.", + blank=True, + ) + intro = wagtail_fields.RichTextField(blank=True, features=INTRO_RICHTEXT_FEATURES) + child_page_listing_heading = models.CharField( + max_length=255, + help_text="A heading shown above the child pages listed.", + blank=True, + ) + content_panels = wagtail_models.Page.content_panels + [ + panels.MultiFieldPanel( + [ + panels.FieldPanel("strapline", classname="full title"), + panels.FieldPanel("sub_title"), + panels.FieldPanel("intro", classname="full"), + ], + heading="Hero", + classname="collapsible", + ), + panels.FieldPanel("child_page_listing_heading"), + ] + + promote_panels = [ + panels.MultiFieldPanel( + wagtail_models.Page.promote_panels, "Common page configuration" + ), + panels.MultiFieldPanel( + utils_models.SocialFields.promote_panels, "Social fields" + ), + ] + + search_fields = wagtail_models.Page.search_fields + [ + index.SearchField("intro"), + index.SearchField("strapline"), + ] + + def _get_subpages(self): + subpages = ( + CourseDetailPage.objects.live() + .descendant_of(self) + .order_by("title") + .only("title", "sessions", "cost", "intro") + ) + return subpages + + def get_context(self, request, *args, **kwargs): + context = super().get_context(request, *args, **kwargs) + context["subpages"] = self._get_subpages() + return context + + +class RelatedCoursePage(wagtail_models.Orderable): + source_page = modelcluster_fields.ParentalKey( + wagtail_models.Page, related_name="related_course_pages" + ) + page = models.ForeignKey("courses.CourseDetailPage", on_delete=models.CASCADE) + + panels = [panels.FieldPanel("page")] + + +class CourseDetailPage(utils_models.SocialFields, wagtail_models.Page): + parent_page_types = ["courses.CourseLandingPage"] + + template = "patterns/pages/courses/course_detail_page.html" + + strapline = models.CharField( + max_length=255, + help_text="Words in tag will display in a contrasting colour.", + ) + sessions = models.CharField(max_length=25, blank=True, help_text="e.g. 4 sessions") + cost = models.CharField( + max_length=255, blank=True, help_text="e.g.£999 / $1,299 per person " + ) + intro = wagtail_fields.RichTextField(blank=True, features=INTRO_RICHTEXT_FEATURES) + header_link = models.URLField(blank=True, help_text="e.g. https://www.example.com") + header_link_text = models.CharField( + max_length=255, blank=True, help_text="e.g. Visit example.com for dates" + ) + + body = wagtail_fields.StreamField( + tbx_course_blocks.CourseDetailStoryBlock(), use_json_field=True + ) + + search_fields = wagtail_models.Page.search_fields + [ + index.SearchField("intro"), + index.SearchField("strapline"), + ] + + content_panels = wagtail_models.Page.content_panels + [ + panels.MultiFieldPanel( + [ + panels.FieldPanel("strapline", classname="full title"), + panels.FieldPanel("sessions"), + panels.FieldPanel("cost"), + panels.FieldPanel("intro", classname="full"), + panels.FieldPanel("header_link", classname="col6"), + panels.FieldPanel("header_link_text", classname="col6"), + ], + heading="Hero", + classname="collapsible", + ), + panels.FieldPanel("body"), + panels.InlinePanel("related_course_pages", label="Related courses"), + ] + + promote_panels = [ + panels.MultiFieldPanel( + wagtail_models.Page.promote_panels, "Common page configuration" + ), + panels.MultiFieldPanel( + utils_models.SocialFields.promote_panels, "Social fields" + ), + ] + + def clean(self): + errors = {} + + if self.header_link and not self.header_link_text: + errors["header_link_text"] = "You must set a text value for the link." + + if self.header_link_text and not self.header_link: + errors["header_link"] = "You must set a link value for the header link." + + if errors: + raise django_exceptions.ValidationError(errors) + + @property + def related_courses(self): + return [ + page.page for page in self.related_course_pages.all().select_related("page") + ] diff --git a/tbx/project_styleguide/templates/patterns/atoms/sprites/sprites.html b/tbx/project_styleguide/templates/patterns/atoms/sprites/sprites.html index dfb6de852..95bfbe145 100644 --- a/tbx/project_styleguide/templates/patterns/atoms/sprites/sprites.html +++ b/tbx/project_styleguide/templates/patterns/atoms/sprites/sprites.html @@ -69,4 +69,8 @@ + + + + diff --git a/tbx/project_styleguide/templates/patterns/molecules/course-grid/course-grid.html b/tbx/project_styleguide/templates/patterns/molecules/course-grid/course-grid.html new file mode 100644 index 000000000..5c7aaf83e --- /dev/null +++ b/tbx/project_styleguide/templates/patterns/molecules/course-grid/course-grid.html @@ -0,0 +1,18 @@ +{% load wagtailcore_tags %} + +
+ {% for card in cards %} +
+ {% if card.sessions or card.cost %} +

+ {% if card.sessions %}{{ card.sessions }}{% endif %} + {% if card.sessions and card.cost %} | {% endif %} + {% if card.cost %}{{ card.cost }}{% endif %} +

+ {% endif %} +

{{ card.title }}

+
{{ card.intro|richtext }}
+ View details +
+ {% endfor %} +
diff --git a/tbx/project_styleguide/templates/patterns/molecules/hero/hero.html b/tbx/project_styleguide/templates/patterns/molecules/hero/hero.html index 21621e13e..e69649c61 100644 --- a/tbx/project_styleguide/templates/patterns/molecules/hero/hero.html +++ b/tbx/project_styleguide/templates/patterns/molecules/hero/hero.html @@ -3,6 +3,9 @@

{{ title|richtext }}

+ {% if sub_title %} +

{{ sub_title|richtext }}

+ {% endif %} {% if desc %}
{{ desc|richtext }}
{% endif %} diff --git a/tbx/project_styleguide/templates/patterns/molecules/streamfield/blocks/course_outline_block.html b/tbx/project_styleguide/templates/patterns/molecules/streamfield/blocks/course_outline_block.html new file mode 100644 index 000000000..13edd9b63 --- /dev/null +++ b/tbx/project_styleguide/templates/patterns/molecules/streamfield/blocks/course_outline_block.html @@ -0,0 +1,23 @@ +{% load wagtailcore_tags %} + +
+ {% if value.heading %} +

{{ value.heading }}

+ {% endif %} + +
    + {% for block in value.course_outline %} +
  1. + +
    +

    + {{ forloop.counter }}. {{ block.title }} +

    + {{ block.text|richtext }} +
    +
  2. + {% endfor %} +
+
diff --git a/tbx/project_styleguide/templates/patterns/molecules/streamfield/blocks/course_outline_block.yaml b/tbx/project_styleguide/templates/patterns/molecules/streamfield/blocks/course_outline_block.yaml new file mode 100644 index 000000000..ed60c7b8e --- /dev/null +++ b/tbx/project_styleguide/templates/patterns/molecules/streamfield/blocks/course_outline_block.yaml @@ -0,0 +1,16 @@ +context: + value: + heading: About the course + course_outline: + - title: UX Research and Design + text: The UX Research and Design course is a 10-week, part-time course + that will teach you core user experience design skills and how to apply + them in the real world. + - title: UX Research and Design + text: The UX Research and Design course is a 10-week, part-time course + that will teach you core user experience design skills and how to apply + them in the real world. + - title: UX Research and Design + text: The UX Research and Design course is a 10-week, part-time course + that will teach you core user experience design skills and how to apply + them in the real world. diff --git a/tbx/project_styleguide/templates/patterns/molecules/streamfield/blocks/external_link_cta_block.html b/tbx/project_styleguide/templates/patterns/molecules/streamfield/blocks/external_link_cta_block.html new file mode 100644 index 000000000..9e59d8455 --- /dev/null +++ b/tbx/project_styleguide/templates/patterns/molecules/streamfield/blocks/external_link_cta_block.html @@ -0,0 +1,13 @@ + + diff --git a/tbx/project_styleguide/templates/patterns/molecules/streamfield/blocks/external_link_cta_block.yaml b/tbx/project_styleguide/templates/patterns/molecules/streamfield/blocks/external_link_cta_block.yaml new file mode 100644 index 000000000..0818a1c41 --- /dev/null +++ b/tbx/project_styleguide/templates/patterns/molecules/streamfield/blocks/external_link_cta_block.yaml @@ -0,0 +1,5 @@ +context: + value: + link_url: '#' + heading: Check upcoming course dates and apply + text: If the dates don’t work, we can notify you of future courses diff --git a/tbx/project_styleguide/templates/patterns/pages/courses/course_detail_page.html b/tbx/project_styleguide/templates/patterns/pages/courses/course_detail_page.html new file mode 100644 index 000000000..dbf13a7a4 --- /dev/null +++ b/tbx/project_styleguide/templates/patterns/pages/courses/course_detail_page.html @@ -0,0 +1,37 @@ +{% extends "patterns/base_page.html" %} +{% load wagtailcore_tags wagtailimages_tags static %} + +{% block content %} + +
+
+

+ {{ page.strapline|richtext }} +

+
+

+ {% if page.sessions %}{{ page.sessions }}{% endif %} + {% if page.cost and page.sessions %} {% endif %} + {% if page.cost %}{{ page.cost }}{% endif %} +

+ +
{{ page.intro|richtext }}
+ + {{ page.header_link_text }} + +
+
+
+ + {% include_block page.body %} + + {% if page.related_courses %} +
+ +
+ {% endif %} + +{% endblock %} diff --git a/tbx/project_styleguide/templates/patterns/pages/courses/course_detail_page.yaml b/tbx/project_styleguide/templates/patterns/pages/courses/course_detail_page.yaml new file mode 100644 index 000000000..71459261d --- /dev/null +++ b/tbx/project_styleguide/templates/patterns/pages/courses/course_detail_page.yaml @@ -0,0 +1,13 @@ +context: + page: + title: Course page + strapline: Course page Example + sessions: 4 sessions + cost: $100 + header_link_text: Find out more + header_link: '#' + +tags: + include_block: + page.body: + template_name: 'patterns/molecules/streamfield/streamfield-example-standard.html' diff --git a/tbx/project_styleguide/templates/patterns/pages/courses/course_landing_page.html b/tbx/project_styleguide/templates/patterns/pages/courses/course_landing_page.html new file mode 100644 index 000000000..da6f10bc9 --- /dev/null +++ b/tbx/project_styleguide/templates/patterns/pages/courses/course_landing_page.html @@ -0,0 +1,18 @@ +{% extends "patterns/base_page.html" %} +{% load wagtailcore_tags wagtailimages_tags static %} + +{% block content %} + + {% include "patterns/molecules/hero/hero.html" with title=page.strapline desc=page.intro sub_title=page.sub_title classes='hero--course-landing' %} + +
+ {% if page.child_page_listing_heading %} +

{{ page.child_page_listing_heading }}

+ {% endif %} + + {% if subpages %} + {% include "patterns/molecules/course-grid/course-grid.html" with cards=subpages %} + {% endif %} +
+ +{% endblock %} diff --git a/tbx/project_styleguide/templates/patterns/pages/courses/course_landing_page.yaml b/tbx/project_styleguide/templates/patterns/pages/courses/course_landing_page.yaml new file mode 100644 index 000000000..e69de29bb diff --git a/tbx/settings/base.py b/tbx/settings/base.py index 66ee7ffb9..c073dab21 100644 --- a/tbx/settings/base.py +++ b/tbx/settings/base.py @@ -40,6 +40,7 @@ "scout_apm.django", "tbx.blog", "tbx.core.apps.TorchboxCoreAppConfig", + "tbx.courses", "tbx.events", "tbx.impact_reports", "tbx.navigation", diff --git a/tbx/static_src/sass/abstracts/_variables.scss b/tbx/static_src/sass/abstracts/_variables.scss index 3bded966e..c1152cbef 100755 --- a/tbx/static_src/sass/abstracts/_variables.scss +++ b/tbx/static_src/sass/abstracts/_variables.scss @@ -17,7 +17,9 @@ --color--light-grey-accessible: #757575; --color--grey: #444; --color--grey-dark: #333; + --color--grey-bg: #fafafa; --color--grey-border: #e6e4ea; + --color--grey-border-dark: #e9e7ee; --color--white: #fff; --color--white-translucent: rgba(255, 255, 255, 0.8); --color--black-translucent: rgba(0, 0, 0, 0.05); diff --git a/tbx/static_src/sass/components/_course-grid-item.scss b/tbx/static_src/sass/components/_course-grid-item.scss new file mode 100644 index 000000000..3cc9bae56 --- /dev/null +++ b/tbx/static_src/sass/components/_course-grid-item.scss @@ -0,0 +1,76 @@ +.course-grid-item { + padding: 25px; + background-color: var(--color--grey-bg); + border: 1px solid var(--color--grey-border-dark); + + @include media-query(medium-large) { + padding: 50px; + } + + &__sessions { + @include font-size(xs); + text-transform: uppercase; + color: var(--color--grey); + font-weight: $weight--bold; + letter-spacing: 2px; + margin-bottom: 10px; + + span { + color: var(--color--coral); + } + } + + &__title { + @include font-size(l); + line-height: 32px; + margin: 15px 0 10px; + + @include media-query(large) { + line-height: 44px; + } + } + + &__intro { + @include font-size(s); + line-height: 27px; + font-weight: $weight--normal; + + p { + color: var(--color--grey-dark); + } + } + + &__link { + @include font-size(s); + line-height: 27px; + color: var(--color--indigo); + font-weight: $weight--bold; + display: inline-block; + position: relative; + text-decoration: underline; + text-decoration-color: var(--color--coral); + text-underline-offset: 5px; + border: 0; + + &:focus, + &:hover { + text-decoration-thickness: 5px; + } + + &::after { + content: ''; + display: block; + position: absolute; + right: -21px; + top: 5px; + width: 15px; + height: 14px; + background-color: var(--color--coral); + clip-path: $arrow-path; + + @include hcm() { + filter: invert(1); + } + } + } +} diff --git a/tbx/static_src/sass/components/_course-grid.scss b/tbx/static_src/sass/components/_course-grid.scss new file mode 100644 index 000000000..36c315f0c --- /dev/null +++ b/tbx/static_src/sass/components/_course-grid.scss @@ -0,0 +1,17 @@ +.course-grid-title { + @include font-size(ml); + margin-bottom: 0; +} + +.course-grid { + display: grid; + gap: 30px; + margin: 25px 0 100px; + + @include media-query(medium-large) { + grid-template-columns: repeat(2, 1fr); + max-width: 1280px; + gap: 50px; + margin: 50px 0 120px; + } +} diff --git a/tbx/static_src/sass/components/_course-outline.scss b/tbx/static_src/sass/components/_course-outline.scss new file mode 100644 index 000000000..e85b93049 --- /dev/null +++ b/tbx/static_src/sass/components/_course-outline.scss @@ -0,0 +1,70 @@ +.course-outline { + @include streamblock-padding(); + margin-bottom: 75px; + + &__heading { + @include font-size(l); + line-height: 38px; + margin: 0 0 40px; + } + + &__sub-heading { + @include font-size(ml); + line-height: 33px; + margin-top: 0; + + @include media-query(large) { + margin-top: 5px; + } + } + + &__list { + max-width: 720px; + } + + &__icon { + flex-shrink: 0; + color: var(--color--coral); + width: 30px; + height: 30px; + + @include media-query(large) { + width: 41px; + height: 41px; + } + } + + // Override existing specificity + &__list-item { + display: flex; + margin-top: 35px; + gap: 15px; + + @include media-query(large) { + gap: 25px; + } + + .streamfield & { + ul { + list-style: disc; + padding-left: 25px; + } + } + } + + .streamfield & { + li { + padding: 0; + + &::before { + display: none; + } + } + + p { + &:last-of-type { + margin-bottom: 0; + } + } + } +} diff --git a/tbx/static_src/sass/components/_external-link-cta.scss b/tbx/static_src/sass/components/_external-link-cta.scss new file mode 100644 index 000000000..1de7c2253 --- /dev/null +++ b/tbx/static_src/sass/components/_external-link-cta.scss @@ -0,0 +1,182 @@ +// Pulled from the Careers site: +// https://github.com/torchbox/careers/blob/main/components/Button/ApplyButton.module.scss + +/* stylelint-disable selector-max-specificity */ +.external-link-cta-wrapper { + @include streamblock-padding(); +} + +.external-link-cta { + @include z-index(zero); + @include font-size(m); + display: block; + position: relative; + width: 100%; + color: var(--color--white); + padding: $grid * 1.5; + border-radius: 7px; + transition: transform $transition-quick; + margin-top: $grid * 3; + background: radial-gradient( + 81.08% 2378.82% at 50% 60%, + var(--color--dark-indigo) 0%, + var(--color--indigo) 100% + ); + max-width: 630px; + + @include media-query(medium) { + margin-top: $grid * 3; + } + + @include media-query(large) { + margin: $grid * 4.5 0 $grid * 3 0; + } + + &:hover, + &:focus { + &, + .streamfield & { + color: var(--color--white); + border-bottom: 2px solid transparent; + + .external-link-cta__chevron { + transform: translateX(8px); + } + + > .external-link-cta__overflow-hider + > .external-link-cta__swish-background { + opacity: 1; + animation-play-state: running; + } + } + } + + &:active { + transform: scale(0.98); + } + + @media (prefers-reduced-motion: reduce) { + &:active { + transform: scale(1); + } + + &:hover, + &:focus { + &, + > .external-link-cta__overflow-hider + > .external-link-cta__swish-background { + opacity: 0; + animation-play-state: paused; + } + + outline: 3px solid var(--color--coral); + } + } + + @include hcm() { + border: 1px solid buttonborder; + } + + &__chevron { + @include z-index(one); + position: absolute; + transition: transform $transition-quick; + width: auto; + color: var(--color--white); + right: 0; + top: 0; + bottom: 0; + margin: auto 30px auto 0; + display: grid; + align-items: center; + + &::after { + content: ''; + display: block; + width: 20px; + height: 19px; + background-color: var(--color--white); + clip-path: $arrow-path; + + @include hcm() { + filter: invert(1); + } + + @include media-query(medium) { + width: 30px; + height: 28px; + } + } + } + + &__title { + font-size: 24px; + color: var(--color--white); + font-weight: $weight--bold; + margin: 0; + + &, + .streamfield & { + line-height: 34px; + } + } + + &__title-container { + display: flex; + align-items: baseline; + padding-right: 40px; + } + + &__text { + @include font-size(s); + pointer-events: none; + user-select: none; + color: var(--color--white); + font-weight: $weight--normal; + padding-right: 40px; + + &, + .streamfield & { + margin: 5px 0 0; + } + } + + &__swish-background { + position: absolute; + z-index: -1; + top: 0; + left: -200%; + right: 0; + bottom: 0; + opacity: 0; + background: linear-gradient( + -70deg, + rgba(1, 0, 0, 0), + rgba(1, 0, 0, 0) 25%, + rgba(73, 44, 231, 1) 40%, + rgba(1, 0, 0, 0) 55%, + rgba(1, 0, 0, 0) + ); + animation: wave 2s linear infinite; + animation-play-state: paused; + transition: opacity $transition; + } + + &__overflow-hider { + border-radius: 7px; + display: block; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + overflow: hidden; + } + + @keyframes wave { + 100% { + transform: translateX(150%); + } + } +} +/* stylelint-enable */ diff --git a/tbx/static_src/sass/components/_hero.scss b/tbx/static_src/sass/components/_hero.scss index faa779b24..779fc3466 100644 --- a/tbx/static_src/sass/components/_hero.scss +++ b/tbx/static_src/sass/components/_hero.scss @@ -120,6 +120,18 @@ } } + &--course-landing { + #{$root}__content { + max-width: 1165px; + } + + #{$root}__description { + @include font-size(s); + line-height: 27px; + max-width: 700px; + } + } + // The following code for image & image-mask is only used on proposition pages currently &__image { display: block; @@ -239,4 +251,16 @@ } } } + + &__sub-title { + @include font-size(l); + color: var(--color--primary); + font-weight: $weight--normal; + margin: 0 0 25px; + line-height: 34px; + + @include media-query(medium) { + line-height: 49px; + } + } } diff --git a/tbx/static_src/sass/components/_page.scss b/tbx/static_src/sass/components/_page.scss index 49e0e2918..870d1fef1 100644 --- a/tbx/static_src/sass/components/_page.scss +++ b/tbx/static_src/sass/components/_page.scss @@ -35,7 +35,8 @@ .template__error &, .template__proposition-page &, .template__sub-proposition-page &, - .template__impact-report-page & { + .template__impact-report-page &, + .template__course-landing-page & { padding: 0; } } diff --git a/tbx/static_src/sass/components/_related-courses.scss b/tbx/static_src/sass/components/_related-courses.scss new file mode 100644 index 000000000..368a43a16 --- /dev/null +++ b/tbx/static_src/sass/components/_related-courses.scss @@ -0,0 +1,5 @@ +.related-courses { + @include media-query(x-large) { + margin-left: $variable-gutter--medium; + } +} diff --git a/tbx/static_src/sass/components/_title-block.scss b/tbx/static_src/sass/components/_title-block.scss index 8d889f1e8..c61f90a1d 100644 --- a/tbx/static_src/sass/components/_title-block.scss +++ b/tbx/static_src/sass/components/_title-block.scss @@ -45,6 +45,10 @@ span { color: var(--color--accent); } + + &--course { + max-width: 900px; + } } &__tags, @@ -75,4 +79,100 @@ &__screen-reader-filter-description { @include hidden(); } + + // Additional fields on course detail + &__course-detail { + margin: 0 0 20px 0; + + @include media-query(medium) { + line-height: 80px; + } + + @include media-query(large) { + margin: 0 $variable-gutter--small 20px $variable-gutter--medium; + } + } + + &__sessions { + @include font-size(l); + line-height: 38px; + font-weight: $weight--normal; + color: var(--color--dark-indigo); + + @include media-query(large) { + line-height: 49px; + } + + // Nicer divider + span { + display: inline-block; + position: relative; + margin: 0 10px; + + @include media-query(large) { + margin: 0 15px; + } + + &::before { + content: ''; + position: absolute; + left: 0; + top: -22px; + width: 1px; + height: 27px; + background-color: currentColor; + + @include media-query(large) { + top: -29px; + height: 35px; + } + } + } + } + + &__intro { + color: var(--color--grey); + line-height: 27px; + max-width: 700px; + + p { + &:last-of-type { + margin-bottom: 5px; + } + } + } + + &__link { + @include font-size(s); + line-height: 27px; + color: var(--color--indigo); + font-weight: $weight--bold; + display: inline-block; + position: relative; + text-decoration: underline; + text-decoration-color: var(--color--coral); + text-underline-offset: 5px; + border: 0; + + &:focus, + &:hover { + text-decoration-thickness: 5px; + } + + &::after { + content: ''; + display: block; + position: absolute; + right: -21px; + top: 5px; + width: 15px; + height: 14px; + background-color: var(--color--coral); + clip-path: $arrow-path; + + @include hcm() { + filter: invert(1); + } + } + } } diff --git a/tbx/static_src/sass/main.scss b/tbx/static_src/sass/main.scss index 23d1d9c36..b64e7a4b6 100644 --- a/tbx/static_src/sass/main.scss +++ b/tbx/static_src/sass/main.scss @@ -28,12 +28,16 @@ @import 'components/contact-block'; @import 'components/contact-slim'; @import 'components/cookie-message'; +@import 'components/course-grid'; +@import 'components/course-grid-item'; +@import 'components/course-outline'; @import 'components/careers'; @import 'components/cta'; @import 'components/embed-cta'; @import 'components/email-signup'; @import 'components/error-hero'; @import 'components/events'; +@import 'components/external-link-cta'; @import 'components/filter'; @import 'components/footer'; @import 'components/header'; @@ -67,6 +71,7 @@ @import 'components/raw-html-block'; @import 'components/reason'; @import 'components/related-content'; +@import 'components/related-courses'; @import 'components/related-item'; @import 'components/report-hero'; @import 'components/report-in-page-nav';