Skip to content

Commit 5d8d446

Browse files
authored
feat(SectionGallery): add section gallery components (#47)
* feat(SectionGallery): add section gallery components * update sectiongallery from rebase * some further updates to piping SectionGallery * fixing interactive content * remove console log * remove console log 2 * make optional props optional
1 parent e4def1f commit 5d8d446

File tree

14 files changed

+996
-43
lines changed

14 files changed

+996
-43
lines changed

src/components/NavEntry.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ export interface TextContentEntry {
66
data: {
77
id: string
88
section: string
9+
tab?: string
10+
sortValue?: number
911
}
1012
}
1113

@@ -22,6 +24,7 @@ export const NavEntry = ({ entry, isActive }: NavEntryProps) => {
2224
section === 'components' || section === 'layouts'
2325
? kebabCase(entryTitle)
2426
: id
27+
2528
return (
2629
<NavItem
2730
itemId={_id}

src/components/Navigation.astro

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,13 @@ const sortedSections = [...orderedSections, ...unorderedSections.sort()]
4040
sortedSections.map((section) => {
4141
const entries = navDataRaw
4242
.filter((entry) => entry.data.section === section)
43-
.map(entry => ({ id: entry.id, data: { id: entry.data.id, section }} as TextContentEntry))
43+
.map(entry => ({ id: entry.id, data: { id: entry.data.id, section, sortValue: entry.data.sortValue }} as TextContentEntry))
4444
45-
const sortedEntries = entries.sort((a, b) =>
46-
a.data.id.localeCompare(b.data.id),
47-
)
48-
49-
let navEntries = sortedEntries
45+
let uniqueEntries = entries
5046
if (section === 'components' || section === 'layouts') {
5147
// only display unique entry.data.id in the nav list if the section is components
52-
navEntries = [
53-
...sortedEntries
48+
uniqueEntries = [
49+
...entries
5450
.reduce((map, entry) => {
5551
if (!map.has(entry.data.id)) {
5652
map.set(entry.data.id, entry)
@@ -61,7 +57,18 @@ sortedSections.map((section) => {
6157
]
6258
}
6359
64-
navData[section] = navEntries;
60+
// Sort alphabetically, unless a sort value is specified in the frontmatter
61+
const sortedUniqueEntries = uniqueEntries.sort((a, b) => {
62+
if (a.data.sortValue || b.data.sortValue) {
63+
const aSortOrder = a.data.sortValue || 50
64+
const bSortOrder = b.data.sortValue || 50
65+
66+
return aSortOrder - bSortOrder
67+
}
68+
return a.data.id.localeCompare(b.data.id)
69+
})
70+
71+
navData[section] = sortedUniqueEntries;
6572
})
6673
6774
---
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
import { SectionGallery as SectionGalleryBase } from './SectionGallery'
3+
4+
const { illustrations, section, galleryItemsData, placeholderText, countText, initialLayout, hasGridText, hasGridImages, hasListText, hasListImages } = Astro.props
5+
---
6+
7+
<SectionGalleryBase
8+
illustrations={illustrations}
9+
section={section}
10+
galleryItemsData={galleryItemsData}
11+
placeholderText={placeholderText}
12+
countText={countText}
13+
initialLayout={initialLayout}
14+
hasGridText={hasGridText}
15+
hasGridImages={hasGridImages}
16+
hasListText={hasListText}
17+
hasListImages={hasListImages}
18+
client:only="react"
19+
/>
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: string
24+
/** Label included in the item footer. Choose from a preset or pass a custom label. */
25+
label?: 'beta' | 'demo' | 'deprecated'
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 = false,
62+
hasListText = false,
63+
hasListImages = false,
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 ? illustrations[snakeCase(galleryItem)] : undefined,
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: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import React from 'react'
2+
import {
3+
Gallery,
4+
GalleryItem,
5+
Card,
6+
CardHeader,
7+
CardTitle,
8+
CardBody,
9+
CardFooter,
10+
Label,
11+
Content,
12+
} from '@patternfly/react-core'
13+
import { SectionGalleryItem } from './SectionGallery'
14+
import { sentenceCase } from 'change-case'
15+
import { convertToReactComponent } from '@patternfly/ast-helpers'
16+
17+
interface SectionGalleryGridLayoutProps {
18+
/** Section where the gallery is located */
19+
section: string
20+
/** List of gallery items */
21+
galleryItems: SectionGalleryItem[]
22+
/** Indicates the grid layout has item summary text */
23+
hasGridText: boolean
24+
/** Indicates the grid layout has item images */
25+
hasGridImages: boolean
26+
}
27+
28+
export const SectionGalleryGridLayout = ({
29+
section,
30+
galleryItems,
31+
hasGridText,
32+
hasGridImages,
33+
}: SectionGalleryGridLayoutProps) => (
34+
<Gallery hasGutter>
35+
{galleryItems.map(({ name, img, data }, idx) => {
36+
const itemLink = data.link || `/${section}/${name}`
37+
38+
//TODO: rethink how JSX / enriched content is passed to framework
39+
const summaryNoLinks = data.summary.replace(
40+
/<a[^>]*>([^<]+)<\/a>/gm,
41+
'$1',
42+
)
43+
const { code } = convertToReactComponent(`<>${summaryNoLinks}</>`)
44+
const getSummaryComponent = new Function('React', code)
45+
46+
return (
47+
<GalleryItem span={4} key={idx}>
48+
<Card id={name} key={idx} isClickable>
49+
<CardHeader
50+
selectableActions={{
51+
to: itemLink,
52+
selectableActionId: `${name}-input`,
53+
selectableActionAriaLabelledby: name,
54+
name: `clickable-card-${idx}`,
55+
}}
56+
>
57+
<CardTitle>{sentenceCase(name)}</CardTitle>
58+
</CardHeader>
59+
{(hasGridImages || hasGridText) && (
60+
<CardBody>
61+
{hasGridImages && img && (
62+
<img src={img.src} alt={`${name} illustration`} /> // verify whether this img.src approach is correct
63+
)}
64+
{hasGridText && (
65+
<Content isEditorial>
66+
<Content component="p">
67+
{getSummaryComponent(React)}
68+
</Content>
69+
</Content>
70+
)}
71+
</CardBody>
72+
)}
73+
{data.label && (
74+
<CardFooter>
75+
{data.label === 'beta' && (
76+
<Label color="blue" isCompact>
77+
Beta
78+
</Label>
79+
)}
80+
{data.label === 'deprecated' && (
81+
<Label color="grey" isCompact>
82+
Deprecated
83+
</Label>
84+
)}
85+
{data.label === 'demo' && (
86+
<Label color="purple" isCompact>
87+
Demo
88+
</Label>
89+
)}
90+
</CardFooter>
91+
)}
92+
</Card>
93+
</GalleryItem>
94+
)
95+
})}
96+
</Gallery>
97+
)

0 commit comments

Comments
 (0)