Merge branch 'pni-2022' into merge-main-into-pni
This commit is contained in:
Коммит
1d020fbf66
|
@ -114,7 +114,7 @@ network-api/media/
|
|||
*.psql
|
||||
|
||||
# Compiled Frontend
|
||||
network-api/networkapi/frontend/*
|
||||
network-api/networkapi/frontend/
|
||||
|
||||
# PyCharm
|
||||
.idea/
|
||||
|
|
|
@ -30,6 +30,21 @@ React is used _à la carte_ for isolated component instances (eg: a tab switcher
|
|||
|
||||
To add a React component, you can target a container element from `/source/js/main.js` and inject it.
|
||||
|
||||
## HTMX
|
||||
|
||||
We have added the [`htmx.org`](https://htmx.org) javascript library to the project.
|
||||
`htmx` allows us to combine server rendered HTML (as we have with Django templates) with dynamic updates on the frontend.
|
||||
|
||||
The main workflow of `htmx` is the following:
|
||||
|
||||
1. A trigger (e.g. button click) leads to an AJAX request to the backend (Django),
|
||||
2. Django renders a template in response to the AJAX request and sends the HTML back,
|
||||
3. `htmx` injects the HTML response into the DOM.
|
||||
|
||||
This workflow fits particularly nice into a Django project like this, because most of this can be configured with a few HTML attributes.
|
||||
|
||||
For more information see the [`htmx` docs](https://htmx.org/docs/).
|
||||
|
||||
## Django and Wagtail
|
||||
|
||||
Django powers the backend of the site, and we use Wagtail with Django to provide CMS features and functionality.
|
||||
|
|
|
@ -48,6 +48,10 @@ const sources = {
|
|||
source: `buyers-guide/search.js`,
|
||||
bundle: true,
|
||||
},
|
||||
"bg-editorial-content-index": {
|
||||
source: `buyers-guide/editorial-content-index.js`,
|
||||
bundle: true,
|
||||
},
|
||||
"research-hub-library": {
|
||||
source: `foundation/pages/research-hub-library.js`,
|
||||
bundle: true,
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
<a href="{% relocalized_url page.localized.url %}" class="tw-group tw-block hover:tw-no-underline">
|
||||
<p class="tw-h4-heading d-inline-block tw-mb-1 medium:tw-my-0 group-hover:tw-underline">{{ page.localized.title }}</p>
|
||||
<p class="tw-text-xs medium:tw-text-lg tw-body tw-line-clamp-3 tw-leading-3 medium:tw-leading-6 medium:tw-mt-2">{{ page.localized.get_meta_description }}</p>
|
||||
<p class="tw-text-xs medium:tw-text-lg tw-body tw-line-clamp-3 tw-leading-3 medium:tw-leading-6 medium:tw-mt-2 tw-mb-0">{{ page.localized.get_meta_description }}</p>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,45 +1,43 @@
|
|||
{% load static wagtailcore_tags wagtailimages_tags %}
|
||||
|
||||
<div>
|
||||
<div class="
|
||||
tw-bg-gradient-to-b tw-from-yellow-10 tw-to-purple-05
|
||||
tw-rounded-2xl
|
||||
tw-p-[24px] {% if not large %} medium:tw-p-[32px] {% else %} medium:tw-p-[40px] {% endif %}
|
||||
">
|
||||
{% if icon %}
|
||||
{# Use image size twice as large as actual rendering size for better image quality. #}
|
||||
{% image icon height-112 as img %}
|
||||
<img
|
||||
src="{{ img.url }}"
|
||||
class="
|
||||
tw-mb-4
|
||||
{% if not large %}
|
||||
tw-max-h-[48px] medium:tw-max-h-[56px]
|
||||
-tw-mt-[44px] medium:-tw-mt-[56px] {# Negative margin to pull the icon up to the top and bleed 20px and 24px on medium+ screens. On large variant we don't need that. #}
|
||||
{% else %}
|
||||
tw-max-h-[36px] medium:tw-max-h-[48px]
|
||||
{% endif %}
|
||||
"
|
||||
/>
|
||||
{% endif %}
|
||||
<div class="
|
||||
tw-bg-gradient-to-b tw-from-yellow-10 tw-to-purple-05
|
||||
tw-rounded-2xl
|
||||
tw-p-[24px] {% if not large %} medium:tw-p-[32px] {% else %} medium:tw-p-[40px] {% endif %}
|
||||
">
|
||||
{% if icon %}
|
||||
{# Use image size twice as large as actual rendering size for better image quality. #}
|
||||
{% image icon height-112 as img %}
|
||||
<img
|
||||
src="{{ img.url }}"
|
||||
class="
|
||||
tw-mb-4
|
||||
{% if not large %}
|
||||
tw-max-h-[48px] medium:tw-max-h-[56px]
|
||||
-tw-mt-[44px] medium:-tw-mt-[56px] {# Negative margin to pull the icon up to the top and bleed 20px and 24px on medium+ screens. On large variant we don't need that. #}
|
||||
{% else %}
|
||||
tw-max-h-[36px] medium:tw-max-h-[48px]
|
||||
{% endif %}
|
||||
"
|
||||
/>
|
||||
{% endif %}
|
||||
|
||||
{% if heading %}
|
||||
<div class="tw-h3-heading tw-mb-2">
|
||||
{{ heading }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if heading %}
|
||||
<div class="tw-h3-heading tw-mb-2">
|
||||
{{ heading }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if body %}
|
||||
{# Arbitrary variant to override the global paragraph style. #}
|
||||
<div class="[&_.rich-text_p]:tw-text-base">
|
||||
{{ body|richtext }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if body %}
|
||||
{# Arbitrary variant to override the global paragraph style. #}
|
||||
<div class="[&_.rich-text_p]:tw-text-base">
|
||||
{{ body|richtext }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if link_text and link_href %}
|
||||
<div class="tw-mt-6">
|
||||
{% include "./arrow_link.html" with link_href=link_href link_text=link_text %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if link_text and link_href %}
|
||||
<div class="tw-mt-6">
|
||||
{% include "./arrow_link.html" with link_href=link_href link_text=link_text %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
{% load i18n querystring_tag wagtailroutablepage_tags %}
|
||||
|
||||
{% for item in items %}
|
||||
<li>
|
||||
{% include "fragments/buyersguide/article_card_horizontal.html" with page=item %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
<li id="index-navigation" class="tw-border-none tw-flex tw-flex-row">
|
||||
{% if not show_load_more_button_immediately %}
|
||||
<nav id="pagination">
|
||||
{% include "fragments/pagination.html" with page=items %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
{% if items.has_next %}
|
||||
<nav id="load-more" {% if not show_load_more_button_immediately %}class="tw-hidden"{% endif %}>
|
||||
<button
|
||||
class="tw-btn-primary"
|
||||
|
||||
{% comment %}
|
||||
Get the next page of items (rendered by this template).
|
||||
Replace the entire list item that currently contains the pagination and load more button with the response.
|
||||
If there is yet another page, the another load more button will be rendered.
|
||||
{% endcomment %}
|
||||
data-hx-get="{% routablepageurl index_page 'items' %}{% querystring 'page'=items.next_page_number %}" {# AJAX GET request to the next page #}
|
||||
data-hx-target="#index-navigation"
|
||||
data-hx-swap="outerHTML"
|
||||
>{% trans "Load more results" %}</button>
|
||||
</nav>
|
||||
{% endif %}
|
||||
</li>
|
|
@ -1,5 +0,0 @@
|
|||
{% extends "./editorial_content_list_base_card.html" %}
|
||||
|
||||
{% block content %}
|
||||
{% include "./article_card_horizontal.html" with page=page %}
|
||||
{% endblock content %}
|
|
@ -1,3 +0,0 @@
|
|||
<div class="tw-m-0 tw-p-0 tw-py-5">
|
||||
{% block content %}{% endblock content %}
|
||||
</div>
|
|
@ -1,15 +1,10 @@
|
|||
{% extends "./editorial_content_list_base_card.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<div>
|
||||
<a href="{{ update.source }}" class="tw-group tw-block hover:tw-no-underline">
|
||||
<p class="tw-h4-heading d-inline-block tw-mb-1 medium:tw-my-0 group-hover:tw-underline">{{ update.localized.title }}</p>
|
||||
<a href="{{ update.source }}" class="tw-text-blue-40 tw-text-xs tw-font-bold tw-uppercase tw-pt-0 medium:tw-pt-1 tw-pb-2 medium:tw-pb-1 tw-leading-5 tw-flex tw-no-underline">
|
||||
{{update.author}}
|
||||
<img src="{% static "_images/buyers-guide/product-update-card-external-link.svg" %}" class="tw-ml-2" />
|
||||
</a>
|
||||
<p class="tw-text-xs medium:tw-text-lg tw-body tw-line-clamp-3 tw-leading-3 medium:tw-leading-6">{{ update.snippet }}</p>
|
||||
</a>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
<a href="{{ update.source }}" class="tw-group tw-block hover:tw-no-underline">
|
||||
<p class="tw-h4-heading d-inline-block tw-mb-1 medium:tw-my-0 group-hover:tw-underline">{{ update.localized.title }}</p>
|
||||
<span class="tw-text-blue-40 tw-text-xs tw-font-bold tw-uppercase tw-pt-0 medium:tw-pt-1 tw-pb-2 medium:tw-pb-1 tw-leading-5 tw-flex tw-no-underline">
|
||||
{{ update.author }}
|
||||
<img src="{% static "_images/buyers-guide/product-update-card-external-link.svg" %}" class="tw-ml-2" />
|
||||
</span>
|
||||
<p class="tw-text-xs medium:tw-text-lg tw-body tw-line-clamp-3 tw-leading-3 medium:tw-leading-6">{{ update.snippet }}</p>
|
||||
</a>
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
{% load static i18n %}
|
||||
<ul
|
||||
id="pni-creepiness"
|
||||
class="
|
||||
tw-list-none
|
||||
tw-relative
|
||||
tw-cursor-pointer
|
||||
tw-m-0 tw-p-0 tw-ml-2
|
||||
tw-flex
|
||||
tw-items-center
|
||||
tw-w-[250px]
|
||||
"
|
||||
>
|
||||
<li
|
||||
role="button"
|
||||
id="pni-creepiness__selected"
|
||||
class="tw-overflow-hidden tw-whitespace-nowrap tw-w-full tw-text-ellipsis tw-flex tw-items-center tw-m-0 tw-p-3"
|
||||
tabindex="0"
|
||||
>
|
||||
<div id="pni-creepiness__selected-text" class="tw-flex tw-items-center tw-text-sm tw-font-bold tw-text-black tw-font-sans tw-mr-auto">
|
||||
<img src="{% static "_images/buyers-guide/cry-face.svg" %}" class="tw-mr-2" />
|
||||
{% trans "Creepiness: Least – Most" %}
|
||||
</div>
|
||||
|
||||
<svg width="11" height="7" viewBox="0 0 11 7" fill="none" xmlns="http://www.w3.org/2000/svg" class="tw-ml-2 tw-origin-center">
|
||||
<title>{% trans "Open drop down" %}</title>
|
||||
<path d="M1 1L5.02504 5.02504L9.05007 1" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</li>
|
||||
|
||||
|
||||
<li aria-expanded="false" role="list" class="tw-absolute tw-top-0 tw-hidden tw-left-0 pni-creepiness__list-container tw-w-[250px] tw-z-50 tw-bg-white tw-border tw-border-gray-20">
|
||||
<ul class="pni-creepiness__list tw-w-full tw-list-none tw-m-0 tw-p-0">
|
||||
<li class="pni-creepiness__list-item tw-whitespace-nowrap tw-p-3 tw-text-sm tw-font-bold tw-group tw-font-sans tw-flex tw-items-center tw-overflow-ellipsis" tabindex="0" id="option-1" data-value="ASCENDING">
|
||||
<div class="tw-flex tw-items-center tw-text-sm tw-font-bold tw-text-gray-40 group-hover:tw-text-black tw-font-sans tw-pointer-events-none">
|
||||
<img src="{% static "_images/buyers-guide/cry-face.svg" %}" class="tw-mr-2" />
|
||||
{% trans "Creepiness: Least – Most" %}
|
||||
</div>
|
||||
<svg width="11" height="7" viewBox="0 0 11 7" fill="none" xmlns="http://www.w3.org/2000/svg" class="tw-ml-4 tw-rotate-180 tw-origin-center tw-pointer-events-none">
|
||||
<title>{% trans "Close drop down" %}</title>
|
||||
<path d="M1 1L5.02504 5.02504L9.05007 1" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</li>
|
||||
<li class="pni-creepiness__list-item tw-whitespace-nowrap tw-p-3 tw-text-sm tw-font-bold tw-group tw-font-sans tw-flex tw-items-center tw-overflow-ellipsis" tabindex="0" id="option-2" data-value="DESCENDING">
|
||||
<div class="tw-flex tw-items-center tw-text-sm tw-font-bold tw-text-gray-40 group-hover:tw-text-black tw-font-sans tw-pointer-events-none">
|
||||
<img src="{% static "_images/buyers-guide/shock-face.svg" %}" class="tw-mr-2" />
|
||||
{% trans "Creepiness: Most – Least" %}
|
||||
</div>
|
||||
</li>
|
||||
<li class="pni-creepiness__list-item tw-whitespace-nowrap tw-p-3 tw-text-sm tw-font-bold tw-group tw-font-sans tw-flex tw-items-center tw-overflow-ellipsis" tabindex="0" id="option-3" data-value="ALPHA">
|
||||
<div class="tw-flex tw-items-center tw-text-sm tw-font-bold tw-text-gray-40 group-hover:tw-text-black tw-font-sans tw-pointer-events-none mr-auto">
|
||||
<img src="{% static "_images/buyers-guide/alpha-emoji.png" %}" class="tw-mr-2" />
|
||||
{% trans "Alphabetical" %}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
</ul>
|
|
@ -3,7 +3,7 @@
|
|||
{% if page.has_other_pages %}
|
||||
<div class="tw-flex tw-flex-row tw-flex-wrap tw-gap-5">
|
||||
<a
|
||||
{% if page.has_previous %}
|
||||
{% if page.has_previous %}
|
||||
href="{{ request.path }}{% querystring 'page'=page.previous_page_number %}"
|
||||
{% else %}
|
||||
disabled
|
||||
|
@ -13,7 +13,7 @@
|
|||
{% translate 'Prev. page' context 'Pagination link' %}
|
||||
</a>
|
||||
<a
|
||||
{% if page.has_next %}
|
||||
{% if page.has_next %}
|
||||
href="{{ request.path }}{% querystring 'page'=page.next_page_number %}"
|
||||
{% else %}
|
||||
disabled
|
|
@ -53,31 +53,35 @@
|
|||
</div>
|
||||
|
||||
<div class="row px-0 tw-mb-6 medium:tw-mb-4">
|
||||
<div class="tw-w-[98vw] tw-relative tw-left-1/2 tw-ml-[-51vw] large:tw-left-auto large:tw-ml-0 large:tw-static large:tw-w-full">
|
||||
<div class="tw-px-4 large:tw-px-0 tw-flex tw-space-x-2 tw-overflow-auto tw-pb-2 tw-no-scrollbar tw-touch-pan-x subcategory-header">
|
||||
|
||||
<div class="tw-w-[98vw] tw-flex tw-relative tw-left-1/2 tw-ml-[-51vw] tw-flex-wrap large:tw-flex-nowrap large:tw-left-auto large:tw-ml-0 large:tw-static large:tw-w-full">
|
||||
<div class="tw-px-4 large:tw-px-0 tw-flex tw-items-end tw-space-x-2 tw-overflow-auto tw-pb-2 tw-no-scrollbar tw-touch-pan-x tw-w-full large:tw-w-auto large:w-auto subcategory-header">
|
||||
|
||||
<span id="product-filter-pni" class="tw-flex tw-cursor-pointer tw-text-gray-60 border tw-border-gray-20 tw-px-2 tw-py-1 tw-font-sans tw-rounded-3xl tw-font-normal tw-text-[12px] tw-leading-[1.3] hover:tw-border-blue-10 hover:tw-bg-blue-10">
|
||||
<input type="checkbox" id="product-filter-pni-toggle" autocomplete="off">
|
||||
<span class="pni-icon tw-mr-1"> </span>
|
||||
<label for="product-filter-pni-toggle" class="tw-flex tw-m-0 tw-w-max">{% trans "Has privacy ding" %} </label>
|
||||
</span>
|
||||
|
||||
|
||||
{% for cat in categories %}
|
||||
{% with original=cat.original selected_classes="active tw-bg-gray-80 tw-text-white tw-border-gray-80" default_classes="hover:tw-border-blue-10 hover:tw-bg-blue-10 tw-text-gray-60 tw-border-gray-20 tw-bg-white" tailwind_classes="tw-no-underline border tw-px-2 tw-py-1 tw-font-sans tw-rounded-3xl tw-font-normal tw-text-[12px] tw-leading-[1.3] tw-whitespace-nowrap" %}
|
||||
{% if cat.parent != None %}
|
||||
{% if original.published_product_page_count > 0 %}
|
||||
{% localizedroutablepageurl home_page 'category-view' original.slug as cat_url %}
|
||||
<a class="{% if current_category.name != cat.parent.name and current_category.parent.name != cat.parent.name %} tw-hidden {% endif %} subcategories {{ tailwind_classes }} {% if current_category.name == cat.name %}{{ selected_classes }}{% else %}{{ default_classes }}{% endif %}"
|
||||
href="{{ cat_url }}"
|
||||
data-parent="{{ cat.parent.localized.name }}"
|
||||
data-name="{{ cat.name }}">
|
||||
{{ cat.name }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if cat.parent != None %}
|
||||
{% if original.published_product_page_count > 0 %}
|
||||
{% localizedroutablepageurl home_page 'category-view' original.slug as cat_url %}
|
||||
<a class="{% if current_category.name != cat.parent.name and current_category.parent.name != cat.parent.name %} tw-hidden {% endif %} subcategories {{ tailwind_classes }} {% if current_category.name == cat.name %}{{ selected_classes }}{% else %}{{ default_classes }}{% endif %}"
|
||||
href="{{ cat_url }}"
|
||||
data-parent="{{ cat.parent.localized.name }}"
|
||||
data-name="{{ cat.name }}">
|
||||
{{ cat.name }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="tw-px-4 large:tw-ml-auto tw-flex tw-items-center">
|
||||
<span class="tw-text-gray-80 tw-font-sans tw-text-xs">{% trans "Sort by " %}</span>
|
||||
{% include "fragments/buyersguide/pni_sort_dropdown.html" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,46 +1,82 @@
|
|||
{% extends "pages/buyersguide/base.html" %}
|
||||
{% load wagtailcore_tags %}
|
||||
{% load wagtailcore_tags wagtailimages_tags localization %}
|
||||
|
||||
{% load i18n localization static wagtailcore_tags wagtailimages_tags %}
|
||||
|
||||
{% block category_nav %}{% endblock category_nav %}
|
||||
|
||||
{% block guts %}
|
||||
<div class="tw-container tw-my-6">
|
||||
<h1>{{ page.title }}</h1>
|
||||
<div class="tw-grid tw-grid-cols-12 tw-divide-y tw-divide-gray-20 tw-gap-x-5">
|
||||
|
||||
{# Content wrapper #}
|
||||
<div class="tw-grid tw-grid-cols-12 large:tw-gap-x-6">
|
||||
|
||||
{# Index wrapper #}
|
||||
<div class="tw-col-span-12 large:tw-col-span-8">
|
||||
{% if items %}
|
||||
{% for item in items %}
|
||||
<div class="tw-col-span-12 large:tw-col-span-8">
|
||||
{% with type=item.specific_class.get_verbose_name|lower %}
|
||||
{% if type == "buyers guide article page" %}
|
||||
{% include "fragments/buyersguide/editorial_content_list_article_card.html" with page=item %}
|
||||
{% else %}
|
||||
{% include "fragments/buyersguide/editorial_content_list_product_update_card.html" with update=item %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<ul
|
||||
id="items-list"
|
||||
class="
|
||||
tw-flex
|
||||
tw-flex-col
|
||||
tw-list-none
|
||||
tw-p-0
|
||||
tw-divide-y
|
||||
tw-divide-gray-20
|
||||
[&>li]:tw-m-0
|
||||
[&>li]:tw-py-5
|
||||
{# Starting with the 4th li child that is not the mobile aside, set order #}
|
||||
[&>li:not(#mobile-aside):nth-child(n+4)]:tw-order-1
|
||||
"
|
||||
>
|
||||
{% include "fragments/buyersguide/editorial_content_index_items.html" with index_page=page items=items %}
|
||||
|
||||
{# Mobile aside #}
|
||||
{% with cta=featured_cta related_articles=page.get_related_articles %}
|
||||
{% if cta or related_articles %}
|
||||
<li id="mobile-aside" class="large:tw-hidden">
|
||||
<aside class="tw-flex tw-flex-col medium:tw-flex-row tw-gap-6 tw-py-5">
|
||||
{% if cta %}
|
||||
<div class="tw-basis-1/2">
|
||||
{% include "fragments/buyersguide/call_to_action_box.html" with icon=cta.sticker_image heading=cta.title body=cta.content link_text=cta.link_label link_href=cta.get_target_url %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if related_articles %}
|
||||
<div class="tw-basis-1/2">
|
||||
{% trans "Popular" as heading %}
|
||||
{% include "fragments/buyersguide/related_reading.html" with articles=related_articles heading=heading %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</aside>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
<div class="tw-col-span-12 large:tw-col-span-8 tw-border-none">
|
||||
{% include "fragments/buyersguide/pni_newsletter_box.html" %}
|
||||
</div>
|
||||
<div class="tw-col-span-12 large:tw-col-span-4 tw-row-start-4 large:tw-row-start-1 tw-col-start-1 large:tw-col-start-9 large:tw-border-none">
|
||||
</div>
|
||||
|
||||
{% include "fragments/buyersguide/pni_newsletter_box.html" %}
|
||||
</div>
|
||||
|
||||
{# Desktop aside #}
|
||||
{% with cta=featured_cta related_articles=page.get_related_articles %}
|
||||
{% if cta or related_articles %}
|
||||
<aside class="tw-hidden large:tw-flex tw-col-span-4 tw-flex-col tw-gap-6">
|
||||
{% if cta %}
|
||||
{% include "fragments/buyersguide/call_to_action_box.html" with icon=cta.sticker_image heading=cta.title body=cta.content link_text=cta.link_label link_href=cta.get_target_url %}
|
||||
{% endif %}
|
||||
|
||||
{% if related_articles %}
|
||||
{% trans "Popular" as heading %}
|
||||
{% include "fragments/buyersguide/related_reading.html" with articles=related_articles heading=heading %}
|
||||
{% endif %}
|
||||
</aside>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% with related_articles=page.get_related_articles %}
|
||||
{% if related_articles %}
|
||||
<div class="tw-m-4">
|
||||
{% include "fragments/buyersguide/related_reading.html" with articles=related_articles %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% with cta=featured_cta %}
|
||||
{% if cta %}
|
||||
{% include "fragments/buyersguide/call_to_action_box.html" with icon=cta.sticker_image heading=cta.title body=cta.content link_text=cta.link_label link_href=cta.get_target_url %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endblock guts %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
{{ block.super }}
|
||||
<script src="{% static "_js/bg-editorial-content-index.compiled.js" %}" async defer></script>
|
||||
{% endblock extra_scripts %}
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
from django.db import models
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django.db import models
|
||||
|
||||
|
||||
def get_related_items(
|
||||
queryset: models.QuerySet,
|
||||
queryset: 'models.QuerySet',
|
||||
related_item_field: str
|
||||
) -> list[models.Model]:
|
||||
) -> list['models.Model']:
|
||||
"""
|
||||
Return list of the related items from the queryset.
|
||||
|
||||
|
|
|
@ -179,7 +179,7 @@ class ProductPageFactory(PageFactory):
|
|||
@post_generation
|
||||
def set_random_creepiness(self, create, extracted, **kwargs):
|
||||
self.get_or_create_votes()
|
||||
single_vote = [0, 0, 0, 0, 0]
|
||||
single_vote = [0, 0, 1, 0, 0]
|
||||
shuffle(single_vote)
|
||||
self.votes.set_votes(single_vote)
|
||||
self.creepiness_value = randint(0, 100)
|
||||
|
@ -399,7 +399,7 @@ def generate(seed):
|
|||
for _ in range(3):
|
||||
BuyersGuideContentCategoryFactory()
|
||||
articles = []
|
||||
for _ in range(7):
|
||||
for _ in range(12):
|
||||
article = BuyersGuideArticlePageFactory(parent=editorial_content_index)
|
||||
for profile in get_random_objects(pagemodels.Profile, max_count=3):
|
||||
BuyersGuideArticlePageAuthorProfileRelationFactory(
|
||||
|
|
|
@ -1,22 +1,27 @@
|
|||
import typing
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from django import shortcuts
|
||||
from django.core import paginator
|
||||
from django.db import models
|
||||
from modelcluster.fields import ParentalKey
|
||||
from wagtail.admin.edit_handlers import PageChooserPanel, InlinePanel, MultiFieldPanel
|
||||
from wagtail.admin.edit_handlers import PageChooserPanel, InlinePanel
|
||||
from wagtail.core import models as wagtail_models
|
||||
from wagtail.core.models import Orderable, TranslatableMixin
|
||||
from networkapi.wagtailpages.pagemodels.buyersguide.utils import get_buyersguide_featured_cta
|
||||
from wagtail.contrib.routable_page import models as routable_models
|
||||
|
||||
from networkapi.wagtailpages.pagemodels.buyersguide.utils import get_buyersguide_featured_cta
|
||||
from networkapi.utility import orderables
|
||||
from networkapi.wagtailpages.pagemodels.mixin import foundation_metadata
|
||||
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from networkapi.wagtailpages.models import BuyersGuideArticlePage
|
||||
if TYPE_CHECKING:
|
||||
from django import http
|
||||
from networkapi.wagtailpages import models as pagemodels
|
||||
|
||||
|
||||
class BuyersGuideEditorialContentIndexPage(
|
||||
foundation_metadata.FoundationMetadataPageMixin,
|
||||
routable_models.RoutablePageMixin,
|
||||
wagtail_models.Page,
|
||||
):
|
||||
parent_page_types = ['wagtailpages.BuyersGuidePage']
|
||||
|
@ -24,27 +29,67 @@ class BuyersGuideEditorialContentIndexPage(
|
|||
template = 'pages/buyersguide/editorial_content_index_page.html'
|
||||
|
||||
content_panels = wagtail_models.Page.content_panels + [
|
||||
MultiFieldPanel(
|
||||
[
|
||||
InlinePanel(
|
||||
'related_article_relations',
|
||||
heading='Related articles',
|
||||
label='Article',
|
||||
max_num=3,
|
||||
),
|
||||
],
|
||||
heading='Related Articles',
|
||||
InlinePanel(
|
||||
'related_article_relations',
|
||||
heading='Popular articles',
|
||||
label='Article',
|
||||
max_num=3,
|
||||
),
|
||||
]
|
||||
|
||||
items_per_page: int = 10
|
||||
|
||||
@routable_models.route('items/', name='items')
|
||||
def items_route(self, request: 'http.HttpRequest') -> 'http.HttpResponse':
|
||||
'''
|
||||
Route to return only the content index items.
|
||||
|
||||
This route does not return a full page, but only an HTML fragment of list items
|
||||
that is meant to be requested with AJAX and used to extend an existing list of
|
||||
items.
|
||||
|
||||
'''
|
||||
items = self.get_paginated_items(page=request.GET.get('page'))
|
||||
return shortcuts.render(
|
||||
request=request,
|
||||
template_name='fragments/buyersguide/editorial_content_index_items.html',
|
||||
context={
|
||||
'index_page': self,
|
||||
'items': items,
|
||||
'show_load_more_button_immediately': True,
|
||||
},
|
||||
)
|
||||
|
||||
def get_context(self, request, *args, **kwargs):
|
||||
context = super().get_context(request, *args, **kwargs)
|
||||
context["home_page"] = self.get_parent().specific
|
||||
context["featured_cta"] = get_buyersguide_featured_cta(self)
|
||||
context["items"] = self.get_descendants().public().live().specific()
|
||||
context["items"] = self.get_paginated_items(request.GET.get('page'))
|
||||
return context
|
||||
|
||||
def get_related_articles(self) -> list['BuyersGuideArticlePage']:
|
||||
def get_paginated_items(
|
||||
self,
|
||||
page: Optional[int] = None
|
||||
) -> 'paginator.Page[pagemodels.BuyersGuideArticlePage]':
|
||||
"""Get a page of items to list in the index."""
|
||||
items = self.get_items()
|
||||
items_paginator = paginator.Paginator(
|
||||
object_list=items,
|
||||
per_page=self.items_per_page,
|
||||
)
|
||||
return items_paginator.get_page(page)
|
||||
|
||||
def get_items(self) -> 'models.QuerySet[pagemodels.BuyersGuideArticlePage]':
|
||||
"""Get items to list in the index."""
|
||||
return (
|
||||
self.get_descendants()
|
||||
.order_by("-first_published_at")
|
||||
.public()
|
||||
.live()
|
||||
.specific()
|
||||
)
|
||||
|
||||
def get_related_articles(self) -> list['pagemodels.BuyersGuideArticlePage']:
|
||||
return orderables.get_related_items(
|
||||
self.related_article_relations.all(),
|
||||
'article',
|
||||
|
|
|
@ -78,7 +78,7 @@
|
|||
</ul>
|
||||
<div>
|
||||
{# PAGINATION #}
|
||||
{% include "wagtailpages/fragments/pagination.html" with page=research_detail_pages %}
|
||||
{% include "fragments/pagination.html" with page=research_detail_pages %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,9 +1,20 @@
|
|||
import contextlib
|
||||
import datetime
|
||||
from http import HTTPStatus
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from unittest import mock
|
||||
|
||||
import bs4
|
||||
from django import test
|
||||
from django.utils import timezone
|
||||
|
||||
from networkapi.wagtailpages.tests import base as test_base
|
||||
from networkapi.wagtailpages import models as pagemodels
|
||||
from networkapi.wagtailpages.factory import buyersguide as buyersguide_factories
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django.core.handlers import wsgi
|
||||
|
||||
|
||||
class BuyersGuideEditorialContentIndexPageFactoryTest(test_base.WagtailpagesTestCase):
|
||||
def test_factory(self):
|
||||
|
@ -14,6 +25,7 @@ class BuyersGuideEditorialContentIndexPageTest(test_base.WagtailpagesTestCase):
|
|||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
super().setUpTestData()
|
||||
|
||||
cls.pni_homepage = buyersguide_factories.BuyersGuidePageFactory(
|
||||
parent=cls.homepage,
|
||||
)
|
||||
|
@ -21,6 +33,55 @@ class BuyersGuideEditorialContentIndexPageTest(test_base.WagtailpagesTestCase):
|
|||
parent=cls.pni_homepage,
|
||||
)
|
||||
|
||||
cls.request_factory = test.RequestFactory()
|
||||
|
||||
def create_request(self, data: Optional[dict] = None) -> 'wsgi.WSGIRequest':
|
||||
return self.request_factory.get(path=self.content_index.url, data=data)
|
||||
|
||||
def create_days_old_article(self, days: int):
|
||||
return buyersguide_factories.BuyersGuideArticlePageFactory(
|
||||
parent=self.content_index,
|
||||
first_published_at=timezone.now() - datetime.timedelta(days=days),
|
||||
)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def setup_content_index_with_pages_of_children(self):
|
||||
"""
|
||||
Context manager setting up the content index with child pages.
|
||||
|
||||
This is implemented as a context manager to allow us to mock / override the
|
||||
`items_per_page` attribute on the content index regardless of how often the
|
||||
page instance is created. This is important for integration tests with the
|
||||
Django test client, because the instance of the content index you set up with
|
||||
the factory is not the same that the client is hitting. A new instance is
|
||||
instantiated with the data from the datbase. Since `items_per_page` is not
|
||||
saved in the database it would be different on that instance. Mocking allows
|
||||
us to set a value we want to use for the test.
|
||||
|
||||
Use this method as a context manager in a `with` statement. The generated child
|
||||
pages are bound to the name after `as`. Example usage:
|
||||
|
||||
```
|
||||
with self.setup_content_index_with_pages_of_children() as children:
|
||||
...
|
||||
```
|
||||
|
||||
Within that `with` block, the `items_per_page` on the context index page are
|
||||
set to 3 and the generated child pages are available as `children`.
|
||||
|
||||
"""
|
||||
self.items_per_page = 3
|
||||
with mock.patch('networkapi.wagtailpages.models.BuyersGuideEditorialContentIndexPage.items_per_page', 3):
|
||||
self.content_index.items_per_page = self.items_per_page
|
||||
articles = []
|
||||
# Create 2 more items then fit on page to check they are not in the page
|
||||
for days_old in range(self.items_per_page + 2):
|
||||
articles.append(self.create_days_old_article(days_old))
|
||||
yield articles
|
||||
|
||||
def get_items_route_url(self):
|
||||
return self.content_index.url + self.content_index.reverse_subpage('items')
|
||||
|
||||
def test_parents(self):
|
||||
self.assertAllowedParentPageTypes(
|
||||
child_model=pagemodels.BuyersGuideEditorialContentIndexPage,
|
||||
|
@ -38,7 +99,8 @@ class BuyersGuideEditorialContentIndexPageTest(test_base.WagtailpagesTestCase):
|
|||
|
||||
self.assertEqual(response.status_code, HTTPStatus.OK)
|
||||
|
||||
def test_template(self):
|
||||
def test_templates_used(self):
|
||||
# Needs to use the client to test the templates
|
||||
response = self.client.get(self.content_index.url)
|
||||
|
||||
self.assertTemplateUsed(
|
||||
|
@ -54,7 +116,7 @@ class BuyersGuideEditorialContentIndexPageTest(test_base.WagtailpagesTestCase):
|
|||
template_name='pages/base.html',
|
||||
)
|
||||
|
||||
def test_children_titles_shown(self):
|
||||
def test_serve_shows_children_titles(self):
|
||||
children = []
|
||||
for _ in range(5):
|
||||
children.append(
|
||||
|
@ -63,11 +125,155 @@ class BuyersGuideEditorialContentIndexPageTest(test_base.WagtailpagesTestCase):
|
|||
)
|
||||
)
|
||||
|
||||
response = self.client.get(self.content_index.url)
|
||||
response = self.client.get(path=self.content_index.url)
|
||||
|
||||
for child in children:
|
||||
self.assertContains(response=response, text=child.title, count=1)
|
||||
|
||||
def test_serve_paginated_items_page_1(self):
|
||||
with self.setup_content_index_with_pages_of_children() as articles:
|
||||
|
||||
response = self.client.get(self.content_index.url)
|
||||
|
||||
self.assertQuerysetEqual(
|
||||
response.context['items'],
|
||||
articles[:self.items_per_page],
|
||||
)
|
||||
|
||||
def test_serve_paginated_items_page_2(self):
|
||||
with self.setup_content_index_with_pages_of_children() as articles:
|
||||
|
||||
response = self.client.get(self.content_index.url, data={'page': 2})
|
||||
|
||||
self.assertQuerysetEqual(
|
||||
response.context['items'],
|
||||
articles[self.items_per_page:],
|
||||
)
|
||||
|
||||
def test_items_route_exists(self):
|
||||
route = self.content_index.reverse_subpage('items')
|
||||
|
||||
self.assertEqual(route, 'items/')
|
||||
|
||||
def test_items_route_template(self):
|
||||
url = self.get_items_route_url()
|
||||
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertTemplateUsed(
|
||||
response=response,
|
||||
template_name='fragments/buyersguide/editorial_content_index_items.html',
|
||||
)
|
||||
self.assertTemplateNotUsed(
|
||||
response=response,
|
||||
template_name='pages/buyersguide/editorial_content_index_page.html',
|
||||
)
|
||||
|
||||
def test_items_route_show_load_more_button_immediately(self):
|
||||
with self.setup_content_index_with_pages_of_children():
|
||||
url = self.get_items_route_url()
|
||||
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertTrue(response.context['show_load_more_button_immediately'])
|
||||
soup = bs4.BeautifulSoup(response.content, 'html.parser')
|
||||
# No pagination element in markup
|
||||
self.assertEqual(soup.find_all(id='pagination'), [])
|
||||
# But the load more element
|
||||
self.assertNotEqual(soup.find_all(id='load-more'), [])
|
||||
|
||||
def test_items_route_shows_children_titles(self):
|
||||
url = self.get_items_route_url()
|
||||
children = []
|
||||
for _ in range(5):
|
||||
children.append(
|
||||
buyersguide_factories.BuyersGuideArticlePageFactory(
|
||||
parent=self.content_index,
|
||||
)
|
||||
)
|
||||
|
||||
response = self.client.get(path=url)
|
||||
|
||||
for child in children:
|
||||
self.assertContains(response=response, text=child.title, count=1)
|
||||
|
||||
def test_items_route_paginated_items_page_1(self):
|
||||
with self.setup_content_index_with_pages_of_children() as articles:
|
||||
url = self.get_items_route_url()
|
||||
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertQuerysetEqual(
|
||||
response.context['items'],
|
||||
articles[:self.items_per_page],
|
||||
)
|
||||
# There are more page so there should be a load more button
|
||||
soup = bs4.BeautifulSoup(response.content, 'html.parser')
|
||||
self.assertNotEqual(soup.find_all(id='load-more'), [])
|
||||
|
||||
def test_items_route_paginated_items_page_2(self):
|
||||
with self.setup_content_index_with_pages_of_children() as articles:
|
||||
url = self.get_items_route_url()
|
||||
|
||||
response = self.client.get(url, data={'page': 2})
|
||||
|
||||
self.assertQuerysetEqual(
|
||||
response.context['items'],
|
||||
articles[self.items_per_page:],
|
||||
)
|
||||
# There are no more page so there should not be a load more button
|
||||
soup = bs4.BeautifulSoup(response.content, 'html.parser')
|
||||
self.assertEqual(soup.find_all(id='load-more'), [])
|
||||
|
||||
def test_get_context_featured_cta(self):
|
||||
featured_cta = buyersguide_factories.BuyersGuideCallToActionFactory()
|
||||
self.pni_homepage.call_to_action = featured_cta
|
||||
self.pni_homepage.save()
|
||||
|
||||
context = self.content_index.get_context(request=self.create_request())
|
||||
|
||||
self.assertEqual(context['featured_cta'], featured_cta)
|
||||
|
||||
def test_get_context_no_cta_set_on_homepage(self):
|
||||
self.pni_homepage.call_to_action = None
|
||||
self.pni_homepage.save()
|
||||
|
||||
context = self.content_index.get_context(request=self.create_request())
|
||||
|
||||
self.assertIsNone(context['featured_cta'])
|
||||
|
||||
def test_get_context_paginated_items_page_1(self):
|
||||
with self.setup_content_index_with_pages_of_children() as articles:
|
||||
|
||||
context = self.content_index.get_context(request=self.create_request())
|
||||
|
||||
self.assertQuerysetEqual(context['items'], articles[:self.items_per_page])
|
||||
|
||||
def test_get_context_paginated_items_page_2(self):
|
||||
with self.setup_content_index_with_pages_of_children() as articles:
|
||||
request = self.create_request(data={'page': '2'})
|
||||
|
||||
context = self.content_index.get_context(request=request)
|
||||
|
||||
self.assertQuerysetEqual(context['items'], articles[self.items_per_page:])
|
||||
|
||||
def test_get_items_ordered_by_publication_date(self):
|
||||
article_middle = self.create_days_old_article(days=10)
|
||||
article_oldest = self.create_days_old_article(days=20)
|
||||
article_newest = self.create_days_old_article(days=5)
|
||||
|
||||
result = self.content_index.get_items()
|
||||
|
||||
self.assertQuerysetEqual(
|
||||
qs=result,
|
||||
values=[
|
||||
article_newest,
|
||||
article_middle,
|
||||
article_oldest,
|
||||
],
|
||||
ordered=True,
|
||||
)
|
||||
|
||||
def test_get_related_articles(self):
|
||||
content_index = self.content_index
|
||||
article1 = buyersguide_factories.BuyersGuideArticlePageFactory()
|
||||
|
@ -96,21 +302,3 @@ class BuyersGuideEditorialContentIndexPageTest(test_base.WagtailpagesTestCase):
|
|||
related_articles,
|
||||
[article2, article1, article3],
|
||||
)
|
||||
|
||||
def test_featured_cta_in_context(self):
|
||||
self.pni_homepage.call_to_action = buyersguide_factories.BuyersGuideCallToActionFactory()
|
||||
self.pni_homepage.save()
|
||||
|
||||
response = self.client.get(self.content_index.url)
|
||||
|
||||
self.assertEqual(response.status_code, HTTPStatus.OK)
|
||||
self.assertIsNotNone(response.context['featured_cta'])
|
||||
|
||||
def test_context_with_no_home_page_cta_set(self):
|
||||
self.pni_homepage.call_to_action = None
|
||||
self.pni_homepage.save()
|
||||
|
||||
response = self.client.get(self.content_index.url)
|
||||
|
||||
self.assertEqual(response.status_code, HTTPStatus.OK)
|
||||
self.assertIsNone(response.context['featured_cta'])
|
||||
|
|
|
@ -5130,6 +5130,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"htmx.org": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-1.8.0.tgz",
|
||||
"integrity": "sha512-xdR2PeSmhftFhUKn/5DYDFRVF8DagJR9d7y3AK+gQzoAQ+08r+0shaCTo1HdXKGKhRfX/uL3rqj4ZwCBNf8tLw=="
|
||||
},
|
||||
"http-cache-semantics": {
|
||||
"version": "3.8.1",
|
||||
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz",
|
||||
|
|
|
@ -84,6 +84,7 @@
|
|||
"esbuild": "^0.12.24",
|
||||
"event-stream": "3.3.4",
|
||||
"gsap": "^3.9.1",
|
||||
"htmx.org": "^1.8.0",
|
||||
"locomotive-scroll": "^4.1.4",
|
||||
"moment": "^2.29.1",
|
||||
"npm-run-all": "^4.1.3",
|
||||
|
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 606 B |
|
@ -0,0 +1,11 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.0146 1.56856e-05C15.525 0.00806397 19.9744 4.48331 19.9604 10.0077C19.9483 15.5207 15.4873 19.9714 9.98327 19.9638C4.47283 19.9537 0.0250395 15.4785 0.0390957 9.95414C0.0495375 4.44187 4.51058 -0.00964226 10.0146 1.56856e-05Z" fill="#F1B846"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.0629 16.363C8.00627 16.8077 9.0143 16.9534 10.128 16.9441C10.6841 16.9508 11.2392 16.8942 11.7826 16.7755C14.0846 16.2841 15.7826 14.4528 16.1143 12.1107C16.1396 11.9208 16.085 11.8624 15.8986 11.8858C15.849 11.8922 15.7995 11.8989 15.75 11.9057C15.6408 11.9207 15.5317 11.9357 15.4211 11.9465C14.0822 12.0789 12.7388 12.1292 11.3942 12.163C9.75406 12.2057 8.11631 12.163 6.47856 12.0825C5.70346 12.0447 4.92997 11.9884 4.16049 11.8837C4.00667 11.8628 3.92916 11.9264 3.95968 12.0906C3.96492 12.1197 3.96989 12.149 3.97485 12.1783C3.98778 12.2546 4.00073 12.3311 4.01872 12.4061C4.44121 14.1823 5.37615 15.5678 7.0629 16.363ZM11.5218 14.1653C11.0099 14.2265 10.4944 14.2542 9.97886 14.2482V14.2478C8.72866 14.2386 7.49733 14.113 6.31902 13.649C6.05312 13.5409 5.79424 13.4162 5.54392 13.2756C5.24841 13.1133 5.17665 12.8283 5.10544 12.5454C5.09381 12.4992 5.08219 12.4531 5.06962 12.4076C5.04352 12.3134 5.1399 12.3142 5.20858 12.3235C5.87886 12.4144 6.55275 12.4559 7.22785 12.4909C8.72718 12.5697 10.2265 12.584 11.7258 12.5335C12.7491 12.4993 13.7716 12.4583 14.7881 12.3231C14.9001 12.3086 14.9439 12.3339 14.9174 12.4667C14.8295 12.9074 14.6198 13.2245 14.1917 13.4257C13.3407 13.8281 12.4495 14.0575 11.5218 14.1653Z" fill="#020201"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.62636 9.20635C7.62636 9.45584 7.48741 9.60675 7.27455 9.61842C7.04243 9.63089 6.89102 9.49246 6.86853 9.24699C6.82837 8.81963 6.50949 8.50091 6.07255 8.45343C5.62154 8.40474 5.20548 8.66389 5.087 9.07113C5.07151 9.13592 5.0613 9.20187 5.05648 9.26832C5.03118 9.49608 4.87536 9.63049 4.65006 9.61842C4.42475 9.60635 4.29263 9.44779 4.29905 9.216C4.31913 8.50373 4.94724 7.87154 5.73279 7.77295C6.46371 7.6812 7.17375 8.04216 7.47897 8.65826C7.56304 8.82967 7.61309 9.01581 7.62636 9.20635Z" fill="#030201"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.9608 7.76378C14.8403 7.76579 15.5227 8.31509 15.6476 9.09175C15.6626 9.17748 15.6626 9.26518 15.6476 9.35091C15.6134 9.52233 15.442 9.6326 15.2516 9.61851C15.0612 9.60443 14.907 9.47606 14.9058 9.27203C14.903 8.82616 14.4825 8.44426 13.9897 8.4495C13.497 8.45473 13.1391 8.77706 13.0893 9.26398C13.0652 9.5006 12.8998 9.63944 12.6676 9.61811C12.4508 9.59799 12.3215 9.43662 12.3327 9.1996C12.3632 8.5501 12.8917 7.96257 13.5837 7.80684C13.7071 7.77672 13.8338 7.76225 13.9608 7.76378Z" fill="#030201"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.37525 9.69818C3.65183 9.67113 3.92659 9.765 4.12907 9.95572C4.49774 10.2958 4.45557 10.7879 4.02987 11.0555C3.64513 11.297 3.23027 11.3026 2.82264 11.13C2.48409 10.9871 2.30055 10.6873 2.32907 10.3996C2.35678 10.1243 2.63991 9.83057 2.95959 9.74003C3.09475 9.70246 3.23533 9.6883 3.37525 9.69818Z" fill="#E49C9B"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.6193 11.2278C16.349 11.255 16.0799 11.1654 15.8795 10.9816C15.502 10.6439 15.537 10.1655 15.9518 9.87732C16.3534 9.59845 17.0221 9.62058 17.3976 9.93447C17.592 10.0974 17.7165 10.3107 17.6587 10.5699C17.5872 10.8918 17.3526 11.0769 17.0522 11.1816C16.9127 11.227 16.7653 11.2428 16.6193 11.2278Z" fill="#E49C9B"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.26387 6.42959C3.08033 6.40827 2.95503 6.33422 2.91688 6.15756C2.87672 5.97406 2.95182 5.83563 3.11768 5.76601C3.30162 5.68553 3.49358 5.62758 3.68274 5.56078C4.41286 5.30324 5.14325 5.04582 5.8739 4.78855C5.91166 4.77487 5.94901 4.75957 5.98756 4.74831C6.19519 4.69599 6.36547 4.77728 6.42933 4.95676C6.49639 5.1475 6.41286 5.32255 6.20001 5.39941C5.6478 5.60062 5.09278 5.79418 4.53897 5.98976C4.17377 6.11906 3.80831 6.2473 3.44258 6.37446C3.37913 6.39821 3.31447 6.41511 3.26387 6.42959Z" fill="#030201"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.7314 4.73405C13.7688 4.74491 13.8339 4.76061 13.8965 4.78234C14.7913 5.09622 15.6858 5.41077 16.58 5.726C16.8053 5.80648 16.9098 5.93727 16.8925 6.11151C16.868 6.35538 16.6459 6.48093 16.3821 6.38878C15.6885 6.14733 14.9965 5.89984 14.3041 5.65517C14.0588 5.56825 13.8114 5.48616 13.5688 5.39199C13.3977 5.32519 13.3246 5.18837 13.3572 5.00648C13.3881 4.83465 13.5282 4.72922 13.7314 4.73405Z" fill="#030201"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.97801 14.2473C8.72781 14.238 7.49648 14.1125 6.32018 13.6477C6.05428 13.5396 5.7954 13.4148 5.54508 13.2742C5.2013 13.0855 5.16034 12.7306 5.07078 12.4062C5.04468 12.3121 5.14106 12.3129 5.20974 12.3221C5.88002 12.4131 6.55391 12.4545 7.22901 12.4895C8.72834 12.5663 10.2277 12.5805 11.727 12.5322C12.7503 12.498 13.7728 12.4569 14.7893 12.3217C14.9013 12.3072 14.9451 12.3326 14.9186 12.4654C14.8306 12.908 14.621 13.2219 14.1929 13.4243C13.3399 13.8268 12.4483 14.0561 11.521 14.1648C11.009 14.2259 10.4936 14.2534 9.97801 14.2473Z" fill="#FCFCFC"/>
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 5.0 KiB |
|
@ -0,0 +1,11 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.0146 1.56856e-05C15.525 0.00806397 19.9744 4.48331 19.9604 10.0077C19.9483 15.5207 15.4873 19.9714 9.98327 19.9638C4.47283 19.9537 0.0250395 15.4785 0.0390957 9.95414C0.0495375 4.44187 4.51058 -0.00964226 10.0146 1.56856e-05Z" fill="#F1B846"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.8304 12.1643C12.2067 12.7844 12.4131 13.4663 12.5375 14.1723C12.6662 14.9065 12.7097 15.645 12.5608 16.384C12.4876 16.7901 12.2525 17.1492 11.9093 17.3792C10.8075 18.1386 9.6755 18.2502 8.4747 17.6017C7.81997 17.2482 7.46343 16.729 7.41393 15.9952C7.32661 14.7101 7.52943 13.4796 8.11655 12.3229C8.14351 12.2695 8.17812 12.2201 8.2091 12.1691C8.26826 12.1804 8.32701 12.1968 8.38657 12.2033C9.53425 12.3278 10.6827 12.3326 11.8304 12.1643Z" fill="#010100"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.959 9.64284C5.31204 9.64417 4.78649 9.12183 4.78516 8.47617C4.78383 7.83051 5.30721 7.30602 5.95417 7.30469C6.60113 7.30336 7.12668 7.82569 7.12801 8.47135C7.13147 8.78226 7.00958 9.08151 6.78973 9.30182C6.56989 9.52213 6.27054 9.64501 5.959 9.64284Z" fill="#020100"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.0136 7.30488C14.6605 7.31044 15.1804 7.83833 15.1748 8.48396C15.1693 9.12959 14.6403 9.64848 13.9934 9.64294C13.3465 9.6374 12.8265 9.10954 12.8321 8.46391C12.8318 8.15294 12.9569 7.85492 13.1791 7.63692C13.4013 7.41893 13.702 7.29927 14.0136 7.30488Z" fill="#020100"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.8284 12.1633C10.6823 12.3316 9.5346 12.3268 8.3845 12.2035C8.32494 12.1971 8.26619 12.1806 8.20703 12.1694C8.49717 11.6533 8.86377 11.2131 9.40945 10.9469C9.81508 10.7493 10.2175 10.7461 10.6235 10.9441C11.1684 11.2083 11.5358 11.6465 11.8284 12.1633Z" fill="#FEFEFD"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.37196 11.249C2.94138 11.2586 2.60215 11.1482 2.39048 10.7972C2.22468 10.5229 2.26613 10.2626 2.48585 10.0281C2.86653 9.62128 3.69309 9.59277 4.09832 9.97309C4.47579 10.3273 4.41542 10.8305 3.96432 11.0928C3.76875 11.2048 3.56231 11.2735 3.37196 11.249Z" fill="#E49C9B"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.6396 9.71832C16.8993 9.69751 17.1574 9.7738 17.3639 9.93238C17.7337 10.2051 17.7607 10.6794 17.4231 10.9842C16.9885 11.3765 16.2009 11.3504 15.7961 10.9304C15.5438 10.6685 15.5446 10.2954 15.7981 10.0332C16.014 9.80289 16.3252 9.68643 16.6396 9.71832Z" fill="#E49C9B"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.1151 5.10648C2.93724 5.10367 2.83341 5.03419 2.77265 4.89805C2.71189 4.7619 2.74529 4.6358 2.84388 4.52857C2.88224 4.49141 2.92509 4.45917 2.97144 4.43259C3.7803 3.92067 4.58821 3.40902 5.39518 2.89765C5.41209 2.8868 5.42859 2.87556 5.44629 2.86552C5.63543 2.75869 5.84026 2.79644 5.94287 2.95708C6.04911 3.12415 6.00444 3.32134 5.81933 3.44303C5.55696 3.61612 5.28854 3.77998 5.02295 3.94825L3.36219 5.00046C3.27768 5.05387 3.19639 5.11692 3.1151 5.10648Z" fill="#040301"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.6354 5.10826C16.5815 5.11348 16.5066 5.07131 16.4318 5.02393C15.613 4.50478 14.7939 3.98577 13.9746 3.4669C13.7593 3.32995 13.7018 3.13878 13.8136 2.96007C13.9255 2.78135 14.1307 2.75445 14.3424 2.88778C15.1682 3.40853 15.9933 3.93062 16.8177 4.45405C16.9678 4.54923 17.0507 4.67654 16.9988 4.85806C16.9529 5.0171 16.8414 5.10103 16.6354 5.10826Z" fill="#040301"/>
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 3.3 KiB |
|
@ -0,0 +1,41 @@
|
|||
import "htmx.org";
|
||||
|
||||
function main() {
|
||||
switchFromPaginationToLoadMore();
|
||||
|
||||
htmx.onLoad(() => {
|
||||
// Define what needs to happen whenever new content is added to the DOM.
|
||||
|
||||
// We need to configure this every time, because the button elements are destroyed
|
||||
// and replace with a new element in the response.
|
||||
setupLoadMoreButtonDisablingOnRequest();
|
||||
});
|
||||
}
|
||||
|
||||
function switchFromPaginationToLoadMore() {
|
||||
const loadMore = document.getElementById("load-more");
|
||||
const pagination = document.getElementById("pagination");
|
||||
|
||||
if (loadMore && pagination) {
|
||||
loadMore.classList.remove("tw-hidden");
|
||||
pagination.classList.add("tw-hidden");
|
||||
}
|
||||
}
|
||||
|
||||
function setupLoadMoreButtonDisablingOnRequest() {
|
||||
// Disable the load more button when the request is triggered.
|
||||
// This is a signal to the user an prevents duplicate triggering.
|
||||
// We don't need to reactivate the button because it is replaced with the response.
|
||||
const loadMore = document.getElementById("load-more");
|
||||
|
||||
if (!loadMore) {
|
||||
return;
|
||||
}
|
||||
|
||||
const loadMoreButton = loadMore.getElementsByTagName("button")[0];
|
||||
loadMoreButton.addEventListener("htmx:beforeRequest", (event) => {
|
||||
event.target.setAttribute("disabled", "");
|
||||
});
|
||||
}
|
||||
|
||||
main();
|
|
@ -4,6 +4,8 @@
|
|||
|
||||
import { SearchFilter } from "./search/search-filter.js";
|
||||
import { PNIToggle } from "./search/pni-toggle.js";
|
||||
import { PNISortDropdown } from "./search/pni-sort-dropdown.js";
|
||||
|
||||
const searchFilter = new SearchFilter();
|
||||
new PNIToggle(searchFilter);
|
||||
new PNISortDropdown(searchFilter);
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
const CREEPINESS_FACE = document.querySelector(".creep-o-meter-information");
|
||||
|
||||
export class CreepUtils {
|
||||
static moveCreepyFace() {
|
||||
// When searching, check to see how many products are still visible
|
||||
// If there are no visible products, there are "no search results"
|
||||
// And when there are no search results, do not show the creepo-meter-face
|
||||
if (document.querySelectorAll(".product-box:not(.d-none)").length) {
|
||||
// If there are search results, show the creepo-meter-face
|
||||
CREEPINESS_FACE.classList.remove("d-none");
|
||||
} else {
|
||||
// If there are no search results, hide the creepo-meter-face
|
||||
CREEPINESS_FACE.classList.add("d-none");
|
||||
}
|
||||
}
|
||||
|
||||
static sortOnCreepiness() {
|
||||
const container = document.querySelector(`.product-box-list`);
|
||||
const list = [...container.querySelectorAll(`.product-box`)];
|
||||
const creepVal = (e) => parseFloat(e.dataset.creepiness);
|
||||
list
|
||||
.sort((a, b) => creepVal(a) - creepVal(b))
|
||||
.forEach((p) => container.append(p));
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
import { gsap } from "gsap";
|
||||
import { Utils } from "./utils.js";
|
||||
import { CreepUtils } from "./creep-utils.js";
|
||||
|
||||
const categoryTitle = document.querySelector(`.category-title`);
|
||||
const parentTitle = document.querySelector(`.parent-title`);
|
||||
|
@ -55,6 +54,7 @@ export function performInitialHistoryReplace(
|
|||
parent: parentTitle.value.trim(),
|
||||
search: history.state?.search ?? "",
|
||||
filter: history.state?.filter,
|
||||
sort: history.state?.sort ?? "ASCENDING",
|
||||
},
|
||||
Utils.getTitle(categoryTitle.value.trim()),
|
||||
location.href
|
||||
|
@ -83,6 +83,7 @@ export function performInitialHistoryReplace(
|
|||
parent: parentTitle.value.trim(),
|
||||
search: searchParameter ?? "",
|
||||
filter: history.state?.filter,
|
||||
sort: history.state?.sort,
|
||||
},
|
||||
Utils.getTitle(categoryTitle.value.trim()),
|
||||
location.href
|
||||
|
@ -115,6 +116,8 @@ export function performInitialHistoryReplace(
|
|||
inline: "start",
|
||||
});
|
||||
}
|
||||
|
||||
Utils.sortProductCards();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -251,8 +254,8 @@ export function applyHistory(instance) {
|
|||
instance.filterCategory(category);
|
||||
instance.filterSubcategory(parent || category);
|
||||
Utils.updateHeader(category, parent);
|
||||
CreepUtils.sortOnCreepiness();
|
||||
CreepUtils.moveCreepyFace();
|
||||
Utils.sortProductCards();
|
||||
Utils.moveCreepyFace();
|
||||
|
||||
if (history.state?.parent && history.state?.category) {
|
||||
document
|
||||
|
|
|
@ -70,6 +70,7 @@ export function setupNavLinks(instance) {
|
|||
parent: "",
|
||||
search: "",
|
||||
filter: history.state?.filter,
|
||||
sort: history.state?.sort,
|
||||
},
|
||||
Utils.getTitle(categoryName),
|
||||
evt.target.href
|
||||
|
@ -123,6 +124,7 @@ export function setupNavLinks(instance) {
|
|||
parent: parentTitle.value.trim(),
|
||||
search: "",
|
||||
filter: history.state?.filter,
|
||||
sort: history.state?.sort,
|
||||
},
|
||||
Utils.getTitle(subcategoryName),
|
||||
href
|
||||
|
@ -165,6 +167,7 @@ export function setupGoBackToAll(instance) {
|
|||
parent: "",
|
||||
search: "",
|
||||
filter: history.state?.filter,
|
||||
sort: history.state?.sort,
|
||||
},
|
||||
Utils.getTitle(evt.target.dataset.name),
|
||||
evt.target.href
|
||||
|
|
|
@ -0,0 +1,151 @@
|
|||
const SPACEBAR_KEY_CODE = [0, 32];
|
||||
const ENTER_KEY_CODE = 13;
|
||||
const DOWN_ARROW_KEY_CODE = 40;
|
||||
const UP_ARROW_KEY_CODE = 38;
|
||||
const ESCAPE_KEY_CODE = 27;
|
||||
|
||||
export class PNISortDropdown {
|
||||
constructor(searchFilter) {
|
||||
this.searchFilter = searchFilter;
|
||||
this.dropdown = document.querySelector(`#pni-creepiness`);
|
||||
this.list = document.querySelector(".pni-creepiness__list");
|
||||
this.listContainer = document.querySelector(
|
||||
".pni-creepiness__list-container"
|
||||
);
|
||||
this.dropdownArrow = document.querySelector(".pni-creepiness__arrow");
|
||||
this.listItems = document.querySelectorAll(".pni-creepiness__list-item");
|
||||
this.dropdownSelectedNode = document.querySelector(
|
||||
"#pni-creepiness__selected"
|
||||
);
|
||||
this.listItemIds = [];
|
||||
|
||||
if (!this.dropdown) {
|
||||
return console.error(
|
||||
`Could not find the PNI Creepiness Dropdown. PNI Creepiness Dropdown will not be available.`
|
||||
);
|
||||
}
|
||||
|
||||
this.dropdownSelectedNode.addEventListener("click", (e) =>
|
||||
this.toggleListVisibility(e)
|
||||
);
|
||||
this.dropdownSelectedNode.addEventListener("keydown", (e) =>
|
||||
this.toggleListVisibility(e)
|
||||
);
|
||||
|
||||
this.listItems.forEach((item) => this.listItemIds.push(item.id));
|
||||
|
||||
this.listItems.forEach((item) => {
|
||||
item.addEventListener("click", (e) => {
|
||||
this.setSelectedListItem(e);
|
||||
this.closeList();
|
||||
});
|
||||
|
||||
item.addEventListener("keydown", (e) => {
|
||||
switch (e.keyCode) {
|
||||
case ENTER_KEY_CODE:
|
||||
this.setSelectedListItem(e);
|
||||
this.closeList();
|
||||
return;
|
||||
|
||||
case DOWN_ARROW_KEY_CODE:
|
||||
this.focusNextListItem(DOWN_ARROW_KEY_CODE);
|
||||
return;
|
||||
|
||||
case UP_ARROW_KEY_CODE:
|
||||
this.focusNextListItem(UP_ARROW_KEY_CODE);
|
||||
return;
|
||||
|
||||
case ESCAPE_KEY_CODE:
|
||||
this.closeList();
|
||||
return;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Update sort selected state on initialization
|
||||
if (history.state?.sort) {
|
||||
document
|
||||
.querySelector(
|
||||
`li.pni-creepiness__list-item[data-value=${history.state.sort}]`
|
||||
)
|
||||
.click();
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedListItem(e, pushUpdate = true) {
|
||||
this.listItems.forEach((item) => {
|
||||
const itemDiv = item.querySelector("div");
|
||||
itemDiv.classList.remove("tw-text-black");
|
||||
itemDiv.classList.add("tw-text-gray-40");
|
||||
});
|
||||
const targetContent = e.target.querySelector("div");
|
||||
targetContent.classList.add("tw-text-black");
|
||||
targetContent.classList.remove("tw-text-gray-40");
|
||||
const content = this.dropdownSelectedNode.querySelector("div");
|
||||
content.innerHTML = targetContent.innerHTML;
|
||||
if (pushUpdate) {
|
||||
this.searchFilter.updateSortHistoryState(e.target.dataset.value);
|
||||
}
|
||||
}
|
||||
|
||||
closeList() {
|
||||
this.listContainer.classList.add("tw-hidden");
|
||||
this.listContainer.setAttribute("aria-expanded", false);
|
||||
}
|
||||
|
||||
toggleListVisibility(e) {
|
||||
let openDropDown =
|
||||
SPACEBAR_KEY_CODE.includes(e.keyCode) || e.keyCode === ENTER_KEY_CODE;
|
||||
|
||||
if (e.keyCode === ESCAPE_KEY_CODE) {
|
||||
this.closeList();
|
||||
}
|
||||
|
||||
if (e.type === "click" || openDropDown) {
|
||||
this.listContainer.classList.remove("tw-hidden");
|
||||
|
||||
this.listContainer.setAttribute(
|
||||
"aria-expanded",
|
||||
this.listContainer.classList.contains("tw-hidden")
|
||||
);
|
||||
}
|
||||
|
||||
if (e.keyCode === DOWN_ARROW_KEY_CODE) {
|
||||
this.focusNextListItem(DOWN_ARROW_KEY_CODE);
|
||||
}
|
||||
|
||||
if (e.keyCode === UP_ARROW_KEY_CODE) {
|
||||
this.focusNextListItem(UP_ARROW_KEY_CODE);
|
||||
}
|
||||
}
|
||||
|
||||
focusNextListItem(direction) {
|
||||
const activeElementId = document.activeElement.id;
|
||||
if (activeElementId === "pni-creepiness__selected") {
|
||||
document.querySelector(`#${this.listItemIds[0]}`).focus();
|
||||
} else {
|
||||
const currentActiveElementIndex =
|
||||
this.listItemIds.indexOf(activeElementId);
|
||||
if (direction === DOWN_ARROW_KEY_CODE) {
|
||||
const currentActiveElementIsNotLastItem =
|
||||
currentActiveElementIndex < this.listItemIds.length - 1;
|
||||
if (currentActiveElementIsNotLastItem) {
|
||||
const nextListItemId =
|
||||
this.listItemIds[currentActiveElementIndex + 1];
|
||||
document.querySelector(`#${nextListItemId}`).focus();
|
||||
}
|
||||
} else if (direction === UP_ARROW_KEY_CODE) {
|
||||
const currentActiveElementIsNotFirstItem =
|
||||
currentActiveElementIndex > 0;
|
||||
if (currentActiveElementIsNotFirstItem) {
|
||||
const nextListItemId =
|
||||
this.listItemIds[currentActiveElementIndex - 1];
|
||||
document.querySelector(`#${nextListItemId}`).focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -55,9 +55,6 @@ export class PNIToggle {
|
|||
const { searchFilter, categoryTitle } = this;
|
||||
|
||||
gsap.set("figure.product-box.privacy-ding", { opacity: 1, y: 0 });
|
||||
// TODO: this might be an A/B testing opportunity to see
|
||||
// whether users assume this toggle is a navigation
|
||||
// action or not?
|
||||
const state = { ...history.state, filter: doFilter };
|
||||
const title = Utils.getTitle(categoryTitle.value.trim());
|
||||
history.replaceState(state, title, location.href);
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { gsap } from "gsap";
|
||||
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
||||
import { Utils } from "./utils.js";
|
||||
import { CreepUtils } from "./creep-utils.js";
|
||||
import { markScrollStart } from "./slider-area.js";
|
||||
import { setupHistoryManagement, applyHistory } from "./history.js";
|
||||
import {
|
||||
|
@ -188,8 +187,8 @@ export class SearchFilter {
|
|||
product.classList.add(`d-flex`);
|
||||
});
|
||||
|
||||
CreepUtils.sortOnCreepiness();
|
||||
CreepUtils.moveCreepyFace();
|
||||
Utils.sortProductCards();
|
||||
Utils.moveCreepyFace();
|
||||
|
||||
const state = { ...history.state, search: "" };
|
||||
const title = Utils.getTitle(this.categoryTitle.value.trim());
|
||||
|
@ -216,7 +215,7 @@ export class SearchFilter {
|
|||
history.replaceState(state, title, this.getURL(text));
|
||||
|
||||
Utils.sortFilteredProducts();
|
||||
CreepUtils.moveCreepyFace();
|
||||
Utils.moveCreepyFace();
|
||||
Utils.checkForEmptyNotice();
|
||||
}
|
||||
|
||||
|
@ -230,8 +229,8 @@ export class SearchFilter {
|
|||
filterCategory(category) {
|
||||
Utils.showProductsForCategory(category);
|
||||
this.categoryTitle.value = category;
|
||||
CreepUtils.sortOnCreepiness();
|
||||
CreepUtils.moveCreepyFace();
|
||||
Utils.sortProductCards();
|
||||
Utils.moveCreepyFace();
|
||||
Utils.checkForEmptyNotice();
|
||||
}
|
||||
|
||||
|
@ -295,4 +294,17 @@ export class SearchFilter {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} value
|
||||
* Stops current card animation, and updates history.state.sort with the new dropdown value.
|
||||
*/
|
||||
updateSortHistoryState(value) {
|
||||
gsap.set("figure.product-box", { opacity: 1, y: 0 });
|
||||
const state = { ...history.state, sort: value };
|
||||
const title = Utils.getTitle(this.categoryTitle.value.trim());
|
||||
history.replaceState(state, title, location.href);
|
||||
Utils.sortProductCards();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -357,4 +357,49 @@ export class Utils {
|
|||
SUBMIT_PRODUCT.classList.remove("d-none");
|
||||
}
|
||||
}
|
||||
|
||||
static moveCreepyFace() {
|
||||
const CREEPINESS_FACE = document.querySelector(
|
||||
".creep-o-meter-information"
|
||||
);
|
||||
// When searching, check to see how many products are still visible
|
||||
// If there are no visible products, there are "no search results"
|
||||
// And when there are no search results, do not show the creepo-meter-face
|
||||
if (document.querySelectorAll(".product-box:not(.d-none)").length) {
|
||||
// If there are search results, show the creepo-meter-face
|
||||
CREEPINESS_FACE.classList.remove("d-none");
|
||||
} else {
|
||||
// If there are no search results, hide the creepo-meter-face
|
||||
CREEPINESS_FACE.classList.add("d-none");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts Products Review Cards based on history.state.sort
|
||||
* value (alphabetical, ascending/descending creepiness value)
|
||||
*/
|
||||
static sortProductCards() {
|
||||
const container = document.querySelector(`.product-box-list`);
|
||||
const list = [...container.querySelectorAll(`.product-box`)];
|
||||
const getCreepinessValue = (e) => parseFloat(e.dataset.creepiness);
|
||||
const getProductTitle = (e) => e.querySelector(".product-name").innerText;
|
||||
switch (history.state?.sort) {
|
||||
case "ALPHA":
|
||||
list
|
||||
.sort((a, b) => getProductTitle(a).localeCompare(getProductTitle(b)))
|
||||
.forEach((p) => container.append(p));
|
||||
break;
|
||||
case "DESCENDING":
|
||||
list
|
||||
.sort((a, b) => getCreepinessValue(b) - getCreepinessValue(a))
|
||||
.forEach((p) => container.append(p));
|
||||
break;
|
||||
case "ASCENDING":
|
||||
default:
|
||||
list
|
||||
.sort((a, b) => getCreepinessValue(a) - getCreepinessValue(b))
|
||||
.forEach((p) => container.append(p));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -107,11 +107,11 @@ class Petition extends Component {
|
|||
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
window.dataLayer.push({
|
||||
'event' : 'form_submission',
|
||||
'form_name' : this.props.ctaName,
|
||||
'form_location' : this.props.formLocation,
|
||||
'form_type' : 'petition-form',
|
||||
'form_id' : this.props.petitionId
|
||||
event: "form_submission",
|
||||
form_name: this.props.ctaName,
|
||||
form_location: this.props.formLocation,
|
||||
form_type: "petition-form",
|
||||
form_id: this.props.petitionId,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -202,12 +202,12 @@ body {
|
|||
);
|
||||
|
||||
&.creep-o-meter-moved {
|
||||
height: 80px;
|
||||
height: 0px;
|
||||
margin-bottom: 0;
|
||||
margin-top: 1rem;
|
||||
margin-top: 0rem;
|
||||
|
||||
@media (max-width: $bp-md) {
|
||||
height: 16px;
|
||||
height: 0px;
|
||||
padding-bottom: 0;
|
||||
#product-filter-pni {
|
||||
top: calc(var(--mobile-creep-offset));
|
||||
|
|
|
@ -38,7 +38,7 @@ test(`Foundation homepage`, async ({ page }, testInfo) => {
|
|||
*
|
||||
* NOTE: this requires a `new-db` run with the seed value set
|
||||
* through RANDOM_SEED=530910203 in your .env file
|
||||
*
|
||||
*
|
||||
* NOTE: This test has the .fixme flag as due to the
|
||||
* recently added load-in animations for products,
|
||||
* the PNI search test fails randomly.
|
||||
|
|
Загрузка…
Ссылка в новой задаче