diff --git a/modules/blox/blox/cta-card/block.html b/modules/blox/blox/cta-card/block.html deleted file mode 100644 index a0ce680c..00000000 --- a/modules/blox/blox/cta-card/block.html +++ /dev/null @@ -1,178 +0,0 @@ -{{/* Hugo Blox: CTA Card */}} -{{/* Documentation: https://hugoblox.com/blocks/ */}} -{{/* License: https://github.com/HugoBlox/kit/blob/main/LICENSE.md */}} - -{{/* Initialise */}} -{{ $page := .wcPage }} -{{ $block := .wcBlock }} - -{{ $content := $block.content }} -{{ if not (reflect.IsMap $content) }}{{ $content = dict }}{{ end }} -{{ $design := $block.design }} -{{ if not (reflect.IsMap $design) }}{{ $design = dict }}{{ end }} -{{ $card := index $design "card" }} -{{ if not (reflect.IsMap $card) }}{{ $card = dict }}{{ end }} - -{{ $title := "" }} -{{ with index $content "title" }}{{ $title = printf "%v" . | emojify | $page.RenderString }}{{ end }} -{{ $text := "" }} -{{ with index $content "text" }}{{ $text = printf "%v" . | emojify | $page.RenderString | safeHTML }}{{ end }} -{{ $card_class := "" }} -{{ $card_class_raw := index $card "css_class" }} -{{ if eq (printf "%T" $card_class_raw) "string" }} - {{ $card_class = $card_class_raw }} -{{ end }} -{{ $card_style := "" }} -{{ $card_style_raw := index $card "css_style" }} -{{ if eq (printf "%T" $card_style_raw) "string" }} - {{ $card_style = $card_style_raw }} -{{ end }} - -{{/* Contrast and accessibility controls */}} -{{ $text_color_mode := "auto" }} -{{ $text_color_raw := index $card "text_color" }} -{{ if eq (printf "%T" $text_color_raw) "string" }} - {{ $text_color_candidate := lower (strings.TrimSpace $text_color_raw) }} - {{ if in (slice "auto" "light" "dark") $text_color_candidate }} - {{ $text_color_mode = $text_color_candidate }} - {{ end }} -{{ end }} -{{ $overlay_opacity := 0.15 }} -{{ $overlay_raw := index $card "overlay_opacity" }} -{{ if ne $overlay_raw nil }} - {{ $overlay_type := printf "%T" $overlay_raw }} - {{ if or (eq $overlay_type "int") (eq $overlay_type "int8") (eq $overlay_type "int16") (eq $overlay_type "int32") (eq $overlay_type "int64") (eq $overlay_type "uint") (eq $overlay_type "uint8") (eq $overlay_type "uint16") (eq $overlay_type "uint32") (eq $overlay_type "uint64") (eq $overlay_type "float32") (eq $overlay_type "float64") }} - {{ $overlay_opacity = float $overlay_raw }} - {{ else if eq $overlay_type "string" }} - {{ $overlay_trimmed := strings.TrimSpace $overlay_raw }} - {{ if gt (len (findRE "^-?[0-9]*\\.?[0-9]+$" $overlay_trimmed)) 0 }} - {{ $overlay_opacity = float $overlay_trimmed }} - {{ end }} - {{ end }} -{{ end }} -{{ if lt $overlay_opacity 0 }}{{ $overlay_opacity = 0 }}{{ end }} -{{ if gt $overlay_opacity 1 }}{{ $overlay_opacity = 1 }}{{ end }} - -{{/* Auto-detect text color based on background */}} -{{ $text_classes := "" }} -{{ $button_classes := "" }} -{{ if eq $text_color_mode "light" }} - {{ $text_classes = "text-white" }} - {{ $button_classes = "text-gray-900 dark:text-white" }} -{{ else if eq $text_color_mode "dark" }} - {{ $text_classes = "text-gray-900 dark:text-white" }} - {{ $button_classes = "text-gray-900 dark:text-white" }} -{{ else }} - {{/* Auto mode - detect based on gradient colors */}} - {{ $is_dark_bg := false }} - {{ $gradient_start := "" }} - {{ $bg := index $design "background" }} - {{ if and $bg (reflect.IsMap $bg) }} - {{ $gradient := index $bg "gradient" }} - {{ if and $gradient (reflect.IsMap $gradient) }} - {{ $start_raw := index $gradient "start" }} - {{ if eq (printf "%T" $start_raw) "string" }} - {{ $gradient_start = $start_raw }} - {{ end }} - {{ end }} - {{ end }} - {{ if $gradient_start }} - {{ if or (hasPrefix $gradient_start "primary-7") (hasPrefix $gradient_start "primary-8") (hasPrefix $gradient_start "primary-9") (hasPrefix $gradient_start "secondary-7") (hasPrefix $gradient_start "secondary-8") (hasPrefix $gradient_start "secondary-9") }} - {{ $is_dark_bg = true }} - {{ end }} - {{ end }} - {{ if $is_dark_bg }} - {{ $text_classes = "text-white" }} - {{ $button_classes = "text-gray-900" }} - {{ else }} - {{ $text_classes = "text-gray-900 dark:text-white" }} - {{ $button_classes = "text-gray-900 dark:text-gray-100" }} - {{ end }} -{{ end }} - - -{{/* Modern 2025 CTA Card - Background controlled via design.background in front matter */}} -{{ $custom_style := printf "--glassmorphism-opacity: %s;" (string $overlay_opacity) }} -{{ if $card_style }} - {{ $custom_style = printf "%s%s" $custom_style $card_style }} -{{ end }} - -
$1")
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1');
+}
+
+export const CtaCardBlock = ({ content, design, id, button_icon_svg }) => {
+ const card = design?.card || {};
+ const cardClass = card.css_class || "";
+ const cardStyle = card.css_style || "";
+
+ // Overlay Opacity Logic
+ let overlayOpacity = 0.15;
+ if (card.overlay_opacity !== undefined) {
+ const val = parseFloat(card.overlay_opacity);
+ if (!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;
+ }
+
+ 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 (
+ {{$text}}
- {{ $icon := "check" }} - {{ $icon_raw := index $item "feature_icon" }} - {{ if eq (printf "%T" $icon_raw) "string" }}{{ $icon = $icon_raw }}{{ end }} - {{ $features_raw := index $item "features" }} - {{ $features := slice }} - {{ if reflect.IsSlice $features_raw }} - {{ $features = $features_raw }} - {{ else if eq (printf "%T" $features_raw) "string" }} - {{ $features = slice $features_raw }} - {{ end }} - {{ if gt (len $features) 0 }} -$1")
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1');
+}
+
+// Arrow icon for CTA buttons
+const ArrowIcon = () => (
+
+);
+
+function CtaItem({ item, idx, imageData, featureIconSvg }) {
+ const isReversed = idx % 2 === 1;
+ const hasImage = !!imageData;
+ const featureIcon = item.feature_icon || "check";
+
+ // Resolve button link
+ let buttonUrl = item.button?.url || "";
+ let isExternal = false;
+ if (buttonUrl) {
+ try {
+ const url = new URL(buttonUrl, window.location.origin);
+ isExternal = url.origin !== window.location.origin;
+ } catch {
+ isExternal = /^https?:\/\//.test(buttonUrl);
+ }
+ }
+
+ const features = Array.isArray(item.features)
+ ? item.features
+ : typeof item.features === "string"
+ ? [item.features]
+ : [];
+
+ return (
+ {{.}}
- {{ end }} -{{ . | $page.RenderString }}
{{ end }} -$1");
+}
+
+function FeatureItem({ item, iconSvg }) {
+ return (
+ {{ printf "%v" . | $page.RenderString | emojify }}
{{end}} - - {{ if and $primary_url $primary_text }} - {{ $link := $primary_url }} - {{ $scheme := (urls.Parse $link).Scheme }} - {{ $target := "" }} - {{ if not $scheme }} - {{/* Check if it's a hash fragment (page anchor) */}} - {{ if hasPrefix $link "#" }} - {{/* Keep hash fragments as-is */}} - {{ $link = $link }} - {{ else }} - {{/* Apply relLangURL for relative paths */}} - {{ $link = $link | relLangURL }} - {{ end }} - {{ else if in (slice "http" "https") $scheme }} - {{ $target = "target=\"_blank\" rel=\"noopener\"" }} - {{ end }} -- {{ . }} -
- {{ end }} -- {{ (index $item "description") | $page.RenderString }} -
- - {{/* Optional Sub-metric */}} - {{ with index $item "sub_metric" }} -- {{ . }} -
- {{ end }} -- {{ (index $item "description") | $page.RenderString }} -
-- {{ (index $item "description") | $page.RenderString }} -
-$1");
+}
+
+// Extract numeric target from a statistic string like "10,000+" → 10000
+function parseTarget(statistic) {
+ if (!statistic) return 0;
+ const raw = String(statistic).replace(/[^0-9.]/g, "");
+ const num = Number(raw);
+ return isNaN(num) ? 0 : num;
+}
+
+// Extract prefix, number, suffix from "~10,000+" → { prefix: "~", number: "10,000", suffix: "+" }
+function extractParts(text) {
+ if (!text) return { prefix: "", numberPart: "", suffix: "" };
+ const parts = String(text).match(/([^0-9]*)([0-9][0-9,.]*)(.*)/);
+ return {
+ prefix: parts ? parts[1] : "",
+ numberPart: parts ? parts[2] : "",
+ suffix: parts ? parts[3] : "",
+ };
+}
+
+// Counter animation hook
+function useCounterAnimation(containerRef) {
+ useEffect(() => {
+ const container = containerRef.current;
+ if (!container) return;
+
+ const prefersReducedMotion = window.matchMedia(
+ "(prefers-reduced-motion: reduce)"
+ ).matches;
+
+ function animateCounters(el) {
+ el.classList.add("animate");
+ const counters = el.querySelectorAll(".counter");
+
+ counters.forEach((counter) => {
+ const target = parseTarget(counter.getAttribute("data-target"));
+ if (target <= 0) return;
+
+ const { prefix, numberPart, suffix } = extractParts(
+ counter.textContent
+ );
+ const decimals = (numberPart.split(".")[1] || "").length;
+ const formatter = new Intl.NumberFormat(undefined, {
+ 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();
+ }, []);
+}
+
+// Grid columns class based on item count
+function gridColsClass(count) {
+ if (count <= 1) return "lg:grid-cols-1";
+ if (count === 2) return "lg:grid-cols-2";
+ if (count >= 4) return "lg:grid-cols-4";
+ return "lg:grid-cols-3";
+}
+
+// ─── Stat Item (Cards) ───────────────────────────────────────────────
+function StatCard({ item, iconSvg }) {
+ return (
+ + {item.sub_metric} +
+ )} +{{.}}
- {{ end }} ---"{{ (index $item "text") | $page.RenderString }}"
-
$1");
+}
+
+function TestimonialItem({ item, avatarUrl, isLast }) {
+ return (
+ + ++