new demos page; refactor catalog filters to make them reusabile

This commit is contained in:
trean
2026-07-20 11:14:28 +02:00
parent f503eaf523
commit 308abef96b
17 changed files with 1354 additions and 603 deletions
+10 -40
View File
@@ -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 }}
@@ -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