Skip to content

Commit ecd7827

Browse files
committed
feat(SectionGallery): add section gallery components
1 parent fb6a78e commit ecd7827

13 files changed

+1382
-33
lines changed

src/components/NavEntry.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ export interface TextContentEntry {
66
data: {
77
id: string
88
section: string
9+
title?: string
910
tab?: string
11+
sortValue?: number
1012
}
1113
collection: string
1214
}
@@ -18,20 +20,23 @@ interface NavEntryProps {
1820

1921
export const NavEntry = ({ entry, isActive }: NavEntryProps) => {
2022
const { id } = entry
21-
const { id: entryTitle, section } = entry.data
23+
const { id: entryId, section, title } = entry.data
2224

2325
const _id =
2426
section === 'components' || section === 'layouts'
25-
? kebabCase(entryTitle)
27+
? kebabCase(entryId)
2628
: id
29+
30+
const displayName = entryId === 'landing' ? title : entryId; // landing pages must specify a title
31+
2732
return (
2833
<NavItem
2934
itemId={_id}
3035
to={`/${section}/${_id}`}
3136
isActive={isActive}
3237
id={`nav-entry-${_id}`}
3338
>
34-
{entryTitle}
39+
{displayName}
3540
</NavItem>
3641
)
3742
}

src/components/NavSection.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,16 @@ export const NavSection = ({
1616
}: NavSectionProps) => {
1717
const isExpanded = window.location.pathname.includes(sectionId)
1818

19-
const sortedNavEntries = entries.sort((a, b) =>
20-
a.data.id.localeCompare(b.data.id),
21-
)
19+
// Sort alphabetically, unless a sort value is specified in the frontmatter
20+
const sortedNavEntries = entries.sort((a, b) => {
21+
if (a.data.sortValue || b.data.sortValue) {
22+
const aSortOrder = a.data.sortValue || 50
23+
const bSortOrder = b.data.sortValue || 50
24+
25+
return aSortOrder - bSortOrder
26+
}
27+
return a.data.id.localeCompare(b.data.id)
28+
})
2229

2330
const isActive = sortedNavEntries.some((entry) => entry.id === activeItem)
2431

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/* Toolbar styles */
2+
.ws-section-gallery .pf-v6-c-toolbar {
3+
margin-block-end: var(--pf-t--global--spacer--md);
4+
}
5+
6+
/* Avoid link styling on gallery/data list item names */
7+
.ws-section-gallery-item {
8+
text-decoration: inherit;
9+
color: inherit;
10+
}
11+
12+
/* Ensure cards within a row stretch vertically to fill row height */
13+
.ws-section-gallery .pf-v6-c-card {
14+
height: 100%;
15+
}
16+
17+
/* Limit width for data list view only */
18+
.ws-section-gallery .pf-v6-c-data-list {
19+
max-width: var(--pf-t--global--breakpoint--lg);
20+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { useState } from 'react'
2+
3+
import { SectionGalleryToolbar } from './SectionGalleryToolbar'
4+
import { SectionGalleryGridLayout } from './SectionGalleryGridLayout'
5+
import { SectionGalleryListLayout } from './SectionGalleryListLayout'
6+
import { snakeCase } from 'change-case'
7+
8+
import './SectionGallery.css'
9+
10+
export interface SectionGalleryItem {
11+
/** Name of the gallery item. Should match the page name of the item for routing, or an item should provide a link property in the SectionGalleryItemData. */
12+
name: string
13+
/** Image file import */
14+
img: any
15+
/** Data of the gallery item */
16+
data: SectionGalleryItemData
17+
}
18+
19+
export interface SectionGalleryItemData {
20+
/** Path to the item illustration */ // TODO: remove if img method is fine
21+
illustration: string
22+
/** Summary text of the item */
23+
summary: JSX.Element
24+
/** Label included in the item footer. Choose from a preset or pass a custom label. */
25+
label?: 'beta' | 'demo' | 'deprecated' | JSX.Element
26+
/** Link to the item, relative to the section, e.g. "/{section}/{page}" */
27+
link?: string
28+
}
29+
30+
interface SectionGalleryProps {
31+
/** Collection of illustations for the gallery */
32+
illustrations: any
33+
/** Section where the gallery is located */
34+
section: string
35+
/** Data of all gallery items */
36+
galleryItemsData: Record<string, SectionGalleryItemData>
37+
/** Placeholder text for the gallery search input */
38+
placeholderText: string
39+
/** Text for the amount of gallery items */
40+
countText: string
41+
/** Starting layout for the gallery */
42+
initialLayout: 'grid' | 'list'
43+
/** Indicates the grid layout has item summary text */
44+
hasGridText: boolean
45+
/** Indicates the grid layout has item images */
46+
hasGridImages: boolean
47+
/** Indicates the list layout has item summary text */
48+
hasListText: boolean
49+
/** Indicates the list layout has item images */
50+
hasListImages: boolean
51+
}
52+
53+
export const SectionGallery = ({
54+
illustrations,
55+
section,
56+
galleryItemsData,
57+
placeholderText,
58+
countText,
59+
initialLayout = 'grid',
60+
hasGridText = false,
61+
hasGridImages = true,
62+
hasListText = true,
63+
hasListImages = true,
64+
}: SectionGalleryProps) => {
65+
const [searchTerm, setSearchTerm] = useState('')
66+
const [layoutView, setLayoutView] = useState(initialLayout)
67+
68+
const galleryItems: SectionGalleryItem[] = Object.entries(galleryItemsData)
69+
.map(([galleryItem, galleryItemData]) => ({
70+
name: galleryItem,
71+
img: illustrations[snakeCase(galleryItem)],
72+
data: galleryItemData,
73+
}))
74+
.sort((item1, item2) => item1.name.localeCompare(item2.name))
75+
76+
const nonCharsRegex = /[^A-Z0-9]+/gi
77+
const filteringTerm = searchTerm.replace(nonCharsRegex, '')
78+
const filteredItems: SectionGalleryItem[] = galleryItems.filter((item) =>
79+
new RegExp(filteringTerm).test(item.name.replace(nonCharsRegex, '')),
80+
)
81+
82+
return (
83+
<div className='ws-section-gallery'>
84+
<SectionGalleryToolbar
85+
galleryItemCount={galleryItems.length}
86+
searchTerm={searchTerm}
87+
setSearchTerm={setSearchTerm}
88+
layoutView={layoutView}
89+
setLayoutView={setLayoutView}
90+
placeholderText={placeholderText}
91+
countText={countText}
92+
/>
93+
{layoutView === 'grid' && (
94+
<SectionGalleryGridLayout
95+
section={section}
96+
galleryItems={filteredItems}
97+
hasGridText={hasGridText}
98+
hasGridImages={hasGridImages}
99+
/>
100+
)}
101+
{layoutView === 'list' && (
102+
<SectionGalleryListLayout
103+
section={section}
104+
galleryItems={filteredItems}
105+
hasListText={hasListText}
106+
hasListImages={hasListImages}
107+
/>
108+
)}
109+
</div>
110+
)
111+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import {
2+
Gallery,
3+
GalleryItem,
4+
Card,
5+
CardHeader,
6+
CardTitle,
7+
CardBody,
8+
CardFooter,
9+
Label,
10+
Content,
11+
} from '@patternfly/react-core'
12+
import { SectionGalleryItem } from './SectionGallery'
13+
import { sentenceCase } from 'change-case'
14+
15+
interface SectionGalleryGridLayoutProps {
16+
/** Section where the gallery is located */
17+
section: string
18+
/** List of gallery items */
19+
galleryItems: SectionGalleryItem[]
20+
/** Indicates the grid layout has item summary text */
21+
hasGridText: boolean
22+
/** Indicates the grid layout has item images */
23+
hasGridImages: boolean
24+
}
25+
26+
export const SectionGalleryGridLayout = ({
27+
section,
28+
galleryItems,
29+
hasGridText,
30+
hasGridImages,
31+
}: SectionGalleryGridLayoutProps) => (
32+
<Gallery hasGutter>
33+
{galleryItems.map(({ name, img, data }, idx) => {
34+
const itemLink = data.link || `/${section}/${name}`
35+
36+
return (
37+
<GalleryItem span={4} key={idx}>
38+
<Card id={name} key={idx} isClickable>
39+
<CardHeader
40+
selectableActions={{
41+
to: itemLink,
42+
selectableActionId: `${name}-input`,
43+
selectableActionAriaLabelledby: name,
44+
name: `clickable-card-${idx}`,
45+
}}
46+
>
47+
<CardTitle>{sentenceCase(name)}</CardTitle>
48+
</CardHeader>
49+
{(hasGridImages || hasGridText) && (
50+
<CardBody>
51+
{hasGridImages && data.illustration && (
52+
<img src={img.src} alt={`${name} illustration`} /> // verify whether this img.src approach is correct
53+
)}
54+
{hasGridText && (
55+
<Content isEditorial>
56+
<Content component="p">{data.summary}</Content>
57+
</Content>
58+
)}
59+
</CardBody>
60+
)}
61+
{data.label && (
62+
<CardFooter>
63+
{data.label === 'beta' && (
64+
<Label color="blue" isCompact>
65+
Beta
66+
</Label>
67+
)}
68+
{data.label === 'deprecated' && (
69+
<Label color="grey" isCompact>
70+
Deprecated
71+
</Label>
72+
)}
73+
{data.label === 'demo' && (
74+
<Label color="purple" isCompact>
75+
Demo
76+
</Label>
77+
)}
78+
{typeof data.label !== 'string' && <>{data.label}</>}
79+
</CardFooter>
80+
)}
81+
</Card>
82+
</GalleryItem>
83+
)})}
84+
</Gallery>
85+
)
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import {
2+
DataList,
3+
DataListItem,
4+
DataListItemRow,
5+
DataListItemCells,
6+
DataListCell,
7+
Split,
8+
SplitItem,
9+
Label,
10+
Content,
11+
ContentVariants,
12+
} from '@patternfly/react-core'
13+
import { SectionGalleryItem } from './SectionGallery'
14+
import { sentenceCase } from 'change-case'
15+
16+
interface SectionGalleryListLayoutProps {
17+
/** Section where the gallery is located */
18+
section: string
19+
/** List of gallery items */
20+
galleryItems: SectionGalleryItem[]
21+
/** Indicates the list layout has item summary text */
22+
hasListText: boolean
23+
/** Indicates the list layout has item images */
24+
hasListImages: boolean
25+
}
26+
27+
export const SectionGalleryListLayout = ({
28+
section,
29+
galleryItems,
30+
hasListText,
31+
hasListImages,
32+
}: SectionGalleryListLayoutProps) => (
33+
<DataList onSelectDataListItem={() => {}} aria-label="gallery-list">
34+
{galleryItems.map(({ name, img, data }, idx) => {
35+
const itemLink = data.link || `/${section}/${name}`
36+
37+
return (
38+
<a href={itemLink} key={idx} className="ws-section-gallery-item">
39+
<DataListItem>
40+
<DataListItemRow>
41+
<DataListItemCells
42+
dataListCells={[
43+
hasListImages && img && (
44+
<DataListCell width={1} key="illustration">
45+
<div>
46+
<img
47+
src={img.src} // same as GridLayout, check whether this is the best method
48+
alt={`${name} illustration`}
49+
/>
50+
</div>
51+
</DataListCell>
52+
),
53+
<DataListCell width={5} key="text-description">
54+
<Split className={hasListText ? "pf-v6-u-mb-md" : undefined}>
55+
<SplitItem isFilled>
56+
<Content isEditorial>
57+
<Content component={ContentVariants.h2}>
58+
<span>{sentenceCase(name)}</span>
59+
</Content>
60+
</Content>
61+
</SplitItem>
62+
<SplitItem>
63+
{data.label === 'beta' && (
64+
<Label color="blue" isCompact>
65+
Beta
66+
</Label>
67+
)}
68+
{data.label === 'deprecated' && (
69+
<Label color="grey" isCompact>
70+
Deprecated
71+
</Label>
72+
)}
73+
{data.label === 'demo' && (
74+
<Label color="purple" isCompact>
75+
Demo
76+
</Label>
77+
)}
78+
{typeof data.label !== 'string' && <>{data.label}</>}
79+
</SplitItem>
80+
</Split>
81+
{hasListText && (
82+
<Content isEditorial>
83+
<Content component="p">{data.summary}</Content>
84+
</Content>
85+
)}
86+
</DataListCell>,
87+
]}
88+
/>
89+
</DataListItemRow>
90+
</DataListItem>
91+
</a>
92+
)})}
93+
</DataList>
94+
)

0 commit comments

Comments
 (0)