Skip to content

Commit

Permalink
wip: Add Search and Filter with results, also add Add No Result view
Browse files Browse the repository at this point in the history
  • Loading branch information
aralovelace authored and jamiehenson committed Jan 9, 2025
1 parent feefabd commit 69b2787
Show file tree
Hide file tree
Showing 8 changed files with 285 additions and 47 deletions.
23 changes: 21 additions & 2 deletions src/components/Examples/ExamplesCheckbox.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
import React from 'react';
import Icon from '@ably/ui/core/Icon';

const ExamplesCheckbox = ({ label, name, value }: { label: string; name: string; value: string }) => {
const ExamplesCheckbox = ({
label,
name,
value,
disabled = false,
isChecked = false,
isDefaultChecked = false,
selectProductOrUseCase,
}: {
label: string;
name: string;
value: string;
disabled?: boolean;
isChecked?: boolean;
isDefaultChecked?: boolean;
selectProductOrUseCase: (e: React.ChangeEvent<HTMLInputElement>) => void;
}) => {
return (
<div className="flex items-center mb-0">
<input
Expand All @@ -11,7 +27,10 @@ const ExamplesCheckbox = ({ label, name, value }: { label: string; name: string;
name={name}
className="ui-checkbox-input"
value={value}
defaultChecked={value === 'all'}
checked={isChecked}
defaultChecked={isDefaultChecked}
disabled={disabled}
onChange={(e) => selectProductOrUseCase(e)}
/>
<div data-ui-checkbox-styled="" className="ui-checkbox-styled">
<Icon size="1rem" name="icon-gui-tick" additionalCSS="ui-checkbox-styled-tick cursor-pointer" />
Expand Down
78 changes: 75 additions & 3 deletions src/components/Examples/ExamplesContent.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,70 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import { StaticImage } from 'gatsby-plugin-image';
import ExamplesGrid from './ExamplesGrid';
import ExamplesFilter from './ExamplesFilter';
import { ImageProps } from '../Image';
import examples from '../../data/examples';
import { filterSearchExamples } from './filter-search-examples';
import ExamplesNoResults from './ExamplesNoResults';

const ExamplesContent = ({ exampleImages }: { exampleImages: ImageProps[] }) => {
const [selectedProducts, setSelectedProducts] = useState<string[]>([]);
const [selectedUseCases, setSelectedUseCases] = useState<string[]>([]);
const [checkAllProducts, setCheckAllProducts] = useState(true);
const [checkAllUseCases, setCheckAllUseCases] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [filteredExamples, setFilteredExamples] = useState(examples.examples);

const selectProduct = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value, checked } = e.target;
if (value === 'all') {
setCheckAllProducts(checked);
if (checked && selectedProducts.length > 0) {
setSelectedProducts([]);
}
} else {
if (checked) {
setSelectedProducts((prev) => [...prev, value].filter((v) => v !== 'all'));
setCheckAllProducts(false);
} else {
setSelectedProducts((prev) => prev.filter((product) => product !== value));
}
}
};

const selectUseCases = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value, checked } = e.target;
if (value === 'all') {
setCheckAllUseCases(checked);
if (checked && selectedUseCases.length > 0) {
setSelectedUseCases([]);
}
} else {
if (checked) {
setSelectedUseCases((prev) => [...prev, value].filter((v) => v !== 'all'));
setCheckAllUseCases(false);
} else {
setSelectedUseCases((prev) => prev.filter((product) => product !== value));
}
}
};

const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchTerm(e.target.value);
};

useEffect(() => {
const filteredExamples = filterSearchExamples(examples.examples, selectedProducts, selectedUseCases, searchTerm);
setFilteredExamples(filteredExamples);

if (selectedProducts.length === 0) {
setCheckAllProducts(true);
}
if (selectedUseCases.length === 0) {
setCheckAllUseCases(true);
}
}, [selectedProducts, selectedUseCases, checkAllProducts, checkAllUseCases, searchTerm]);

return (
<>
<section className="mx-auto px-24 md:px-0 max-w-[1152px] relative z-10">
Expand All @@ -17,10 +77,22 @@ const ExamplesContent = ({ exampleImages }: { exampleImages: ImageProps[] }) =>
</div>
<div className="w-full my-40 sm:my-64 flex flex-col sm:flex-row gap-x-40">
<div className="w-full sm:w-[20%] relative">
<ExamplesFilter />
<ExamplesFilter
selectProduct={selectProduct}
selectUseCases={selectUseCases}
checkAllProducts={checkAllProducts}
selectedProducts={selectedProducts}
checkAllUseCases={checkAllUseCases}
selectedUseCases={selectedUseCases}
handleSearch={handleSearch}
/>
</div>
<div className="w-full sm:w-[80%] mt-40 sm:mt-0">
<ExamplesGrid exampleImages={exampleImages} />
{filteredExamples.length > 0 ? (
<ExamplesGrid exampleImages={exampleImages} examples={filteredExamples} />
) : (
<ExamplesNoResults />
)}
</div>
</div>
</section>
Expand Down
62 changes: 55 additions & 7 deletions src/components/Examples/ExamplesFilter.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
import Icon from '@ably/ui/core/Icon';
import React from 'react';
import ExamplesCheckbox from './ExamplesCheckbox';
import Icon from '@ably/ui/core/Icon';
import { products } from '@ably/ui/core/ProductTile/data';
import ExamplesCheckbox from './ExamplesCheckbox';
import examples from '../../data/examples';
import './examples-checkbox.css';

const ExamplesFilter = () => {
const ExamplesFilter = ({
selectProduct,
selectUseCases,
handleSearch,
checkAllProducts,
selectedProducts,
checkAllUseCases,
selectedUseCases,
}: {
selectProduct: (e: React.ChangeEvent<HTMLInputElement>) => void;
selectUseCases: (e: React.ChangeEvent<HTMLInputElement>) => void;
handleSearch: (e: React.ChangeEvent<HTMLInputElement>) => void;
checkAllProducts: boolean;
selectedProducts: string[];
checkAllUseCases: boolean;
selectedUseCases: string[];
}) => {
return (
<>
<div className="h-[34px] sm:h-[30px] w-20 absolute left-8 top-4 flex items-center justify-center select-none cursor-default">
Expand All @@ -15,22 +32,53 @@ const ExamplesFilter = () => {
className="ui-input pl-36 w-full h-40 sm:h-34 ui-text-p3"
placeholder="Find an example"
autoComplete="off"
onChange={(e) => handleSearch(e)}
/>
<div className="mt-20 mb-8">
<p className="ui-text-overline2 font-medium text-neutral-700">PRODUCT</p>
<div className="mt-8 flex ui-text-p4 flex-col gap-8">
<ExamplesCheckbox label="All" name="products-all" value="all" />
<ExamplesCheckbox
label="All"
name="products-all"
value="all"
disabled={checkAllProducts}
isChecked={checkAllProducts}
selectProductOrUseCase={selectProduct}
/>
{Object.entries(products).map(([key, product]) => (
<ExamplesCheckbox key={key} label={product.label} name="products-all" value={key} />
<ExamplesCheckbox
key={key}
label={product.label}
name="products-all"
value={key}
selectProductOrUseCase={selectProduct}
isDefaultChecked={!checkAllProducts}
isChecked={selectedProducts.includes(key)}
/>
))}
</div>
</div>
<div className="mt-20">
<p className="ui-text-overline2 font-medium text-neutral-700">USE CASE</p>
<div className="mt-8 flex ui-text-p4 flex-col gap-8">
<ExamplesCheckbox label="All" name="use-case-all" value="all" />
<ExamplesCheckbox
label="All"
name="use-case-all"
value="all"
disabled={checkAllUseCases}
isChecked={checkAllUseCases}
selectProductOrUseCase={selectUseCases}
/>
{Object.entries(examples.useCases).map(([key, useCase]) => (
<ExamplesCheckbox key={key} label={useCase.label} name="use-case-all" value={key} />
<ExamplesCheckbox
key={key}
label={useCase.label}
name="use-case-all"
value={key}
selectProductOrUseCase={selectUseCases}
isDefaultChecked={!checkAllUseCases}
isChecked={selectedUseCases.includes(key)}
/>
))}
</div>
</div>
Expand Down
94 changes: 62 additions & 32 deletions src/components/Examples/ExamplesGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,73 @@
import examples from '../../data/examples';
import React from 'react';
import Badge from '@ably/ui/core/Badge';
import Icon from '@ably/ui/core/Icon';
import { IconName } from '@ably/ui/core/Icon/types';
import Badge from '@ably/ui/core/Badge';
import React from 'react';
import { ProductName, products as dataProducts } from '@ably/ui/core/ProductTile/data';
import cn from '@ably/ui/core/utils/cn';
import { Image, ImageProps } from '../Image';
import examplesData, { Example } from '../../data/examples';

const ExamplesGrid = ({ examples, exampleImages }: { examples: Example[]; exampleImages: ImageProps[] }) => {
const displayExampleImage = (exampleImages: ImageProps[], selectedImage: string, productName: string) => {
const productImage = exampleImages.find((image) => image.name === selectedImage);
return productImage ? <Image image={productImage} alt={productName} /> : null;
};

const badgeColorForProduct = (product: ProductName) => {
switch (product) {
case 'chat':
return 'text-violet-400';
case 'spaces':
return 'text-pink-500';
case 'liveSync':
return 'text-blue-600';
case 'assetTracking':
return 'text-green-600';
case 'liveObjects':
return 'text-green-600';
default:
return 'text-orange-700';
}
};

const displayProductLabel = (product: ProductName, dataProducts: { [key: string]: { label: string } }) =>
dataProducts[product] ? (
<Badge key={product} className={cn('uppercase', badgeColorForProduct(product))}>
{dataProducts[product].label}
</Badge>
) : null;

const displayUseCaseLabel = (useCase: string, useCases: { [key: string]: { label: string } }) =>
useCases ? (
<Badge key={useCase} className="uppercase">
{useCases[useCase].label}
</Badge>
) : null;

const ExamplesGrid = ({ exampleImages }: { exampleImages: ImageProps[] }) => {
const findExampleImage = (exampleImages: ImageProps[], selectedImage: string) =>
exampleImages.find((image) => image.name === selectedImage) as ImageProps;
return (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-x-20 gap-y-32">
{examples.examples.map(({ name, description, languages, products, useCases, image }, key) => (
<div className="w-full" key={cn(name, '-', key)}>
<div className="bg-neutral-100 h-256 sm:h-200 relative flex justify-center items-center">
<Image image={findExampleImage(exampleImages, image)} alt={name} />
<div className="flex bg-neutral-000 gap-x-6 py-6 px-8 absolute right-12 bottom-12 rounded border border-neutral-200 z-20">
{languages
? languages.map((language) => (
<Icon key={language} name={`icon-tech-${language}` as IconName} size="18px" />
))
: null}
{examples
? examples.map(({ name, description, languages, products, useCases, image }, key) => (
<div className="w-full" key={`${name}-${key}`}>
<div className="bg-neutral-100 h-256 sm:h-200 relative flex justify-center items-center">
{exampleImages ? displayExampleImage(exampleImages, image, name) : null}
<div className="flex bg-neutral-000 gap-x-6 py-6 px-8 absolute right-12 bottom-12 rounded border border-neutral-200 z-20">
{languages
? languages.map((language) => (
<Icon key={language} name={`icon-tech-${language}` as IconName} size="18px" />
))
: null}
</div>
</div>
<p className="ui-text-h4 mt-16 text-neutral-1300">{name}</p>
<p className="ui-text-p3 mt-8 text-neutral-900">{description}</p>
<div className="mt-16 flex gap-x-4">
{products ? products.map((product) => displayProductLabel(product as ProductName, dataProducts)) : null}
{useCases ? useCases.map((useCase) => displayUseCaseLabel(useCase, examplesData.useCases)) : null}
</div>
</div>
</div>
<p className="ui-text-h4 mt-16 text-neutral-1300">{name}</p>
<p className="ui-text-p3 mt-8 text-neutral-900">{description}</p>
<div className="mt-16 flex gap-x-4">
{products
? products.map((product) => (
<Badge key={product} className="text-orange-700">
{product}
</Badge>
))
: null}

{useCases ? useCases.map((useCase) => <Badge key={useCase}>{useCase}</Badge>) : null}
</div>
</div>
))}
))
: null}
</div>
);
};
Expand Down
28 changes: 28 additions & 0 deletions src/components/Examples/ExamplesNoResults.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Badge from '@ably/ui/core/Badge';
import { Link } from 'gatsby';

const ExamplesNoResults = () => {
const popularSearchTerm = ['Avatar stack', 'Live Cursors', 'Occupancy', 'Presence'];
return (
<div className="w-full flex flex-col justify-center items-center h-full">
<p className="ui-text-h2">🫣</p>
<p className="ui-text-p1 mt-12 text-neutral-1300 font-bold">No matching examples found</p>
<p className="ui-text-p3 mt-24 text-neutral-1100">Try these popular search terms</p>
<div className="flex mt-8 gap-x-6">
{popularSearchTerm.map((term) => (
<Badge key={`search-term-${term}`} className="text-neutral-1300 ui-text-menu3 font-medium">
{term}
</Badge>
))}
</div>
<p className="mt-16 ui-text-p3 text-neutral-1000">
or{' '}
<Link to="/contact" className="ui-link">
Suggest an example
</Link>
</p>
</div>
);
};

export default ExamplesNoResults;
8 changes: 8 additions & 0 deletions src/components/Examples/examples-checkbox.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.ui-checkbox-input:disabled + .ui-checkbox-styled {
border-color: var(--color-neutral-800);
background-color: var(--color-orange-600);
}

.ui-checkbox-input:disabled + .ui-checkbox-styled-tick {
color: var(--color-neutral-000);
}
19 changes: 19 additions & 0 deletions src/components/Examples/filter-search-examples.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Example } from '../../data/examples';
import { ProductName } from '@ably/ui/core/ProductTile/data';

export const filterSearchExamples = (
examples: Example[],
selectedProducts: ProductName | string[],
selectedUseCases: string[],
searchTerm: string,
) => {
return examples.filter(
(example) =>
(selectedProducts.length === 0 || example.products.some((product) => selectedProducts.includes(product))) &&
(selectedUseCases.length === 0 || example.useCases.some((useCase) => selectedUseCases.includes(useCase))) &&
(searchTerm === '' ||
example.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
example.products.some((product) => product.toLowerCase().includes(searchTerm.toLowerCase())) ||
example.useCases.some((useCase) => useCase.toLowerCase().includes(searchTerm.toLowerCase()))),
);
};
Loading

0 comments on commit 69b2787

Please sign in to comment.