Skip to content

Commit 4bf7da1

Browse files
committed
temp
1 parent f1aff60 commit 4bf7da1

File tree

4 files changed

+207
-85
lines changed

4 files changed

+207
-85
lines changed

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +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';
5+
import { CaseStudyItem, CaseStudyType, isExternalCaseStudy, CasePlatform } from '../case-studies';
66

77

88
/**
@@ -50,7 +50,7 @@ const badgeClass: Record<CaseStudyType, string> = {
5050

5151
// Platform icon path builder. If you keep icons in (for example) /images/platforms/*.svg,
5252
// they’ll be resolved automatically by key. If an icon is missing, we still render the label.
53-
const getPlatformIcon = (p: Platform) => {
53+
const getPlatformIcon = (p: CasePlatform) => {
5454
switch (p) {
5555
case 'android':
5656
return <AndroidIcon/>;
@@ -218,7 +218,7 @@ function hideBrokenIcon(img: HTMLImageElement) {
218218
/**
219219
* Humanize platform name for label.
220220
*/
221-
function humanizePlatform(p: Platform): string {
221+
function humanizePlatform(p: CasePlatform): string {
222222
switch (p) {
223223
case 'compose-multiplatform':
224224
return 'Compose Multiplatform';

blocks/case-studies/case-studies.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
export type CaseStudyType = 'multiplatform' | 'server-side';
22

3+
export type CaseTypeSwitch = 'all' | CaseStudyType;
4+
35
type CaseStudyDestination = 'internal' | 'external';
46

5-
export type Platform =
6-
| 'android'
7-
| 'ios'
8-
| 'desktop'
9-
| 'frontend'
10-
| 'backend'
11-
| 'compose-multiplatform';
7+
export const Platforms = [
8+
'android',
9+
'ios',
10+
'desktop',
11+
'frontend',
12+
'backend',
13+
] as const;
14+
15+
export type CasePlatform = typeof Platforms[number] | 'compose-multiplatform';
1216

1317
type Signature = {
1418
line1: string;
@@ -34,7 +38,7 @@ interface CaseStudyItemBase {
3438
destination: CaseStudyDestination;
3539
logo?: string[];
3640
signature?: Signature;
37-
platforms?: Platform[];
41+
platforms?: CasePlatform[];
3842
media?: Media;
3943
featuredOnMainPage?: boolean;
4044
slug?: string;

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

Lines changed: 130 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,111 @@
1-
import React, { useCallback, useId, useMemo, useState } from 'react';
1+
import React, { useCallback, useEffect, useId, useMemo, useState } from 'react';
2+
import { useRouter } from 'next/router';
23
import Switcher from '@rescui/switcher';
34
import Checkbox from '@rescui/checkbox';
45
import { createTextCn } from '@rescui/typography';
56
import '@jetbrains/kotlin-web-site-ui/out/components/layout';
67
import styles from './case-studies-filter.module.css';
8+
import { CasePlatform, CaseStudyType, CaseTypeSwitch, Platforms } from '../case-studies';
9+
10+
11+
function parseType(maybeCaseType: unknown): CaseTypeSwitch {
12+
const maybeCaseTypeString = String(maybeCaseType || 'all');
13+
return (maybeCaseTypeString === 'multiplatform' || maybeCaseTypeString === 'server-side') ? maybeCaseTypeString : 'all';
14+
}
15+
16+
function parsePlatforms(maybePlatforms: unknown): CasePlatform[] {
17+
if (!maybePlatforms) {
18+
return [...Platforms];
19+
}
20+
const list = String(maybePlatforms).split(',').map((x) => x.trim()).filter(Boolean);
21+
const set = new Set<CasePlatform>();
22+
for (const i of list) {
23+
if ((Platforms as readonly string[]).includes(i)) {
24+
set.add(i as CasePlatform);
25+
}
26+
}
27+
return set.size === 0 ? [...Platforms] : Array.from(set);
28+
}
29+
30+
function parseCompose(v: unknown): boolean {
31+
if (v === true) return true;
32+
const s = String(v || 'true').toLowerCase();
33+
return s === 'true' || s === '1' || s === 'yes';
34+
}
35+
36+
function buildQuery(type: CaseTypeSwitch, platforms: CasePlatform[], compose: boolean) {
37+
const q: Record<string, any> = {};
38+
if (type && type !== 'all') q.type = type;
39+
// only keep platforms if not all selected
40+
const allSelected = platforms.length === Platforms.length && Platforms.every((p) => platforms.includes(p));
41+
if (!allSelected && (type === 'multiplatform' || type === 'all')) {
42+
q.platforms = platforms.join(',');
43+
}
44+
if (type === 'multiplatform' || type === 'all') q.compose = compose ? 'true' : 'false';
45+
return q;
46+
}
747

848
export const CaseStudiesFilter: React.FC = () => {
49+
const router = useRouter();
950
const darkTextCn = createTextCn('dark');
51+
1052
// Case study type switcher
11-
const typeOptions = useMemo(
53+
const typeOptions: Array<{value: CaseTypeSwitch, label: string}> = useMemo(
1254
() => [
1355
{ value: 'all', label: 'All' },
14-
{ value: 'kotlin-multiplatform', label: 'Kotlin Multiplatform' },
15-
{ value: 'server-side', label: 'Server-side' },
56+
{ value: 'multiplatform', label: 'Kotlin Multiplatform' },
57+
{ value: 'server-side', label: 'Server-side' }
1658
], []
1759
);
18-
const [type, setType] = useState<string>('all');
19-
const onTypeChange = useCallback((value: string) => setType(value), []);
2060

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-
}, []);
61+
// State synchronized with URL
62+
const [type, setType] = useState<CaseTypeSwitch>('all');
63+
const [platforms, setPlatforms] = useState<CasePlatform[]>([...Platforms]);
64+
const [compose, setCompose] = useState<boolean>(true);
3565

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-
}, []);
66+
// Initialize/Sync from URL
67+
useEffect(() => {
68+
const q = router.query;
69+
const t = parseType(q.type);
70+
const p = parsePlatforms(q.platforms);
71+
const c = parseCompose(q.compose);
72+
setType(t);
73+
setPlatforms(p);
74+
setCompose(c);
75+
// eslint-disable-next-line react-hooks/exhaustive-deps
76+
}, [router.query.type, router.query.platforms, router.query.compose]);
77+
78+
// Handlers update both state and URL (shallow)
79+
const pushQuery = useCallback((nextType: CaseTypeSwitch, nextPlatforms: CasePlatform[], nextCompose: boolean) => {
80+
const q = buildQuery(nextType, nextPlatforms, nextCompose);
81+
router.replace({ pathname: router.pathname, query: q }, undefined, { shallow: true });
82+
}, [router]);
83+
84+
const onTypeChange = useCallback((value: string) => {
85+
const nextType = parseType(value);
86+
// When switching to server-side, we can keep platforms/compose but they won't be shown
87+
pushQuery(nextType, platforms, compose);
88+
}, [platforms, compose, pushQuery]);
89+
90+
const togglePlatform = useCallback((id: CasePlatform) => {
91+
let next = platforms.includes(id) ? platforms.filter((x) => x !== id) : [...platforms, id];
92+
// If user unchecks all, reset to all selected
93+
if (next.length === 0) next = [...Platforms];
94+
pushQuery(type, next, compose);
95+
}, [platforms, type, compose, pushQuery]);
96+
97+
const onComposeChange = useCallback(() => {
98+
const next = !compose;
99+
pushQuery(type, platforms, next);
100+
}, [compose, type, platforms, pushQuery]);
46101

47102
// for accessibility ids
48103
const typeTitleId = useId();
49104
const codeSharedTitleId = useId();
50105
const uiTechTitleId = useId();
51106

107+
const showKmpFilters = type === 'multiplatform' || type === 'all';
108+
52109
return (
53110
<section data-testid="case-studies-filter" aria-label="Case Studies Filter" className={styles.wrapper}>
54111
<div className={'ktl-layout ktl-layout--center'}>
@@ -57,56 +114,57 @@ export const CaseStudiesFilter: React.FC = () => {
57114
</h2>
58115
<div className={styles.inner}>
59116
{/* 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>
117+
<div className={`${styles.group} ${styles.groupType}`} role="group" aria-labelledby={typeTitleId}
118+
data-test="filter-type">
119+
<h3 id={typeTitleId} className={styles.groupTitle}><span className={darkTextCn('rs-h4')}>Case study type</span>
120+
</h3>
62121
<div className={styles.switcherSmall}>
63122
<Switcher mode={'rock'} value={type} onChange={onTypeChange} options={typeOptions} />
64123
</div>
65124
</div>
66125

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>
70-
<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-
})}
126+
{showKmpFilters && (
127+
<div className={styles.group} role="group" aria-labelledby={codeSharedTitleId}
128+
data-test="filter-code-shared">
129+
<h3 id={codeSharedTitleId} className={styles.groupTitle}><span
130+
className={darkTextCn('rs-h4')}>Code shared across</span></h3>
131+
<div className={styles.checkboxes}>
132+
{Platforms.map((pid) => {
133+
const checked = platforms.includes(pid);
134+
const label = pid === 'ios' ? 'iOS' : pid.charAt(0).toUpperCase() + pid.slice(1);
135+
return (
136+
<Checkbox
137+
key={pid}
138+
checked={checked}
139+
onChange={() => togglePlatform(pid)}
140+
mode="classic"
141+
size="m"
142+
>
143+
{label}
144+
</Checkbox>
145+
);
146+
})}
147+
</div>
86148
</div>
87-
</div>
149+
)}
88150

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>
92-
<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-
})}
151+
{showKmpFilters && (
152+
<div className={styles.group} role="group" aria-labelledby={uiTechTitleId}
153+
data-test="filter-ui-technology">
154+
<h3 id={uiTechTitleId} className={styles.groupTitle}><span className={darkTextCn('rs-h4')}>UI technology</span>
155+
</h3>
156+
<div className={styles.checkboxes}>
157+
<Checkbox
158+
checked={compose}
159+
onChange={onComposeChange}
160+
mode="classic"
161+
size="m"
162+
>
163+
Built with Compose Multiplatform
164+
</Checkbox>
165+
</div>
108166
</div>
109-
</div>
167+
)}
110168
</div>
111169
</div>
112170
</section>

blocks/case-studies/grid/case-studies-grid.tsx

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,75 @@
1-
import React from 'react';
1+
import React, { useMemo } from 'react';
2+
import { useRouter } from 'next/router';
23

34
import caseStudiesDataRaw from '../../../data/case-studies/case-studies.yml';
45
import { CaseStudyCard } from '../card/case-studies-card';
56
import styles from './case-studies-grid.module.css';
7+
import type { CaseStudyItem } from '../case-studies';
8+
9+
const ALL_PLATFORMS = ['android', 'ios', 'desktop', 'frontend', 'backend'] as const;
10+
type PlatformFilter = typeof ALL_PLATFORMS[number];
11+
12+
type TypeFilter = 'all' | 'kotlin-multiplatform' | 'server-side';
13+
14+
function parseType(v: unknown): TypeFilter {
15+
const s = String(v || 'all');
16+
return (s === 'kotlin-multiplatform' || s === 'server-side') ? (s as TypeFilter) : 'all';
17+
}
18+
19+
function parsePlatforms(v: unknown): PlatformFilter[] {
20+
if (!v) return [...ALL_PLATFORMS];
21+
const list = String(v).split(',').map((x) => x.trim()).filter(Boolean);
22+
const set = new Set<PlatformFilter>();
23+
for (const i of list) {
24+
if ((ALL_PLATFORMS as readonly string[]).includes(i)) set.add(i as PlatformFilter);
25+
}
26+
return set.size === 0 ? [...ALL_PLATFORMS] : Array.from(set);
27+
}
28+
29+
function parseCompose(v: unknown): boolean {
30+
if (v === true) return true;
31+
const s = String(v || 'true').toLowerCase();
32+
return s === 'true' || s === '1' || s === 'yes';
33+
}
634

735
export const CaseStudiesGrid: React.FC = () => {
36+
const router = useRouter();
37+
38+
const type = parseType(router.query.type);
39+
const platforms = parsePlatforms(router.query.platforms);
40+
const compose = parseCompose(router.query.compose);
41+
42+
const items = useMemo(() => {
43+
const source: CaseStudyItem[] = caseStudiesDataRaw.items as any;
44+
return source.filter((it) => {
45+
// Map filter type to item types
46+
const isMp = it.type === 'multiplatform';
47+
const isSs = it.type === 'server-side';
48+
49+
if (type === 'server-side') return isSs;
50+
if (type === 'kotlin-multiplatform') {
51+
// Only multiplatform items with filters applied
52+
if (!isMp) return false;
53+
// Compose filter
54+
if (compose && !(it.platforms || []).includes('compose-multiplatform')) return false;
55+
// Platform filter applies only to MP: must intersect
56+
const intersects = (it.platforms || []).some((p: string) => (platforms as string[]).includes(p));
57+
return intersects;
58+
}
59+
// type === 'all': include server-side as is; apply filters to MP only
60+
if (isSs) return true;
61+
// MP item: apply compose + platforms
62+
if (compose && !(it.platforms || []).includes('compose-multiplatform')) return false;
63+
const intersects = (it.platforms || []).some((p: string) => (platforms as string[]).includes(p));
64+
return intersects;
65+
});
66+
}, [type, platforms, compose]);
67+
868
return (
969
<section data-testid="case-studies-grid" aria-label="Case Studies Grid">
1070
<h2>Case studies</h2>
1171
<div role="list" className={styles['masonry-tiles-container']}>
12-
{caseStudiesDataRaw.items.map((it) => (
72+
{items.map((it) => (
1373
<div
1474
key={it.id}
1575
role="listitem"

0 commit comments

Comments
 (0)