@@ -26,6 +26,20 @@ 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}
|
||||
|
||||
64
components/context/ProductSubLandingContext.tsx
Normal file
64
components/context/ProductSubLandingContext.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { createContext, useContext } from 'react'
|
||||
import pick from 'lodash/pick'
|
||||
|
||||
export type FeaturedTrack = {
|
||||
trackName: string
|
||||
title: string
|
||||
description: string
|
||||
guides?: Array<{ href: string; page: { type: string }; title: string; intro: string }>
|
||||
}
|
||||
|
||||
export type ArticleGuide = {
|
||||
href: string
|
||||
title: string
|
||||
intro: string
|
||||
type: string
|
||||
topics: Array<string>
|
||||
}
|
||||
|
||||
export type ProductSubLandingContextT = {
|
||||
title: string
|
||||
intro: string
|
||||
featuredTrack?: FeaturedTrack
|
||||
learningTracks?: Array<FeaturedTrack>
|
||||
includeGuides?: Array<ArticleGuide>
|
||||
allTopics?: Array<string>
|
||||
}
|
||||
|
||||
export const ProductSubLandingContext = createContext<ProductSubLandingContextT | null>(null)
|
||||
|
||||
export const useProductSubLandingContext = (): ProductSubLandingContextT => {
|
||||
const context = useContext(ProductSubLandingContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'"useProductSubLandingContext" may only be used inside "ProductSubLandingContext.Provider"'
|
||||
)
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
export const getProductSubLandingContextFromRequest = (req: any): ProductSubLandingContextT => {
|
||||
const page = req.context.page
|
||||
|
||||
return {
|
||||
...pick(page, ['intro', 'allTopics']),
|
||||
title: req.context.productMap[req.context.currentProduct].name,
|
||||
featuredTrack: {
|
||||
...pick(page.featuredTrack, ['title', 'description', 'trackName', 'guides']),
|
||||
guides: (page.featuredTrack?.guides || []).map((guide: any) => {
|
||||
return pick(guide, ['title', 'intro', 'href', 'page.type'])
|
||||
}),
|
||||
},
|
||||
learningTracks: (page.learningTracks || []).map((track: any) => ({
|
||||
...pick(track, ['title', 'description', 'trackName', 'guides']),
|
||||
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', 'type', 'topics'])
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,11 @@ export const CodeExampleCard = ({ example }: Props) => {
|
||||
</div>
|
||||
<footer className="border-top p-4 color-text-secondary d-flex flex-items-center">
|
||||
<RepoIcon className="flex-shrink-0" />
|
||||
<TruncateLines as="span" maxLines={1} className="ml-2 text-mono text-small color-text-link line-break-anywhere">
|
||||
<TruncateLines
|
||||
as="span"
|
||||
maxLines={1}
|
||||
className="ml-2 text-mono text-small color-text-link line-break-anywhere"
|
||||
>
|
||||
{example.href}
|
||||
</TruncateLines>
|
||||
</footer>
|
||||
|
||||
@@ -1,19 +1,29 @@
|
||||
import cx from 'classnames'
|
||||
import { useTranslation } from 'components/hooks/useTranslation'
|
||||
|
||||
type Props = {
|
||||
title?: React.ReactNode
|
||||
sectionLink?: string
|
||||
children?: React.ReactNode
|
||||
className?: string
|
||||
description?: string
|
||||
}
|
||||
export const LandingSection = ({ title, children, className, sectionLink }: Props) => {
|
||||
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 && (
|
||||
<h2 className="font-mktg h1 mb-4">
|
||||
<h2 className={cx('font-mktg', !description ? 'mb-3' : 'mb-4')}>
|
||||
{sectionLink ? <a href={`#${sectionLink}`}>{title}</a> : title}
|
||||
</h2>
|
||||
)}
|
||||
{description && (
|
||||
<div
|
||||
className="lead-mktg color-text-secondary f4 description-text"
|
||||
dangerouslySetInnerHTML={{ __html: t(description) }}
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
34
components/sublanding/ArticleCard.tsx
Normal file
34
components/sublanding/ArticleCard.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { ArticleGuide } from 'components/context/ProductSubLandingContext'
|
||||
|
||||
type Props = {
|
||||
card: ArticleGuide
|
||||
type: string
|
||||
display?: string
|
||||
}
|
||||
|
||||
export const ArticleCard = ({ card, type, display }: 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(',')}
|
||||
>
|
||||
<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>
|
||||
<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">
|
||||
{topic}
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
67
components/sublanding/ArticleCards.tsx
Normal file
67
components/sublanding/ArticleCards.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { useProductSubLandingContext } from 'components/context/ProductSubLandingContext'
|
||||
import { useTranslation } from 'components/hooks/useTranslation'
|
||||
import { ArticleCard } from './ArticleCard'
|
||||
|
||||
const MAX_ARTICLES = 9
|
||||
export const ArticleCards = () => {
|
||||
const { t } = useTranslation('product_sublanding')
|
||||
const guideTypes: Record<string, string> = t('guide_types')
|
||||
const { allTopics, includeGuides } = useProductSubLandingContext()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form className="mt-2 mb-5 d-flex d-flex">
|
||||
<div>
|
||||
<label htmlFor="type" className="text-uppercase f6 color-text-secondary d-block">
|
||||
{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"
|
||||
name="type"
|
||||
aria-label="guide types"
|
||||
>
|
||||
<option value="">{t('filters.all')}</option>
|
||||
{Object.entries(guideTypes).map(([key, val]) => {
|
||||
return <option key={key} value={key}>{val}</option>
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
<div className="mx-4">
|
||||
<label htmlFor="topic" className="text-uppercase f6 color-text-secondary d-block">
|
||||
{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"
|
||||
name="topics"
|
||||
aria-label="guide topics"
|
||||
>
|
||||
<option value="">{t('filters.all')}</option>
|
||||
{allTopics?.map((topic) => {
|
||||
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]} />
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{includeGuides && includeGuides.length > MAX_ARTICLES && (
|
||||
<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}
|
||||
>
|
||||
{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>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
80
components/sublanding/LearningTrack.tsx
Normal file
80
components/sublanding/LearningTrack.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { useTranslation } from 'components/hooks/useTranslation'
|
||||
import { ArrowRightIcon } from '@primer/octicons-react'
|
||||
import { useState } from 'react'
|
||||
import { FeaturedTrack } from 'components/context/ProductSubLandingContext'
|
||||
|
||||
type Props = {
|
||||
track: FeaturedTrack
|
||||
}
|
||||
|
||||
const MAX_VISIBLE_GUIDES = 4
|
||||
export const LearningTrack = ({ track }: Props) => {
|
||||
const [visibleGuides, setVisibleGuides] = useState(track.guides?.slice(0, 4))
|
||||
const showAll = () => {
|
||||
setVisibleGuides(track.guides)
|
||||
}
|
||||
const { t } = useTranslation('product_sublanding')
|
||||
|
||||
return (
|
||||
<div className="my-3 px-4 col-12 col-md-6 learning-track">
|
||||
<div className="Box js-show-more-container d-flex flex-column">
|
||||
<div className="Box-header bg-gradient--blue-pink p-4 d-flex flex-1 flex-items-start flex-wrap">
|
||||
<div className="d-flex flex-auto flex-items-start col-8 col-md-12 col-xl-8">
|
||||
<div className="my-xl-0 mr-xl-3">
|
||||
<h5 className="mb-3 color-text-inverse font-mktg h3-mktg ">{track.title}</h5>
|
||||
<p className="color-text-inverse truncate-overflow-3 learning-track--description">
|
||||
{track.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
className="d-inline-block border color-border-inverse color-text-inverse px-3 py-2 f5 no-underline text-bold no-wrap mt-3 mt-md-0"
|
||||
role="button"
|
||||
href={`${track.guides && track.guides[0].href}?learn=${track.trackName}`}
|
||||
>
|
||||
{t('start')}
|
||||
<span className="mr-2">
|
||||
<ArrowRightIcon size={20} />
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
{visibleGuides?.map((guide) => (
|
||||
<div>
|
||||
<a
|
||||
className="Box-row d-flex flex-items-center color-text-primary no-underline js-show-more-item"
|
||||
href={`${guide.href}?learn=${track.trackName}`}
|
||||
>
|
||||
<div className="circle color-bg-tertiary d-inline-flex mr-4">
|
||||
{track.guides && (
|
||||
<span className="m-2 f3 lh-condensed-ultra text-center text-bold step-circle-text">
|
||||
{track.guides?.indexOf(guide) + 1}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<h5 className="flex-auto pr-2">{guide.title}</h5>
|
||||
<div className="color-text-tertiary h6 text-uppercase">
|
||||
{t('guide_types')[guide.page.type]}
|
||||
</div>
|
||||
</a>
|
||||
{track.guides && track.guides?.indexOf(guide) + 1 === MAX_VISIBLE_GUIDES ? (
|
||||
<button
|
||||
className="Box-footer btn-link border-top-0 position-relative text-center text-bold color-text-link pt-1 pb-3 col-12 js-show-more-button"
|
||||
onClick={showAll}
|
||||
>
|
||||
<div
|
||||
className="position-absolute left-0 right-0 py-5 fade-background-bottom"
|
||||
style={{ bottom: '50px' }}
|
||||
></div>
|
||||
<span>
|
||||
Show {track.guides?.length - MAX_VISIBLE_GUIDES} {t(`more_guides`)}
|
||||
</span>
|
||||
</button>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
16
components/sublanding/LearningTracks.tsx
Normal file
16
components/sublanding/LearningTracks.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { useProductSubLandingContext } from 'components/context/ProductSubLandingContext'
|
||||
import { LearningTrack } from 'components/sublanding/LearningTrack'
|
||||
|
||||
export const LearningTracks = () => {
|
||||
const { learningTracks } = useProductSubLandingContext()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="d-flex flex-wrap flex-items-start my-5 gutter">
|
||||
{(learningTracks || []).map((track) => {
|
||||
return <LearningTrack key={track.title} track={track} />
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
40
components/sublanding/ProductSubLanding.tsx
Normal file
40
components/sublanding/ProductSubLanding.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { DefaultLayout } from 'components/DefaultLayout'
|
||||
import { useProductSubLandingContext } from 'components/context/ProductSubLandingContext'
|
||||
import React from 'react'
|
||||
import { LandingSection } from 'components/landing/LandingSection'
|
||||
import { SubLandingHero } from 'components/sublanding/SubLandingHero'
|
||||
import { LearningTracks } from 'components/sublanding/LearningTracks'
|
||||
import { ArticleCards } from 'components/sublanding/ArticleCards'
|
||||
|
||||
export const ProductSubLanding = () => {
|
||||
const { title, learningTracks, includeGuides } = useProductSubLandingContext()
|
||||
|
||||
return (
|
||||
<DefaultLayout>
|
||||
<LandingSection className="pt-3">
|
||||
<SubLandingHero />
|
||||
</LandingSection>
|
||||
|
||||
{learningTracks && learningTracks.length > 0 && (
|
||||
<LandingSection
|
||||
title={`${title} learning paths`}
|
||||
className="border-top py-6"
|
||||
sectionLink="learning-paths"
|
||||
description="learning_paths_desc"
|
||||
>
|
||||
<LearningTracks />
|
||||
</LandingSection>
|
||||
)}
|
||||
|
||||
{includeGuides && (
|
||||
<LandingSection
|
||||
title={`All ${title} guides`}
|
||||
className="border-top py-6 color-border-primary"
|
||||
sectionLink="all-guides"
|
||||
>
|
||||
<ArticleCards />
|
||||
</LandingSection>
|
||||
)}
|
||||
</DefaultLayout>
|
||||
)
|
||||
}
|
||||
88
components/sublanding/SubLandingHero.tsx
Normal file
88
components/sublanding/SubLandingHero.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { Breadcrumbs } from '../Breadcrumbs'
|
||||
import { useProductSubLandingContext } from 'components/context/ProductSubLandingContext'
|
||||
import { ArrowRightIcon, StarFillIcon } from '@primer/octicons-react'
|
||||
import { useTranslation } from 'components/hooks/useTranslation'
|
||||
|
||||
export const SubLandingHero = () => {
|
||||
const { title, intro, featuredTrack } = useProductSubLandingContext()
|
||||
const { t } = useTranslation('product_sublanding')
|
||||
|
||||
const guideItems = featuredTrack?.guides?.map((guide) => (
|
||||
<li className="px-2 d-flex flex-shrink-0">
|
||||
<a
|
||||
href={`${guide.href}?learn=${featuredTrack.trackName}`}
|
||||
className="d-inline-block Box p-5 color-bg-primary color-border-primary no-underline"
|
||||
>
|
||||
<div className="d-flex flex-justify-between flex-items-center">
|
||||
<div className="circle color-bg-primary color-text-link border-gradient--pink-blue-dark d-inline-flex">
|
||||
{featuredTrack.guides && (
|
||||
<span
|
||||
className="m-2 f2 lh-condensed-ultra text-center text-bold step-circle-text"
|
||||
style={{ width: '24px', height: '24px' }}
|
||||
>
|
||||
{featuredTrack.guides?.indexOf(guide) + 1}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="color-text-tertiary h6 text-uppercase">
|
||||
{t('guide_types')[guide.page.type]}
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="font-mktg h3-mktg my-4 color-text-primary">{guide.title}</h3>
|
||||
<div className="lead-mktg color-text-secondary f5 my-4 truncate-overflow-8">
|
||||
{guide.intro}
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
|
||||
return (
|
||||
<div>
|
||||
<header className="d-flex gutter mb-6">
|
||||
<div className="col-12">
|
||||
<Breadcrumbs />
|
||||
<h1 className="my-3 font-mktg">{title} guides</h1>
|
||||
<div
|
||||
className="lead-mktg color-text-secondary f4 description-text"
|
||||
dangerouslySetInnerHTML={{ __html: intro }}
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
{featuredTrack && (
|
||||
<div className="mb-6 position-relative overflow-hidden mr-n3 ml-n3 px-3">
|
||||
<ul className="list-style-none d-flex flex-nowrap overflow-x-scroll px-2 feature-track">
|
||||
<li className="px-2 d-flex flex-shrink-0">
|
||||
<div className="d-inline-block Box p-5 bg-gradient--blue-pink color-text-inverse">
|
||||
<div
|
||||
className="circle color-text-inverse d-inline-flex"
|
||||
style={{ border: '2px white solid' }}
|
||||
>
|
||||
<StarFillIcon className="v-align-middle m-2" size={24} />
|
||||
</div>
|
||||
<h3 className="font-mktg h3-mktg my-4">{featuredTrack.title}</h3>
|
||||
<div className="lead-mktg color-text-inverse f5 my-4">
|
||||
{featuredTrack.description}
|
||||
</div>
|
||||
{featuredTrack.guides && (
|
||||
<a
|
||||
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}`}
|
||||
>
|
||||
<span className="mr-2">
|
||||
<ArrowRightIcon size={20} />
|
||||
</span>
|
||||
{t(`start_path`)}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
{guideItems}
|
||||
</ul>
|
||||
<div className="position-absolute top-0 bottom-0 left-0 ml-3 pl-3 fade-background-left"></div>
|
||||
<div className="position-absolute top-0 bottom-0 right-0 mr-3 pr-3 fade-background-right"></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -11,6 +11,11 @@ import {
|
||||
ProductLandingContextT,
|
||||
ProductLandingContext,
|
||||
} from 'components/context/ProductLandingContext'
|
||||
import {
|
||||
getProductSubLandingContextFromRequest,
|
||||
ProductSubLandingContextT,
|
||||
ProductSubLandingContext,
|
||||
} from 'components/context/ProductSubLandingContext'
|
||||
|
||||
import {
|
||||
getArticleContextFromRequest,
|
||||
@@ -20,6 +25,7 @@ import {
|
||||
import { ArticlePage } from 'components/article/ArticlePage'
|
||||
|
||||
import { ProductLanding } from 'components/landing/ProductLanding'
|
||||
import { ProductSubLanding } from 'components/sublanding/ProductSubLanding'
|
||||
import { TocLanding } from 'components/landing/TocLanding'
|
||||
import {
|
||||
getTocLandingContextFromRequest,
|
||||
@@ -30,12 +36,14 @@ import {
|
||||
type Props = {
|
||||
mainContext: MainContextT
|
||||
productLandingContext: ProductLandingContextT
|
||||
productSubLandingContext: ProductSubLandingContextT
|
||||
tocLandingContext: TocLandingContextT
|
||||
articleContext: ArticleContextT
|
||||
}
|
||||
const GlobalPage = ({
|
||||
mainContext,
|
||||
productLandingContext,
|
||||
productSubLandingContext,
|
||||
tocLandingContext,
|
||||
articleContext,
|
||||
}: Props) => {
|
||||
@@ -49,7 +57,11 @@ const GlobalPage = ({
|
||||
</ProductLandingContext.Provider>
|
||||
)
|
||||
} else if (currentLayoutName === 'product-sublanding') {
|
||||
content = <p>todo: product sub-landing</p>
|
||||
content = (
|
||||
<ProductSubLandingContext.Provider value={productSubLandingContext}>
|
||||
<ProductSubLanding />
|
||||
</ProductSubLandingContext.Provider>
|
||||
)
|
||||
} else if (relativePath?.endsWith('index.md')) {
|
||||
content = (
|
||||
<TocLandingContext.Provider value={tocLandingContext}>
|
||||
@@ -76,6 +88,7 @@ export const getServerSideProps: GetServerSideProps<Props> = async (context) =>
|
||||
props: {
|
||||
mainContext: getMainContextFromRequest(req),
|
||||
productLandingContext: getProductLandingContextFromRequest(req),
|
||||
productSubLandingContext: getProductSubLandingContextFromRequest(req),
|
||||
tocLandingContext: getTocLandingContextFromRequest(req),
|
||||
articleContext: getArticleContextFromRequest(req),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user