Skip to content
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
9 changes: 9 additions & 0 deletions apps/herbatika/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,15 @@ const nextConfig: NextConfig = {
],
reactCompiler: true,
cacheComponents: true,
async redirects() {
return [
{
source: "/homepage-promo",
destination: "/#homepage-promo",
permanent: false,
},
]
},
outputFileTracingRoot: join(__dirname, "../../"),
outputFileTracingExcludes: {
"*": [
Expand Down
21 changes: 15 additions & 6 deletions apps/herbatika/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
import { HydrationBoundary } from "@tanstack/react-query"
import { HerbatikaHomepage } from "@/components/herbatika-homepage"
import { fetchCmsHeroBanners } from "@/lib/storefront/cms"
import {
fetchCmsHeroBanners,
fetchCmsHomepagePromo,
} from "@/lib/storefront/cms"
import { prefetchHomePageStorefrontData } from "@/lib/storefront/ssr"

export default async function HomePage() {
const [{ dehydratedState }, heroBanners] = await Promise.all([
prefetchHomePageStorefrontData(),
fetchCmsHeroBanners(),
])
const [{ dehydratedState }, heroBanners, homepagePromo] = await Promise.all(
[
prefetchHomePageStorefrontData(),
fetchCmsHeroBanners(),
fetchCmsHomepagePromo(),
]
)

return (
<HydrationBoundary state={dehydratedState}>
<HerbatikaHomepage heroBanners={heroBanners} />
<HerbatikaHomepage
heroBanners={heroBanners}
homepagePromo={homepagePromo}
/>
</HydrationBoundary>
)
}
9 changes: 7 additions & 2 deletions apps/herbatika/src/components/herbatika-homepage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { BENEFITS } from "@/assets/benefits"
import type { HeroBannerItem } from "@/components/homepage/homepage.data"
import { BLOG_POSTS, HERO_BANNERS } from "@/components/homepage/homepage.data"
import type { HomepagePromoContent } from "@/components/homepage/homepage.data.types"
import { HomepageBlogSection } from "@/components/homepage/sections/homepage-blog-section"
import { HomepageHeroCarouselSection } from "@/components/homepage/sections/homepage-hero-carousel-section"
import { HomepageProductCollectionSection } from "@/components/homepage/sections/homepage-product-collection-section"
Expand All @@ -14,9 +15,13 @@ import { PurposeCarousel } from "./homepage/sections/purpose-carousel"

type HerbatikaHomepageProps = {
heroBanners?: HeroBannerItem[]
homepagePromo?: HomepagePromoContent | null
}

export function HerbatikaHomepage({ heroBanners }: HerbatikaHomepageProps) {
export function HerbatikaHomepage({
heroBanners,
homepagePromo,
}: HerbatikaHomepageProps) {
const controller = useHomepageController()
const banners = heroBanners?.length ? heroBanners : HERO_BANNERS

Expand Down Expand Up @@ -49,7 +54,7 @@ export function HerbatikaHomepage({ heroBanners }: HerbatikaHomepageProps) {
))}

<HomepageBlogSection posts={BLOG_POSTS} />
<HomepagePromoSection />
<HomepagePromoSection promo={homepagePromo} />
<RecentlyVisitedProductsSection />
</main>
)
Expand Down
7 changes: 7 additions & 0 deletions apps/herbatika/src/components/homepage/homepage.data.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,10 @@ export type ProductSectionDefinition = {
titleKey: HomepageProductSectionTitleKey
viewAllHref: string
}

export type HomepagePromoContent = {
contentHtml: string
heading: string
imageAlt?: string
imageSrc?: string
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { describe, expect, it } from "vitest"
import { sanitizeHomepagePromoHtml } from "./homepage-promo-html"

describe("sanitizeHomepagePromoHtml", () => {
it("preserves the rich-text formatting enabled for Payload pages", () => {
const html = `
<h1>Heading 1</h1><h5>Heading 5</h5><h6>Heading 6</h6>
<p><code>code</code> H<sub>2</sub>O x<sup>2</sup></p>
<p><span style="text-decoration: line-through;">removed</span></p>
<p><span style="text-decoration: underline;">important</span></p>
<hr />
<ul class="list-check">
<li aria-checked="true" class="list-item-checkbox" role="checkbox">
<input checked type="checkbox" /><label>Done</label>
</li>
</ul>
`

const result = sanitizeHomepagePromoHtml(html)

expect(result).toContain("<h1>Heading 1</h1>")
expect(result).toContain("<h5>Heading 5</h5>")
expect(result).toContain("<h6>Heading 6</h6>")
expect(result).toContain("<code>code</code>")
expect(result).toContain("H<sub>2</sub>O x<sup>2</sup>")
expect(result).toContain(
'<span style="text-decoration: line-through;">removed</span>'
)
expect(result).toContain(
'<span style="text-decoration: underline;">important</span>'
)
expect(result).toContain("<hr>")
expect(result).toContain('<li aria-checked="true" role="checkbox">')
expect(result).not.toContain("<input")
expect(result).not.toContain("<label")
})

it("rejects attributes and style values outside the promo policy", () => {
const result = sanitizeHomepagePromoHtml(`
<h1 onclick="alert(1)">Heading</h1>
<span style="background-image: url(javascript:alert(1))">Text</span>
<li aria-checked="invalid" role="button">Item</li>
<script>alert(1)</script>
`)

expect(result).toBe(
"<h1>Heading</h1>\n <span>Text</span>\n <li>Item</li>"
)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {
type SanitizeHtmlOptions,
sanitizeHtml,
} from "@/components/product-detail/utils/html-sanitizer"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

const HOMEPAGE_PROMO_SANITIZE_OPTIONS = {
additionalAllowedAttributeValues: {
li: {
"aria-checked": new Set(["false", "true"]),
role: new Set(["checkbox"]),
},
span: {
style: new Set([
"text-decoration: line-through;",
"text-decoration: underline;",
]),
},
},
additionalAllowedTagAttributes: {
li: new Set(["aria-checked", "role"]),
span: new Set(["style"]),
},
additionalAllowedTags: new Set([
"code",
"h1",
"h5",
"h6",
"hr",
"sub",
"sup",
]),
} satisfies SanitizeHtmlOptions

export const sanitizeHomepagePromoHtml = (html: string): string =>
sanitizeHtml(html, HOMEPAGE_PROMO_SANITIZE_OPTIONS)
Original file line number Diff line number Diff line change
@@ -1,41 +1,82 @@
import NextImage from "next/image"
import { useEffect, useRef } from "react"
import type { HomepagePromoContent } from "@/components/homepage/homepage.data.types"
import { sanitizeHomepagePromoHtml } from "./homepage-promo-html"

const HOMEPAGE_PROMO_SECTION_ID = "homepage-promo"

const DEFAULT_IMAGE = {
alt: "Predajňa Herbatika",
src: "https://images.unsplash.com/photo-1600093463592-8e36ae95ef56?auto=format&fit=crop&w=1100&q=80",
}

type HomepagePromoSectionProps = {
promo?: HomepagePromoContent | null
}

export function HomepagePromoSection({ promo }: HomepagePromoSectionProps) {
const sectionRef = useRef<HTMLElement>(null)
const imageAlt = promo?.imageAlt || DEFAULT_IMAGE.alt
const imageSrc = promo?.imageSrc || DEFAULT_IMAGE.src
const contentHtml = sanitizeHomepagePromoHtml(promo?.contentHtml ?? "")

useEffect(() => {
if (window.location.hash === `#${HOMEPAGE_PROMO_SECTION_ID}`) {
sectionRef.current?.scrollIntoView({ block: "start" })
}
}, [])

export function HomepagePromoSection() {
return (
<section className="grid gap-400 rounded-2xl border border-border-secondary bg-surface p-400 md:grid-cols-2 md:p-550">
<section
className="grid scroll-mt-homepage-promo-scroll-offset gap-400 rounded-2xl border border-border-secondary bg-surface p-400 md:grid-cols-2 md:p-550"
id={HOMEPAGE_PROMO_SECTION_ID}
ref={sectionRef}
>
<div className="overflow-hidden rounded-2xl border border-border-secondary">
<NextImage
alt="Predajňa Herbatika"
alt={imageAlt}
className="h-full min-h-950 w-full object-cover"
height={900}
quality={50}
src="https://images.unsplash.com/photo-1600093463592-8e36ae95ef56?auto=format&fit=crop&w=1100&q=80"
src={imageSrc}
width={1100}
/>
</div>

<div className="flex flex-col justify-center gap-300">
<h2 className="font-bold text-2xl text-fg-primary leading-tight">
Prírodná kozmetika, doplnky výživy a tradičná medicína
{promo?.heading ??
"Prírodná kozmetika, doplnky výživy a tradičná medicína"}
</h2>
<p className="text-fg-secondary text-sm leading-relaxed">
Spoznajte blahodarné účinky prírodnej kozmetiky a jej pozitívny vplyv
nielen na pokožku. Upevnite si vaše zdravie pomocou doplnkov stravy a
tradičnej medicíny.Toto všetko nájdete v našej pestrej ponuke, ktorá
je navyše obohatená aj o zdravotné doplnky z prírodných materiálov.
</p>
<p className="text-fg-secondary text-sm leading-relaxed">
Špecializujeme sa na výber tých najkvalitnejších produktov, ktoré aj
my sami používame, vylepšujeme a opakovane testujeme. Máme radi
kvalitu a potrpíme si na detaily. Sme pripravení, pomôcť vám s výberom
produktov špeciálne podľa vašich potrieb alebo na váš zdravotný
problém.
</p>
<p className="text-fg-secondary text-sm leading-relaxed">
Herbatica má už aj svoju značku, pod ktorou vyrábame množstvo
produktov, ktoré inde nenájdete. Máme radi kvalitu a detaily a na tie
sa sústredíme v každom našom produkte.
</p>
{contentHtml ? (
<div
className="text-fg-secondary text-sm leading-relaxed [&_a]:font-bold [&_a]:underline [&_code]:font-mono [&_code]:text-xs [&_em]:italic [&_h1]:font-bold [&_h1]:text-fg-primary [&_h1]:text-xl [&_h2]:font-bold [&_h2]:text-fg-primary [&_h2]:text-lg [&_h3]:font-bold [&_h3]:text-fg-primary [&_h3]:text-md [&_h4]:font-bold [&_h4]:text-fg-primary [&_h5]:font-bold [&_h5]:text-fg-primary [&_h6]:font-bold [&_h6]:text-fg-primary [&_hr]:my-300 [&_hr]:border-border-secondary [&_li]:ml-400 [&_li]:list-disc [&_li[aria-checked=true]]:before:bg-primary [&_li[role=checkbox]]:list-none [&_li[role=checkbox]]:before:mr-200 [&_li[role=checkbox]]:before:inline-block [&_li[role=checkbox]]:before:size-300 [&_li[role=checkbox]]:before:rounded-sm [&_li[role=checkbox]]:before:border [&_li[role=checkbox]]:before:border-border-primary [&_li[role=checkbox]]:before:align-middle [&_li[role=checkbox]]:before:content-[''] [&_ol]:list-decimal [&_p+p]:mt-300 [&_strong]:font-bold [&_ul]:mt-250"
// biome-ignore lint/security/noDangerouslySetInnerHtml: Payload rich text is normalized through the promo-specific sanitizer before rendering.
dangerouslySetInnerHTML={{ __html: contentHtml }}
/>
) : (
<>
<p className="text-fg-secondary text-sm leading-relaxed">
Spoznajte blahodarné účinky prírodnej kozmetiky a jej pozitívny
vplyv nielen na pokožku. Upevnite si vaše zdravie pomocou doplnkov
stravy a tradičnej medicíny.Toto všetko nájdete v našej pestrej
ponuke, ktorá je navyše obohatená aj o zdravotné doplnky z
prírodných materiálov.
</p>
<p className="text-fg-secondary text-sm leading-relaxed">
Špecializujeme sa na výber tých najkvalitnejších produktov, ktoré
aj my sami používame, vylepšujeme a opakovane testujeme. Máme radi
kvalitu a potrpíme si na detaily. Sme pripravení, pomôcť vám s
výberom produktov špeciálne podľa vašich potrieb alebo na váš
zdravotný problém.
</p>
<p className="text-fg-secondary text-sm leading-relaxed">
Herbatica má už aj svoju značku, pod ktorou vyrábame množstvo
produktov, ktoré inde nenájdete. Máme radi kvalitu a detaily a na
tie sa sústredíme v každom našom produkte.
</p>
</>
)}
</div>
</section>
)
Expand Down
Loading
Loading