Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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