mirror of
https://github.com/tiennm99/HugoBlox-kit.git
synced 2026-10-19 08:14:38 +00:00
fix: lint
This commit is contained in:
1 parent
00147612cd
commit
a3c21b401e
29 files changed
+667
-797
No files matched your search
+8
-3
@@ -16,13 +16,12 @@
|
|||||||
"!**/vendor",
|
"!**/vendor",
|
||||||
"!**/*.html",
|
"!**/*.html",
|
||||||
"!**/layouts/**/*.html",
|
"!**/layouts/**/*.html",
|
||||||
"!**/layouts/**/*.htm",
|
|
||||||
"!**/*.webmanifest",
|
"!**/*.webmanifest",
|
||||||
"!**/*.backlinks.json",
|
"!**/*.backlinks.json",
|
||||||
"!**/hugo_stats.json",
|
"!**/hugo_stats.json",
|
||||||
"!**/*.code-workspace",
|
"!**/*.code-workspace",
|
||||||
"!**/modules/blox/data/icons/devicon.json",
|
"!**/modules/blox/data/icons/devicon.json",
|
||||||
"!**/templates",
|
"!**/templates/**/*.html",
|
||||||
"!**/test",
|
"!**/test",
|
||||||
"!**/modules/**/package.json",
|
"!**/modules/**/package.json",
|
||||||
"!**/assets/jsconfig.json",
|
"!**/assets/jsconfig.json",
|
||||||
@@ -64,7 +63,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"assist": { "actions": { "source": { "organizeImports": "on" } } },
|
"assist": {
|
||||||
|
"actions": {
|
||||||
|
"source": {
|
||||||
|
"organizeImports": "on"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"css": {
|
"css": {
|
||||||
"parser": {
|
"parser": {
|
||||||
"tailwindDirectives": true
|
"tailwindDirectives": true
|
||||||
|
|||||||
@@ -90,22 +90,20 @@
|
|||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
|
|
||||||
/* Subtle gradient text for premium feel */
|
/* Subtle gradient text for premium feel */
|
||||||
background: linear-gradient(135deg,
|
background: linear-gradient(135deg, var(--color-gray-500) 0%, var(--color-gray-400) 100%);
|
||||||
var(--color-gray-500) 0%,
|
|
||||||
var(--color-gray-400) 100%);
|
|
||||||
-webkit-background-clip: text;
|
-webkit-background-clip: text;
|
||||||
background-clip: text;
|
background-clip: text;
|
||||||
-webkit-text-fill-color: transparent;
|
-webkit-text-fill-color: transparent;
|
||||||
|
|
||||||
/* Smooth reveal animation */
|
/* Smooth reveal animation */
|
||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
transition:
|
||||||
|
opacity 0.3s ease,
|
||||||
|
transform 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .hb-ruby-name rt {
|
.dark .hb-ruby-name rt {
|
||||||
background: linear-gradient(135deg,
|
background: linear-gradient(135deg, var(--color-gray-400) 0%, var(--color-gray-300) 100%);
|
||||||
var(--color-gray-400) 0%,
|
|
||||||
var(--color-gray-300) 100%);
|
|
||||||
-webkit-background-clip: text;
|
-webkit-background-clip: text;
|
||||||
background-clip: text;
|
background-clip: text;
|
||||||
-webkit-text-fill-color: transparent;
|
-webkit-text-fill-color: transparent;
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
import { render } from "preact";
|
import {render} from "preact";
|
||||||
import { CtaCardBlock } from "./component.jsx";
|
import {CtaCardBlock} from "./component.jsx";
|
||||||
|
|
||||||
function renderCtaCardBlock() {
|
function renderCtaCardBlock() {
|
||||||
const blocks = document.querySelectorAll('.cta-card-block-container[data-preact-render="true"]');
|
const blocks = document.querySelectorAll('.cta-card-block-container[data-preact-render="true"]');
|
||||||
|
|
||||||
blocks.forEach((block) => {
|
blocks.forEach((block) => {
|
||||||
try {
|
try {
|
||||||
if (block.dataset.rendered === "true") return;
|
if (block.dataset.rendered === "true") return;
|
||||||
|
|
||||||
const props = JSON.parse(block.dataset.props);
|
const props = JSON.parse(block.dataset.props);
|
||||||
render(<CtaCardBlock {...props} />, block);
|
render(<CtaCardBlock {...props} />, block);
|
||||||
block.dataset.rendered = "true";
|
block.dataset.rendered = "true";
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error rendering CTA Card Block:", e, block);
|
console.error("Error rendering CTA Card Block:", e, block);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initial render
|
// Initial render
|
||||||
if (document.readyState === "loading") {
|
if (document.readyState === "loading") {
|
||||||
document.addEventListener("DOMContentLoaded", renderCtaCardBlock);
|
document.addEventListener("DOMContentLoaded", renderCtaCardBlock);
|
||||||
} else {
|
} else {
|
||||||
renderCtaCardBlock();
|
renderCtaCardBlock();
|
||||||
}
|
}
|
||||||
@@ -4,125 +4,125 @@
|
|||||||
* Supports glassmorphism, custom backgrounds, and automatic text color adjustment.
|
* Supports glassmorphism, custom backgrounds, and automatic text color adjustment.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Icon } from "../../shared/components/Icon.jsx";
|
import {Icon} from "../../shared/components/Icon.jsx";
|
||||||
|
|
||||||
// Simple markdown renderer
|
// Simple markdown renderer
|
||||||
function renderText(text) {
|
function renderText(text) {
|
||||||
if (!text) return "";
|
if (!text) return "";
|
||||||
return String(text)
|
return String(text)
|
||||||
.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>")
|
.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>")
|
||||||
.replace(/\*(.*?)\*/g, "<em>$1</em>")
|
.replace(/\*(.*?)\*/g, "<em>$1</em>")
|
||||||
.replace(/`(.*?)`/g, "<code>$1</code>")
|
.replace(/`(.*?)`/g, "<code>$1</code>")
|
||||||
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
|
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CtaCardBlock = ({ content, design, id, button_icon_svg }) => {
|
export const CtaCardBlock = ({content, design, _id, button_icon_svg}) => {
|
||||||
const card = design?.card || {};
|
const card = design?.card || {};
|
||||||
const cardClass = card.css_class || "";
|
const cardClass = card.css_class || "";
|
||||||
const cardStyle = card.css_style || "";
|
const cardStyle = card.css_style || "";
|
||||||
|
|
||||||
// Overlay Opacity Logic
|
// Overlay Opacity Logic
|
||||||
let overlayOpacity = 0.15;
|
let overlayOpacity = 0.15;
|
||||||
if (card.overlay_opacity !== undefined) {
|
if (card.overlay_opacity !== undefined) {
|
||||||
const val = parseFloat(card.overlay_opacity);
|
const val = parseFloat(card.overlay_opacity);
|
||||||
if (!isNaN(val)) overlayOpacity = Math.max(0, Math.min(1, val));
|
if (!Number.isNaN(val)) overlayOpacity = Math.max(0, Math.min(1, val));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text Color Logic
|
||||||
|
let textColorMode = "auto";
|
||||||
|
if (["light", "dark", "auto"].includes(card.text_color?.toLowerCase())) {
|
||||||
|
textColorMode = card.text_color.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
let textClasses = "text-gray-900 dark:text-white";
|
||||||
|
let buttonClasses = "text-gray-900 dark:text-gray-100";
|
||||||
|
|
||||||
|
// Auto-detect based on background gradient
|
||||||
|
const bg = design?.background || {};
|
||||||
|
const gradientStart = bg.gradient?.start || "";
|
||||||
|
const isDarkBg = ["primary-7", "primary-8", "primary-9", "secondary-7", "secondary-8", "secondary-9"].some((p) => gradientStart.startsWith(p));
|
||||||
|
|
||||||
|
if (textColorMode === "light") {
|
||||||
|
textClasses = "text-white";
|
||||||
|
buttonClasses = "text-gray-900 dark:text-white";
|
||||||
|
} else if (textColorMode === "dark") {
|
||||||
|
textClasses = "text-gray-900 dark:text-white";
|
||||||
|
buttonClasses = "text-gray-900 dark:text-white";
|
||||||
|
} else if (isDarkBg) {
|
||||||
|
textClasses = "text-white";
|
||||||
|
buttonClasses = "text-gray-900";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Button Logic
|
||||||
|
const button = content?.button || {};
|
||||||
|
const buttonText = button.text?.trim();
|
||||||
|
const buttonUrl = button.url?.trim();
|
||||||
|
const showButton = !!(buttonText && buttonUrl);
|
||||||
|
|
||||||
|
let isExternal = false;
|
||||||
|
let isNewTab = button.new_tab === true;
|
||||||
|
if (buttonUrl) {
|
||||||
|
try {
|
||||||
|
const url = new URL(buttonUrl, window.location.origin);
|
||||||
|
isExternal = url.origin !== window.location.origin;
|
||||||
|
} catch {
|
||||||
|
isExternal = /^https?:\/\//.test(buttonUrl);
|
||||||
}
|
}
|
||||||
|
if (buttonUrl.endsWith(".pdf")) isNewTab = true;
|
||||||
|
if (isExternal) isNewTab = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Text Color Logic
|
const defaultBgClass = "bg-gradient-to-br from-primary-500/90 via-primary-600/95 to-primary-700/90";
|
||||||
let textColorMode = "auto";
|
const finalCardClass = cardClass || defaultBgClass;
|
||||||
if (["light", "dark", "auto"].includes(card.text_color?.toLowerCase())) {
|
|
||||||
textColorMode = card.text_color.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
let textClasses = "text-gray-900 dark:text-white";
|
const style = `--glassmorphism-opacity: ${overlayOpacity}; ${cardStyle}`;
|
||||||
let buttonClasses = "text-gray-900 dark:text-gray-100";
|
|
||||||
|
|
||||||
// Auto-detect based on background gradient
|
return (
|
||||||
const bg = design?.background || {};
|
<div
|
||||||
const gradientStart = bg.gradient?.start || "";
|
class={`relative overflow-hidden ${finalCardClass} p-8 sm:p-12 lg:p-16 xl:p-20 mx-auto max-w-6xl rounded-3xl shadow-2xl flex flex-col items-center text-center`}
|
||||||
const isDarkBg = ["primary-7", "primary-8", "primary-9", "secondary-7", "secondary-8", "secondary-9"].some(p => gradientStart.startsWith(p));
|
style={style}
|
||||||
|
>
|
||||||
|
{/* Title */}
|
||||||
|
{content.title && (
|
||||||
|
<h2
|
||||||
|
class={`${textClasses} text-4xl sm:text-5xl lg:text-6xl xl:text-7xl font-bold tracking-tight leading-tight`}
|
||||||
|
dangerouslySetInnerHTML={{__html: renderText(content.title)}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
if (textColorMode === "light") {
|
{/* Text */}
|
||||||
textClasses = "text-white";
|
{content.text && (
|
||||||
buttonClasses = "text-gray-900 dark:text-white";
|
|
||||||
} else if (textColorMode === "dark") {
|
|
||||||
textClasses = "text-gray-900 dark:text-white";
|
|
||||||
buttonClasses = "text-gray-900 dark:text-white";
|
|
||||||
} else if (isDarkBg) {
|
|
||||||
textClasses = "text-white";
|
|
||||||
buttonClasses = "text-gray-900";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Button Logic
|
|
||||||
const button = content?.button || {};
|
|
||||||
const buttonText = button.text?.trim();
|
|
||||||
const buttonUrl = button.url?.trim();
|
|
||||||
const showButton = !!(buttonText && buttonUrl);
|
|
||||||
|
|
||||||
let isExternal = false;
|
|
||||||
let isNewTab = button.new_tab === true;
|
|
||||||
if (buttonUrl) {
|
|
||||||
try {
|
|
||||||
const url = new URL(buttonUrl, window.location.origin);
|
|
||||||
isExternal = url.origin !== window.location.origin;
|
|
||||||
} catch {
|
|
||||||
isExternal = /^https?:\/\//.test(buttonUrl);
|
|
||||||
}
|
|
||||||
if (buttonUrl.endsWith(".pdf")) isNewTab = true;
|
|
||||||
if (isExternal) isNewTab = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultBgClass = "bg-gradient-to-br from-primary-500/90 via-primary-600/95 to-primary-700/90";
|
|
||||||
const finalCardClass = cardClass || defaultBgClass;
|
|
||||||
|
|
||||||
const style = `--glassmorphism-opacity: ${overlayOpacity}; ${cardStyle}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
<div
|
||||||
class={`relative overflow-hidden ${finalCardClass} p-8 sm:p-12 lg:p-16 xl:p-20 mx-auto max-w-6xl rounded-3xl shadow-2xl flex flex-col items-center text-center`}
|
class={`${textClasses}/80 mt-6 text-lg sm:text-xl lg:text-2xl max-w-3xl leading-relaxed font-light`}
|
||||||
style={style}
|
dangerouslySetInnerHTML={{__html: renderText(content.text)}}
|
||||||
>
|
/>
|
||||||
{/* Title */}
|
)}
|
||||||
{content.title && (
|
|
||||||
<h2
|
{/* Button */}
|
||||||
class={`${textClasses} text-4xl sm:text-5xl lg:text-6xl xl:text-7xl font-bold tracking-tight leading-tight`}
|
{showButton && (
|
||||||
dangerouslySetInnerHTML={{ __html: renderText(content.title) }}
|
<div class="flex mt-10">
|
||||||
/>
|
<a
|
||||||
|
href={buttonUrl}
|
||||||
|
target={isNewTab ? "_blank" : undefined}
|
||||||
|
rel={isNewTab ? "noopener" : undefined}
|
||||||
|
class={`group relative inline-flex items-center gap-3 px-8 py-4 text-lg font-semibold ${buttonClasses} transition-all duration-300 ease-out hover:scale-105 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-500/50 dark:focus-visible:ring-white/50 focus-visible:ring-offset-2 focus-visible:ring-offset-transparent`}
|
||||||
|
>
|
||||||
|
{/* Glassmorphism Background */}
|
||||||
|
<div class="absolute inset-0 bg-white/95 dark:bg-gray-900/95 backdrop-blur-md rounded-2xl ring-1 ring-gray-200/30 dark:ring-white/30 shadow-lg group-hover:bg-white dark:group-hover:bg-gray-800 group-hover:ring-gray-300/50 dark:group-hover:ring-white/50 group-hover:shadow-2xl transition-all duration-300"></div>
|
||||||
|
|
||||||
|
{/* Button Content */}
|
||||||
|
<span class="relative z-10">{buttonText}</span>
|
||||||
|
{button_icon_svg && (
|
||||||
|
<span class="relative z-10 transition-transform duration-300 group-hover:translate-x-1">
|
||||||
|
<Icon svg={button_icon_svg} attributes={{style: "height: 1.25em", class: "inline-block"}} />
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Text */}
|
{/* Glow Effect */}
|
||||||
{content.text && (
|
<div class="absolute inset-0 bg-white/20 dark:bg-white/10 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-300 blur-xl"></div>
|
||||||
<div
|
</a>
|
||||||
class={`${textClasses}/80 mt-6 text-lg sm:text-xl lg:text-2xl max-w-3xl leading-relaxed font-light`}
|
|
||||||
dangerouslySetInnerHTML={{ __html: renderText(content.text) }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Button */}
|
|
||||||
{showButton && (
|
|
||||||
<div class="flex mt-10">
|
|
||||||
<a
|
|
||||||
href={buttonUrl}
|
|
||||||
target={isNewTab ? "_blank" : undefined}
|
|
||||||
rel={isNewTab ? "noopener" : undefined}
|
|
||||||
class={`group relative inline-flex items-center gap-3 px-8 py-4 text-lg font-semibold ${buttonClasses} transition-all duration-300 ease-out hover:scale-105 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-500/50 dark:focus-visible:ring-white/50 focus-visible:ring-offset-2 focus-visible:ring-offset-transparent`}
|
|
||||||
>
|
|
||||||
{/* Glassmorphism Background */}
|
|
||||||
<div class="absolute inset-0 bg-white/95 dark:bg-gray-900/95 backdrop-blur-md rounded-2xl ring-1 ring-gray-200/30 dark:ring-white/30 shadow-lg group-hover:bg-white dark:group-hover:bg-gray-800 group-hover:ring-gray-300/50 dark:group-hover:ring-white/50 group-hover:shadow-2xl transition-all duration-300"></div>
|
|
||||||
|
|
||||||
{/* Button Content */}
|
|
||||||
<span class="relative z-10">{buttonText}</span>
|
|
||||||
{button_icon_svg && (
|
|
||||||
<span class="relative z-10 transition-transform duration-300 group-hover:translate-x-1">
|
|
||||||
<Icon svg={button_icon_svg} attributes={{ style: "height: 1.25em", class: "inline-block" }} />
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Glow Effect */}
|
|
||||||
<div class="absolute inset-0 bg-white/20 dark:bg-white/10 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-300 blur-xl"></div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
@@ -2,24 +2,24 @@
|
|||||||
* CTA Image Paragraph Block - Client-side Hydration
|
* CTA Image Paragraph Block - Client-side Hydration
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { render } from "preact";
|
import {render} from "preact";
|
||||||
import { CtaImageParagraphBlock } from "./component.jsx";
|
import {CtaImageParagraphBlock} from "./component.jsx";
|
||||||
|
|
||||||
function renderCtaImageParagraphBlocks() {
|
function renderCtaImageParagraphBlocks() {
|
||||||
const blocks = document.querySelectorAll('[data-block-type="cta-image-paragraph"]');
|
const blocks = document.querySelectorAll('[data-block-type="cta-image-paragraph"]');
|
||||||
|
|
||||||
blocks.forEach((block) => {
|
blocks.forEach((block) => {
|
||||||
const propsData = block.dataset.props;
|
const propsData = block.dataset.props;
|
||||||
if (propsData) {
|
if (propsData) {
|
||||||
try {
|
try {
|
||||||
const props = JSON.parse(propsData);
|
const props = JSON.parse(propsData);
|
||||||
render(<CtaImageParagraphBlock {...props} />, block);
|
render(<CtaImageParagraphBlock {...props} />, block);
|
||||||
console.debug(`✓ CTA Image Paragraph block "${block.id}" rendered with Preact`);
|
console.debug(`✓ CTA Image Paragraph block "${block.id}" rendered with Preact`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to render CTA Image Paragraph block "${block.id}":`, error);
|
console.error(`Failed to render CTA Image Paragraph block "${block.id}":`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCtaImageParagraphBlocks();
|
renderCtaImageParagraphBlocks();
|
||||||
@@ -4,137 +4,131 @@
|
|||||||
* Images are pre-processed by Hugo's responsive image pipeline (srcset)
|
* Images are pre-processed by Hugo's responsive image pipeline (srcset)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Icon } from "../../shared/components/Icon.jsx";
|
import {Icon} from "../../shared/components/Icon.jsx";
|
||||||
|
|
||||||
// Simple markdown renderer
|
// Simple markdown renderer
|
||||||
function renderText(text) {
|
function renderText(text) {
|
||||||
if (!text) return "";
|
if (!text) return "";
|
||||||
return String(text)
|
return String(text)
|
||||||
.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>")
|
.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>")
|
||||||
.replace(/\*(.*?)\*/g, "<em>$1</em>")
|
.replace(/\*(.*?)\*/g, "<em>$1</em>")
|
||||||
.replace(/`(.*?)`/g, "<code>$1</code>")
|
.replace(/`(.*?)`/g, "<code>$1</code>")
|
||||||
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
|
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Arrow icon for CTA buttons
|
// Arrow icon for CTA buttons
|
||||||
const ArrowIcon = () => (
|
const ArrowIcon = () => (
|
||||||
<svg class="ml-2 -mr-1 w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
<svg aria-hidden="true" class="ml-2 -mr-1 w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd" />
|
<path
|
||||||
</svg>
|
fill-rule="evenodd"
|
||||||
|
d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
function CtaItem({ item, idx, imageData, featureIconSvg }) {
|
function CtaItem({item, idx, imageData, featureIconSvg}) {
|
||||||
const isReversed = idx % 2 === 1;
|
const isReversed = idx % 2 === 1;
|
||||||
const hasImage = !!imageData;
|
const hasImage = !!imageData;
|
||||||
const featureIcon = item.feature_icon || "check";
|
const _featureIcon = item.feature_icon || "check";
|
||||||
|
|
||||||
// Resolve button link
|
// Resolve button link
|
||||||
let buttonUrl = item.button?.url || "";
|
const buttonUrl = item.button?.url || "";
|
||||||
let isExternal = false;
|
let isExternal = false;
|
||||||
if (buttonUrl) {
|
if (buttonUrl) {
|
||||||
try {
|
try {
|
||||||
const url = new URL(buttonUrl, window.location.origin);
|
const url = new URL(buttonUrl, window.location.origin);
|
||||||
isExternal = url.origin !== window.location.origin;
|
isExternal = url.origin !== window.location.origin;
|
||||||
} catch {
|
} catch {
|
||||||
isExternal = /^https?:\/\//.test(buttonUrl);
|
isExternal = /^https?:\/\//.test(buttonUrl);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const features = Array.isArray(item.features)
|
const features = Array.isArray(item.features) ? item.features : typeof item.features === "string" ? [item.features] : [];
|
||||||
? item.features
|
|
||||||
: typeof item.features === "string"
|
|
||||||
? [item.features]
|
|
||||||
: [];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={`gap-8 items-center py-8 px-4 mx-auto max-w-screen-xl xl:gap-16 ${hasImage ? "md:grid md:grid-cols-2" : ""} sm:py-16 lg:px-6`}>
|
<div class={`gap-8 items-center py-8 px-4 mx-auto max-w-screen-xl xl:gap-16 ${hasImage ? "md:grid md:grid-cols-2" : ""} sm:py-16 lg:px-6`}>
|
||||||
{/* Image */}
|
{/* Image */}
|
||||||
{hasImage && (
|
{hasImage &&
|
||||||
imageData.srcset ? (
|
(imageData.srcset ? (
|
||||||
<img
|
<img
|
||||||
class="w-full"
|
class="w-full"
|
||||||
srcset={imageData.srcset}
|
srcset={imageData.srcset}
|
||||||
sizes="(max-width: 768px) 100vw, 50vw"
|
sizes="(max-width: 768px) 100vw, 50vw"
|
||||||
src={imageData.src}
|
src={imageData.src}
|
||||||
width={imageData.width}
|
width={imageData.width}
|
||||||
height={imageData.height}
|
height={imageData.height}
|
||||||
alt={item.title || ""}
|
alt={item.title || ""}
|
||||||
style={isReversed ? { order: 1 } : undefined}
|
style={isReversed ? {order: 1} : undefined}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<img
|
<img
|
||||||
class="w-full"
|
class="w-full"
|
||||||
src={imageData.src}
|
src={imageData.src}
|
||||||
width={imageData.width}
|
width={imageData.width}
|
||||||
height={imageData.height}
|
height={imageData.height}
|
||||||
alt={item.title || ""}
|
alt={item.title || ""}
|
||||||
style={isReversed ? { order: 1 } : undefined}
|
style={isReversed ? {order: 1} : undefined}
|
||||||
/>
|
/>
|
||||||
)
|
))}
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Text Content */}
|
{/* Text Content */}
|
||||||
<div class="mt-4 md:mt-0">
|
<div class="mt-4 md:mt-0">
|
||||||
{item.title && (
|
{item.title && (
|
||||||
<h2
|
<h2
|
||||||
class="mb-4 text-4xl tracking-tight font-extrabold text-gray-900 dark:text-white"
|
class="mb-4 text-4xl tracking-tight font-extrabold text-gray-900 dark:text-white"
|
||||||
dangerouslySetInnerHTML={{ __html: renderText(item.title) }}
|
dangerouslySetInnerHTML={{__html: renderText(item.title)}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{item.text && (
|
{item.text && (
|
||||||
<p
|
<p class="mb-6 font-light text-gray-500 md:text-lg dark:text-gray-400" dangerouslySetInnerHTML={{__html: renderText(item.text)}} />
|
||||||
class="mb-6 font-light text-gray-500 md:text-lg dark:text-gray-400"
|
)}
|
||||||
dangerouslySetInnerHTML={{ __html: renderText(item.text) }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Feature List */}
|
{/* Feature List */}
|
||||||
{features.length > 0 && (
|
{features.length > 0 && (
|
||||||
<ul>
|
<ul>
|
||||||
{features.map((feature, fIdx) => (
|
{features.map((feature, fIdx) => (
|
||||||
<li key={fIdx} class="relative mb-4 pl-6">
|
<li key={fIdx} class="relative mb-4 pl-6">
|
||||||
{featureIconSvg ? (
|
{featureIconSvg ? <Icon svg={featureIconSvg} attributes={{class: "inline-block pr-5", style: "height: 1em;"}} /> : null}
|
||||||
<Icon svg={featureIconSvg} attributes={{ class: "inline-block pr-5", style: "height: 1em;" }} />
|
<span dangerouslySetInnerHTML={{__html: renderText(String(feature))}} />
|
||||||
) : null}
|
</li>
|
||||||
<span dangerouslySetInnerHTML={{ __html: renderText(String(feature)) }} />
|
))}
|
||||||
</li>
|
</ul>
|
||||||
))}
|
)}
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* CTA Button */}
|
{/* CTA Button */}
|
||||||
{item.button?.text && item.button?.url && (
|
{item.button?.text && item.button?.url && (
|
||||||
<a
|
<a
|
||||||
href={buttonUrl}
|
href={buttonUrl}
|
||||||
target={isExternal ? "_blank" : undefined}
|
target={isExternal ? "_blank" : undefined}
|
||||||
rel={isExternal ? "noopener" : undefined}
|
rel={isExternal ? "noopener" : undefined}
|
||||||
class="mt-3 inline-flex items-center text-white bg-primary-700 hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:focus:ring-primary-900"
|
class="mt-3 inline-flex items-center text-white bg-primary-700 hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:focus:ring-primary-900"
|
||||||
>
|
>
|
||||||
{item.button.text}
|
{item.button.text}
|
||||||
<ArrowIcon />
|
<ArrowIcon />
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CtaImageParagraphBlock = ({ content, design, id, item_images, icon_svgs }) => {
|
export const CtaImageParagraphBlock = ({content, _design, _id, item_images, icon_svgs}) => {
|
||||||
const items = Array.isArray(content?.items) ? content.items : [];
|
const items = Array.isArray(content?.items) ? content.items : [];
|
||||||
const imageMap = item_images || {};
|
const imageMap = item_images || {};
|
||||||
const iconMap = icon_svgs || {};
|
const iconMap = icon_svgs || {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{items.map((item, idx) => (
|
{items.map((item, idx) => (
|
||||||
<CtaItem
|
<CtaItem
|
||||||
key={idx}
|
key={idx}
|
||||||
item={item}
|
item={item}
|
||||||
idx={idx}
|
idx={idx}
|
||||||
imageData={imageMap[String(idx)] || null}
|
imageData={imageMap[String(idx)] || null}
|
||||||
featureIconSvg={item.feature_icon ? iconMap[item.feature_icon] : iconMap["check"]}
|
featureIconSvg={item.feature_icon ? iconMap[item.feature_icon] : iconMap.check}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -2,24 +2,24 @@
|
|||||||
* Features Block - Client-side Hydration
|
* Features Block - Client-side Hydration
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { render } from "preact";
|
import {render} from "preact";
|
||||||
import { FeaturesBlock } from "./component.jsx";
|
import {FeaturesBlock} from "./component.jsx";
|
||||||
|
|
||||||
function renderFeaturesBlocks() {
|
function renderFeaturesBlocks() {
|
||||||
const blocks = document.querySelectorAll('[data-block-type="features"]');
|
const blocks = document.querySelectorAll('[data-block-type="features"]');
|
||||||
|
|
||||||
blocks.forEach((block) => {
|
blocks.forEach((block) => {
|
||||||
const propsData = block.dataset.props;
|
const propsData = block.dataset.props;
|
||||||
if (propsData) {
|
if (propsData) {
|
||||||
try {
|
try {
|
||||||
const props = JSON.parse(propsData);
|
const props = JSON.parse(propsData);
|
||||||
render(<FeaturesBlock {...props} />, block);
|
render(<FeaturesBlock {...props} />, block);
|
||||||
console.debug(`✓ Features block "${block.id}" rendered with Preact`);
|
console.debug(`✓ Features block "${block.id}" rendered with Preact`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to render Features block "${block.id}":`, error);
|
console.error(`Failed to render Features block "${block.id}":`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
renderFeaturesBlocks();
|
renderFeaturesBlocks();
|
||||||
@@ -3,79 +3,60 @@
|
|||||||
* Responsive grid of feature items with icon, name, and description
|
* Responsive grid of feature items with icon, name, and description
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Icon } from "../../shared/components/Icon.jsx";
|
import {Icon} from "../../shared/components/Icon.jsx";
|
||||||
|
|
||||||
// Simple markdown renderer
|
// Simple markdown renderer
|
||||||
function renderText(text) {
|
function renderText(text) {
|
||||||
if (!text) return "";
|
if (!text) return "";
|
||||||
return String(text)
|
return String(text)
|
||||||
.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>")
|
.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>")
|
||||||
.replace(/\*(.*?)\*/g, "<em>$1</em>")
|
.replace(/\*(.*?)\*/g, "<em>$1</em>")
|
||||||
.replace(/`(.*?)`/g, "<code>$1</code>");
|
.replace(/`(.*?)`/g, "<code>$1</code>");
|
||||||
}
|
}
|
||||||
|
|
||||||
function FeatureItem({ item, iconSvg }) {
|
function FeatureItem({item, iconSvg}) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{iconSvg && (
|
{iconSvg && (
|
||||||
<div class="flex justify-center items-center mb-4 w-12 h-12 rounded-full bg-primary-100 lg:h-14 lg:w-14 dark:bg-primary-900 text-primary-600 dark:text-primary-300">
|
<div class="flex justify-center items-center mb-4 w-12 h-12 rounded-full bg-primary-100 lg:h-14 lg:w-14 dark:bg-primary-900 text-primary-600 dark:text-primary-300">
|
||||||
<Icon svg={iconSvg} attributes={{ class: "flex items-center justify-center w-6 h-6 lg:w-8 lg:h-8" }} />
|
<Icon svg={iconSvg} attributes={{class: "flex items-center justify-center w-6 h-6 lg:w-8 lg:h-8"}} />
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{item.name && (
|
|
||||||
<h3
|
|
||||||
class="mb-2 text-xl font-bold dark:text-white"
|
|
||||||
dangerouslySetInnerHTML={{ __html: renderText(item.name) }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{item.description && (
|
|
||||||
<p
|
|
||||||
class="text-gray-500 dark:text-gray-400"
|
|
||||||
dangerouslySetInnerHTML={{ __html: renderText(item.description) }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
)}
|
||||||
|
{item.name && <h3 class="mb-2 text-xl font-bold dark:text-white" dangerouslySetInnerHTML={{__html: renderText(item.name)}} />}
|
||||||
|
{item.description && <p class="text-gray-500 dark:text-gray-400" dangerouslySetInnerHTML={{__html: renderText(item.description)}} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FeaturesBlock = ({ content, design, id, icon_svgs }) => {
|
export const FeaturesBlock = ({content, _design, _id, icon_svgs}) => {
|
||||||
const title = content?.title;
|
const title = content?.title;
|
||||||
const text = content?.text;
|
const text = content?.text;
|
||||||
const items = Array.isArray(content?.items) ? content.items : [];
|
const items = Array.isArray(content?.items) ? content.items : [];
|
||||||
const iconMap = icon_svgs || {};
|
const iconMap = icon_svgs || {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<div class="py-8 px-4 mx-auto max-w-screen-xl sm:py-16 lg:px-6">
|
<div class="py-8 px-4 mx-auto max-w-screen-xl sm:py-16 lg:px-6">
|
||||||
{/* Section Header */}
|
{/* Section Header */}
|
||||||
{(title || text) && (
|
{(title || text) && (
|
||||||
<div class="max-w-screen-md mb-8 lg:mb-16 flex flex-col justify-center mx-auto">
|
<div class="max-w-screen-md mb-8 lg:mb-16 flex flex-col justify-center mx-auto">
|
||||||
{title && (
|
{title && (
|
||||||
<h2
|
<h2
|
||||||
class="mb-4 text-4xl tracking-tight font-extrabold text-gray-900 dark:text-white text-center"
|
class="mb-4 text-4xl tracking-tight font-extrabold text-gray-900 dark:text-white text-center"
|
||||||
dangerouslySetInnerHTML={{ __html: renderText(title) }}
|
dangerouslySetInnerHTML={{__html: renderText(title)}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{text && (
|
{text && <p class="text-gray-500 sm:text-xl dark:text-gray-400 text-center" dangerouslySetInnerHTML={{__html: renderText(text)}} />}
|
||||||
<p
|
</div>
|
||||||
class="text-gray-500 sm:text-xl dark:text-gray-400 text-center"
|
)}
|
||||||
dangerouslySetInnerHTML={{ __html: renderText(text) }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Feature Grid */}
|
{/* Feature Grid */}
|
||||||
<div class="space-y-8 md:grid md:grid-cols-2 lg:grid-cols-3 md:gap-12 md:space-y-0">
|
<div class="space-y-8 md:grid md:grid-cols-2 lg:grid-cols-3 md:gap-12 md:space-y-0">
|
||||||
{items.map((item, idx) => (
|
{items.map((item, idx) => (
|
||||||
<FeatureItem
|
<FeatureItem key={idx} item={item} iconSvg={item.icon ? iconMap[item.icon] : null} />
|
||||||
key={idx}
|
))}
|
||||||
item={item}
|
</div>
|
||||||
iconSvg={item.icon ? iconMap[item.icon] : null}
|
</div>
|
||||||
/>
|
</section>
|
||||||
))}
|
);
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
* Used for both SSR and client-side hydration
|
* Used for both SSR and client-side hydration
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Icon } from "../../shared/components/Icon.jsx";
|
import {Icon} from "../../shared/components/Icon.jsx";
|
||||||
|
|
||||||
// Simple markdown renderer
|
// Simple markdown renderer
|
||||||
export function renderText(text) {
|
export function renderText(text) {
|
||||||
@@ -16,7 +16,7 @@ export function renderText(text) {
|
|||||||
|
|
||||||
// Process URLs
|
// Process URLs
|
||||||
export function processUrl(url) {
|
export function processUrl(url) {
|
||||||
if (!url) return { href: "#" };
|
if (!url) return {href: "#"};
|
||||||
|
|
||||||
if (url.startsWith("http://") || url.startsWith("https://")) {
|
if (url.startsWith("http://") || url.startsWith("https://")) {
|
||||||
return {
|
return {
|
||||||
@@ -27,14 +27,14 @@ export function processUrl(url) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (url.startsWith("#")) {
|
if (url.startsWith("#")) {
|
||||||
return { href: url };
|
return {href: url};
|
||||||
}
|
}
|
||||||
|
|
||||||
return { href: url };
|
return {href: url};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hero Block Component - Single implementation
|
// Hero Block Component - Single implementation
|
||||||
export const HeroBlock = ({ content, design, id, icon_svg }) => {
|
export const HeroBlock = ({content, design, _id, icon_svg}) => {
|
||||||
const paddingClasses = design?.no_padding ? "" : "py-32 sm:py-48 lg:py-56";
|
const paddingClasses = design?.no_padding ? "" : "py-32 sm:py-48 lg:py-56";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -73,7 +73,7 @@ export const HeroBlock = ({ content, design, id, icon_svg }) => {
|
|||||||
{content.title && (
|
{content.title && (
|
||||||
<h1
|
<h1
|
||||||
class="text-4xl font-bold tracking-tight text-gray-900 dark:text-gray-100 sm:text-6xl"
|
class="text-4xl font-bold tracking-tight text-gray-900 dark:text-gray-100 sm:text-6xl"
|
||||||
dangerouslySetInnerHTML={{ __html: renderText(content.title) }}
|
dangerouslySetInnerHTML={{__html: renderText(content.title)}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ export const HeroBlock = ({ content, design, id, icon_svg }) => {
|
|||||||
{content.text && (
|
{content.text && (
|
||||||
<p
|
<p
|
||||||
class="mt-6 text-lg leading-8 text-gray-600 dark:text-gray-300 max-w-2xl mx-auto"
|
class="mt-6 text-lg leading-8 text-gray-600 dark:text-gray-300 max-w-2xl mx-auto"
|
||||||
dangerouslySetInnerHTML={{ __html: renderText(content.text) }}
|
dangerouslySetInnerHTML={{__html: renderText(content.text)}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// biome-ignore lint/correctness/noUnusedImports: Classic Preact JSX runtime may require 'h' for JSX transform
|
// biome-ignore lint/correctness/noUnusedImports: Classic Preact JSX runtime may require 'h' for JSX transform
|
||||||
import { h } from "preact";
|
import {h} from "preact";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Icon component
|
* Icon component
|
||||||
@@ -13,7 +13,7 @@ import { h } from "preact";
|
|||||||
* the SVG fills that container with height:100%;width:auto instead.
|
* the SVG fills that container with height:100%;width:auto instead.
|
||||||
* - If the caller provides explicit w-/h- Tailwind classes, those take precedence.
|
* - If the caller provides explicit w-/h- Tailwind classes, those take precedence.
|
||||||
*/
|
*/
|
||||||
export const Icon = ({ svg, attributes }) => {
|
export const Icon = ({svg, attributes}) => {
|
||||||
if (!svg) return null;
|
if (!svg) return null;
|
||||||
|
|
||||||
// Clean the SVG string: decode HTML entities and TRIM whitespace
|
// Clean the SVG string: decode HTML entities and TRIM whitespace
|
||||||
@@ -35,9 +35,7 @@ export const Icon = ({ svg, attributes }) => {
|
|||||||
// use 100% to fill the container; otherwise default to 1em for self-sizing.
|
// use 100% to fill the container; otherwise default to 1em for self-sizing.
|
||||||
const callerStyle = attributes?.style || "";
|
const callerStyle = attributes?.style || "";
|
||||||
const hasExplicitHeight = /height\s*:/i.test(String(callerStyle));
|
const hasExplicitHeight = /height\s*:/i.test(String(callerStyle));
|
||||||
const svgStyle = hasExplicitHeight
|
const svgStyle = hasExplicitHeight ? "height:100%;width:auto" : "height:1em;width:auto";
|
||||||
? "height:100%;width:auto"
|
|
||||||
: "height:1em;width:auto";
|
|
||||||
|
|
||||||
decoded = decoded.replace(/^(<svg\b)/i, `$1 style="${svgStyle}"`);
|
decoded = decoded.replace(/^(<svg\b)/i, `$1 style="${svgStyle}"`);
|
||||||
|
|
||||||
@@ -47,7 +45,7 @@ export const Icon = ({ svg, attributes }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line lint/security/noDangerouslySetInnerHtml
|
// eslint-disable-next-line lint/security/noDangerouslySetInnerHtml
|
||||||
return <span {...wrapperProps} dangerouslySetInnerHTML={{ __html: decoded }} />;
|
return <span {...wrapperProps} dangerouslySetInnerHTML={{__html: decoded}} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const finalAttributes = {
|
const finalAttributes = {
|
||||||
@@ -62,5 +60,5 @@ export const Icon = ({ svg, attributes }) => {
|
|||||||
.join(" ");
|
.join(" ");
|
||||||
|
|
||||||
// eslint-disable-next-line lint/security/noDangerouslySetInnerHtml
|
// eslint-disable-next-line lint/security/noDangerouslySetInnerHtml
|
||||||
return <span class="inline-block" dangerouslySetInnerHTML={{ __html: `<svg ${attrs}>${decoded}</svg>` }} />;
|
return <span class="inline-block" dangerouslySetInnerHTML={{__html: `<svg ${attrs}>${decoded}</svg>`}} />;
|
||||||
};
|
};
|
||||||
@@ -3,28 +3,28 @@
|
|||||||
* Uses the shared StatsBlock component for consistency
|
* Uses the shared StatsBlock component for consistency
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { render } from "preact";
|
import {render} from "preact";
|
||||||
import { StatsBlock } from "./component.jsx";
|
import {StatsBlock} from "./component.jsx";
|
||||||
|
|
||||||
function renderStatsBlocks() {
|
function renderStatsBlocks() {
|
||||||
const statsBlocks = document.querySelectorAll('[data-block-type="stats"]');
|
const statsBlocks = document.querySelectorAll('[data-block-type="stats"]');
|
||||||
|
|
||||||
statsBlocks.forEach((block) => {
|
statsBlocks.forEach((block) => {
|
||||||
const propsData = block.dataset.props;
|
const propsData = block.dataset.props;
|
||||||
if (propsData) {
|
if (propsData) {
|
||||||
try {
|
try {
|
||||||
const props = JSON.parse(propsData);
|
const props = JSON.parse(propsData);
|
||||||
render(<StatsBlock {...props} />, block);
|
render(<StatsBlock {...props} />, block);
|
||||||
console.debug(`✓ Stats block "${block.id}" rendered with Preact`);
|
console.debug(`✓ Stats block "${block.id}" rendered with Preact`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to render Stats block "${block.id}":`, error);
|
console.error(`Failed to render Stats block "${block.id}":`, error);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (statsBlocks.length > 0) {
|
|
||||||
console.debug(`✓ ${statsBlocks.length} Stats blocks initialized with Preact`);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (statsBlocks.length > 0) {
|
||||||
|
console.debug(`✓ ${statsBlocks.length} Stats blocks initialized with Preact`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize immediately — script is already deferred
|
// Initialize immediately — script is already deferred
|
||||||
|
|||||||
@@ -6,280 +6,264 @@
|
|||||||
* Features: counter animation, per-item icons, stagger reveal
|
* Features: counter animation, per-item icons, stagger reveal
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect, useRef } from "preact/hooks";
|
import {useEffect, useRef} from "preact/hooks";
|
||||||
import { Icon } from "../../shared/components/Icon.jsx";
|
import {Icon} from "../../shared/components/Icon.jsx";
|
||||||
|
|
||||||
// Simple markdown renderer (shared with Hero)
|
// Simple markdown renderer (shared with Hero)
|
||||||
function renderText(text) {
|
function renderText(text) {
|
||||||
if (!text) return "";
|
if (!text) return "";
|
||||||
return text
|
return text
|
||||||
.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>")
|
.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>")
|
||||||
.replace(/\*(.*?)\*/g, "<em>$1</em>")
|
.replace(/\*(.*?)\*/g, "<em>$1</em>")
|
||||||
.replace(/`(.*?)`/g, "<code>$1</code>");
|
.replace(/`(.*?)`/g, "<code>$1</code>");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract numeric target from a statistic string like "10,000+" → 10000
|
// Extract numeric target from a statistic string like "10,000+" → 10000
|
||||||
function parseTarget(statistic) {
|
function parseTarget(statistic) {
|
||||||
if (!statistic) return 0;
|
if (!statistic) return 0;
|
||||||
const raw = String(statistic).replace(/[^0-9.]/g, "");
|
const raw = String(statistic).replace(/[^0-9.]/g, "");
|
||||||
const num = Number(raw);
|
const num = Number(raw);
|
||||||
return isNaN(num) ? 0 : num;
|
return Number.isNaN(num) ? 0 : num;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract prefix, number, suffix from "~10,000+" → { prefix: "~", number: "10,000", suffix: "+" }
|
// Extract prefix, number, suffix from "~10,000+" → { prefix: "~", number: "10,000", suffix: "+" }
|
||||||
function extractParts(text) {
|
function extractParts(text) {
|
||||||
if (!text) return { prefix: "", numberPart: "", suffix: "" };
|
if (!text) return {prefix: "", numberPart: "", suffix: ""};
|
||||||
const parts = String(text).match(/([^0-9]*)([0-9][0-9,.]*)(.*)/);
|
const parts = String(text).match(/([^0-9]*)([0-9][0-9,.]*)(.*)/);
|
||||||
return {
|
return {
|
||||||
prefix: parts ? parts[1] : "",
|
prefix: parts ? parts[1] : "",
|
||||||
numberPart: parts ? parts[2] : "",
|
numberPart: parts ? parts[2] : "",
|
||||||
suffix: parts ? parts[3] : "",
|
suffix: parts ? parts[3] : "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Counter animation hook
|
// Counter animation hook
|
||||||
function useCounterAnimation(containerRef) {
|
function useCounterAnimation(containerRef) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
const prefersReducedMotion = window.matchMedia(
|
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||||
"(prefers-reduced-motion: reduce)"
|
|
||||||
).matches;
|
|
||||||
|
|
||||||
function animateCounters(el) {
|
function animateCounters(el) {
|
||||||
el.classList.add("animate");
|
el.classList.add("animate");
|
||||||
const counters = el.querySelectorAll(".counter");
|
const counters = el.querySelectorAll(".counter");
|
||||||
|
|
||||||
counters.forEach((counter) => {
|
counters.forEach((counter) => {
|
||||||
const target = parseTarget(counter.getAttribute("data-target"));
|
const target = parseTarget(counter.getAttribute("data-target"));
|
||||||
if (target <= 0) return;
|
if (target <= 0) return;
|
||||||
|
|
||||||
const { prefix, numberPart, suffix } = extractParts(
|
const {prefix, numberPart, suffix} = extractParts(counter.textContent);
|
||||||
counter.textContent
|
const decimals = (numberPart.split(".")[1] || "").length;
|
||||||
);
|
const formatter = new Intl.NumberFormat(undefined, {
|
||||||
const decimals = (numberPart.split(".")[1] || "").length;
|
minimumFractionDigits: decimals,
|
||||||
const formatter = new Intl.NumberFormat(undefined, {
|
maximumFractionDigits: decimals,
|
||||||
minimumFractionDigits: decimals,
|
|
||||||
maximumFractionDigits: decimals,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (prefersReducedMotion) {
|
|
||||||
counter.textContent = `${prefix}${formatter.format(target)}${suffix}`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const duration = 2000;
|
|
||||||
const start = Date.now();
|
|
||||||
|
|
||||||
function update() {
|
|
||||||
const now = Date.now();
|
|
||||||
const progress = Math.min((now - start) / duration, 1);
|
|
||||||
const easeOut = 1 - Math.pow(1 - progress, 3);
|
|
||||||
const current = target * easeOut;
|
|
||||||
counter.textContent = `${prefix}${formatter.format(current)}${suffix}`;
|
|
||||||
if (progress < 1) requestAnimationFrame(update);
|
|
||||||
}
|
|
||||||
|
|
||||||
requestAnimationFrame(update);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const observer = new IntersectionObserver(
|
|
||||||
(entries) => {
|
|
||||||
entries.forEach((entry) => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
animateCounters(entry.target);
|
|
||||||
observer.unobserve(entry.target);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{ threshold: 0.2, rootMargin: "0px 0px -10% 0px" }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Observe each stat item
|
|
||||||
const items = container.querySelectorAll(".stats-item");
|
|
||||||
items.forEach((item) => {
|
|
||||||
const rect = item.getBoundingClientRect();
|
|
||||||
const vh = window.innerHeight;
|
|
||||||
if (rect.top < vh * 0.9 && rect.bottom > 0) {
|
|
||||||
animateCounters(item);
|
|
||||||
} else {
|
|
||||||
observer.observe(item);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => observer.disconnect();
|
if (prefersReducedMotion) {
|
||||||
}, []);
|
counter.textContent = `${prefix}${formatter.format(target)}${suffix}`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const duration = 2000;
|
||||||
|
const start = Date.now();
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
const now = Date.now();
|
||||||
|
const progress = Math.min((now - start) / duration, 1);
|
||||||
|
const easeOut = 1 - (1 - progress) ** 3;
|
||||||
|
const current = target * easeOut;
|
||||||
|
counter.textContent = `${prefix}${formatter.format(current)}${suffix}`;
|
||||||
|
if (progress < 1) requestAnimationFrame(update);
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(update);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
animateCounters(entry.target);
|
||||||
|
observer.unobserve(entry.target);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{threshold: 0.2, rootMargin: "0px 0px -10% 0px"},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Observe each stat item
|
||||||
|
const items = container.querySelectorAll(".stats-item");
|
||||||
|
items.forEach((item) => {
|
||||||
|
const rect = item.getBoundingClientRect();
|
||||||
|
const vh = window.innerHeight;
|
||||||
|
if (rect.top < vh * 0.9 && rect.bottom > 0) {
|
||||||
|
animateCounters(item);
|
||||||
|
} else {
|
||||||
|
observer.observe(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grid columns class based on item count
|
// Grid columns class based on item count
|
||||||
function gridColsClass(count) {
|
function gridColsClass(count) {
|
||||||
if (count <= 1) return "lg:grid-cols-1";
|
if (count <= 1) return "lg:grid-cols-1";
|
||||||
if (count === 2) return "lg:grid-cols-2";
|
if (count === 2) return "lg:grid-cols-2";
|
||||||
if (count >= 4) return "lg:grid-cols-4";
|
if (count >= 4) return "lg:grid-cols-4";
|
||||||
return "lg:grid-cols-3";
|
return "lg:grid-cols-3";
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Stat Item (Cards) ───────────────────────────────────────────────
|
// ─── Stat Item (Cards) ───────────────────────────────────────────────
|
||||||
function StatCard({ item, iconSvg }) {
|
function StatCard({item, iconSvg}) {
|
||||||
return (
|
return (
|
||||||
<div class="stats-item group relative bg-white dark:bg-gray-800 rounded-2xl shadow-lg hover:shadow-2xl transition-all duration-500 overflow-hidden border border-gray-100 dark:border-gray-700 hover:border-primary-200 dark:hover:border-primary-800">
|
<div class="stats-item group relative bg-white dark:bg-gray-800 rounded-2xl shadow-lg hover:shadow-2xl transition-all duration-500 overflow-hidden border border-gray-100 dark:border-gray-700 hover:border-primary-200 dark:hover:border-primary-800">
|
||||||
{/* Hover gradient */}
|
{/* Hover gradient */}
|
||||||
<div class="absolute inset-0 bg-gradient-to-br from-primary-50 to-transparent dark:from-primary-900/20 dark:to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
<div class="absolute inset-0 bg-gradient-to-br from-primary-50 to-transparent dark:from-primary-900/20 dark:to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
||||||
|
|
||||||
<div class="relative p-8 text-center">
|
<div class="relative p-8 text-center">
|
||||||
{/* Icon */}
|
{/* Icon */}
|
||||||
{iconSvg && (
|
{iconSvg && (
|
||||||
<div class="inline-flex items-center justify-center w-16 h-16 mb-6 bg-primary-100 dark:bg-primary-900/50 rounded-xl group-hover:scale-110 transition-transform duration-300">
|
<div class="inline-flex items-center justify-center w-16 h-16 mb-6 bg-primary-100 dark:bg-primary-900/50 rounded-xl group-hover:scale-110 transition-transform duration-300">
|
||||||
<Icon svg={iconSvg} attributes={{ class: "w-8 h-8 text-primary-600 dark:text-primary-400" }} />
|
<Icon svg={iconSvg} attributes={{class: "w-8 h-8 text-primary-600 dark:text-primary-400"}} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Statistic */}
|
{/* Statistic */}
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<h3
|
<h3
|
||||||
class="text-4xl sm:text-5xl lg:text-6xl xl:text-7xl font-black text-gray-900 dark:text-white group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors duration-300 counter"
|
class="text-4xl sm:text-5xl lg:text-6xl xl:text-7xl font-black text-gray-900 dark:text-white group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors duration-300 counter"
|
||||||
data-target={parseTarget(item.statistic)}
|
data-target={parseTarget(item.statistic)}
|
||||||
dangerouslySetInnerHTML={{ __html: renderText(item.statistic) }}
|
dangerouslySetInnerHTML={{__html: renderText(item.statistic)}}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Description */}
|
|
||||||
<p
|
|
||||||
class="text-sm sm:text-base font-medium text-gray-600 dark:text-gray-300 leading-relaxed group-hover:text-gray-700 dark:group-hover:text-gray-200 transition-colors duration-300"
|
|
||||||
dangerouslySetInnerHTML={{ __html: renderText(item.description) }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Sub-metric */}
|
|
||||||
{item.sub_metric && (
|
|
||||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
{item.sub_metric}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
|
||||||
|
{/* Description */}
|
||||||
|
<p
|
||||||
|
class="text-sm sm:text-base font-medium text-gray-600 dark:text-gray-300 leading-relaxed group-hover:text-gray-700 dark:group-hover:text-gray-200 transition-colors duration-300"
|
||||||
|
dangerouslySetInnerHTML={{__html: renderText(item.description)}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Sub-metric */}
|
||||||
|
{item.sub_metric && <p class="mt-2 text-xs text-gray-500 dark:text-gray-400">{item.sub_metric}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Stat Item (Compact) ─────────────────────────────────────────────
|
// ─── Stat Item (Compact) ─────────────────────────────────────────────
|
||||||
function StatCompact({ item, iconSvg }) {
|
function StatCompact({item, iconSvg}) {
|
||||||
return (
|
return (
|
||||||
<div class="stats-item group p-8 text-center hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors duration-300">
|
<div class="stats-item group p-8 text-center hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors duration-300">
|
||||||
{iconSvg && (
|
{iconSvg && (
|
||||||
<div class="inline-flex items-center justify-center w-12 h-12 mb-4 bg-primary-100 dark:bg-primary-900/50 rounded-lg group-hover:scale-110 transition-transform duration-300">
|
<div class="inline-flex items-center justify-center w-12 h-12 mb-4 bg-primary-100 dark:bg-primary-900/50 rounded-lg group-hover:scale-110 transition-transform duration-300">
|
||||||
<Icon svg={iconSvg} attributes={{ class: "w-6 h-6 text-primary-600 dark:text-primary-400" }} />
|
<Icon svg={iconSvg} attributes={{class: "w-6 h-6 text-primary-600 dark:text-primary-400"}} />
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<h3
|
|
||||||
class="text-3xl sm:text-4xl lg:text-5xl font-bold text-gray-900 dark:text-white mb-2 counter"
|
|
||||||
data-target={parseTarget(item.statistic)}
|
|
||||||
dangerouslySetInnerHTML={{ __html: renderText(item.statistic) }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<p
|
|
||||||
class="text-sm font-medium text-gray-600 dark:text-gray-300 leading-relaxed"
|
|
||||||
dangerouslySetInnerHTML={{ __html: renderText(item.description) }}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
)}
|
||||||
|
|
||||||
|
<h3
|
||||||
|
class="text-3xl sm:text-4xl lg:text-5xl font-bold text-gray-900 dark:text-white mb-2 counter"
|
||||||
|
data-target={parseTarget(item.statistic)}
|
||||||
|
dangerouslySetInnerHTML={{__html: renderText(item.statistic)}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p
|
||||||
|
class="text-sm font-medium text-gray-600 dark:text-gray-300 leading-relaxed"
|
||||||
|
dangerouslySetInnerHTML={{__html: renderText(item.description)}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Stat Item (Minimal) ─────────────────────────────────────────────
|
// ─── Stat Item (Minimal) ─────────────────────────────────────────────
|
||||||
function StatMinimal({ item, iconSvg }) {
|
function StatMinimal({item, iconSvg}) {
|
||||||
return (
|
return (
|
||||||
<div class="stats-item text-center group">
|
<div class="stats-item text-center group">
|
||||||
{iconSvg && (
|
{iconSvg && (
|
||||||
<div class="inline-flex items-center justify-center w-14 h-14 mb-4 bg-primary-100 dark:bg-primary-900/50 rounded-full group-hover:scale-110 transition-transform duration-300">
|
<div class="inline-flex items-center justify-center w-14 h-14 mb-4 bg-primary-100 dark:bg-primary-900/50 rounded-full group-hover:scale-110 transition-transform duration-300">
|
||||||
<Icon svg={iconSvg} attributes={{ class: "w-7 h-7 text-primary-600 dark:text-primary-400" }} />
|
<Icon svg={iconSvg} attributes={{class: "w-7 h-7 text-primary-600 dark:text-primary-400"}} />
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<h3
|
|
||||||
class="text-3xl sm:text-4xl lg:text-5xl font-bold text-gray-900 dark:text-white mb-2 counter"
|
|
||||||
data-target={parseTarget(item.statistic)}
|
|
||||||
dangerouslySetInnerHTML={{ __html: renderText(item.statistic) }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<p
|
|
||||||
class="text-sm font-medium text-gray-600 dark:text-gray-300"
|
|
||||||
dangerouslySetInnerHTML={{ __html: renderText(item.description) }}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
)}
|
||||||
|
|
||||||
|
<h3
|
||||||
|
class="text-3xl sm:text-4xl lg:text-5xl font-bold text-gray-900 dark:text-white mb-2 counter"
|
||||||
|
data-target={parseTarget(item.statistic)}
|
||||||
|
dangerouslySetInnerHTML={{__html: renderText(item.statistic)}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p class="text-sm font-medium text-gray-600 dark:text-gray-300" dangerouslySetInnerHTML={{__html: renderText(item.description)}} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Main Stats Block ────────────────────────────────────────────────
|
// ─── Main Stats Block ────────────────────────────────────────────────
|
||||||
export const StatsBlock = ({ content, design, id, icon_svgs }) => {
|
export const StatsBlock = ({content, design, _id, icon_svgs}) => {
|
||||||
const containerRef = useRef(null);
|
const containerRef = useRef(null);
|
||||||
useCounterAnimation(containerRef);
|
useCounterAnimation(containerRef);
|
||||||
|
|
||||||
const title = content?.title;
|
const title = content?.title;
|
||||||
const text = content?.text;
|
const text = content?.text;
|
||||||
const items = Array.isArray(content?.items) ? content.items : [];
|
const items = Array.isArray(content?.items) ? content.items : [];
|
||||||
const layout = (design?.layout || "cards").toLowerCase();
|
const layout = (design?.layout || "cards").toLowerCase();
|
||||||
|
|
||||||
const count = Math.max(1, Math.min(items.length, 4));
|
const count = Math.max(1, Math.min(items.length, 4));
|
||||||
const lgCols = gridColsClass(count);
|
const lgCols = gridColsClass(count);
|
||||||
const baseCols = count === 1 ? "grid-cols-1" : "grid-cols-2";
|
const baseCols = count === 1 ? "grid-cols-1" : "grid-cols-2";
|
||||||
|
|
||||||
// icon_svgs is a dict keyed by icon name, provided by preact-wrapper
|
// icon_svgs is a dict keyed by icon name, provided by preact-wrapper
|
||||||
const iconMap = icon_svgs || {};
|
const iconMap = icon_svgs || {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="py-8 sm:py-12 lg:py-16" ref={containerRef}>
|
<div class="py-8 sm:py-12 lg:py-16" ref={containerRef}>
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
{/* Section Header */}
|
{/* Section Header */}
|
||||||
{(title || text) && (
|
{(title || text) && (
|
||||||
<div class="text-center mb-12 lg:mb-16">
|
<div class="text-center mb-12 lg:mb-16">
|
||||||
{title && (
|
{title && (
|
||||||
<h2
|
<h2
|
||||||
class="text-3xl sm:text-4xl font-bold text-gray-900 dark:text-white tracking-tight mb-4"
|
class="text-3xl sm:text-4xl font-bold text-gray-900 dark:text-white tracking-tight mb-4"
|
||||||
dangerouslySetInnerHTML={{ __html: renderText(title) }}
|
dangerouslySetInnerHTML={{__html: renderText(title)}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{text && (
|
{text && <p class="text-lg text-gray-600 dark:text-gray-400 max-w-3xl mx-auto" dangerouslySetInnerHTML={{__html: renderText(text)}} />}
|
||||||
<p
|
</div>
|
||||||
class="text-lg text-gray-600 dark:text-gray-400 max-w-3xl mx-auto"
|
)}
|
||||||
dangerouslySetInnerHTML={{ __html: renderText(text) }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Cards Layout */}
|
{/* Cards Layout */}
|
||||||
{layout === "cards" && (
|
{layout === "cards" && (
|
||||||
<div class={`grid grid-cols-1 sm:grid-cols-2 ${lgCols} gap-6 lg:gap-8`}>
|
<div class={`grid grid-cols-1 sm:grid-cols-2 ${lgCols} gap-6 lg:gap-8`}>
|
||||||
{items.map((item, idx) => (
|
{items.map((item, idx) => (
|
||||||
<StatCard key={idx} item={item} iconSvg={item.icon ? iconMap[item.icon] : null} />
|
<StatCard key={idx} item={item} iconSvg={item.icon ? iconMap[item.icon] : null} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Compact Layout */}
|
{/* Compact Layout */}
|
||||||
{layout === "compact" && (
|
{layout === "compact" && (
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-xl border border-gray-100 dark:border-gray-700 overflow-hidden">
|
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-xl border border-gray-100 dark:border-gray-700 overflow-hidden">
|
||||||
<div class={`grid grid-cols-1 sm:grid-cols-2 ${lgCols} divide-y sm:divide-y-0 sm:divide-x divide-gray-200 dark:divide-gray-700`}>
|
<div class={`grid grid-cols-1 sm:grid-cols-2 ${lgCols} divide-y sm:divide-y-0 sm:divide-x divide-gray-200 dark:divide-gray-700`}>
|
||||||
{items.map((item, idx) => (
|
{items.map((item, idx) => (
|
||||||
<StatCompact key={idx} item={item} iconSvg={item.icon ? iconMap[item.icon] : null} />
|
<StatCompact key={idx} item={item} iconSvg={item.icon ? iconMap[item.icon] : null} />
|
||||||
))}
|
))}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Minimal Layout */}
|
|
||||||
{layout === "minimal" && (
|
|
||||||
<div class={`grid ${baseCols} ${lgCols} gap-8 lg:gap-12`}>
|
|
||||||
{items.map((item, idx) => (
|
|
||||||
<StatMinimal key={idx} item={item} iconSvg={item.icon ? iconMap[item.icon] : null} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Animation styles (scoped via container ref) */}
|
{/* Minimal Layout */}
|
||||||
<style>{`
|
{layout === "minimal" && (
|
||||||
|
<div class={`grid ${baseCols} ${lgCols} gap-8 lg:gap-12`}>
|
||||||
|
{items.map((item, idx) => (
|
||||||
|
<StatMinimal key={idx} item={item} iconSvg={item.icon ? iconMap[item.icon] : null} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Animation styles (scoped via container ref) */}
|
||||||
|
<style>{`
|
||||||
.stats-item {
|
.stats-item {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(30px);
|
transform: translateY(30px);
|
||||||
@@ -295,6 +279,6 @@ export const StatsBlock = ({ content, design, id, icon_svgs }) => {
|
|||||||
.stats-item:nth-child(4) { transition-delay: 450ms; }
|
.stats-item:nth-child(4) { transition-delay: 450ms; }
|
||||||
.stats-item:nth-child(5) { transition-delay: 600ms; }
|
.stats-item:nth-child(5) { transition-delay: 600ms; }
|
||||||
`}</style>
|
`}</style>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -2,24 +2,24 @@
|
|||||||
* Testimonials Block - Client-side Hydration
|
* Testimonials Block - Client-side Hydration
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { render } from "preact";
|
import {render} from "preact";
|
||||||
import { TestimonialsBlock } from "./component.jsx";
|
import {TestimonialsBlock} from "./component.jsx";
|
||||||
|
|
||||||
function renderTestimonialsBlocks() {
|
function renderTestimonialsBlocks() {
|
||||||
const blocks = document.querySelectorAll('[data-block-type="testimonials"]');
|
const blocks = document.querySelectorAll('[data-block-type="testimonials"]');
|
||||||
|
|
||||||
blocks.forEach((block) => {
|
blocks.forEach((block) => {
|
||||||
const propsData = block.dataset.props;
|
const propsData = block.dataset.props;
|
||||||
if (propsData) {
|
if (propsData) {
|
||||||
try {
|
try {
|
||||||
const props = JSON.parse(propsData);
|
const props = JSON.parse(propsData);
|
||||||
render(<TestimonialsBlock {...props} />, block);
|
render(<TestimonialsBlock {...props} />, block);
|
||||||
console.debug(`✓ Testimonials block "${block.id}" rendered with Preact`);
|
console.debug(`✓ Testimonials block "${block.id}" rendered with Preact`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to render Testimonials block "${block.id}":`, error);
|
console.error(`Failed to render Testimonials block "${block.id}":`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
renderTestimonialsBlocks();
|
renderTestimonialsBlocks();
|
||||||
@@ -6,103 +6,76 @@
|
|||||||
|
|
||||||
// Quote mark SVG (inline for zero dependencies)
|
// Quote mark SVG (inline for zero dependencies)
|
||||||
const QuoteSvg = () => (
|
const QuoteSvg = () => (
|
||||||
<svg
|
<svg
|
||||||
class="h-16 mx-auto mb-3 text-gray-400 dark:text-gray-500"
|
aria-hidden="true"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
class="h-16 mx-auto mb-3 text-gray-400 dark:text-gray-500"
|
||||||
fill="currentColor"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 32 32"
|
fill="currentColor"
|
||||||
>
|
viewBox="0 0 32 32"
|
||||||
<path d="M9.563 8.469l-0.813-1.25c-5.625 3.781-8.75 8.375-8.75 12.156 0 3.656 2.688 5.375 4.969 5.375 2.875 0 4.906-2.438 4.906-5 0-2.156-1.375-4-3.219-4.688-0.531-0.188-1.031-0.344-1.031-1.25 0-1.156 0.844-2.875 3.938-5.344zM21.969 8.469l-0.813-1.25c-5.563 3.781-8.75 8.375-8.75 12.156 0 3.656 2.75 5.375 5.031 5.375 2.906 0 4.969-2.438 4.969-5 0-2.156-1.406-4-3.313-4.688-0.531-0.188-1-0.344-1-1.25 0-1.156 0.875-2.875 3.875-5.344z" />
|
>
|
||||||
</svg>
|
<path d="M9.563 8.469l-0.813-1.25c-5.625 3.781-8.75 8.375-8.75 12.156 0 3.656 2.688 5.375 4.969 5.375 2.875 0 4.906-2.438 4.906-5 0-2.156-1.375-4-3.219-4.688-0.531-0.188-1.031-0.344-1.031-1.25 0-1.156 0.844-2.875 3.938-5.344zM21.969 8.469l-0.813-1.25c-5.563 3.781-8.75 8.375-8.75 12.156 0 3.656 2.75 5.375 5.031 5.375 2.906 0 4.969-2.438 4.969-5 0-2.156-1.406-4-3.313-4.688-0.531-0.188-1-0.344-1-1.25 0-1.156 0.875-2.875 3.875-5.344z" />
|
||||||
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
// Simple markdown renderer
|
// Simple markdown renderer
|
||||||
function renderText(text) {
|
function renderText(text) {
|
||||||
if (!text) return "";
|
if (!text) return "";
|
||||||
return String(text)
|
return String(text)
|
||||||
.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>")
|
.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>")
|
||||||
.replace(/\*(.*?)\*/g, "<em>$1</em>")
|
.replace(/\*(.*?)\*/g, "<em>$1</em>")
|
||||||
.replace(/`(.*?)`/g, "<code>$1</code>");
|
.replace(/`(.*?)`/g, "<code>$1</code>");
|
||||||
}
|
}
|
||||||
|
|
||||||
function TestimonialItem({ item, avatarUrl, isLast }) {
|
function TestimonialItem({item, avatarUrl, isLast}) {
|
||||||
return (
|
return (
|
||||||
<div class={`max-w-screen-md mx-auto ${isLast ? "" : "mb-12"}`}>
|
<div class={`max-w-screen-md mx-auto ${isLast ? "" : "mb-12"}`}>
|
||||||
<QuoteSvg />
|
<QuoteSvg />
|
||||||
<blockquote>
|
<blockquote>
|
||||||
<p
|
<p class="text-2xl font-medium text-gray-900 dark:text-white" dangerouslySetInnerHTML={{__html: `"${renderText(item.text)}"`}} />
|
||||||
class="text-2xl font-medium text-gray-900 dark:text-white"
|
</blockquote>
|
||||||
dangerouslySetInnerHTML={{ __html: `"${renderText(item.text)}"` }}
|
<div class="flex items-center justify-center mt-6 space-x-3">
|
||||||
/>
|
{/* Avatar */}
|
||||||
</blockquote>
|
{avatarUrl ? (
|
||||||
<div class="flex items-center justify-center mt-6 space-x-3">
|
<img class="w-12 h-12 rounded-full" src={avatarUrl} width="48" height="48" alt={item.name || ""} loading="lazy" />
|
||||||
{/* Avatar */}
|
) : (
|
||||||
{avatarUrl ? (
|
<div class="w-12 h-12 rounded-full bg-gray-200 dark:bg-gray-700" />
|
||||||
<img
|
)}
|
||||||
class="w-12 h-12 rounded-full"
|
<div class="flex flex-col items-center">
|
||||||
src={avatarUrl}
|
{item.name && <div class="pr-3 font-medium text-gray-900 dark:text-white">{item.name}</div>}
|
||||||
width="48"
|
{item.role && <div class="pl-3 text-sm font-light text-gray-500 dark:text-gray-400">{item.role}</div>}
|
||||||
height="48"
|
|
||||||
alt={item.name || ""}
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div class="w-12 h-12 rounded-full bg-gray-200 dark:bg-gray-700" />
|
|
||||||
)}
|
|
||||||
<div class="flex flex-col items-center">
|
|
||||||
{item.name && (
|
|
||||||
<div class="pr-3 font-medium text-gray-900 dark:text-white">
|
|
||||||
{item.name}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{item.role && (
|
|
||||||
<div class="pl-3 text-sm font-light text-gray-500 dark:text-gray-400">
|
|
||||||
{item.role}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TestimonialsBlock = ({ content, design, id, item_images }) => {
|
export const TestimonialsBlock = ({content, _design, _id, item_images}) => {
|
||||||
const title = content?.title;
|
const title = content?.title;
|
||||||
const text = content?.text;
|
const text = content?.text;
|
||||||
const items = Array.isArray(content?.items) ? content.items : [];
|
const items = Array.isArray(content?.items) ? content.items : [];
|
||||||
const imageMap = item_images || {};
|
const imageMap = item_images || {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<div class="max-w-screen-xl px-4 py-8 mx-auto text-center lg:py-16 lg:px-6">
|
<div class="max-w-screen-xl px-4 py-8 mx-auto text-center lg:py-16 lg:px-6">
|
||||||
{/* Section Header */}
|
{/* Section Header */}
|
||||||
{title && (
|
{title && (
|
||||||
<div class="py-8 px-4 mx-auto max-w-screen-xl sm:py-16 lg:px-6">
|
<div class="py-8 px-4 mx-auto max-w-screen-xl sm:py-16 lg:px-6">
|
||||||
<div class="max-w-screen-md mb-8 lg:mb-16 flex flex-col justify-center mx-auto">
|
<div class="max-w-screen-md mb-8 lg:mb-16 flex flex-col justify-center mx-auto">
|
||||||
<h2
|
<h2
|
||||||
class="mb-4 text-4xl tracking-tight font-extrabold text-gray-900 dark:text-white text-center"
|
class="mb-4 text-4xl tracking-tight font-extrabold text-gray-900 dark:text-white text-center"
|
||||||
dangerouslySetInnerHTML={{ __html: renderText(title) }}
|
dangerouslySetInnerHTML={{__html: renderText(title)}}
|
||||||
/>
|
/>
|
||||||
{text && (
|
{text && <p class="text-gray-500 sm:text-xl dark:text-gray-400 text-center" dangerouslySetInnerHTML={{__html: renderText(text)}} />}
|
||||||
<p
|
|
||||||
class="text-gray-500 sm:text-xl dark:text-gray-400 text-center"
|
|
||||||
dangerouslySetInnerHTML={{ __html: renderText(text) }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Testimonial Items */}
|
|
||||||
{items.map((item, idx) => (
|
|
||||||
<TestimonialItem
|
|
||||||
key={idx}
|
|
||||||
item={item}
|
|
||||||
avatarUrl={imageMap[String(idx)]?.src || null}
|
|
||||||
isLast={idx === items.length - 1}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
);
|
)}
|
||||||
|
|
||||||
|
{/* Testimonial Items */}
|
||||||
|
{items.map((item, idx) => (
|
||||||
|
<TestimonialItem key={idx} item={item} avatarUrl={imageMap[String(idx)]?.src || null} isLast={idx === items.length - 1} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
+3
-6
@@ -34,12 +34,9 @@
|
|||||||
"dev": "scripts/view-starter-dev.sh",
|
"dev": "scripts/view-starter-dev.sh",
|
||||||
"dev:stop": "lsof -ti :8081 | xargs kill -9",
|
"dev:stop": "lsof -ti :8081 | xargs kill -9",
|
||||||
"test": "scripts/view-test.sh",
|
"test": "scripts/view-test.sh",
|
||||||
"lint": "biome check --reporter=summary .",
|
"lint": "biome check --reporter=summary . --linter-enabled=true --formatter-enabled=false",
|
||||||
"lint:code": "biome check --reporter=summary . --linter-enabled=true --formatter-enabled=false",
|
"lint:fix": "biome check --write . --linter-enabled=true --formatter-enabled=false",
|
||||||
"lint:code:fix": "biome check --write . --linter-enabled=true --formatter-enabled=false",
|
"format": "biome format --write .",
|
||||||
"lint:style": "biome check --reporter=summary modules/blox/assets/css/ modules/slides/assets/css/ --linter-enabled=true --formatter-enabled=false",
|
|
||||||
"lint:style:fix": "biome check --write modules/blox/assets/css/ modules/slides/assets/css/ --linter-enabled=true --formatter-enabled=false",
|
|
||||||
"format": "biome check --write .",
|
|
||||||
"update:starters": "poetry run python ./scripts/update.py",
|
"update:starters": "poetry run python ./scripts/update.py",
|
||||||
"vendor:libs": "vite build --config vite.config.js",
|
"vendor:libs": "vite build --config vite.config.js",
|
||||||
"vendor:libs:watch": "vite build --config vite.config.js --watch",
|
"vendor:libs:watch": "vite build --config vite.config.js --watch",
|
||||||
|
|||||||
@@ -5,18 +5,12 @@
|
|||||||
"postCreateCommand": "pnpm --version && hugo version",
|
"postCreateCommand": "pnpm --version && hugo version",
|
||||||
"customizations": {
|
"customizations": {
|
||||||
"vscode": {
|
"vscode": {
|
||||||
"extensions": [
|
"extensions": ["ownable.ownable"]
|
||||||
"ownable.ownable"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mounts": [
|
"mounts": ["source=pnpm-store,target=/home/vscode/.local/share/pnpm,type=volume"],
|
||||||
"source=pnpm-store,target=/home/vscode/.local/share/pnpm,type=volume"
|
|
||||||
],
|
|
||||||
"remoteUser": "vscode",
|
"remoteUser": "vscode",
|
||||||
"forwardPorts": [
|
"forwardPorts": [1313],
|
||||||
1313
|
|
||||||
],
|
|
||||||
"portsAttributes": {
|
"portsAttributes": {
|
||||||
"1313": {
|
"1313": {
|
||||||
"label": "Hugo Server"
|
"label": "Hugo Server"
|
||||||
|
|||||||
+1
-3
@@ -1,5 +1,3 @@
|
|||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": ["ownable.ownable"]
|
||||||
"ownable.ownable"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
@@ -5,18 +5,12 @@
|
|||||||
"postCreateCommand": "pnpm --version && hugo version",
|
"postCreateCommand": "pnpm --version && hugo version",
|
||||||
"customizations": {
|
"customizations": {
|
||||||
"vscode": {
|
"vscode": {
|
||||||
"extensions": [
|
"extensions": ["ownable.ownable"]
|
||||||
"ownable.ownable"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mounts": [
|
"mounts": ["source=pnpm-store,target=/home/vscode/.local/share/pnpm,type=volume"],
|
||||||
"source=pnpm-store,target=/home/vscode/.local/share/pnpm,type=volume"
|
|
||||||
],
|
|
||||||
"remoteUser": "vscode",
|
"remoteUser": "vscode",
|
||||||
"forwardPorts": [
|
"forwardPorts": [1313],
|
||||||
1313
|
|
||||||
],
|
|
||||||
"portsAttributes": {
|
"portsAttributes": {
|
||||||
"1313": {
|
"1313": {
|
||||||
"label": "Hugo Server"
|
"label": "Hugo Server"
|
||||||
|
|||||||
+1
-3
@@ -1,5 +1,3 @@
|
|||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": ["ownable.ownable"]
|
||||||
"ownable.ownable"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
@@ -5,18 +5,12 @@
|
|||||||
"postCreateCommand": "pnpm --version && hugo version",
|
"postCreateCommand": "pnpm --version && hugo version",
|
||||||
"customizations": {
|
"customizations": {
|
||||||
"vscode": {
|
"vscode": {
|
||||||
"extensions": [
|
"extensions": ["ownable.ownable"]
|
||||||
"ownable.ownable"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mounts": [
|
"mounts": ["source=pnpm-store,target=/home/vscode/.local/share/pnpm,type=volume"],
|
||||||
"source=pnpm-store,target=/home/vscode/.local/share/pnpm,type=volume"
|
|
||||||
],
|
|
||||||
"remoteUser": "vscode",
|
"remoteUser": "vscode",
|
||||||
"forwardPorts": [
|
"forwardPorts": [1313],
|
||||||
1313
|
|
||||||
],
|
|
||||||
"portsAttributes": {
|
"portsAttributes": {
|
||||||
"1313": {
|
"1313": {
|
||||||
"label": "Hugo Server"
|
"label": "Hugo Server"
|
||||||
|
|||||||
@@ -5,18 +5,12 @@
|
|||||||
"postCreateCommand": "pnpm --version && hugo version",
|
"postCreateCommand": "pnpm --version && hugo version",
|
||||||
"customizations": {
|
"customizations": {
|
||||||
"vscode": {
|
"vscode": {
|
||||||
"extensions": [
|
"extensions": ["ownable.ownable"]
|
||||||
"ownable.ownable"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mounts": [
|
"mounts": ["source=pnpm-store,target=/home/vscode/.local/share/pnpm,type=volume"],
|
||||||
"source=pnpm-store,target=/home/vscode/.local/share/pnpm,type=volume"
|
|
||||||
],
|
|
||||||
"remoteUser": "vscode",
|
"remoteUser": "vscode",
|
||||||
"forwardPorts": [
|
"forwardPorts": [1313],
|
||||||
1313
|
|
||||||
],
|
|
||||||
"portsAttributes": {
|
"portsAttributes": {
|
||||||
"1313": {
|
"1313": {
|
||||||
"label": "Hugo Server"
|
"label": "Hugo Server"
|
||||||
|
|||||||
+1
-3
@@ -1,5 +1,3 @@
|
|||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": ["ownable.ownable"]
|
||||||
"ownable.ownable"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
@@ -5,18 +5,12 @@
|
|||||||
"postCreateCommand": "pnpm --version && hugo version",
|
"postCreateCommand": "pnpm --version && hugo version",
|
||||||
"customizations": {
|
"customizations": {
|
||||||
"vscode": {
|
"vscode": {
|
||||||
"extensions": [
|
"extensions": ["ownable.ownable"]
|
||||||
"ownable.ownable"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mounts": [
|
"mounts": ["source=pnpm-store,target=/home/vscode/.local/share/pnpm,type=volume"],
|
||||||
"source=pnpm-store,target=/home/vscode/.local/share/pnpm,type=volume"
|
|
||||||
],
|
|
||||||
"remoteUser": "vscode",
|
"remoteUser": "vscode",
|
||||||
"forwardPorts": [
|
"forwardPorts": [1313],
|
||||||
1313
|
|
||||||
],
|
|
||||||
"portsAttributes": {
|
"portsAttributes": {
|
||||||
"1313": {
|
"1313": {
|
||||||
"label": "Hugo Server"
|
"label": "Hugo Server"
|
||||||
|
|||||||
+1
-3
@@ -1,5 +1,3 @@
|
|||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": ["ownable.ownable"]
|
||||||
"ownable.ownable"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
@@ -5,18 +5,12 @@
|
|||||||
"postCreateCommand": "pnpm --version && hugo version",
|
"postCreateCommand": "pnpm --version && hugo version",
|
||||||
"customizations": {
|
"customizations": {
|
||||||
"vscode": {
|
"vscode": {
|
||||||
"extensions": [
|
"extensions": ["ownable.ownable"]
|
||||||
"ownable.ownable"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mounts": [
|
"mounts": ["source=pnpm-store,target=/home/vscode/.local/share/pnpm,type=volume"],
|
||||||
"source=pnpm-store,target=/home/vscode/.local/share/pnpm,type=volume"
|
|
||||||
],
|
|
||||||
"remoteUser": "vscode",
|
"remoteUser": "vscode",
|
||||||
"forwardPorts": [
|
"forwardPorts": [1313],
|
||||||
1313
|
|
||||||
],
|
|
||||||
"portsAttributes": {
|
"portsAttributes": {
|
||||||
"1313": {
|
"1313": {
|
||||||
"label": "Hugo Server"
|
"label": "Hugo Server"
|
||||||
|
|||||||
@@ -5,18 +5,12 @@
|
|||||||
"postCreateCommand": "pnpm --version && hugo version",
|
"postCreateCommand": "pnpm --version && hugo version",
|
||||||
"customizations": {
|
"customizations": {
|
||||||
"vscode": {
|
"vscode": {
|
||||||
"extensions": [
|
"extensions": ["ownable.ownable"]
|
||||||
"ownable.ownable"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mounts": [
|
"mounts": ["source=pnpm-store,target=/home/vscode/.local/share/pnpm,type=volume"],
|
||||||
"source=pnpm-store,target=/home/vscode/.local/share/pnpm,type=volume"
|
|
||||||
],
|
|
||||||
"remoteUser": "vscode",
|
"remoteUser": "vscode",
|
||||||
"forwardPorts": [
|
"forwardPorts": [1313],
|
||||||
1313
|
|
||||||
],
|
|
||||||
"portsAttributes": {
|
"portsAttributes": {
|
||||||
"1313": {
|
"1313": {
|
||||||
"label": "Hugo Server"
|
"label": "Hugo Server"
|
||||||
|
|||||||
+1
-3
@@ -1,5 +1,3 @@
|
|||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": ["ownable.ownable"]
|
||||||
"ownable.ownable"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
@@ -5,18 +5,12 @@
|
|||||||
"postCreateCommand": "pnpm --version && hugo version",
|
"postCreateCommand": "pnpm --version && hugo version",
|
||||||
"customizations": {
|
"customizations": {
|
||||||
"vscode": {
|
"vscode": {
|
||||||
"extensions": [
|
"extensions": ["ownable.ownable"]
|
||||||
"ownable.ownable"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mounts": [
|
"mounts": ["source=pnpm-store,target=/home/vscode/.local/share/pnpm,type=volume"],
|
||||||
"source=pnpm-store,target=/home/vscode/.local/share/pnpm,type=volume"
|
|
||||||
],
|
|
||||||
"remoteUser": "vscode",
|
"remoteUser": "vscode",
|
||||||
"forwardPorts": [
|
"forwardPorts": [1313],
|
||||||
1313
|
|
||||||
],
|
|
||||||
"portsAttributes": {
|
"portsAttributes": {
|
||||||
"1313": {
|
"1313": {
|
||||||
"label": "Hugo Server"
|
"label": "Hugo Server"
|
||||||
|
|||||||
+1
-3
@@ -1,5 +1,3 @@
|
|||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": ["ownable.ownable"]
|
||||||
"ownable.ownable"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user