refactor: extract configurable home layout (#561)
This commit is contained in:
parent
de5a81c606
commit
04132e93b6
28
dashboard/cms/templates/default/config.ts
Normal file
28
dashboard/cms/templates/default/config.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import type { CommonHomeLayoutConfig } from '@templates/layouts/commonHome'
|
||||
|
||||
export const defaultHomeLayoutConfig: CommonHomeLayoutConfig = {
|
||||
rootClassName: 'bg-slate-950',
|
||||
hero: {
|
||||
sectionClassName: 'relative isolate overflow-hidden pb-24 pt-24 text-slate-100',
|
||||
overlays: [
|
||||
'absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(56,189,248,0.18),_transparent_60%)]',
|
||||
'absolute inset-0 bg-[linear-gradient(130deg,_rgba(12,18,32,0.92),_rgba(8,47,73,0.45))]',
|
||||
],
|
||||
containerClassName: 'relative px-4',
|
||||
contentClassName: 'mx-auto max-w-6xl',
|
||||
slot: {
|
||||
key: 'ProductMatrix',
|
||||
},
|
||||
},
|
||||
content: {
|
||||
sectionClassName: 'relative bg-slate-50 pb-16 pt-20',
|
||||
overlays: ['absolute inset-x-0 top-0 h-24 bg-gradient-to-b from-slate-950 via-slate-900/0'],
|
||||
containerClassName: 'relative px-4',
|
||||
contentClassName: 'mx-auto max-w-6xl',
|
||||
gridClassName: 'grid gap-8 lg:grid-cols-[minmax(0,2.5fr)_minmax(0,1fr)] lg:items-start',
|
||||
slots: [
|
||||
{ key: 'ArticleFeed' },
|
||||
{ key: 'Sidebar' },
|
||||
],
|
||||
},
|
||||
}
|
||||
@ -1 +1,2 @@
|
||||
export { default } from '../../../src/templates/default'
|
||||
export { defaultHomeLayoutConfig } from './config'
|
||||
|
||||
@ -2,44 +2,16 @@ import ArticleFeed from '@components/home/ArticleFeed'
|
||||
import ProductMatrix from '@components/home/ProductMatrix'
|
||||
import Sidebar from '@components/home/Sidebar'
|
||||
|
||||
import type { HomePageTemplateProps, TemplateDefinition } from '../types'
|
||||
import { defaultHomeLayoutConfig } from '@cms/templates/default/config'
|
||||
|
||||
function DefaultHomePageTemplate({ slots }: HomePageTemplateProps) {
|
||||
const ProductMatrixComponent = slots.ProductMatrix ?? ProductMatrix
|
||||
const ArticleFeedComponent = slots.ArticleFeed ?? ArticleFeed
|
||||
const SidebarComponent = slots.Sidebar ?? Sidebar
|
||||
import { createCommonHomeTemplate } from '../layouts/commonHome'
|
||||
import type { TemplateDefinition } from '../types'
|
||||
|
||||
return (
|
||||
<main className="bg-slate-950">
|
||||
<section className="relative isolate overflow-hidden pb-24 pt-24 text-slate-100">
|
||||
<div
|
||||
className="absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(56,189,248,0.18),_transparent_60%)]"
|
||||
aria-hidden
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-0 bg-[linear-gradient(130deg,_rgba(12,18,32,0.92),_rgba(8,47,73,0.45))]"
|
||||
aria-hidden
|
||||
/>
|
||||
<div className="relative px-4">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<ProductMatrixComponent />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section className="relative bg-slate-50 pb-16 pt-20">
|
||||
<div className="absolute inset-x-0 top-0 h-24 bg-gradient-to-b from-slate-950 via-slate-900/0" aria-hidden />
|
||||
<div className="relative px-4">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<div className="grid gap-8 lg:grid-cols-[minmax(0,2.5fr)_minmax(0,1fr)] lg:items-start">
|
||||
<ArticleFeedComponent />
|
||||
<SidebarComponent />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
const DefaultHomePageTemplate = createCommonHomeTemplate(defaultHomeLayoutConfig, {
|
||||
ProductMatrix,
|
||||
ArticleFeed,
|
||||
Sidebar,
|
||||
})
|
||||
|
||||
const defaultTemplate: TemplateDefinition = {
|
||||
name: 'default',
|
||||
|
||||
113
dashboard/src/templates/layouts/commonHome.tsx
Normal file
113
dashboard/src/templates/layouts/commonHome.tsx
Normal file
@ -0,0 +1,113 @@
|
||||
import { Fragment, type ComponentType } from 'react'
|
||||
import clsx from 'clsx'
|
||||
|
||||
import type { HomePageSlotKey, HomePageTemplateSlots, TemplateComponent } from '../types'
|
||||
|
||||
type SlotProps = Record<string, unknown>
|
||||
|
||||
type HeroSlotConfig = {
|
||||
key: HomePageSlotKey
|
||||
wrapperClassName?: string
|
||||
props?: SlotProps
|
||||
}
|
||||
|
||||
type ContentSlotConfig = {
|
||||
key: HomePageSlotKey
|
||||
wrapperClassName?: string
|
||||
props?: SlotProps
|
||||
}
|
||||
|
||||
type SectionBaseConfig = {
|
||||
sectionClassName?: string
|
||||
containerClassName?: string
|
||||
contentClassName?: string
|
||||
overlays?: string[]
|
||||
}
|
||||
|
||||
type HeroSectionConfig = SectionBaseConfig & {
|
||||
slot: HeroSlotConfig
|
||||
}
|
||||
|
||||
type ContentSectionConfig = SectionBaseConfig & {
|
||||
gridClassName?: string
|
||||
slots: ContentSlotConfig[]
|
||||
}
|
||||
|
||||
export interface CommonHomeLayoutConfig {
|
||||
rootClassName?: string
|
||||
hero: HeroSectionConfig
|
||||
content: ContentSectionConfig
|
||||
}
|
||||
|
||||
type FallbackMap = Partial<Record<HomePageSlotKey, ComponentType<any>>>
|
||||
|
||||
function renderOverlays(overlays?: string[]) {
|
||||
return overlays?.map((className, index) => (
|
||||
<div key={index} className={className} aria-hidden />
|
||||
))
|
||||
}
|
||||
|
||||
export function createCommonHomeTemplate(
|
||||
config: CommonHomeLayoutConfig,
|
||||
fallbacks: FallbackMap = {},
|
||||
): TemplateComponent<HomePageTemplateSlots> {
|
||||
const Template: TemplateComponent<HomePageTemplateSlots> = ({ slots }) => {
|
||||
const heroSlotConfig = config.hero.slot
|
||||
const HeroComponent = (slots[heroSlotConfig.key] ?? fallbacks[heroSlotConfig.key]) as
|
||||
| ComponentType<any>
|
||||
| undefined
|
||||
|
||||
const heroContent = HeroComponent ? <HeroComponent {...(heroSlotConfig.props ?? {})} /> : null
|
||||
|
||||
return (
|
||||
<main className={clsx(config.rootClassName)}>
|
||||
<section className={clsx(config.hero.sectionClassName)}>
|
||||
{renderOverlays(config.hero.overlays)}
|
||||
<div className={clsx(config.hero.containerClassName)}>
|
||||
<div className={clsx(config.hero.contentClassName)}>
|
||||
{heroSlotConfig.wrapperClassName ? (
|
||||
<div className={clsx(heroSlotConfig.wrapperClassName)}>{heroContent}</div>
|
||||
) : (
|
||||
heroContent
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section className={clsx(config.content.sectionClassName)}>
|
||||
{renderOverlays(config.content.overlays)}
|
||||
<div className={clsx(config.content.containerClassName)}>
|
||||
<div className={clsx(config.content.contentClassName)}>
|
||||
<div className={clsx(config.content.gridClassName)}>
|
||||
{config.content.slots.map((slotConfig) => {
|
||||
const SlotComponent = (slots[slotConfig.key] ?? fallbacks[slotConfig.key]) as
|
||||
| ComponentType<any>
|
||||
| undefined
|
||||
|
||||
if (!SlotComponent) {
|
||||
return null
|
||||
}
|
||||
|
||||
const slotElement = <SlotComponent {...(slotConfig.props ?? {})} />
|
||||
|
||||
if (slotConfig.wrapperClassName) {
|
||||
return (
|
||||
<div key={slotConfig.key} className={clsx(slotConfig.wrapperClassName)}>
|
||||
{slotElement}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return <Fragment key={slotConfig.key}>{slotElement}</Fragment>
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
return Template
|
||||
}
|
||||
|
||||
export type { HeroSectionConfig, ContentSectionConfig }
|
||||
@ -17,6 +17,8 @@ export interface HomePageTemplateSlots extends TemplateSlots {
|
||||
Sidebar: ComponentType
|
||||
}
|
||||
|
||||
export type HomePageSlotKey = 'ProductMatrix' | 'ArticleFeed' | 'Sidebar'
|
||||
|
||||
export type HomePageTemplateProps = TemplateRenderProps<HomePageTemplateSlots>
|
||||
|
||||
export interface TemplateDefinition {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user