mirror of
https://github.com/qdrant/landing_page.git
synced 2026-09-26 22:48:30 +02:00
new demos page; refactor catalog filters to make them reusabile
This commit is contained in:
@@ -1,41 +1,11 @@
|
||||
---
|
||||
title: Qdrant Demos and Tutorials
|
||||
description: Experience firsthand how Qdrant powers intelligent search, anomaly detection, and personalized recommendations, showcasing the full capabilities of vector search to revolutionize data exploration and insights.
|
||||
cards:
|
||||
- id: 0
|
||||
title: Semantic Search Demo - Startup Search
|
||||
paragraphs:
|
||||
- id: 0
|
||||
content: This demo leverages a pre-trained SentenceTransformer model to perform semantic searches on startup descriptions, transforming them into vectors for the Qdrant engine.
|
||||
- id: 1
|
||||
content: Enter a query to see how neural search compares to traditional full-text search, with the option to toggle neural search on and off for direct comparison.
|
||||
link:
|
||||
text: View Demo
|
||||
url: https://qdrant.to/semantic-search-demo
|
||||
- id: 1
|
||||
title: Semantic Search and Recommendations Demo - Food Discovery
|
||||
paragraphs:
|
||||
- id: 0
|
||||
content: Explore personalized meal recommendations with our demo, using Delivery Service data. Like or dislike dish photos to refine suggestions based on visual appeal.
|
||||
- id: 1
|
||||
content: Filter options allow for restaurant selections within your delivery area, tailoring your dining experience to your preferences.
|
||||
link:
|
||||
text: View Demo
|
||||
url: https://food-discovery.qdrant.tech/
|
||||
- id: 2
|
||||
title: Categorization Demo -<br> E-Commerce Products
|
||||
paragraphs:
|
||||
- id: 0
|
||||
content: Discover the power of vector search in e-commerce through our demo. Simply input a product name and watch as our multi-language model intelligently categorizes it. The dots you see represent product clusters, highlighting our system's efficient categorization.
|
||||
link:
|
||||
text: View Demo
|
||||
url: https://qdrant.to/extreme-classification-demo
|
||||
- id: 3
|
||||
title: Code Search Demo -<br> Explore Qdrant's Codebase
|
||||
paragraphs:
|
||||
- id: 0
|
||||
content: Semantic search isn't just for natural language. By combining results from two models, qdrant is able to locate relevant code snippets down to the exact line.
|
||||
link:
|
||||
text: View Demo
|
||||
url: https://code-search.qdrant.tech/
|
||||
---
|
||||
title: Qdrant Demos
|
||||
description: Experience how Qdrant powers applications such as RAG, agentic AI, semantic/multimodal search, anomaly detection, and recommendations.
|
||||
build:
|
||||
render: always
|
||||
cascade:
|
||||
- build:
|
||||
list: local
|
||||
publishResources: false
|
||||
render: never
|
||||
---
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
sitemapExclude: true
|
||||
categories:
|
||||
- Basics
|
||||
- Search Engineering
|
||||
- Operations
|
||||
- Develop
|
||||
demos:
|
||||
- id: semantic-search
|
||||
title: Semantic Search Demo - Startup Search
|
||||
description: This demo leverages a pre-trained SentenceTransformer model to perform semantic searches on startup descriptions, transforming them into vectors for the Qdrant engine. Enter a query to see how neural search compares to traditional full-text search.
|
||||
category: Search Engineering
|
||||
image: /img/demos/demo-0.png
|
||||
link:
|
||||
text: View Demo
|
||||
url: https://qdrant.to/semantic-search-demo
|
||||
- id: food-discovery
|
||||
title: Semantic Search and Recommendations Demo - Food Discovery
|
||||
description: Explore personalized meal recommendations with our demo, using Delivery Service data. Like or dislike dish photos to refine suggestions based on visual appeal.
|
||||
category: Basics
|
||||
image: /img/demos/demo-1.png
|
||||
link:
|
||||
text: View Demo
|
||||
url: https://food-discovery.qdrant.tech/
|
||||
- id: ecommerce-categorization
|
||||
title: Categorization Demo - E-Commerce Products
|
||||
description: Discover the power of vector search in e-commerce. Input a product name and watch as our multi-language model intelligently categorizes it into product clusters.
|
||||
category: Basics
|
||||
image: /img/demos/demo-2.png
|
||||
link:
|
||||
text: View Demo
|
||||
url: https://qdrant.to/extreme-classification-demo
|
||||
- id: code-search
|
||||
title: Code Search Demo - Explore Qdrant's Codebase
|
||||
description: Semantic search isn't just for natural language. By combining results from two models, Qdrant is able to locate relevant code snippets down to the exact line.
|
||||
category: Develop
|
||||
image: /img/demos/demo-3.png
|
||||
link:
|
||||
text: View Demo
|
||||
url: https://code-search.qdrant.tech/
|
||||
---
|
||||
@@ -1,140 +1,571 @@
|
||||
@use '../helpers/functions' as *;
|
||||
|
||||
.demos {
|
||||
.demos-hero {
|
||||
background-color: $neutral-94;
|
||||
padding-top: $spacer * 5;
|
||||
padding-bottom: $spacer * 2.5;
|
||||
|
||||
padding-top: $spacer * 4;
|
||||
padding-bottom: $spacer * 4;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
&__content {
|
||||
max-width: pxToRem(635);
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: pxToRem(40);
|
||||
line-height: pxToRem(48);
|
||||
text-align: start;
|
||||
margin-bottom: $spacer * 1.5;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
font-size: map-get($font-sizes-text, 'l');
|
||||
line-height: pxToRem(27);
|
||||
margin: 0;
|
||||
max-width: 100%;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
&__cards {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
|
||||
min-width: 100%;
|
||||
row-gap: $spacer * 2;
|
||||
margin-top: ($spacer * 4);
|
||||
}
|
||||
|
||||
&__card-title {
|
||||
font-size: $h4-font-size; // 40px mobile (H4/Semibold)
|
||||
line-height: 1.2;
|
||||
letter-spacing: -0.5px;
|
||||
font-weight: $font-weight-semibold;
|
||||
color: $neutral-20;
|
||||
margin-top: calc($spacer / 2);
|
||||
margin-bottom: $spacer * 1.5;
|
||||
font-size: pxToRem(24);
|
||||
line-height: pxToRem(34);
|
||||
}
|
||||
|
||||
&__card-description {
|
||||
color: $neutral-30;
|
||||
font-size: pxToRem(16);
|
||||
line-height: pxToRem(24);
|
||||
margin-bottom: $spacer * 1.5;
|
||||
}
|
||||
|
||||
&__card-description p {
|
||||
margin-bottom: $spacer * 1.5;
|
||||
}
|
||||
|
||||
&__card-description p:last-of-type {
|
||||
&__description {
|
||||
margin-bottom: 0;
|
||||
font-size: $font-size-l; // 18px (Text-LG/Regular)
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
color: $neutral-30;
|
||||
}
|
||||
|
||||
&__card-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin-top: $spacer * 2.5;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: pxToRem(287);
|
||||
max-height: pxToRem(197);
|
||||
aspect-ratio: 1.46;
|
||||
background-size: contain;
|
||||
background-color: $neutral-98;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
box-shadow: 0px 5.13px 10.25px 0px rgba(22, 30, 51, 0.1);
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(xl) {
|
||||
padding-top: $spacer * 7.5;
|
||||
padding-bottom: $spacer * 10;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
@include media-breakpoint-up(lg) {
|
||||
padding-top: $spacer * 5;
|
||||
padding-bottom: $spacer * 2.5;
|
||||
|
||||
&__title {
|
||||
font-size: map-get($font-sizes, 2);
|
||||
line-height: pxToRem(67);
|
||||
max-width: pxToRem(445);
|
||||
font-size: $h3-font-size; // 48px desktop (H3/Semibold)
|
||||
line-height: 1.1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demos-catalog {
|
||||
padding-top: $spacer * 2.5;
|
||||
padding-bottom: $spacer * 4;
|
||||
background-color: $neutral-94;
|
||||
|
||||
&__container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__filters {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: pxToRem(60);
|
||||
width: 100%;
|
||||
height: calc(100vh - pxToRem(60));
|
||||
overflow-y: auto;
|
||||
padding: pxToRem(20) pxToRem(20) $spacer * 2.5;
|
||||
background-color: $neutral-94;
|
||||
z-index: 2;
|
||||
|
||||
&.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: $spacer;
|
||||
padding: pxToRem(12) $spacer * 0.5 $spacer * 2;
|
||||
border-bottom: pxToRem(1) solid $neutral-80;
|
||||
}
|
||||
|
||||
&-close-button {
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&-title {
|
||||
margin-bottom: 0;
|
||||
font-size: $font-size-xl;
|
||||
line-height: 1.5;
|
||||
font-weight: $font-weight-medium;
|
||||
color: $neutral-30;
|
||||
}
|
||||
|
||||
&-footer {
|
||||
display: flex;
|
||||
gap: $spacer;
|
||||
padding-top: $spacer * 5;
|
||||
margin-top: auto;
|
||||
|
||||
button {
|
||||
flex: 1;
|
||||
|
||||
&.button_outlined {
|
||||
color: $neutral-30;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
width: 100%;
|
||||
|
||||
&-header {
|
||||
display: flex;
|
||||
gap: $spacer * 0.5;
|
||||
margin-bottom: $spacer * 3;
|
||||
}
|
||||
}
|
||||
|
||||
&__filter-mobile-button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
padding: 0 pxToRem(20);
|
||||
border-radius: $spacer * 0.5;
|
||||
border: pxToRem(1) solid $neutral-90;
|
||||
background-color: $neutral-98;
|
||||
|
||||
svg {
|
||||
display: block;
|
||||
width: pxToRem(15);
|
||||
height: pxToRem(15);
|
||||
}
|
||||
}
|
||||
|
||||
&__search {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
gap: $spacer * 0.5;
|
||||
height: $spacer * 2.5;
|
||||
align-items: center;
|
||||
padding: 0 $spacer;
|
||||
border-radius: $spacer * 0.5;
|
||||
border: pxToRem(1) solid $neutral-90;
|
||||
background-color: $neutral-98;
|
||||
margin-bottom: 0;
|
||||
cursor: text;
|
||||
|
||||
svg {
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
width: pxToRem(17);
|
||||
height: pxToRem(17);
|
||||
}
|
||||
|
||||
&-input {
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
font-size: $font-size-s; // 14px
|
||||
line-height: 1.5;
|
||||
font-weight: $font-weight-medium;
|
||||
color: $neutral-30;
|
||||
|
||||
&::placeholder {
|
||||
color: $neutral-30;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__accordion {
|
||||
width: 100%;
|
||||
|
||||
&-item {
|
||||
border-bottom: pxToRem(1) solid $neutral-80;
|
||||
|
||||
&.active {
|
||||
.demos-catalog__accordion-header:after {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-header {
|
||||
position: relative;
|
||||
margin-bottom: 0;
|
||||
padding: $spacer $spacer * 0.5;
|
||||
font-size: $font-size-l;
|
||||
line-height: 1.5;
|
||||
font-weight: $font-weight-medium;
|
||||
color: $neutral-20;
|
||||
cursor: pointer;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: $spacer * 0.5;
|
||||
top: calc(50% - pxToRem(8));
|
||||
height: $spacer;
|
||||
width: $spacer;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10' fill='none'%3E%3Cpath d='M0.833984 2.91602L5.00065 7.08268L9.16732 2.91602' stroke='%23161E33' stroke-width='0.833333' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
transition: transform 0.5s ease;
|
||||
}
|
||||
}
|
||||
|
||||
&-body {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.2s ease-out;
|
||||
}
|
||||
|
||||
&-inner {
|
||||
padding-bottom: $spacer;
|
||||
}
|
||||
|
||||
&-option {
|
||||
position: relative;
|
||||
padding: $spacer * 0.5;
|
||||
border-radius: $spacer * 0.5;
|
||||
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
gap: pxToRem(12);
|
||||
align-items: center;
|
||||
font-size: $font-size-s; // 14px
|
||||
line-height: 1.5;
|
||||
font-weight: $font-weight-medium;
|
||||
color: $neutral-30;
|
||||
cursor: pointer;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: $spacer * 1.5;
|
||||
height: $spacer * 1.5;
|
||||
cursor: pointer;
|
||||
border-radius: calc($spacer / 4);
|
||||
border: pxToRem(1) solid $neutral-90;
|
||||
background: $neutral-98;
|
||||
}
|
||||
}
|
||||
|
||||
input[type='checkbox']:checked + label:before {
|
||||
background-image:
|
||||
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none'%3E%3Cpath d='M10 3L4.5 8.5L2 6' stroke='white' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
|
||||
linear-gradient(180deg, $primary-50, $primary-40);
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: pxToRem(18) pxToRem(18), cover;
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__toggle {
|
||||
display: none;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: calc($spacer / 4);
|
||||
padding: calc($spacer / 4);
|
||||
border-radius: $spacer * 0.5;
|
||||
border: pxToRem(1) solid $neutral-90;
|
||||
background-color: $neutral-98;
|
||||
|
||||
&-btn {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: calc($spacer / 4);
|
||||
width: pxToRem(88);
|
||||
height: pxToRem(30);
|
||||
padding: 0 $spacer;
|
||||
border-radius: calc($spacer / 4);
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: $spacer;
|
||||
height: $spacer;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
&[data-view-btn='grid']:before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='15' height='15' viewBox='0 0 15 15' fill='none'%3E%3Cpath d='M5.53801 1.8457H2.46109C2.12122 1.8457 1.8457 2.12122 1.8457 2.46109V5.53801C1.8457 5.87788 2.12122 6.1534 2.46109 6.1534H5.53801C5.87788 6.1534 6.1534 5.87788 6.1534 5.53801V2.46109C6.1534 2.12122 5.87788 1.8457 5.53801 1.8457Z' stroke='%23111824' stroke-width='1.23077' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M12.3072 1.8457H9.23032C8.89045 1.8457 8.61493 2.12122 8.61493 2.46109V5.53801C8.61493 5.87788 8.89045 6.1534 9.23032 6.1534H12.3072C12.6471 6.1534 12.9226 5.87788 12.9226 5.53801V2.46109C12.9226 2.12122 12.6471 1.8457 12.3072 1.8457Z' stroke='%23111824' stroke-width='1.23077' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M12.3072 8.61493H9.23032C8.89045 8.61493 8.61493 8.89045 8.61493 9.23032V12.3072C8.61493 12.6471 8.89045 12.9226 9.23032 12.9226H12.3072C12.6471 12.9226 12.9226 12.6471 12.9226 12.3072V9.23032C12.9226 8.89045 12.6471 8.61493 12.3072 8.61493Z' stroke='%23111824' stroke-width='1.23077' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M5.53801 8.61493H2.46109C2.12122 8.61493 1.8457 8.89045 1.8457 9.23032V12.3072C1.8457 12.6471 2.12122 12.9226 2.46109 12.9226H5.53801C5.87788 12.9226 6.1534 12.6471 6.1534 12.3072V9.23032C6.1534 8.89045 5.87788 8.61493 5.53801 8.61493Z' stroke='%23111824' stroke-width='1.23077' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
&[data-view-btn='list']:before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='15' height='15' viewBox='0 0 15 15' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4.92263 3.69238H12.9226M4.92263 7.38469H12.9226M4.92263 11.077H12.9226M1.8457 3.69238H1.85186M1.8457 7.38469H1.85186M1.8457 11.077H1.85186' stroke='%23111824' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: $neutral-94;
|
||||
|
||||
span {
|
||||
font-weight: $font-weight-semibold;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: $font-size-xs; // 12px
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
color: $neutral-30;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__results {
|
||||
&--grid,
|
||||
&--list {
|
||||
display: none;
|
||||
gap: $spacer * 2.5;
|
||||
|
||||
&.active {
|
||||
display: grid;
|
||||
}
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
font-size: map-get($font-sizes-text, 'xl');
|
||||
max-width: pxToRem(540);
|
||||
line-height: pxToRem(30);
|
||||
&--grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
&__cards {
|
||||
margin-top: $spacer * 7.5;
|
||||
row-gap: $spacer * 2.5;
|
||||
&--list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
&__card {
|
||||
flex-direction: row;
|
||||
&__card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacer;
|
||||
padding: pxToRem(4) pxToRem(4) $spacer * 1.5;
|
||||
border-radius: $spacer;
|
||||
border: pxToRem(0.64) solid $neutral-90;
|
||||
background-color: $neutral-98;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
transition: box-shadow 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
box-shadow:
|
||||
0 pxToRem(5) pxToRem(11) 0 rgba(22, 30, 51, 0.1),
|
||||
0 pxToRem(19) pxToRem(21) 0 rgba(22, 30, 51, 0.09);
|
||||
}
|
||||
|
||||
&-image {
|
||||
width: 100%;
|
||||
aspect-ratio: 404 / 148;
|
||||
overflow: hidden;
|
||||
border-radius: pxToRem(12) pxToRem(12) pxToRem(4) pxToRem(4);
|
||||
background-color: $neutral-90;
|
||||
|
||||
padding-top: $spacer * 4;
|
||||
padding-left: $spacer * 5;
|
||||
padding-bottom: $spacer * 4;
|
||||
padding-right: 0;
|
||||
picture,
|
||||
img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
&__card-content {
|
||||
max-width: pxToRem(400);
|
||||
&-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacer * 0.5;
|
||||
padding: 0 $spacer;
|
||||
}
|
||||
|
||||
&-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
align-self: flex-start;
|
||||
padding: pxToRem(2) $spacer * 0.5;
|
||||
border-radius: calc($spacer / 4);
|
||||
border: pxToRem(1) solid $neutral-90;
|
||||
background-color: $neutral-98;
|
||||
font-family: $font-family-monospace;
|
||||
font-size: $font-size-xs; // 12px
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
color: $neutral-50;
|
||||
}
|
||||
|
||||
&-title {
|
||||
margin-bottom: 0;
|
||||
font-size: $font-size-l; // 18px
|
||||
line-height: 1.5;
|
||||
font-weight: $font-weight-semibold;
|
||||
color: $neutral-20;
|
||||
}
|
||||
|
||||
&-description {
|
||||
display: none;
|
||||
margin-bottom: 0;
|
||||
font-size: $font-size-s; // 14px
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
color: $neutral-30;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
&__card-title {
|
||||
margin-top: 0;
|
||||
font-size: pxToRem(32);
|
||||
line-height: pxToRem(38);
|
||||
&--list {
|
||||
.demos-catalog__card-description {
|
||||
display: -webkit-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__button {
|
||||
display: block;
|
||||
margin: $spacer * 2.5 auto 0;
|
||||
color: $neutral-30;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
&__results--grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: $spacer * 2.5 $spacer * 2;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
padding-top: 0;
|
||||
padding-bottom: $spacer * 5;
|
||||
|
||||
&__container {
|
||||
display: flex;
|
||||
gap: $spacer * 2;
|
||||
}
|
||||
|
||||
&__card-description {
|
||||
font-size: pxToRem(16);
|
||||
line-height: pxToRem(24);
|
||||
&__filter-mobile-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&__filters {
|
||||
display: flex;
|
||||
position: relative;
|
||||
left: auto;
|
||||
top: auto;
|
||||
width: pxToRem(191);
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
margin-right: $spacer * 2;
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
z-index: auto;
|
||||
|
||||
&-header {
|
||||
padding: pxToRem(12) $spacer * 0.5;
|
||||
}
|
||||
|
||||
&-close-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&-title {
|
||||
font-size: $font-size-s; // 14px
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
&-footer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__accordion {
|
||||
&-header {
|
||||
font-size: $font-size-xs; // 12px
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
&:after {
|
||||
right: 0;
|
||||
top: calc(50% - pxToRem(5));
|
||||
height: pxToRem(10);
|
||||
width: pxToRem(10);
|
||||
}
|
||||
}
|
||||
|
||||
&-option {
|
||||
label {
|
||||
gap: $spacer * 0.5;
|
||||
|
||||
&:before {
|
||||
width: $spacer;
|
||||
height: $spacer;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $neutral-90;
|
||||
|
||||
label:before {
|
||||
border: pxToRem(1) solid $neutral-94;
|
||||
outline: pxToRem(2) solid $neutral-94;
|
||||
}
|
||||
}
|
||||
|
||||
input[type='checkbox']:checked + label:before {
|
||||
background-size: pxToRem(12) pxToRem(12), cover;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__toggle {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&__content-header {
|
||||
gap: $spacer;
|
||||
margin-bottom: $spacer * 2.5;
|
||||
}
|
||||
|
||||
&__card {
|
||||
gap: $spacer * 1.5;
|
||||
padding-bottom: $spacer * 1.5;
|
||||
|
||||
&-body {
|
||||
padding: 0 pxToRem(20);
|
||||
}
|
||||
|
||||
&-description {
|
||||
display: -webkit-box;
|
||||
max-height: pxToRem(48);
|
||||
}
|
||||
|
||||
&--list {
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
gap: $spacer * 2;
|
||||
padding: $spacer * 0.5 $spacer * 0.5 $spacer * 0.5 $spacer * 0.5;
|
||||
|
||||
.demos-catalog__card-image {
|
||||
width: pxToRem(280);
|
||||
flex-shrink: 0;
|
||||
aspect-ratio: 280 / 148;
|
||||
}
|
||||
|
||||
.demos-catalog__card-body {
|
||||
justify-content: center;
|
||||
padding: $spacer $spacer * 1.5 $spacer 0;
|
||||
}
|
||||
|
||||
&__card-image {
|
||||
margin: 0;
|
||||
max-width: pxToRem(560);
|
||||
max-height: pxToRem(384);
|
||||
.demos-catalog__card-description {
|
||||
max-height: none;
|
||||
-webkit-line-clamp: 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,575 @@
|
||||
const SORT_PARAM = "sort";
|
||||
const SEARCH_PARAM = "q";
|
||||
const DESKTOP_MIN_WIDTH = 992;
|
||||
|
||||
function getFilterKeys(root) {
|
||||
const fromAttr = (root.dataset.filterKeys ?? "")
|
||||
.split(",")
|
||||
.map((key) => key.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
if (fromAttr.length) return fromAttr;
|
||||
|
||||
return [
|
||||
...new Set(
|
||||
[...root.querySelectorAll("[data-filter-input]")]
|
||||
.map((input) => input.dataset.filterKey)
|
||||
.filter(Boolean)
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
function getMultiValueKeys(root) {
|
||||
return new Set(
|
||||
(root.dataset.multiValueKeys ?? "")
|
||||
.split(",")
|
||||
.map((key) => key.trim())
|
||||
.filter(Boolean)
|
||||
);
|
||||
}
|
||||
|
||||
function buildValidFilterValues(filterInputs, filterKeys) {
|
||||
return filterInputs.reduce((acc, input) => {
|
||||
const key = input.dataset.filterKey;
|
||||
if (!filterKeys.includes(key)) return acc;
|
||||
if (!acc[key]) acc[key] = new Set();
|
||||
acc[key].add(input.value);
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function buildEmptyFilters(filterKeys) {
|
||||
return filterKeys.reduce((acc, key) => {
|
||||
acc[key] = [];
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function parseStateFromUrl(validFilterValues, filterKeys, validSortValues) {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const sortParam = params.get(SORT_PARAM) ?? "";
|
||||
const sortValue = validSortValues.has(sortParam) ? sortParam : "";
|
||||
const searchQuery = (params.get(SEARCH_PARAM) ?? "").trim();
|
||||
|
||||
const filters = buildEmptyFilters(filterKeys);
|
||||
|
||||
for (const key of filterKeys) {
|
||||
const valid = validFilterValues[key] ?? new Set();
|
||||
const raw = params
|
||||
.getAll(key)
|
||||
.flatMap((value) => value.split(",").map((part) => part.trim()))
|
||||
.filter(Boolean);
|
||||
filters[key] = [...new Set(raw.filter((value) => valid.has(value)))];
|
||||
}
|
||||
|
||||
return { sortValue, filters, searchQuery };
|
||||
}
|
||||
|
||||
function syncStateToUrl(state, filterKeys) {
|
||||
const url = new URL(window.location.href);
|
||||
|
||||
url.searchParams.delete(SORT_PARAM);
|
||||
if (state.sortValue) {
|
||||
url.searchParams.set(SORT_PARAM, state.sortValue);
|
||||
}
|
||||
|
||||
url.searchParams.delete(SEARCH_PARAM);
|
||||
if (state.searchQuery) {
|
||||
url.searchParams.set(SEARCH_PARAM, state.searchQuery);
|
||||
}
|
||||
|
||||
for (const key of filterKeys) {
|
||||
url.searchParams.delete(key);
|
||||
(state.filters[key] ?? []).forEach((value) => {
|
||||
url.searchParams.append(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
const nextUrl = `${url.pathname}${url.search}${url.hash}`;
|
||||
const currentUrl = `${window.location.pathname}${window.location.search}${window.location.hash}`;
|
||||
|
||||
if (nextUrl !== currentUrl) {
|
||||
window.history.replaceState(null, "", nextUrl);
|
||||
}
|
||||
}
|
||||
|
||||
function hasActiveState(state, filterKeys) {
|
||||
return (
|
||||
Boolean(state.sortValue) ||
|
||||
Boolean(state.searchQuery) ||
|
||||
filterKeys.some((key) => (state.filters[key] ?? []).length > 0)
|
||||
);
|
||||
}
|
||||
|
||||
function initViewToggle(root) {
|
||||
const bar = root.querySelector("[data-catalog-view-toggle], [data-customers-view-toggle]");
|
||||
if (!bar) return;
|
||||
|
||||
const btnGrid = bar.querySelector('[data-view-btn="grid"]');
|
||||
const btnList = bar.querySelector('[data-view-btn="list"]');
|
||||
const blockGrid = root.querySelector('[data-view-block="grid"]');
|
||||
const blockList = root.querySelector('[data-view-block="list"]');
|
||||
|
||||
if (!btnGrid || !btnList || !blockGrid || !blockList) return;
|
||||
|
||||
const setMode = (mode) => {
|
||||
const isGrid = mode === "grid";
|
||||
btnGrid.classList.toggle("active", isGrid);
|
||||
btnList.classList.toggle("active", !isGrid);
|
||||
blockGrid.classList.toggle("active", isGrid);
|
||||
blockList.classList.toggle("active", !isGrid);
|
||||
};
|
||||
|
||||
btnGrid.addEventListener("click", () => setMode("grid"));
|
||||
btnList.addEventListener("click", () => setMode("list"));
|
||||
setMode("grid");
|
||||
}
|
||||
|
||||
function isDesktop() {
|
||||
return window.matchMedia(`(min-width: ${DESKTOP_MIN_WIDTH}px)`).matches;
|
||||
}
|
||||
|
||||
function getActiveFilters(filterInputs, filterKeys) {
|
||||
return filterInputs.reduce(
|
||||
(acc, input) => {
|
||||
if (!input.checked || input.hasAttribute("data-filter-all")) return acc;
|
||||
const key = input.dataset.filterKey;
|
||||
if (filterKeys.includes(key)) acc[key].push(input.value);
|
||||
return acc;
|
||||
},
|
||||
buildEmptyFilters(filterKeys)
|
||||
);
|
||||
}
|
||||
|
||||
function matchesFilters(card, filters, filterKeys, multiValueKeys) {
|
||||
for (const key of filterKeys) {
|
||||
const selected = filters[key];
|
||||
if (!selected.length) continue;
|
||||
|
||||
if (multiValueKeys.has(key)) {
|
||||
const cardValues = (card.dataset[key] ?? "").split("||").filter(Boolean);
|
||||
if (!selected.some((value) => cardValues.includes(value))) return false;
|
||||
continue;
|
||||
}
|
||||
|
||||
const val = card.dataset[key] ?? "";
|
||||
if (!selected.includes(val)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function matchesSearch(card, searchQuery) {
|
||||
if (!searchQuery) return true;
|
||||
const haystack = (card.dataset.search ?? "").toLowerCase();
|
||||
return haystack.includes(searchQuery.toLowerCase());
|
||||
}
|
||||
|
||||
function parseSortValue(value) {
|
||||
if (!value) return null;
|
||||
const [field, dir = "asc"] = value.split("-");
|
||||
return { field, dir };
|
||||
}
|
||||
|
||||
function sortCardsByField(cards, sortValue) {
|
||||
const parsed = parseSortValue(sortValue);
|
||||
if (!parsed) return [...cards];
|
||||
|
||||
const { field, dir } = parsed;
|
||||
const order = dir === "desc" ? -1 : 1;
|
||||
|
||||
return [...cards].sort((a, b) => {
|
||||
const av = (a.dataset[field] ?? "").toLowerCase();
|
||||
const bv = (b.dataset[field] ?? "").toLowerCase();
|
||||
if (av < bv) return -1 * order;
|
||||
if (av > bv) return 1 * order;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
function getAccordionHeaders(root) {
|
||||
return [
|
||||
...root.querySelectorAll(
|
||||
"[data-catalog-accordion-header], .customers-case-studies__accordion-header, .demos-catalog__accordion-header"
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
function getAccordionBodyClass(header) {
|
||||
if (header.hasAttribute("data-catalog-accordion-header")) {
|
||||
return null;
|
||||
}
|
||||
if (header.classList.contains("customers-case-studies__accordion-header")) {
|
||||
return "customers-case-studies__accordion-body";
|
||||
}
|
||||
if (header.classList.contains("demos-catalog__accordion-header")) {
|
||||
return "demos-catalog__accordion-body";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function closeAllFilterAccordions(root) {
|
||||
getAccordionHeaders(root).forEach((header) => {
|
||||
header.parentElement?.classList.remove("active");
|
||||
const panel = header.nextElementSibling;
|
||||
const bodyClass = getAccordionBodyClass(header);
|
||||
const isBody =
|
||||
panel &&
|
||||
(header.hasAttribute("data-catalog-accordion-header") ||
|
||||
(bodyClass && panel.classList.contains(bodyClass)));
|
||||
|
||||
if (isBody) {
|
||||
panel.style.maxHeight = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function expandAccordionItem(item) {
|
||||
const header = item.querySelector(
|
||||
"[data-catalog-accordion-header], .customers-case-studies__accordion-header, .demos-catalog__accordion-header"
|
||||
);
|
||||
const panel = header?.nextElementSibling;
|
||||
const bodyClass = header ? getAccordionBodyClass(header) : null;
|
||||
const isBody =
|
||||
panel &&
|
||||
(header?.hasAttribute("data-catalog-accordion-header") ||
|
||||
(bodyClass && panel.classList.contains(bodyClass)));
|
||||
|
||||
if (!header || !isBody) return;
|
||||
|
||||
item.classList.add("active");
|
||||
panel.style.maxHeight = `${panel.scrollHeight}px`;
|
||||
}
|
||||
|
||||
function syncAccordionExpansion(root, state, filterKeys) {
|
||||
root
|
||||
.querySelectorAll(
|
||||
"[data-catalog-accordion-item], .customers-case-studies__accordion-item, .demos-catalog__accordion-item"
|
||||
)
|
||||
.forEach((item) => {
|
||||
const sortSelect = item.querySelector("[data-sort-select]");
|
||||
const filterInputs = [...item.querySelectorAll("[data-filter-input]")];
|
||||
let shouldExpand = false;
|
||||
|
||||
if (sortSelect) {
|
||||
shouldExpand = Boolean(state.sortValue);
|
||||
} else if (filterInputs.length) {
|
||||
const key = filterInputs[0].dataset.filterKey;
|
||||
shouldExpand = (state.filters[key] ?? []).length > 0;
|
||||
}
|
||||
|
||||
if (shouldExpand) {
|
||||
expandAccordionItem(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initAccordionToggle(root) {
|
||||
getAccordionHeaders(root).forEach((header) => {
|
||||
if (header.dataset.catalogAccordionBound) return;
|
||||
header.dataset.catalogAccordionBound = "true";
|
||||
header.addEventListener("click", () => {
|
||||
header.parentElement?.classList.toggle("active");
|
||||
const panel = header.nextElementSibling;
|
||||
const bodyClass = getAccordionBodyClass(header);
|
||||
const isBody =
|
||||
panel &&
|
||||
(header.hasAttribute("data-catalog-accordion-header") ||
|
||||
(bodyClass && panel.classList.contains(bodyClass)));
|
||||
if (!isBody) return;
|
||||
|
||||
if (panel.style.maxHeight) {
|
||||
panel.style.maxHeight = null;
|
||||
} else {
|
||||
panel.style.maxHeight = `${panel.scrollHeight}px`;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initMobileFilters(root, { onClose, onOpen, onApply, onClear, onAfterApply }) {
|
||||
const openBtn = root.querySelector(
|
||||
"[data-filters-open], .customers-case-studies__filter-mobile-button, .demos-catalog__filter-mobile-button"
|
||||
);
|
||||
const closeBtn = root.querySelector(
|
||||
"[data-filters-close], .customers-case-studies__filters-close-button, .demos-catalog__filters-close-button"
|
||||
);
|
||||
const applyBtn = root.querySelector(
|
||||
"[data-filters-apply], .customers-case-studies__filters-apply-button, .demos-catalog__filters-apply-button"
|
||||
);
|
||||
const clearBtn = root.querySelector(
|
||||
"[data-filters-clear], .customers-case-studies__filters-clear-button, .demos-catalog__filters-clear-button"
|
||||
);
|
||||
const panel = root.querySelector(
|
||||
"[data-filters], .customers-case-studies__filters, .demos-catalog__filters"
|
||||
);
|
||||
|
||||
if (!panel) return;
|
||||
|
||||
const open = () => {
|
||||
panel.classList.add("active");
|
||||
onOpen?.();
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
panel.classList.remove("active");
|
||||
onClose?.();
|
||||
closeAllFilterAccordions(root);
|
||||
};
|
||||
|
||||
openBtn?.addEventListener("click", open);
|
||||
closeBtn?.addEventListener("click", close);
|
||||
applyBtn?.addEventListener("click", () => {
|
||||
onApply?.();
|
||||
panel.classList.remove("active");
|
||||
closeAllFilterAccordions(root);
|
||||
onAfterApply?.();
|
||||
});
|
||||
clearBtn?.addEventListener("click", () => onClear?.());
|
||||
}
|
||||
|
||||
function syncAllCheckbox(filterInputs, filters, filterKeys) {
|
||||
const allInput = filterInputs.find((input) => input.hasAttribute("data-filter-all"));
|
||||
if (!allInput) return;
|
||||
|
||||
const hasCategoryFilters = filterKeys.some((key) => (filters[key] ?? []).length > 0);
|
||||
allInput.checked = !hasCategoryFilters;
|
||||
}
|
||||
|
||||
function handleAllCheckboxChange(changedInput, filterInputs) {
|
||||
if (!changedInput.hasAttribute("data-filter-all")) return false;
|
||||
|
||||
if (changedInput.checked) {
|
||||
filterInputs.forEach((input) => {
|
||||
if (!input.hasAttribute("data-filter-all")) {
|
||||
input.checked = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleCategoryCheckboxChange(changedInput, filterInputs) {
|
||||
if (changedInput.hasAttribute("data-filter-all") || !changedInput.checked) return;
|
||||
|
||||
filterInputs.forEach((input) => {
|
||||
if (input.hasAttribute("data-filter-all")) {
|
||||
input.checked = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getCardSelector() {
|
||||
return "[data-catalog-card], [data-client-card]";
|
||||
}
|
||||
|
||||
function initCatalogFilters(root) {
|
||||
initViewToggle(root);
|
||||
initAccordionToggle(root);
|
||||
|
||||
const filterKeys = getFilterKeys(root);
|
||||
const multiValueKeys = getMultiValueKeys(root);
|
||||
const batchSize = Number(root.dataset.batchSize ?? 12) || 12;
|
||||
const resultsGrid = root.querySelector("[data-results-grid]");
|
||||
const resultsList = root.querySelector("[data-results-list]");
|
||||
const sortSelect = root.querySelector("[data-sort-select]");
|
||||
const searchInput = root.querySelector("[data-search-input]");
|
||||
const filterInputs = [...root.querySelectorAll("[data-filter-input]")];
|
||||
const loadMoreBtn = root.querySelector("[data-load-more]");
|
||||
|
||||
if (!resultsGrid) return;
|
||||
|
||||
const gridCards = [...resultsGrid.querySelectorAll(getCardSelector())];
|
||||
const listCards = resultsList
|
||||
? [...resultsList.querySelectorAll(getCardSelector())]
|
||||
: [];
|
||||
const validFilterValues = buildValidFilterValues(filterInputs, filterKeys);
|
||||
const validSortValues = new Set(
|
||||
[...(sortSelect?.options ?? [])].map((option) => option.value).filter(Boolean)
|
||||
);
|
||||
|
||||
let visibleBatchCount = batchSize;
|
||||
|
||||
const readControlsState = () => ({
|
||||
sortValue: sortSelect?.value ?? "",
|
||||
searchQuery: (searchInput?.value ?? "").trim(),
|
||||
filters: getActiveFilters(filterInputs, filterKeys)
|
||||
});
|
||||
|
||||
const applyStateToControls = (state) => {
|
||||
if (sortSelect) sortSelect.value = state?.sortValue ?? "";
|
||||
if (searchInput) searchInput.value = state?.searchQuery ?? "";
|
||||
|
||||
const selected = state?.filters ?? buildEmptyFilters(filterKeys);
|
||||
|
||||
filterInputs.forEach((input) => {
|
||||
if (input.hasAttribute("data-filter-all")) return;
|
||||
const key = input.dataset.filterKey;
|
||||
if (!filterKeys.includes(key)) return;
|
||||
input.checked = (selected[key] ?? []).includes(input.value);
|
||||
});
|
||||
|
||||
syncAllCheckbox(filterInputs, selected, filterKeys);
|
||||
};
|
||||
|
||||
let appliedState = readControlsState();
|
||||
let draftState = appliedState;
|
||||
|
||||
const urlState = parseStateFromUrl(validFilterValues, filterKeys, validSortValues);
|
||||
if (hasActiveState(urlState, filterKeys)) {
|
||||
appliedState = urlState;
|
||||
draftState = urlState;
|
||||
applyStateToControls(appliedState);
|
||||
syncAccordionExpansion(root, appliedState, filterKeys);
|
||||
} else {
|
||||
syncAllCheckbox(filterInputs, appliedState.filters, filterKeys);
|
||||
}
|
||||
|
||||
const commitAppliedState = () => {
|
||||
syncStateToUrl(appliedState, filterKeys);
|
||||
refresh();
|
||||
syncAccordionExpansion(root, appliedState, filterKeys);
|
||||
};
|
||||
|
||||
const getSortedMatchingIds = (state) => {
|
||||
const matching = gridCards.filter(
|
||||
(card) =>
|
||||
matchesFilters(card, state.filters, filterKeys, multiValueKeys) &&
|
||||
matchesSearch(card, state.searchQuery)
|
||||
);
|
||||
const sorted = sortCardsByField(matching, state.sortValue);
|
||||
const orderedIds = sorted.map((card) => card.dataset.id);
|
||||
const gridById = new Map(gridCards.map((card) => [card.dataset.id, card]));
|
||||
const listById = new Map(listCards.map((card) => [card.dataset.id, card]));
|
||||
|
||||
orderedIds.forEach((id) => {
|
||||
const gridCard = gridById.get(id);
|
||||
const listCard = listById.get(id);
|
||||
if (gridCard) resultsGrid.append(gridCard);
|
||||
if (listCard && resultsList) resultsList.append(listCard);
|
||||
});
|
||||
|
||||
return orderedIds;
|
||||
};
|
||||
|
||||
const applyBatchVisibility = (orderedMatchingIds) => {
|
||||
const allowedIds = new Set(orderedMatchingIds.slice(0, visibleBatchCount));
|
||||
|
||||
gridCards.forEach((card) => {
|
||||
card.hidden = !allowedIds.has(card.dataset.id);
|
||||
});
|
||||
|
||||
listCards.forEach((card) => {
|
||||
card.hidden = !allowedIds.has(card.dataset.id);
|
||||
});
|
||||
|
||||
if (loadMoreBtn) {
|
||||
const more = orderedMatchingIds.length > visibleBatchCount;
|
||||
loadMoreBtn.hidden = !more;
|
||||
loadMoreBtn.disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
const refresh = () => {
|
||||
visibleBatchCount = batchSize;
|
||||
const orderedMatchingIds = getSortedMatchingIds(appliedState);
|
||||
applyBatchVisibility(orderedMatchingIds);
|
||||
};
|
||||
|
||||
const loadMore = () => {
|
||||
const orderedMatchingIds = getSortedMatchingIds(appliedState);
|
||||
visibleBatchCount += batchSize;
|
||||
applyBatchVisibility(orderedMatchingIds);
|
||||
};
|
||||
|
||||
const onControlsChange = (event) => {
|
||||
const changedInput = event?.target;
|
||||
|
||||
if (changedInput?.matches?.("[data-filter-input]")) {
|
||||
handleAllCheckboxChange(changedInput, filterInputs);
|
||||
handleCategoryCheckboxChange(changedInput, filterInputs);
|
||||
}
|
||||
|
||||
if (isDesktop()) {
|
||||
appliedState = readControlsState();
|
||||
commitAppliedState();
|
||||
return;
|
||||
}
|
||||
|
||||
draftState = readControlsState();
|
||||
|
||||
// Search applies immediately on mobile too
|
||||
if (changedInput === searchInput) {
|
||||
appliedState = { ...appliedState, searchQuery: draftState.searchQuery };
|
||||
commitAppliedState();
|
||||
}
|
||||
};
|
||||
|
||||
filterInputs.forEach((input) => input.addEventListener("change", onControlsChange));
|
||||
sortSelect?.addEventListener("change", onControlsChange);
|
||||
searchInput?.addEventListener("input", onControlsChange);
|
||||
loadMoreBtn?.addEventListener("click", loadMore);
|
||||
|
||||
initMobileFilters(root, {
|
||||
onOpen: () => {
|
||||
if (isDesktop()) return;
|
||||
draftState = appliedState;
|
||||
applyStateToControls(draftState);
|
||||
},
|
||||
onClose: () => {
|
||||
if (isDesktop()) return;
|
||||
draftState = appliedState;
|
||||
applyStateToControls(appliedState);
|
||||
},
|
||||
onClear: () => {
|
||||
if (sortSelect) sortSelect.value = "";
|
||||
filterInputs.forEach((input) => {
|
||||
input.checked = input.hasAttribute("data-filter-all");
|
||||
});
|
||||
|
||||
if (isDesktop()) {
|
||||
appliedState = readControlsState();
|
||||
commitAppliedState();
|
||||
return;
|
||||
}
|
||||
|
||||
draftState = readControlsState();
|
||||
},
|
||||
onApply: () => {
|
||||
if (isDesktop()) return;
|
||||
appliedState = draftState;
|
||||
commitAppliedState();
|
||||
},
|
||||
onAfterApply: () => {
|
||||
if (isDesktop()) return;
|
||||
syncAccordionExpansion(root, appliedState, filterKeys);
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("popstate", () => {
|
||||
const nextState = parseStateFromUrl(validFilterValues, filterKeys, validSortValues);
|
||||
appliedState = nextState;
|
||||
draftState = nextState;
|
||||
applyStateToControls(appliedState);
|
||||
refresh();
|
||||
syncAccordionExpansion(root, appliedState, filterKeys);
|
||||
});
|
||||
|
||||
// Expand accordions marked active in markup (e.g. demos Categories)
|
||||
root
|
||||
.querySelectorAll(
|
||||
"[data-catalog-accordion-item].active, .customers-case-studies__accordion-item.active, .demos-catalog__accordion-item.active"
|
||||
)
|
||||
.forEach((item) => {
|
||||
expandAccordionItem(item);
|
||||
});
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
document
|
||||
.querySelectorAll("[data-catalog-filters], [data-customers-case-study]")
|
||||
.forEach((root) => {
|
||||
initCatalogFilters(root);
|
||||
});
|
||||
});
|
||||
@@ -1,424 +1,2 @@
|
||||
const FILTER_KEYS = [
|
||||
"industry",
|
||||
"product",
|
||||
"company_size",
|
||||
"location",
|
||||
"use_cases"
|
||||
];
|
||||
|
||||
const SORT_PARAM = "sort";
|
||||
|
||||
const VALID_SORT_VALUES = new Set([
|
||||
"name-asc",
|
||||
"name-desc",
|
||||
"industry-asc",
|
||||
"product-asc",
|
||||
"company_size-asc",
|
||||
"location-asc"
|
||||
]);
|
||||
|
||||
const DESKTOP_MIN_WIDTH = 992;
|
||||
|
||||
function buildValidFilterValues(filterInputs) {
|
||||
return filterInputs.reduce((acc, input) => {
|
||||
const key = input.dataset.filterKey;
|
||||
if (!FILTER_KEYS.includes(key)) return acc;
|
||||
if (!acc[key]) acc[key] = new Set();
|
||||
acc[key].add(input.value);
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function parseStateFromUrl(validFilterValues) {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const sortParam = params.get(SORT_PARAM) ?? "";
|
||||
const sortValue = VALID_SORT_VALUES.has(sortParam) ? sortParam : "";
|
||||
|
||||
const filters = {
|
||||
industry: [],
|
||||
product: [],
|
||||
company_size: [],
|
||||
location: [],
|
||||
use_cases: []
|
||||
};
|
||||
|
||||
for (const key of FILTER_KEYS) {
|
||||
const valid = validFilterValues[key] ?? new Set();
|
||||
const raw = params
|
||||
.getAll(key)
|
||||
.flatMap((value) => value.split(",").map((part) => part.trim()))
|
||||
.filter(Boolean);
|
||||
filters[key] = [...new Set(raw.filter((value) => valid.has(value)))];
|
||||
}
|
||||
|
||||
return { sortValue, filters };
|
||||
}
|
||||
|
||||
function syncStateToUrl(state) {
|
||||
const url = new URL(window.location.href);
|
||||
|
||||
url.searchParams.delete(SORT_PARAM);
|
||||
if (state.sortValue) {
|
||||
url.searchParams.set(SORT_PARAM, state.sortValue);
|
||||
}
|
||||
|
||||
for (const key of FILTER_KEYS) {
|
||||
url.searchParams.delete(key);
|
||||
(state.filters[key] ?? []).forEach((value) => {
|
||||
url.searchParams.append(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
const nextUrl = `${url.pathname}${url.search}${url.hash}`;
|
||||
const currentUrl = `${window.location.pathname}${window.location.search}${window.location.hash}`;
|
||||
|
||||
if (nextUrl !== currentUrl) {
|
||||
window.history.replaceState(null, "", nextUrl);
|
||||
}
|
||||
}
|
||||
|
||||
function hasActiveState(state) {
|
||||
return (
|
||||
Boolean(state.sortValue) ||
|
||||
FILTER_KEYS.some((key) => (state.filters[key] ?? []).length > 0)
|
||||
);
|
||||
}
|
||||
|
||||
function initCustomersViewToggle(root) {
|
||||
const bar = root.querySelector("[data-customers-view-toggle]");
|
||||
if (!bar) return;
|
||||
|
||||
const btnGrid = bar.querySelector('[data-view-btn="grid"]');
|
||||
const btnList = bar.querySelector('[data-view-btn="list"]');
|
||||
|
||||
const blockGrid = root.querySelector('[data-view-block="grid"]');
|
||||
const blockList = root.querySelector('[data-view-block="list"]');
|
||||
|
||||
if (!btnGrid || !btnList || !blockGrid || !blockList) return;
|
||||
|
||||
const setMode = (mode) => {
|
||||
const isGrid = mode === "grid";
|
||||
|
||||
btnGrid.classList.toggle("active", isGrid);
|
||||
btnList.classList.toggle("active", !isGrid);
|
||||
|
||||
blockGrid.classList.toggle("active", isGrid);
|
||||
blockList.classList.toggle("active", !isGrid);
|
||||
};
|
||||
|
||||
btnGrid.addEventListener("click", () => setMode("grid"));
|
||||
btnList.addEventListener("click", () => setMode("list"));
|
||||
|
||||
setMode("grid");
|
||||
}
|
||||
|
||||
function isDesktop() {
|
||||
return window.matchMedia(`(min-width: ${DESKTOP_MIN_WIDTH}px)`).matches;
|
||||
}
|
||||
|
||||
function getActiveFilters(filterInputs) {
|
||||
return filterInputs.reduce(
|
||||
(acc, input) => {
|
||||
if (!input.checked) return acc;
|
||||
const key = input.dataset.filterKey;
|
||||
if (FILTER_KEYS.includes(key)) acc[key].push(input.value);
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
industry: [],
|
||||
product: [],
|
||||
company_size: [],
|
||||
location: [],
|
||||
use_cases: []
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function matchesFilters(card, filters) {
|
||||
for (const key of FILTER_KEYS) {
|
||||
const selected = filters[key];
|
||||
if (!selected.length) continue;
|
||||
|
||||
if (key === "use_cases") {
|
||||
const cardUseCases = (card.dataset.use_cases ?? "")
|
||||
.split("||")
|
||||
.filter(Boolean);
|
||||
const ok = selected.some((v) => cardUseCases.includes(v));
|
||||
if (!ok) return false;
|
||||
continue;
|
||||
}
|
||||
|
||||
const val = card.dataset[key] ?? "";
|
||||
if (!selected.includes(val)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function parseSortValue(value) {
|
||||
if (!value) return null;
|
||||
const [field, dir = "asc"] = value.split("-");
|
||||
return { field, dir };
|
||||
}
|
||||
|
||||
function sortCardsByField(cards, sortValue) {
|
||||
const parsed = parseSortValue(sortValue);
|
||||
if (!parsed) return [...cards];
|
||||
|
||||
const { field, dir } = parsed;
|
||||
const order = dir === "desc" ? -1 : 1;
|
||||
|
||||
return [...cards].sort((a, b) => {
|
||||
const av = (a.dataset[field] ?? "").toLowerCase();
|
||||
const bv = (b.dataset[field] ?? "").toLowerCase();
|
||||
if (av < bv) return -1 * order;
|
||||
if (av > bv) return 1 * order;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
function closeAllFilterAccordions(root) {
|
||||
const headers = root.querySelectorAll(".customers-case-studies__accordion-header");
|
||||
|
||||
headers.forEach((header) => {
|
||||
header.parentElement?.classList.remove("active");
|
||||
const panel = header.nextElementSibling;
|
||||
if (panel && panel.classList.contains("customers-case-studies__accordion-body")) {
|
||||
panel.style.maxHeight = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function expandAccordionItem(item) {
|
||||
const header = item.querySelector(".customers-case-studies__accordion-header");
|
||||
const panel = header?.nextElementSibling;
|
||||
|
||||
if (!header || !panel?.classList.contains("customers-case-studies__accordion-body")) {
|
||||
return;
|
||||
}
|
||||
|
||||
item.classList.add("active");
|
||||
panel.style.maxHeight = `${panel.scrollHeight}px`;
|
||||
}
|
||||
|
||||
function syncAccordionExpansion(root, state) {
|
||||
root.querySelectorAll(".customers-case-studies__accordion-item").forEach((item) => {
|
||||
const sortSelect = item.querySelector("[data-sort-select]");
|
||||
const filterInputs = [...item.querySelectorAll("[data-filter-input]")];
|
||||
|
||||
let shouldExpand = false;
|
||||
|
||||
if (sortSelect) {
|
||||
shouldExpand = Boolean(state.sortValue);
|
||||
} else if (filterInputs.length) {
|
||||
const key = filterInputs[0].dataset.filterKey;
|
||||
shouldExpand = (state.filters[key] ?? []).length > 0;
|
||||
}
|
||||
|
||||
if (shouldExpand) {
|
||||
expandAccordionItem(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initCustomersMobileFilters(root, { onClose, onOpen, onApply, onClear, onAfterApply }) {
|
||||
const openBtn = root.querySelector(".customers-case-studies__filter-mobile-button");
|
||||
const closeBtn = root.querySelector(".customers-case-studies__filters-close-button");
|
||||
const applyBtn = root.querySelector(".customers-case-studies__filters-apply-button");
|
||||
const clearBtn = root.querySelector(".customers-case-studies__filters-clear-button");
|
||||
const panel = root.querySelector(".customers-case-studies__filters");
|
||||
|
||||
if (!panel) return;
|
||||
|
||||
const open = () => {
|
||||
panel.classList.add("active");
|
||||
onOpen?.();
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
panel.classList.remove("active");
|
||||
onClose?.();
|
||||
closeAllFilterAccordions(root);
|
||||
};
|
||||
|
||||
openBtn?.addEventListener("click", open);
|
||||
closeBtn?.addEventListener("click", close);
|
||||
applyBtn?.addEventListener("click", () => {
|
||||
onApply?.();
|
||||
panel.classList.remove("active");
|
||||
closeAllFilterAccordions(root);
|
||||
onAfterApply?.();
|
||||
});
|
||||
clearBtn?.addEventListener("click", () => onClear?.());
|
||||
}
|
||||
|
||||
function initCustomersCaseStudies(root) {
|
||||
initCustomersViewToggle(root);
|
||||
|
||||
const batchSize = Number(root.dataset.batchSize ?? 12) || 12;
|
||||
const resultsGrid = root.querySelector("[data-results-grid]");
|
||||
const resultsList = root.querySelector("[data-results-list]");
|
||||
const sortSelect = root.querySelector("[data-sort-select]");
|
||||
const filterInputs = [...root.querySelectorAll("[data-filter-input]")];
|
||||
const loadMoreBtn = root.querySelector("[data-load-more]");
|
||||
|
||||
if (!resultsGrid || !resultsList) return;
|
||||
|
||||
const gridCards = [...resultsGrid.querySelectorAll("[data-client-card]")];
|
||||
const listCards = [...resultsList.querySelectorAll("[data-client-card]")];
|
||||
const validFilterValues = buildValidFilterValues(filterInputs);
|
||||
|
||||
let visibleBatchCount = batchSize;
|
||||
|
||||
const readControlsState = () => ({
|
||||
sortValue: sortSelect?.value ?? "",
|
||||
filters: getActiveFilters(filterInputs)
|
||||
});
|
||||
|
||||
const applyStateToControls = (state) => {
|
||||
if (sortSelect) sortSelect.value = state?.sortValue ?? "";
|
||||
|
||||
const selected = state?.filters ?? {
|
||||
industry: [],
|
||||
product: [],
|
||||
company_size: [],
|
||||
location: [],
|
||||
use_cases: []
|
||||
};
|
||||
|
||||
filterInputs.forEach((input) => {
|
||||
const key = input.dataset.filterKey;
|
||||
if (!FILTER_KEYS.includes(key)) return;
|
||||
input.checked = (selected[key] ?? []).includes(input.value);
|
||||
});
|
||||
};
|
||||
|
||||
let appliedState = readControlsState();
|
||||
let draftState = appliedState;
|
||||
|
||||
const urlState = parseStateFromUrl(validFilterValues);
|
||||
if (hasActiveState(urlState)) {
|
||||
appliedState = urlState;
|
||||
draftState = urlState;
|
||||
applyStateToControls(appliedState);
|
||||
syncAccordionExpansion(root, appliedState);
|
||||
}
|
||||
|
||||
const commitAppliedState = () => {
|
||||
syncStateToUrl(appliedState);
|
||||
refresh();
|
||||
syncAccordionExpansion(root, appliedState);
|
||||
};
|
||||
|
||||
const getSortedMatchingIds = (state) => {
|
||||
const filters = state.filters;
|
||||
|
||||
const gridMatching = gridCards.filter((card) => matchesFilters(card, filters));
|
||||
const gridSorted = sortCardsByField(gridMatching, state.sortValue);
|
||||
const orderedIds = gridSorted.map((card) => card.dataset.id);
|
||||
const gridById = new Map(gridCards.map((c) => [c.dataset.id, c]));
|
||||
const listById = new Map(listCards.map((c) => [c.dataset.id, c]));
|
||||
|
||||
orderedIds.forEach((id) => {
|
||||
const g = gridById.get(id);
|
||||
const l = listById.get(id);
|
||||
if (g) resultsGrid.append(g);
|
||||
if (l) resultsList.append(l);
|
||||
});
|
||||
|
||||
return orderedIds;
|
||||
};
|
||||
|
||||
const applyBatchVisibility = (orderedMatchingIds) => {
|
||||
const allowedIds = new Set(orderedMatchingIds.slice(0, visibleBatchCount));
|
||||
|
||||
gridCards.forEach((card) => {
|
||||
card.hidden = !allowedIds.has(card.dataset.id);
|
||||
});
|
||||
|
||||
listCards.forEach((card) => {
|
||||
card.hidden = !allowedIds.has(card.dataset.id);
|
||||
});
|
||||
|
||||
if (loadMoreBtn) {
|
||||
const more = orderedMatchingIds.length > visibleBatchCount;
|
||||
loadMoreBtn.hidden = !more;
|
||||
loadMoreBtn.disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
const refresh = () => {
|
||||
visibleBatchCount = batchSize;
|
||||
const orderedMatchingIds = getSortedMatchingIds(appliedState);
|
||||
applyBatchVisibility(orderedMatchingIds);
|
||||
};
|
||||
|
||||
const loadMore = () => {
|
||||
const orderedMatchingIds = getSortedMatchingIds(appliedState);
|
||||
visibleBatchCount += batchSize;
|
||||
applyBatchVisibility(orderedMatchingIds);
|
||||
};
|
||||
|
||||
const onControlsChange = () => {
|
||||
if (isDesktop()) {
|
||||
appliedState = readControlsState();
|
||||
commitAppliedState();
|
||||
return;
|
||||
}
|
||||
draftState = readControlsState();
|
||||
};
|
||||
|
||||
filterInputs.forEach((input) => input.addEventListener("change", onControlsChange));
|
||||
sortSelect?.addEventListener("change", onControlsChange);
|
||||
loadMoreBtn?.addEventListener("click", loadMore);
|
||||
|
||||
initCustomersMobileFilters(root, {
|
||||
onOpen: () => {
|
||||
if (isDesktop()) return;
|
||||
draftState = appliedState;
|
||||
applyStateToControls(draftState);
|
||||
},
|
||||
onClose: () => {
|
||||
if (isDesktop()) return;
|
||||
draftState = appliedState;
|
||||
applyStateToControls(appliedState);
|
||||
},
|
||||
onClear: () => {
|
||||
if (isDesktop()) {
|
||||
if (sortSelect) sortSelect.value = "";
|
||||
filterInputs.forEach((i) => (i.checked = false));
|
||||
appliedState = readControlsState();
|
||||
commitAppliedState();
|
||||
return;
|
||||
}
|
||||
if (sortSelect) sortSelect.value = "";
|
||||
filterInputs.forEach((i) => (i.checked = false));
|
||||
draftState = readControlsState();
|
||||
},
|
||||
onApply: () => {
|
||||
if (isDesktop()) return;
|
||||
appliedState = draftState;
|
||||
commitAppliedState();
|
||||
},
|
||||
onAfterApply: () => {
|
||||
if (isDesktop()) return;
|
||||
syncAccordionExpansion(root, appliedState);
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("popstate", () => {
|
||||
const nextState = parseStateFromUrl(validFilterValues);
|
||||
appliedState = nextState;
|
||||
draftState = nextState;
|
||||
applyStateToControls(appliedState);
|
||||
refresh();
|
||||
syncAccordionExpansion(root, appliedState);
|
||||
});
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
document.querySelectorAll("[data-customers-case-study]").forEach((root) => {
|
||||
initCustomersCaseStudies(root);
|
||||
});
|
||||
})
|
||||
// Backwards-compatible entry point. Shared logic lives in catalog-filters.js.
|
||||
import "./catalog-filters.js";
|
||||
|
||||
@@ -169,11 +169,6 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
el.addEventListener('click', toggleAccordion);
|
||||
});
|
||||
|
||||
const customersAccordionButtons = Array.from(document.getElementsByClassName('customers-case-studies__accordion-header'));
|
||||
customersAccordionButtons.forEach((el) => {
|
||||
el.addEventListener('click', toggleAccordion);
|
||||
});
|
||||
|
||||
// Pricing doors tabs
|
||||
const pricingDoorsTabs = document.querySelectorAll('.qdrant-pricing-doors-b__tab');
|
||||
const pricingDoorsContainers = document.querySelectorAll('[data-doors-tab]');
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
{{ define "main" }}
|
||||
{{ partial "site-header" . }}
|
||||
|
||||
{{ partial "demo" . }}
|
||||
{{ partial "tutorials" . }}
|
||||
{{ partial "demo/demos-hero" . }}
|
||||
|
||||
{{ with (.Site.GetPage "/demo/items") }}
|
||||
{{ partial "demo/demos-catalog" . }}
|
||||
{{ end }}
|
||||
|
||||
{{ partial "get-started" . }}
|
||||
{{ end }}
|
||||
|
||||
+3
@@ -34,6 +34,9 @@
|
||||
<section
|
||||
class="customers-case-studies"
|
||||
data-customers-case-study
|
||||
data-catalog-filters
|
||||
data-filter-keys="industry,product,company_size,location,use_cases"
|
||||
data-multi-value-keys="use_cases"
|
||||
data-batch-size="12"
|
||||
>
|
||||
<div class="container customers-case-studies__container">
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
<section class="demos">
|
||||
<div class="container">
|
||||
<div class="demos__header mb-5 mb-md-7">
|
||||
<h2 class="demos__title mx-auto mx-lg-0">{{ .Params.title }}</h2>
|
||||
<p class="demos__subtitle mx-auto mx-lg-0">{{ .Params.description }}</p>
|
||||
</div>
|
||||
<div class="demos__cards row">
|
||||
{{ range $index, $element := .Params.cards }}
|
||||
<div class="col-12">
|
||||
<div class="card demos__card">
|
||||
<div class="demos__card-content">
|
||||
<h6 class="demos__card-title">{{ .title | safeHTML }}</h6>
|
||||
<div class="demos__card-description">
|
||||
{{ range .paragraphs }}
|
||||
<p>{{ .content }}</p>
|
||||
{{ end }}
|
||||
</div>
|
||||
<a class="link link_dark link_md align-self-start" href="{{ .link.url }}"> {{ .link.text }} </a>
|
||||
</div>
|
||||
<div class="demos__card-image" style="background-image: url(/img/demos/demo-{{ $index }}.png)"></div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,172 @@
|
||||
{{- $all := .Params.demos | default slice -}}
|
||||
{{- $categories := .Params.categories | default slice -}}
|
||||
|
||||
{{- if eq (len $categories) 0 -}}
|
||||
{{- range $all -}}
|
||||
{{- with .category }}{{ $categories = $categories | append . }}{{ end -}}
|
||||
{{- end -}}
|
||||
{{- $categories = sort (uniq $categories) -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- $batchSize := 8 -}}
|
||||
|
||||
<section
|
||||
class="demos-catalog"
|
||||
data-catalog-filters
|
||||
data-filter-keys="category"
|
||||
data-batch-size="{{ $batchSize }}"
|
||||
>
|
||||
<div class="container demos-catalog__container">
|
||||
<aside class="demos-catalog__filters" data-filters>
|
||||
<div class="demos-catalog__filters-header">
|
||||
<h6 class="demos-catalog__filters-title">Filter and Sort</h6>
|
||||
<button type="button" class="demos-catalog__filters-close-button" data-filters-close aria-label="Close filters">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.3642 4.22258C18.7547 3.83206 19.3887 3.83206 19.7792 4.22258C20.1695 4.61305 20.1695 5.24617 19.7792 5.63664L13.415 11.9999L19.7792 18.3642C20.1697 18.7547 20.1697 19.3887 19.7792 19.7792C19.3888 20.1696 18.7557 20.1693 18.3652 19.7792L12.0009 13.415L5.63664 19.7792C5.24618 20.1694 4.61305 20.1695 4.22258 19.7792C3.83206 19.3887 3.83206 18.7547 4.22258 18.3642L10.5859 11.9999L4.22258 5.63664C3.83209 5.24612 3.83209 4.6131 4.22258 4.22258C4.6131 3.83206 5.24612 3.83208 5.63664 4.22258L11.9999 10.5859L18.3642 4.22258Z" fill="#161E33"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="demos-catalog__accordion">
|
||||
<div class="demos-catalog__accordion-item active" data-catalog-accordion-item>
|
||||
<p class="demos-catalog__accordion-header" data-catalog-accordion-header>
|
||||
Categories
|
||||
</p>
|
||||
<div class="demos-catalog__accordion-body" data-catalog-accordion-body>
|
||||
<div class="demos-catalog__accordion-inner">
|
||||
<div class="demos-catalog__accordion-option">
|
||||
<input type="checkbox" data-filter-input data-filter-all data-filter-key="category" value="all" id="demo-category-all" checked>
|
||||
<label for="demo-category-all">All</label>
|
||||
</div>
|
||||
{{ range $categories }}
|
||||
<div class="demos-catalog__accordion-option">
|
||||
<input type="checkbox" data-filter-input data-filter-key="category" value="{{ . }}" id="demo-category-{{ . | urlize }}">
|
||||
<label for="demo-category-{{ . | urlize }}">{{ . }}</label>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demos-catalog__filters-footer">
|
||||
<button type="button" class="button button_outlined button_lg demos-catalog__filters-clear-button" data-filters-clear>
|
||||
Clear Filters
|
||||
</button>
|
||||
<button type="button" class="button button_contained button_lg demos-catalog__filters-apply-button" data-filters-apply>
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="demos-catalog__content">
|
||||
<div class="demos-catalog__content-header">
|
||||
<button type="button" class="demos-catalog__filter-mobile-button" data-filters-open aria-label="Open filters">
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d="M1.8457 3.69238H12.9226M4.30724 7.38469H10.4611M6.1534 11.077H8.61493" stroke="#111824" stroke-width="1.23" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<label class="demos-catalog__search">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 17 17" fill="none" aria-hidden="true">
|
||||
<path d="M14.6723 14.6723L11.6679 11.6679M13.2749 7.68531C13.2749 10.7724 10.7724 13.2749 7.68531 13.2749C4.59825 13.2749 2.0957 10.7724 2.0957 7.68531C2.0957 4.59825 4.59825 2.0957 7.68531 2.0957C10.7724 2.0957 13.2749 4.59825 13.2749 7.68531Z" stroke="#28324D" stroke-width="1.3974" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<input
|
||||
type="search"
|
||||
class="demos-catalog__search-input"
|
||||
data-search-input
|
||||
placeholder="Search demos..."
|
||||
aria-label="Search demos"
|
||||
>
|
||||
</label>
|
||||
|
||||
<div class="demos-catalog__toggle" data-catalog-view-toggle>
|
||||
<button type="button" class="demos-catalog__toggle-btn active" data-view-btn="grid">
|
||||
<span>Grid</span>
|
||||
</button>
|
||||
<button type="button" class="demos-catalog__toggle-btn" data-view-btn="list">
|
||||
<span>List</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="demos-catalog__results demos-catalog__results--grid active"
|
||||
data-view-block="grid"
|
||||
data-results-grid
|
||||
>
|
||||
{{ range $all }}
|
||||
{{ $isExternal := strings.HasPrefix .link.url "http" }}
|
||||
<a
|
||||
href="{{ .link.url }}"
|
||||
class="demos-catalog__card"
|
||||
data-catalog-card
|
||||
data-id="{{ .id }}"
|
||||
data-name="{{ .title }}"
|
||||
data-category="{{ .category }}"
|
||||
data-search="{{ .title }} {{ .description }} {{ .category }}"
|
||||
{{ if $isExternal }}target="_blank" rel="noopener noreferrer"{{ end }}
|
||||
>
|
||||
<div class="demos-catalog__card-image">
|
||||
{{ partial "picture.html" (dict
|
||||
"src" .image
|
||||
"alt" .title
|
||||
"loading" "lazy"
|
||||
"decoding" "async"
|
||||
"width" "800"
|
||||
"height" "296"
|
||||
) }}
|
||||
</div>
|
||||
<div class="demos-catalog__card-body">
|
||||
<span class="demos-catalog__card-badge">{{ .category }}</span>
|
||||
<h3 class="demos-catalog__card-title">{{ .title }}</h3>
|
||||
<p class="demos-catalog__card-description">{{ .description }}</p>
|
||||
</div>
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="demos-catalog__results demos-catalog__results--list"
|
||||
data-view-block="list"
|
||||
data-results-list
|
||||
>
|
||||
{{ range $all }}
|
||||
{{ $isExternal := strings.HasPrefix .link.url "http" }}
|
||||
<a
|
||||
href="{{ .link.url }}"
|
||||
class="demos-catalog__card demos-catalog__card--list"
|
||||
data-catalog-card
|
||||
data-id="{{ .id }}"
|
||||
data-name="{{ .title }}"
|
||||
data-category="{{ .category }}"
|
||||
data-search="{{ .title }} {{ .description }} {{ .category }}"
|
||||
{{ if $isExternal }}target="_blank" rel="noopener noreferrer"{{ end }}
|
||||
>
|
||||
<div class="demos-catalog__card-image">
|
||||
{{ partial "picture.html" (dict
|
||||
"src" .image
|
||||
"alt" .title
|
||||
"loading" "lazy"
|
||||
"decoding" "async"
|
||||
"width" "800"
|
||||
"height" "296"
|
||||
) }}
|
||||
</div>
|
||||
<div class="demos-catalog__card-body">
|
||||
<span class="demos-catalog__card-badge">{{ .category }}</span>
|
||||
<h3 class="demos-catalog__card-title">{{ .title }}</h3>
|
||||
<p class="demos-catalog__card-description">{{ .description }}</p>
|
||||
</div>
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
{{- if gt (len $all) $batchSize -}}
|
||||
<button type="button" class="button button_outlined demos-catalog__button" data-load-more hidden>
|
||||
View More
|
||||
</button>
|
||||
{{- end -}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,8 @@
|
||||
<section class="demos-hero">
|
||||
<div class="container">
|
||||
<div class="demos-hero__content">
|
||||
<h1 class="demos-hero__title">{{ .Params.title }}</h1>
|
||||
<p class="demos-hero__description">{{ .Params.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -112,9 +112,9 @@
|
||||
<script src="{{ $pageSearchScrollJs.RelPermalink }}"></script>
|
||||
{{ end }}
|
||||
|
||||
{{ if eq .Section "customers" }}
|
||||
{{ $customersJs := resources.Get "js/customers-filters.js" | js.Build | minify | resources.Fingerprint "sha512" }}
|
||||
<script src="{{ $customersJs.RelPermalink }}"></script>
|
||||
{{ if in (slice "customers" "demo") .Section }}
|
||||
{{ $catalogFiltersJs := resources.Get "js/catalog-filters.js" | js.Build | minify | resources.Fingerprint "sha512" }}
|
||||
<script src="{{ $catalogFiltersJs.RelPermalink }}"></script>
|
||||
{{ end }}
|
||||
|
||||
{{ if eq .Section "industries" }}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 250 KiB After Width: | Height: | Size: 70 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
Reference in New Issue
Block a user