зеркало из https://github.com/github/docs.git
SubLanding page filter (#19918)
* sub-landing: implement filtering in react, other cleanup
This commit is contained in:
Родитель
cfd962c2f5
Коммит
e26a3446a7
|
@ -42,6 +42,7 @@ module.exports = {
|
|||
'no-undef': 'off',
|
||||
'no-use-before-define': 'off',
|
||||
'@typescript-eslint/no-unused-vars': ['error'],
|
||||
'jsx-a11y/no-onchange': 'off',
|
||||
}
|
||||
},
|
||||
]
|
||||
|
|
|
@ -9,7 +9,10 @@ export type BreadcrumbT = {
|
|||
href?: string
|
||||
}
|
||||
|
||||
export const Breadcrumbs = () => {
|
||||
type Props = {
|
||||
variant?: 'default' | 'large'
|
||||
}
|
||||
export const Breadcrumbs = ({ variant = 'default' }: Props) => {
|
||||
const router = useRouter()
|
||||
const pathWithLocale = `/${router.locale}${router.asPath.split('?')[0]}` // remove query string
|
||||
const { breadcrumbs } = useMainContext()
|
||||
|
@ -26,20 +29,6 @@ export const Breadcrumbs = () => {
|
|||
<span key={title} title={title}>
|
||||
{breadcrumb.title}
|
||||
</span>
|
||||
) : pathWithLocale.includes('/guides') ? (
|
||||
<span className="text-mono color-text-secondary text-uppercase">
|
||||
<Link
|
||||
key={title}
|
||||
href={breadcrumb.href}
|
||||
title={title}
|
||||
className={cx(
|
||||
'd-inline-block',
|
||||
pathWithLocale === breadcrumb.href && 'color-text-tertiary'
|
||||
)}
|
||||
>
|
||||
{breadcrumb.title}
|
||||
</Link>
|
||||
</span>
|
||||
) : (
|
||||
<Link
|
||||
key={title}
|
||||
|
@ -47,6 +36,7 @@ export const Breadcrumbs = () => {
|
|||
title={title}
|
||||
className={cx(
|
||||
'd-inline-block',
|
||||
variant === 'large' && 'text-uppercase text-mono',
|
||||
pathWithLocale === breadcrumb.href && 'color-text-tertiary'
|
||||
)}
|
||||
>
|
||||
|
|
|
@ -45,20 +45,22 @@ export const getProductSubLandingContextFromRequest = (req: any): ProductSubLand
|
|||
return {
|
||||
...pick(page, ['intro', 'allTopics']),
|
||||
title: req.context.productMap[req.context.currentProduct].name,
|
||||
featuredTrack: page.featuredTrack ? {
|
||||
...pick(page.featuredTrack, ['title', 'description', 'trackName', 'guides']),
|
||||
guides: (page.featuredTrack?.guides || []).map((guide: any) => {
|
||||
return pick(guide, ['title', 'intro', 'href', 'page.type'])
|
||||
})
|
||||
} : null,
|
||||
featuredTrack: page.featuredTrack
|
||||
? {
|
||||
...pick(page.featuredTrack, ['title', 'description', 'trackName']),
|
||||
guides: (page.featuredTrack?.guides || []).map((guide: any) => {
|
||||
return pick(guide, ['title', 'intro', 'href', 'page.type'])
|
||||
}),
|
||||
}
|
||||
: null,
|
||||
learningTracks: (page.learningTracks || []).map((track: any) => ({
|
||||
...pick(track, ['title', 'description', 'trackName', 'guides']),
|
||||
...pick(track, ['title', 'description', 'trackName']),
|
||||
guides: (track.guides || []).map((guide: any) => {
|
||||
return pick(guide, ['title', 'intro', 'href', 'page.type'])
|
||||
}),
|
||||
})),
|
||||
includeGuides: (page.includeGuides || []).map((guide: any) => {
|
||||
return pick(guide, ['href', 'title', 'intro', 'page.type', 'topics'])
|
||||
return pick(guide, ['href', 'title', 'intro', 'type', 'topics'])
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,22 +34,24 @@ export const LandingHero = () => {
|
|||
/>
|
||||
|
||||
{introLinks &&
|
||||
Object.entries(introLinks).filter(([key, link])=> {
|
||||
return link && !key.includes('raw')
|
||||
}).map(([key, link], i) => {
|
||||
if (!link) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<FullLink
|
||||
key={link}
|
||||
href={link}
|
||||
className={cx('btn-mktg bt-large f4 mt-3 mr-3', i !== 0 && 'btn-outline-mktg')}
|
||||
>
|
||||
{t(key)}
|
||||
</FullLink>
|
||||
)
|
||||
})}
|
||||
Object.entries(introLinks)
|
||||
.filter(([key, link]) => {
|
||||
return link && !key.includes('raw')
|
||||
})
|
||||
.map(([key, link], i) => {
|
||||
if (!link) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<FullLink
|
||||
key={link}
|
||||
href={link}
|
||||
className={cx('btn-mktg bt-large f4 mt-3 mr-3', i !== 0 && 'btn-outline-mktg')}
|
||||
>
|
||||
{t(key)}
|
||||
</FullLink>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{product_video && (
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import cx from 'classnames'
|
||||
import { useTranslation } from 'components/hooks/useTranslation'
|
||||
|
||||
type Props = {
|
||||
title?: React.ReactNode
|
||||
|
@ -9,8 +8,6 @@ type Props = {
|
|||
description?: string
|
||||
}
|
||||
export const LandingSection = ({ title, children, className, sectionLink, description }: Props) => {
|
||||
const { t } = useTranslation('product_sublanding')
|
||||
|
||||
return (
|
||||
<div className={cx('container-xl px-3 px-md-6', className)} id={sectionLink}>
|
||||
{title && (
|
||||
|
@ -21,7 +18,7 @@ export const LandingSection = ({ title, children, className, sectionLink, descri
|
|||
{description && (
|
||||
<div
|
||||
className="lead-mktg color-text-secondary f4 description-text"
|
||||
dangerouslySetInnerHTML={{ __html: t(description) }}
|
||||
dangerouslySetInnerHTML={{ __html: description }}
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
|
|
|
@ -32,7 +32,10 @@ export const TocLanding = () => {
|
|||
</div>
|
||||
|
||||
{productCallout && (
|
||||
<div className="product-callout border rounded-1 mb-4 p-3 color-border-success color-bg-success" dangerouslySetInnerHTML={{__html: productCallout }} />
|
||||
<div
|
||||
className="product-callout border rounded-1 mb-4 p-3 color-border-success color-bg-success"
|
||||
dangerouslySetInnerHTML={{ __html: productCallout }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -2,26 +2,24 @@ import { ArticleGuide } from 'components/context/ProductSubLandingContext'
|
|||
|
||||
type Props = {
|
||||
card: ArticleGuide
|
||||
type: string
|
||||
display?: string
|
||||
typeLabel: string
|
||||
}
|
||||
|
||||
export const ArticleCard = ({ card, type, display }: Props) => {
|
||||
export const ArticleCard = ({ card, typeLabel }: Props) => {
|
||||
return (
|
||||
<div
|
||||
className={`d-flex col-12 col-md-4 pr-0 pr-md-6 pr-lg-8 ${display} js-filter-card`}
|
||||
data-type={card.type}
|
||||
data-topics={card.topics.join(',')}
|
||||
>
|
||||
<div className="d-flex col-12 col-md-4 pr-0 pr-md-6 pr-lg-8">
|
||||
<a className="no-underline d-flex flex-column py-3 border-bottom" href={card.href}>
|
||||
<h4 className="h4 color-text-primary mb-1">{card.title}</h4>
|
||||
<div className="h6 text-uppercase">{type}</div>
|
||||
<div className="h6 text-uppercase">{typeLabel}</div>
|
||||
<p className="color-text-secondary my-3">{card.intro}</p>
|
||||
{card.topics.length > 0 && (
|
||||
<div>
|
||||
{card.topics.map((topic) => {
|
||||
return (
|
||||
<span key={topic} className="IssueLabel bg-gradient--pink-blue color-text-inverse mr-1">
|
||||
<span
|
||||
key={topic}
|
||||
className="IssueLabel bg-gradient--pink-blue color-text-inverse mr-1"
|
||||
>
|
||||
{topic}
|
||||
</span>
|
||||
)
|
||||
|
|
|
@ -1,12 +1,42 @@
|
|||
import { useProductSubLandingContext } from 'components/context/ProductSubLandingContext'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import {
|
||||
ArticleGuide,
|
||||
useProductSubLandingContext,
|
||||
} from 'components/context/ProductSubLandingContext'
|
||||
import { useTranslation } from 'components/hooks/useTranslation'
|
||||
import { ArticleCard } from './ArticleCard'
|
||||
|
||||
const MAX_ARTICLES = 9
|
||||
const PAGE_SIZE = 9
|
||||
export const ArticleCards = () => {
|
||||
const { t } = useTranslation('product_sublanding')
|
||||
const guideTypes: Record<string, string> = t('guide_types')
|
||||
const { allTopics, includeGuides } = useProductSubLandingContext()
|
||||
const [numVisible, setNumVisible] = useState(PAGE_SIZE)
|
||||
const [typeFilter, setTypeFilter] = useState('')
|
||||
const [topicFilter, setTopicFilter] = useState('')
|
||||
const [filteredResults, setFilteredResults] = useState<Array<ArticleGuide>>([])
|
||||
|
||||
useEffect(() => {
|
||||
setNumVisible(PAGE_SIZE)
|
||||
setFilteredResults(
|
||||
(includeGuides || []).filter((card) => {
|
||||
const matchesType = card.type === typeFilter
|
||||
const matchesTopic = card.topics.some((key) => key === topicFilter)
|
||||
return (typeFilter ? matchesType : true) && (topicFilter ? matchesTopic : true)
|
||||
})
|
||||
)
|
||||
}, [typeFilter, topicFilter])
|
||||
|
||||
const isUserFiltering = typeFilter !== '' || topicFilter !== ''
|
||||
const onChangeTypeFilter = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setTypeFilter(e.target.value)
|
||||
}
|
||||
const onChangeTopicFilter = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setTopicFilter(e.target.value)
|
||||
}
|
||||
|
||||
const guides = isUserFiltering ? filteredResults : includeGuides || []
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
@ -16,13 +46,19 @@ export const ArticleCards = () => {
|
|||
{t('filters.type')}
|
||||
</label>
|
||||
<select
|
||||
className="form-select js-filter-card-filter-dropdown f4 text-bold border-0 rounded-0 border-top box-shadow-none pl-0 js-filter-card-filter-dropdown"
|
||||
value={typeFilter}
|
||||
className="form-select f4 text-bold border-0 rounded-0 border-top box-shadow-none pl-0"
|
||||
name="type"
|
||||
aria-label="guide types"
|
||||
onChange={onChangeTypeFilter}
|
||||
>
|
||||
<option value="">{t('filters.all')}</option>
|
||||
{Object.entries(guideTypes).map(([key, val]) => {
|
||||
return <option key={key} value={key}>{val}</option>
|
||||
return (
|
||||
<option key={key} value={key}>
|
||||
{val}
|
||||
</option>
|
||||
)
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
|
@ -31,37 +67,44 @@ export const ArticleCards = () => {
|
|||
{t('filters.topic')}
|
||||
</label>
|
||||
<select
|
||||
className="form-select js-filter-card-filter-dropdown f4 text-bold border-0 rounded-0 border-top box-shadow-none pl-0 js-filter-card-filter-dropdown"
|
||||
value={topicFilter}
|
||||
className="form-select f4 text-bold border-0 rounded-0 border-top box-shadow-none pl-0"
|
||||
name="topics"
|
||||
aria-label="guide topics"
|
||||
onChange={onChangeTopicFilter}
|
||||
>
|
||||
<option value="">{t('filters.all')}</option>
|
||||
{allTopics?.map((topic) => {
|
||||
return <option key={topic} value={topic}>{topic}</option>
|
||||
return (
|
||||
<option key={topic} value={topic}>
|
||||
{topic}
|
||||
</option>
|
||||
)
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="d-flex flex-wrap mr-0 mr-md-n6 mr-lg-n8">
|
||||
{(includeGuides || []).map((card, index) => {
|
||||
return index + 1 > MAX_ARTICLES ? (
|
||||
<ArticleCard key={card.title} card={card} type={guideTypes[card.type]} display={'d-none'} />
|
||||
) : (
|
||||
<ArticleCard key={card.title} card={card} type={guideTypes[card.type]} />
|
||||
)
|
||||
{guides.slice(0, numVisible).map((card) => {
|
||||
return <ArticleCard key={card.href} card={card} typeLabel={guideTypes[card.type]} />
|
||||
})}
|
||||
</div>
|
||||
{includeGuides && includeGuides.length > MAX_ARTICLES && (
|
||||
|
||||
{guides.length > numVisible && (
|
||||
<button
|
||||
className="col-12 mt-5 text-center text-bold color-text-link btn-link js-filter-card-show-more"
|
||||
data-js-filter-card-max={MAX_ARTICLES}
|
||||
className="col-12 mt-5 text-center text-bold color-text-link btn-link"
|
||||
onClick={() => setNumVisible(numVisible + PAGE_SIZE)}
|
||||
>
|
||||
{t('load_more')}
|
||||
</button>
|
||||
)}
|
||||
<div className="js-filter-card-no-results d-none py-4 text-center color-text-secondary">
|
||||
<h4 className="text-normal">{t('no_result')}</h4>
|
||||
</div>
|
||||
|
||||
{guides.length === 0 && (
|
||||
<div className="py-4 text-center color-text-secondary">
|
||||
<h4 className="text-normal">{t('no_result')}</h4>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -5,10 +5,11 @@ import { LandingSection } from 'components/landing/LandingSection'
|
|||
import { SubLandingHero } from 'components/sublanding/SubLandingHero'
|
||||
import { LearningTracks } from 'components/sublanding/LearningTracks'
|
||||
import { ArticleCards } from 'components/sublanding/ArticleCards'
|
||||
import { useTranslation } from 'components/hooks/useTranslation'
|
||||
|
||||
export const ProductSubLanding = () => {
|
||||
const { title, learningTracks, includeGuides } = useProductSubLandingContext()
|
||||
|
||||
const { t } = useTranslation('sub_landing')
|
||||
return (
|
||||
<DefaultLayout>
|
||||
<LandingSection className="pt-3">
|
||||
|
@ -20,7 +21,7 @@ export const ProductSubLanding = () => {
|
|||
title={`${title} learning paths`}
|
||||
className="border-top py-6"
|
||||
sectionLink="learning-paths"
|
||||
description="learning_paths_desc"
|
||||
description={t('learning_paths_desc')}
|
||||
>
|
||||
<LearningTracks />
|
||||
</LandingSection>
|
||||
|
|
|
@ -2,6 +2,7 @@ import { Breadcrumbs } from '../Breadcrumbs'
|
|||
import { useProductSubLandingContext } from 'components/context/ProductSubLandingContext'
|
||||
import { ArrowRightIcon, StarFillIcon } from '@primer/octicons-react'
|
||||
import { useTranslation } from 'components/hooks/useTranslation'
|
||||
import { Link } from 'components/Link'
|
||||
|
||||
export const SubLandingHero = () => {
|
||||
const { title, intro, featuredTrack } = useProductSubLandingContext()
|
||||
|
@ -9,7 +10,7 @@ export const SubLandingHero = () => {
|
|||
|
||||
const guideItems = featuredTrack?.guides?.map((guide) => (
|
||||
<li className="px-2 d-flex flex-shrink-0">
|
||||
<a
|
||||
<Link
|
||||
href={`${guide.href}?learn=${featuredTrack.trackName}`}
|
||||
className="d-inline-block Box p-5 color-bg-primary color-border-primary no-underline"
|
||||
>
|
||||
|
@ -32,7 +33,7 @@ export const SubLandingHero = () => {
|
|||
<div className="lead-mktg color-text-secondary f5 my-4 truncate-overflow-8">
|
||||
{guide.intro}
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
))
|
||||
|
||||
|
@ -40,7 +41,7 @@ export const SubLandingHero = () => {
|
|||
<div>
|
||||
<header className="d-flex gutter mb-6">
|
||||
<div className="col-12">
|
||||
<Breadcrumbs />
|
||||
<Breadcrumbs variant="large" />
|
||||
<h1 className="my-3 font-mktg">{title} guides</h1>
|
||||
<div
|
||||
className="lead-mktg color-text-secondary f4 description-text"
|
||||
|
@ -64,7 +65,7 @@ export const SubLandingHero = () => {
|
|||
{featuredTrack.description}
|
||||
</div>
|
||||
{featuredTrack.guides && (
|
||||
<a
|
||||
<Link
|
||||
className="d-inline-block border color-border-inverse color-text-inverse px-4 py-2 f5 no-underline text-bold"
|
||||
role="button"
|
||||
href={`${featuredTrack.guides[0].href}?learn=${featuredTrack.trackName}`}
|
||||
|
@ -73,7 +74,7 @@ export const SubLandingHero = () => {
|
|||
<ArrowRightIcon size={20} />
|
||||
</span>
|
||||
{t(`start_path`)}
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
|
|
|
@ -148,7 +148,7 @@ product_landing:
|
|||
sorry: Sorry, there is no result for
|
||||
no_example: It looks like we don't have an example that fits your filter.
|
||||
try_another: Try another filter or add your code example.
|
||||
no_result: Sorry, there is no guide that match your filter.
|
||||
no_result: Sorry, there are no guides that match your filter.
|
||||
learn: Learn how to add a code example
|
||||
communities_using_discussions: Communities using discussions
|
||||
add_your_community: Add your community
|
||||
|
@ -165,6 +165,7 @@ product_sublanding:
|
|||
learning_paths_desc: Learning paths are a collection of guides that help you master a particular subject.
|
||||
guides: '{{ productMap[currentProduct].name }} guides'
|
||||
more_guides: more guides
|
||||
no_result: Sorry, there are no guides that match your filter.
|
||||
load_more: Load more guides
|
||||
all_guides: 'All {{ productMap[currentProduct].name }} guides'
|
||||
filters:
|
||||
|
|
|
@ -19,8 +19,7 @@ export default function displayPlatformSpecificContent() {
|
|||
if (!platformsInContent.includes(platform)) {
|
||||
// uses the order of the supportedPlatforms array to
|
||||
// determine the default platform
|
||||
platform = supportedPlatforms
|
||||
.filter(elem => platformsInContent.includes(elem))[0]
|
||||
platform = supportedPlatforms.filter((elem) => platformsInContent.includes(elem))[0]
|
||||
}
|
||||
|
||||
showPlatformSpecificContent(platform)
|
||||
|
@ -83,17 +82,20 @@ function hideSwitcherLinks(platformsInContent: Array<string>) {
|
|||
}
|
||||
|
||||
// gets the list of detected platforms in the current article
|
||||
function getDetectedPlatforms (): Array<string> {
|
||||
function getDetectedPlatforms(): Array<string> {
|
||||
// find all platform-specific *block* elements and hide or show as appropriate
|
||||
// example: {{ #mac }} block content {{/mac}}
|
||||
const allEls = Array.from(document.querySelectorAll('.extended-markdown')) as Array<HTMLElement>
|
||||
allEls.filter(el => supportedPlatforms.some(platform => el.classList.contains(platform)))
|
||||
.forEach(el => detectPlatforms(el))
|
||||
allEls
|
||||
.filter((el) => supportedPlatforms.some((platform) => el.classList.contains(platform)))
|
||||
.forEach((el) => detectPlatforms(el))
|
||||
|
||||
// find all platform-specific *inline* elements and hide or show as appropriate
|
||||
// example: <span class="platform-mac">inline content</span>
|
||||
const platformEls = Array.from(document.querySelectorAll('.platform-mac, .platform-windows, .platform-linux')) as Array<HTMLElement>
|
||||
platformEls.forEach(el => detectPlatforms(el))
|
||||
const platformEls = Array.from(
|
||||
document.querySelectorAll('.platform-mac, .platform-windows, .platform-linux')
|
||||
) as Array<HTMLElement>
|
||||
platformEls.forEach((el) => detectPlatforms(el))
|
||||
|
||||
return Array.from(detectedPlatforms) as Array<string>
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче