Skip to content

Commit edfe0da

Browse files
committed
feat(ktl-2781): implement filtering for cases
1 parent 9872f7a commit edfe0da

14 files changed

+337
-197
lines changed

blocks/case-studies/card/case-studies-card.module.css

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
background: #fff;
88
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
99
border: 1px solid rgba(0,0,0,0.12);
10-
max-width: 500px;
1110
box-sizing: border-box;
1211
}
1312

blocks/case-studies/card/case-studies-card.tsx

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ import React from 'react';
22
import cn from 'classnames';
33
import styles from './case-studies-card.module.css';
44
import { AndroidIcon, AppleIcon, ServerIcon, ComputerIcon, GlobusIcon } from '@rescui/icons';
5-
import { CaseStudyItem, CaseStudyType, isExternalCaseStudy, Platform } from '../case-studies';
6-
5+
import { CaseItem, CaseType, isExternalCase, CasePlatform } from '../case-studies';
76

87
/**
98
* Resolve asset path from YAML:
@@ -38,30 +37,30 @@ const mdToHtml = (md: string) => {
3837
return withLinks;
3938
};
4039

41-
const badgeText: Record<CaseStudyType, string> = {
40+
const badgeText: Record<CaseType, string> = {
4241
'multiplatform': 'Kotlin Multiplatform',
4342
'server-side': 'Server-side'
4443
};
4544

46-
const badgeClass: Record<CaseStudyType, string> = {
45+
const badgeClass: Record<CaseType, string> = {
4746
'multiplatform': 'badgeMultiplatform',
4847
'server-side': 'badgeServerSide'
4948
};
5049

5150
// Platform icon path builder. If you keep icons in (for example) /images/platforms/*.svg,
5251
// they’ll be resolved automatically by key. If an icon is missing, we still render the label.
53-
const getPlatformIcon = (p: Platform) => {
52+
const getPlatformIcon = (p: CasePlatform) => {
5453
switch (p) {
5554
case 'android':
56-
return <AndroidIcon/>;
55+
return <AndroidIcon />;
5756
case 'ios':
58-
return <AppleIcon/>;
57+
return <AppleIcon />;
5958
case 'desktop':
60-
return <ComputerIcon/>;
59+
return <ComputerIcon />;
6160
case 'frontend':
62-
return <GlobusIcon/>;
61+
return <GlobusIcon />;
6362
case 'backend':
64-
return <ServerIcon/>;
63+
return <ServerIcon />;
6564
case 'compose-multiplatform':
6665
return <img className={styles.platformIcon} src={'/images/platforms/compose-multiplatform.svg'}
6766
alt="Compose Multiplatform icon"
@@ -71,12 +70,11 @@ const getPlatformIcon = (p: Platform) => {
7170
}
7271
};
7372

74-
type Props = {
75-
item: CaseStudyItem;
73+
type Props = CaseItem &{
7674
className?: string;
7775
};
7876

79-
export const CaseStudyCard: React.FC<Props> = ({ item, className }) => {
77+
export const CaseStudyCard: React.FC<Props> = ({ className, ...item }) => {
8078
const logos = item.logo ?? [];
8179
const logoSrc1 = resolveAssetPath(logos[0]);
8280
const logoSrc2 = resolveAssetPath(logos[1]);
@@ -159,7 +157,7 @@ export const CaseStudyCard: React.FC<Props> = ({ item, className }) => {
159157
})}
160158
</div>
161159
)}
162-
{(isExternalCaseStudy(item)) ? (
160+
{(isExternalCase(item)) ? (
163161
<div className={styles.actions}>
164162
{item.externalLink && (
165163
<a
@@ -218,14 +216,10 @@ function hideBrokenIcon(img: HTMLImageElement) {
218216
/**
219217
* Humanize platform name for label.
220218
*/
221-
function humanizePlatform(p: Platform): string {
219+
function humanizePlatform(p: CasePlatform): string {
222220
switch (p) {
223221
case 'compose-multiplatform':
224222
return 'Compose Multiplatform';
225-
case 'frontend':
226-
return 'Frontend';
227-
case 'backend':
228-
return 'Backend';
229223
case 'ios':
230224
return 'iOS';
231225
default:
Lines changed: 24 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,42 @@
1-
.wrapper {
2-
padding: 32px 0;
3-
color: #ffffff; /* ensure white text */
4-
}
5-
6-
.title {
7-
margin: 0 0 16px 0;
8-
color: inherit;
9-
}
10-
11-
.inner {
1+
.content {
2+
padding: 24px 0 72px;
123
display: grid;
13-
grid-template-columns: 1fr;
14-
gap: 24px;
4+
grid-template-columns: 1fr 1fr;
5+
grid-column-gap: 80px;
6+
grid-template-areas:
7+
"group1 group1"
8+
"group2 group3";
9+
@media (width <= 1000px) {
10+
display: block;
11+
grid-template-columns: unset;
12+
grid-template-areas: unset;
13+
}
1514
}
1615

17-
.group {
18-
background: #0f0f0f;
19-
border-radius: 12px;
20-
padding: 16px;
16+
.group:first-child {
17+
grid-area: group1;
2118
}
2219

23-
.groupType {
24-
/* will span both columns on desktop */
20+
.group:not(:last-child) {
21+
margin-bottom: 32px;
2522
}
2623

2724
.groupTitle {
28-
margin: 0 0 12px 0;
29-
color: inherit;
25+
margin: 0 0 8px;
3026
}
3127

3228
.checkboxes {
3329
display: flex;
34-
flex-wrap: wrap;
35-
gap: 8px 16px;
3630
align-items: center;
31+
gap: 8px 24px;
3732
}
3833

39-
.switcherSmall {
40-
}
34+
.checkbox {
35+
display: flex;
36+
align-items: center;
4137

42-
@media (min-width: 768px) {
43-
.inner {
44-
grid-template-columns: repeat(2, 1fr);
45-
}
46-
.groupType {
47-
grid-column: 1 / -1;
38+
& span + span {
39+
display: flex;
40+
gap: 8px;
4841
}
4942
}

blocks/case-studies/filter/case-studies-filter.tsx

Lines changed: 73 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,113 +1,96 @@
1-
import React, { useCallback, useId, useMemo, useState } from 'react';
1+
import cn from 'classnames';
2+
import React from 'react';
23
import Switcher from '@rescui/switcher';
34
import Checkbox from '@rescui/checkbox';
45
import { createTextCn } from '@rescui/typography';
5-
import '@jetbrains/kotlin-web-site-ui/out/components/layout';
66
import styles from './case-studies-filter.module.css';
7+
import { CasePlatform, CaseType, CaseTypeSwitch, PlatformNames, Platforms } from '../case-studies';
8+
import { useQueryState } from '../../../hooks';
9+
import { parseCompose, parsePlatforms, parseType, serializeCompose, serializePlatforms, serializeType } from '../utils';
10+
11+
const caseTypeOptions: Array<{ value: CaseTypeSwitch, label: string }> = [
12+
{ value: 'all', label: 'All' },
13+
{ value: 'multiplatform', label: 'Kotlin Multiplatform' },
14+
{ value: 'server-side', label: 'Server-side' }
15+
];
716

817
export const CaseStudiesFilter: React.FC = () => {
918
const darkTextCn = createTextCn('dark');
10-
// Case study type switcher
11-
const typeOptions = useMemo(
12-
() => [
13-
{ value: 'all', label: 'All' },
14-
{ value: 'kotlin-multiplatform', label: 'Kotlin Multiplatform' },
15-
{ value: 'server-side', label: 'Server-side' },
16-
], []
17-
);
18-
const [type, setType] = useState<string>('all');
19-
const onTypeChange = useCallback((value: string) => setType(value), []);
2019

21-
// Code shared across (checkboxes)
22-
const codeSharedOptions = useMemo(
23-
() => [
24-
{ id: 'android', label: 'Android' },
25-
{ id: 'ios', label: 'iOS' },
26-
{ id: 'desktop', label: 'Desktop' },
27-
{ id: 'frontend', label: 'Frontend' },
28-
{ id: 'backend', label: 'Backend' },
29-
], []
30-
);
31-
const [codeShared, setCodeShared] = useState<string[]>([]);
32-
const toggleCodeShared = useCallback((id: string) => {
33-
setCodeShared((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]));
34-
}, []);
20+
// State synchronized with URL
21+
const [type, setType] = useQueryState<CaseTypeSwitch>('type', parseType, serializeType);
22+
const [platforms, setPlatforms] = useQueryState<CasePlatform[]>('platforms', parsePlatforms, serializePlatforms);
23+
const [compose, setCompose] = useQueryState<boolean>('compose', parseCompose, serializeCompose);
3524

36-
// UI Technology (checkboxes)
37-
const uiTechOptions = useMemo(
38-
() => [
39-
{ id: 'built-with-compose-multiplatform', label: 'Built with Compose Multiplatform' },
40-
], []
41-
);
42-
const [uiTech, setUiTech] = useState<string[]>([]);
43-
const toggleUiTech = useCallback((id: string) => {
44-
setUiTech((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]));
45-
}, []);
25+
const togglePlatform = (id: CasePlatform) => {
26+
let nextPlatforms = platforms.includes(id)
27+
? platforms.filter((x) => x !== id)
28+
: [...platforms, id];
29+
if (nextPlatforms.length === 0) {
30+
// If user unchecks all, reset to all selected
31+
nextPlatforms = [...Platforms];
32+
}
33+
setPlatforms(nextPlatforms);
34+
};
35+
36+
const toggleCompose = () => {
37+
const nextCompose = !compose;
38+
setCompose(nextCompose, () => setType('multiplatform'));
39+
};
4640

47-
// for accessibility ids
48-
const typeTitleId = useId();
49-
const codeSharedTitleId = useId();
50-
const uiTechTitleId = useId();
41+
const showKmpFilters = type === 'multiplatform' || type === 'all';
5142

5243
return (
53-
<section data-testid="case-studies-filter" aria-label="Case Studies Filter" className={styles.wrapper}>
54-
<div className={'ktl-layout ktl-layout--center'}>
55-
<h2 className={styles.title}>
56-
<span className={darkTextCn('rs-h4')}>Filters</span>
57-
</h2>
58-
<div className={styles.inner}>
59-
{/* Case study type */}
60-
<div className={`${styles.group} ${styles.groupType}`} role="group" aria-labelledby={typeTitleId} data-test="filter-type">
61-
<h3 id={typeTitleId} className={styles.groupTitle}><span className={darkTextCn('rs-h4')}>Case study type</span></h3>
62-
<div className={styles.switcherSmall}>
63-
<Switcher mode={'rock'} value={type} onChange={onTypeChange} options={typeOptions} />
64-
</div>
65-
</div>
44+
<section className="ktl-layout ktl-layout--center" data-testid="case-studies-filter"
45+
aria-label="Case Studies Filter">
46+
<div className={styles.content}>
47+
<div className={styles.group} role="group" aria-labelledby="case-study-type-title"
48+
data-test="filter-type">
49+
<h3 id="case-study-type-title" className={cn(styles.groupTitle, darkTextCn('rs-h4'))}>Case study
50+
type</h3>
51+
<Switcher mode={'rock'} value={type} onChange={setType} options={caseTypeOptions} />
52+
</div>
6653

67-
{/* Code shared across */}
68-
<div className={styles.group} role="group" aria-labelledby={codeSharedTitleId} data-test="filter-code-shared">
69-
<h3 id={codeSharedTitleId} className={styles.groupTitle}><span className={darkTextCn('rs-h4')}>Code shared across</span></h3>
54+
{showKmpFilters && (
55+
<div className={styles.group} role="group" aria-labelledby="code-shared-across-titile"
56+
data-test="filter-code-shared">
57+
<h3 id="code-shared-across-titile"
58+
className={cn(styles.groupTitle, darkTextCn('rs-h4'))}>Code shared across</h3>
7059
<div className={styles.checkboxes}>
71-
{codeSharedOptions.map((opt) => {
72-
const id = `code-shared-${opt.id}`;
73-
const checked = codeShared.includes(opt.id);
74-
return (
75-
<Checkbox
76-
key={opt.id}
77-
checked={checked}
78-
onChange={() => toggleCodeShared(opt.id)}
79-
mode="classic"
80-
size="m"
81-
>
82-
{opt.label}
83-
</Checkbox>
84-
);
85-
})}
60+
{Platforms.map((platformId) =>
61+
<Checkbox
62+
key={platformId}
63+
checked={platforms.includes(platformId)}
64+
onChange={() => togglePlatform(platformId)}
65+
mode="classic"
66+
size="m"
67+
>
68+
{PlatformNames[platformId]}
69+
</Checkbox>
70+
)}
8671
</div>
8772
</div>
73+
)}
8874

89-
{/* UI technology */}
90-
<div className={styles.group} role="group" aria-labelledby={uiTechTitleId} data-test="filter-ui-technology">
91-
<h3 id={uiTechTitleId} className={styles.groupTitle}><span className={darkTextCn('rs-h4')}>UI technology</span></h3>
75+
{showKmpFilters && (
76+
<div className={styles.group} role="group" aria-labelledby="ui-technology-title"
77+
data-test="filter-ui-technology">
78+
<h3 id="ui-technology-title"
79+
className={cn(styles.groupTitle, darkTextCn('rs-h4'))}>UI&nbsp;technology</h3>
9280
<div className={styles.checkboxes}>
93-
{uiTechOptions.map((opt) => {
94-
const id = `ui-tech-${opt.id}`;
95-
const checked = uiTech.includes(opt.id);
96-
return (
97-
<Checkbox
98-
key={opt.id}
99-
checked={checked}
100-
onChange={() => toggleUiTech(opt.id)}
101-
mode="classic"
102-
size="m"
103-
>
104-
{opt.label}
105-
</Checkbox>
106-
);
107-
})}
81+
<Checkbox
82+
className={styles.checkbox}
83+
checked={compose}
84+
onChange={toggleCompose}
85+
mode="classic"
86+
size="m"
87+
>
88+
Built with Compose Multiplatform <img
89+
src="/images/case-studies/compose-multiplatform.svg" alt="" />
90+
</Checkbox>
10891
</div>
10992
</div>
110-
</div>
93+
)}
11194
</div>
11295
</section>
11396
);

0 commit comments

Comments
 (0)