fix: product texts, mobile nav background, mobile product page layout
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🧪 QA (push) Successful in 2m20s
Build & Deploy / 🏗️ Build (push) Successful in 4m22s
Build & Deploy / 🚀 Deploy (push) Successful in 23s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 5m16s
Build & Deploy / 🔔 Notify (push) Successful in 1s

- Fix PayloadRichText: migrate custom JSX converters to Lexical v3 nodesToJSX API
  - paragraph, heading, list, listitem, quote, link converters now use nodesToJSX
  - Resolves missing product texts since PayloadCMS migration
- Fix mobile navigation: move overlay outside <header> to prevent fixed-position clipping
  - Header transform/backdrop-filter was containing the fixed overlay
  - Use bg-primary/95 backdrop-blur-3xl for premium blue background
- Fix product image mobile layout: use md:-mt-32 responsive prefix
  - Negative margin only applies on md+ to avoid overlap on mobile
- Improve mobile product page UX:
  - Breadcrumbs: flex-wrap, truncate, reduced separator spacing
  - Hero: reduced top padding pt-28 on mobile
  - Product image card: 4/3 aspect ratio and smaller padding on mobile
  - Section spacing: use responsive md: prefixes throughout
  - Data tables: 2-col grid on mobile, smaller card padding/radius
  - Tables: add right-edge scroll hint gradient on mobile
This commit is contained in:
2026-02-28 10:51:58 +01:00
parent ce719a1d70
commit aa4e3aab4f
10 changed files with 171 additions and 222 deletions

View File

@@ -39,95 +39,17 @@ import CTA from '@/components/home/CTA';
const jsxConverters: JSXConverters = {
...defaultJSXConverters,
// Let the default converters handle text nodes to preserve valid formatting
// If the text node contains raw HTML (from messy migrations), render it as HTML instead of escaping it
text: ({ node }: any) => {
const text = node.text;
// Handle markdown-style lists embedded in text nodes from Markdown migration
if (text && text.includes('\n- ')) {
const parts = text.split('\n- ').filter((p: string) => p.trim() !== '');
// If first part doesn't start with "- ", it's a prefix paragraph
const startsWithDash = text.trimStart().startsWith('- ');
const prefix = startsWithDash ? null : parts.shift();
return (
<div className="my-4">
{prefix && (
<div dangerouslySetInnerHTML={prefix.includes('<') ? { __html: prefix } : undefined}>
{!prefix.includes('<') ? prefix : undefined}
</div>
)}
<ul className="list-disc pl-6 my-4 space-y-2">
{parts.map((item: string, i: number) => {
const cleanItem = item.trim();
if (cleanItem.includes('<')) {
return <li key={i} dangerouslySetInnerHTML={{ __html: cleanItem }} />;
}
return <li key={i}>{cleanItem}</li>;
})}
</ul>
</div>
);
}
if (text && (text.includes('<') || text.includes('data-start'))) {
return <span dangerouslySetInnerHTML={{ __html: text }} />;
}
// Handle markdown-style links [text](url) from Markdown migration
if (text && /\[([^\]]+)\]\(([^)]+)\)/.test(text)) {
const parts: React.ReactNode[] = [];
const remaining = text;
let key = 0;
const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
let match;
let lastIndex = 0;
while ((match = linkRegex.exec(remaining)) !== null) {
if (match.index > lastIndex) {
parts.push(<span key={key++}>{remaining.slice(lastIndex, match.index)}</span>);
}
parts.push(
<a
key={key++}
href={match[2]}
target="_blank"
rel="noopener noreferrer"
className="text-primary underline decoration-primary/30 hover:decoration-primary transition-colors"
>
{match[1]}
</a>,
);
lastIndex = match.index + match[0].length;
}
if (lastIndex < remaining.length) {
parts.push(<span key={key++}>{remaining.slice(lastIndex)}</span>);
}
return <>{parts}</>;
}
// Handle newlines in text nodes — convert to <br> for proper line breaks
if (text && text.includes('\n')) {
const lines = text.split('\n');
return (
<>
{lines.map((line: string, i: number) => (
<span key={i}>
{line}
{i < lines.length - 1 && <br />}
</span>
))}
</>
);
}
if (node.format === 1) return <strong key="bold">{text}</strong>;
if (node.format === 2) return <em key="italic">{text}</em>;
return <span key="text">{text}</span>;
},
// Use div instead of p for paragraphs to allow nested block elements (like the lists above)
paragraph: ({ children }: any) => (
<div className="mb-6 leading-relaxed text-text-secondary">{children}</div>
),
paragraph: ({ node, nodesToJSX }: any) => {
return (
<div className="mb-6 leading-relaxed text-text-secondary">
{nodesToJSX({ nodes: node.children })}
</div>
);
},
// Scale headings to prevent multiple H1s (H1 -> H2, etc) and style natively
heading: ({ node, children }: any) => {
heading: ({ node, nodesToJSX }: any) => {
const children = nodesToJSX({ nodes: node.children });
const tag = node?.tag;
if (tag === 'h1')
return (
@@ -151,7 +73,8 @@ const jsxConverters: JSXConverters = {
);
return <h6 className="text-base font-bold mt-6 mb-4 text-text-primary">{children}</h6>;
},
list: ({ node, children }: any) => {
list: ({ node, nodesToJSX }: any) => {
const children = nodesToJSX({ nodes: node.children });
if (node?.listType === 'number') {
return (
<ol className="list-decimal pl-6 my-6 space-y-2 text-text-secondary marker:text-primary marker:font-bold">
@@ -168,7 +91,8 @@ const jsxConverters: JSXConverters = {
</ul>
);
},
listitem: ({ node, children }: any) => {
listitem: ({ node, nodesToJSX }: any) => {
const children = nodesToJSX({ nodes: node.children });
if (node?.checked != null) {
return (
<li className="flex items-center gap-3 mb-2 leading-relaxed">
@@ -184,12 +108,16 @@ const jsxConverters: JSXConverters = {
}
return <li className="mb-2 leading-relaxed">{children}</li>;
},
quote: ({ children }: any) => (
<blockquote className="border-l-4 border-primary bg-primary/5 rounded-r-2xl pl-6 py-4 my-8 italic text-text-secondary shadow-sm">
{children}
</blockquote>
),
link: ({ node, children }: any) => {
quote: ({ node, nodesToJSX }: any) => {
const children = nodesToJSX({ nodes: node.children });
return (
<blockquote className="border-l-4 border-primary bg-primary/5 rounded-r-2xl pl-6 py-4 my-8 italic text-text-secondary shadow-sm">
{children}
</blockquote>
);
},
link: ({ node, nodesToJSX }: any) => {
const children = nodesToJSX({ nodes: node.children });
// Handling Payload CMS link nodes
const href = node?.fields?.url || node?.url || '#';
const newTab = node?.fields?.newTab || node?.newTab;
@@ -1090,6 +1018,10 @@ export default function PayloadRichText({
if (!data) return null;
if (data.root?.children?.length > 0) {
console.log('[PayloadRichText DEBUG] received children', data.root.children.length);
}
const dynamicConverters: JSXConverters = {
...jsxConverters,
blocks: {