Skip to content

feat(SectionGallery): add section gallery components #47

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
May 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/components/NavEntry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export interface TextContentEntry {
data: {
id: string
section: string
tab?: string
sortValue?: number
}
}

Expand All @@ -22,6 +24,7 @@ export const NavEntry = ({ entry, isActive }: NavEntryProps) => {
section === 'components' || section === 'layouts'
? kebabCase(entryTitle)
: id

return (
<NavItem
itemId={_id}
Expand Down
25 changes: 16 additions & 9 deletions src/components/Navigation.astro
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,13 @@ const sortedSections = [...orderedSections, ...unorderedSections.sort()]
sortedSections.map((section) => {
const entries = navDataRaw
.filter((entry) => entry.data.section === section)
.map(entry => ({ id: entry.id, data: { id: entry.data.id, section }} as TextContentEntry))
.map(entry => ({ id: entry.id, data: { id: entry.data.id, section, sortValue: entry.data.sortValue }} as TextContentEntry))

const sortedEntries = entries.sort((a, b) =>
a.data.id.localeCompare(b.data.id),
)

let navEntries = sortedEntries
let uniqueEntries = entries
if (section === 'components' || section === 'layouts') {
// only display unique entry.data.id in the nav list if the section is components
navEntries = [
...sortedEntries
uniqueEntries = [
...entries
.reduce((map, entry) => {
if (!map.has(entry.data.id)) {
map.set(entry.data.id, entry)
Expand All @@ -61,7 +57,18 @@ sortedSections.map((section) => {
]
}

navData[section] = navEntries;
// Sort alphabetically, unless a sort value is specified in the frontmatter
const sortedUniqueEntries = uniqueEntries.sort((a, b) => {
if (a.data.sortValue || b.data.sortValue) {
const aSortOrder = a.data.sortValue || 50
const bSortOrder = b.data.sortValue || 50

return aSortOrder - bSortOrder
}
return a.data.id.localeCompare(b.data.id)
})

navData[section] = sortedUniqueEntries;
})

---
Expand Down
19 changes: 19 additions & 0 deletions src/components/section-gallery/SectionGallery.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
import { SectionGallery as SectionGalleryBase } from './SectionGallery'

const { illustrations, section, galleryItemsData, placeholderText, countText, initialLayout, hasGridText, hasGridImages, hasListText, hasListImages } = Astro.props
---

<SectionGalleryBase
illustrations={illustrations}
section={section}
galleryItemsData={galleryItemsData}
placeholderText={placeholderText}
countText={countText}
initialLayout={initialLayout}
hasGridText={hasGridText}
hasGridImages={hasGridImages}
hasListText={hasListText}
hasListImages={hasListImages}
client:only="react"
/>
20 changes: 20 additions & 0 deletions src/components/section-gallery/SectionGallery.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/* Toolbar styles */
.ws-section-gallery .pf-v6-c-toolbar {
margin-block-end: var(--pf-t--global--spacer--md);
}

/* Avoid link styling on gallery/data list item names */
.ws-section-gallery-item {
text-decoration: inherit;
color: inherit;
}

/* Ensure cards within a row stretch vertically to fill row height */
.ws-section-gallery .pf-v6-c-card {
height: 100%;
}

/* Limit width for data list view only */
.ws-section-gallery .pf-v6-c-data-list {
max-width: var(--pf-t--global--breakpoint--lg);
}
111 changes: 111 additions & 0 deletions src/components/section-gallery/SectionGallery.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { useState } from 'react'

import { SectionGalleryToolbar } from './SectionGalleryToolbar'
import { SectionGalleryGridLayout } from './SectionGalleryGridLayout'
import { SectionGalleryListLayout } from './SectionGalleryListLayout'
import { snakeCase } from 'change-case'

import './SectionGallery.css'

export interface SectionGalleryItem {
/** 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. */
name: string
/** Image file import */
img: any
/** Data of the gallery item */
data: SectionGalleryItemData
}

export interface SectionGalleryItemData {
/** Path to the item illustration */ // TODO: remove if img method is fine
illustration: string
/** Summary text of the item */
summary: string
/** Label included in the item footer. Choose from a preset or pass a custom label. */
label?: 'beta' | 'demo' | 'deprecated'
/** Link to the item, relative to the section, e.g. "/{section}/{page}" */
link?: string
}

interface SectionGalleryProps {
/** Collection of illustations for the gallery */
illustrations?: any
/** Section where the gallery is located */
section: string
/** Data of all gallery items */
galleryItemsData: Record<string, SectionGalleryItemData>
/** Placeholder text for the gallery search input */
placeholderText?: string
/** Text for the amount of gallery items */
countText?: string
/** Starting layout for the gallery */
initialLayout?: 'grid' | 'list'
/** Indicates the grid layout has item summary text */
hasGridText?: boolean
/** Indicates the grid layout has item images */
hasGridImages?: boolean
/** Indicates the list layout has item summary text */
hasListText?: boolean
/** Indicates the list layout has item images */
hasListImages?: boolean
}

export const SectionGallery = ({
illustrations,
section,
galleryItemsData,
placeholderText,
countText,
initialLayout = 'grid',
hasGridText = false,
hasGridImages = false,
hasListText = false,
hasListImages = false,
}: SectionGalleryProps) => {
const [searchTerm, setSearchTerm] = useState('')
const [layoutView, setLayoutView] = useState(initialLayout)

const galleryItems: SectionGalleryItem[] = Object.entries(galleryItemsData)
.map(([galleryItem, galleryItemData]) => ({
name: galleryItem,
img: illustrations ? illustrations[snakeCase(galleryItem)] : undefined,
data: galleryItemData,
}))
.sort((item1, item2) => item1.name.localeCompare(item2.name))

const nonCharsRegex = /[^A-Z0-9]+/gi
const filteringTerm = searchTerm.replace(nonCharsRegex, '')
const filteredItems: SectionGalleryItem[] = galleryItems.filter((item) =>
new RegExp(filteringTerm).test(item.name.replace(nonCharsRegex, '')),
)

return (
<div className="ws-section-gallery">
<SectionGalleryToolbar
galleryItemCount={galleryItems.length}
searchTerm={searchTerm}
setSearchTerm={setSearchTerm}
layoutView={layoutView}
setLayoutView={setLayoutView}
placeholderText={placeholderText}
countText={countText}
/>
{layoutView === 'grid' && (
<SectionGalleryGridLayout
section={section}
galleryItems={filteredItems}
hasGridText={hasGridText}
hasGridImages={hasGridImages}
/>
)}
{layoutView === 'list' && (
<SectionGalleryListLayout
section={section}
galleryItems={filteredItems}
hasListText={hasListText}
hasListImages={hasListImages}
/>
)}
</div>
)
}
97 changes: 97 additions & 0 deletions src/components/section-gallery/SectionGalleryGridLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import React from 'react'
import {
Gallery,
GalleryItem,
Card,
CardHeader,
CardTitle,
CardBody,
CardFooter,
Label,
Content,
} from '@patternfly/react-core'
import { SectionGalleryItem } from './SectionGallery'
import { sentenceCase } from 'change-case'
import { convertToReactComponent } from '@patternfly/ast-helpers'

interface SectionGalleryGridLayoutProps {
/** Section where the gallery is located */
section: string
/** List of gallery items */
galleryItems: SectionGalleryItem[]
/** Indicates the grid layout has item summary text */
hasGridText: boolean
/** Indicates the grid layout has item images */
hasGridImages: boolean
}

export const SectionGalleryGridLayout = ({
section,
galleryItems,
hasGridText,
hasGridImages,
}: SectionGalleryGridLayoutProps) => (
<Gallery hasGutter>
{galleryItems.map(({ name, img, data }, idx) => {
const itemLink = data.link || `/${section}/${name}`

//TODO: rethink how JSX / enriched content is passed to framework
const summaryNoLinks = data.summary.replace(
/<a[^>]*>([^<]+)<\/a>/gm,
'$1',
)
const { code } = convertToReactComponent(`<>${summaryNoLinks}</>`)
const getSummaryComponent = new Function('React', code)

return (
<GalleryItem span={4} key={idx}>
<Card id={name} key={idx} isClickable>
<CardHeader
selectableActions={{
to: itemLink,
selectableActionId: `${name}-input`,
selectableActionAriaLabelledby: name,
name: `clickable-card-${idx}`,
}}
>
<CardTitle>{sentenceCase(name)}</CardTitle>
</CardHeader>
{(hasGridImages || hasGridText) && (
<CardBody>
{hasGridImages && img && (
<img src={img.src} alt={`${name} illustration`} /> // verify whether this img.src approach is correct
)}
{hasGridText && (
<Content isEditorial>
<Content component="p">
{getSummaryComponent(React)}
</Content>
</Content>
)}
</CardBody>
)}
{data.label && (
<CardFooter>
{data.label === 'beta' && (
<Label color="blue" isCompact>
Beta
</Label>
)}
{data.label === 'deprecated' && (
<Label color="grey" isCompact>
Deprecated
</Label>
)}
{data.label === 'demo' && (
<Label color="purple" isCompact>
Demo
</Label>
)}
</CardFooter>
)}
</Card>
</GalleryItem>
)
})}
</Gallery>
)
Loading