From bf8b2aa631a7efa302b097ba727f1ff37c8ce73c Mon Sep 17 00:00:00 2001 From: DavidHerran Date: Fri, 5 Jun 2026 12:17:08 -0500 Subject: [PATCH] feat(map): connect Global Map cases to their full application case pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the disconnect where a case study opened from the 3D globe had no path to its full write-up — you had to leave the globe, open the right application, and hunt for it. - CaseStudyModal: new "View full case study" CTA for real installations (not events / HQ). It deep-links via the locale-aware next-intl Link to /applications/{application}#case-{nodeId}, closes the modal, and fires a case_study_viewed GA event. - ApplicationClient: on mount it reads a "#case-" hash, auto-expands the matching case in the "Proven Solutions" wall, and smooth-scrolls to it. Each case row now carries id="case-" + scroll-mt for correct offset. - viewFullCase string added to the CaseStudyModal namespace in all 5 locales. The GlobalNode.application field (already equal to the Application slug) is the join key — no schema change needed. Verified: production build compiles, TypeScript clean, 5 message files valid. Co-Authored-By: Claude Opus 4.8 (1M context) --- messages/de.json | 3 +- messages/en.json | 3 +- messages/es.json | 3 +- messages/it.json | 3 +- messages/vec.json | 3 +- .../applications/[slug]/ApplicationClient.tsx | 17 ++++++++++- src/components/ui/CaseStudyModal.tsx | 30 +++++++++++++++++-- 7 files changed, 53 insertions(+), 9 deletions(-) diff --git a/messages/de.json b/messages/de.json index 153d106..8856bc0 100644 --- a/messages/de.json +++ b/messages/de.json @@ -159,7 +159,8 @@ "eventOverview": "Veranstaltungsübersicht", "projectChronicle": "Projektchronik", "pendingData": "[ Chronikdaten für diesen Knoten ausstehend ]", - "mediaGallery": "Mediengalerie" + "mediaGallery": "Mediengalerie", + "viewFullCase": "Vollständige Fallstudie ansehen" }, "Footer": { "madeInItaly": "Hergestellt in Italien", diff --git a/messages/en.json b/messages/en.json index 51aa833..37bd151 100644 --- a/messages/en.json +++ b/messages/en.json @@ -159,7 +159,8 @@ "eventOverview": "Event Overview", "projectChronicle": "Project Chronicle", "pendingData": "[ Chronicle data pending for this node ]", - "mediaGallery": "Media Gallery" + "mediaGallery": "Media Gallery", + "viewFullCase": "View full case study" }, "Footer": { "madeInItaly": "Made in Italy", diff --git a/messages/es.json b/messages/es.json index dcea1c4..cd9ec88 100644 --- a/messages/es.json +++ b/messages/es.json @@ -159,7 +159,8 @@ "eventOverview": "Resumen del Evento", "projectChronicle": "Crónica del Proyecto", "pendingData": "[ Datos de crónica pendientes para este nodo ]", - "mediaGallery": "Galería de Medios" + "mediaGallery": "Galería de Medios", + "viewFullCase": "Ver el caso completo" }, "Footer": { "madeInItaly": "Hecho en Italia", diff --git a/messages/it.json b/messages/it.json index 223a2a2..e86117e 100644 --- a/messages/it.json +++ b/messages/it.json @@ -159,7 +159,8 @@ "eventOverview": "Panoramica Evento", "projectChronicle": "Cronaca del Progetto", "pendingData": "[ Dati cronaca in attesa per questo nodo ]", - "mediaGallery": "Galleria Media" + "mediaGallery": "Galleria Media", + "viewFullCase": "Vedi il caso completo" }, "Footer": { "madeInItaly": "Made in Italy", diff --git a/messages/vec.json b/messages/vec.json index 4577ba2..8397883 100644 --- a/messages/vec.json +++ b/messages/vec.json @@ -159,7 +159,8 @@ "eventOverview": "Detaji de l'evento", "projectChronicle": "Storia del projeto", "pendingData": "[ Dati de la storia drio rivar par sto nodo ]", - "mediaGallery": "Gałeria de foto" + "mediaGallery": "Gałeria de foto", + "viewFullCase": "Varda el caso completo" }, "Footer": { "madeInItaly": "Fato in Itaia", diff --git a/src/app/[locale]/applications/[slug]/ApplicationClient.tsx b/src/app/[locale]/applications/[slug]/ApplicationClient.tsx index 9a8d195..d70e997 100644 --- a/src/app/[locale]/applications/[slug]/ApplicationClient.tsx +++ b/src/app/[locale]/applications/[slug]/ApplicationClient.tsx @@ -1004,6 +1004,21 @@ function ExpandedCaseStudy({ node }: { node: any }) { export default function ApplicationClient({ data, realCases, images, breadcrumbs }: { data: any, realCases: any[], images: any, breadcrumbs?: BreadcrumbItem[] }) { const [expandedCase, setExpandedCase] = useState(null); + // Deep-link from the Global Map: a "#case-" hash opens the matching + // case study, expands it, and scrolls to it. This is the bridge that + // connects a node's modal on the 3D globe to its full write-up here. + useEffect(() => { + if (typeof window === "undefined") return; + const hash = window.location.hash; + if (!hash.startsWith("#case-")) return; + const id = decodeURIComponent(hash.slice("#case-".length)); + setExpandedCase(id); + const timer = setTimeout(() => { + document.getElementById(`case-${id}`)?.scrollIntoView({ behavior: "smooth", block: "start" }); + }, 350); + return () => clearTimeout(timer); + }, []); + const [mainLightboxOpen, setMainLightboxOpen] = useState(false); const [mainLightboxImages, setMainLightboxImages] = useState([]); const [mainLightboxInitialIndex, setMainLightboxInitialIndex] = useState(0); @@ -1147,7 +1162,7 @@ export default function ApplicationClient({ data, realCases, images, breadcrumbs {realCases.map((node) => { const isExpanded = expandedCase === node.id; return ( -
+
setExpandedCase(isExpanded ? null : node.id)} className="p-5 md:p-8 cursor-pointer flex flex-col md:flex-row items-start md:items-center justify-between gap-5 group hover:bg-black/5 dark:hover:bg-white/[0.02] transition-colors">
diff --git a/src/components/ui/CaseStudyModal.tsx b/src/components/ui/CaseStudyModal.tsx index edae5f3..d05e5df 100644 --- a/src/components/ui/CaseStudyModal.tsx +++ b/src/components/ui/CaseStudyModal.tsx @@ -1,10 +1,12 @@ "use client"; import { motion, AnimatePresence } from "framer-motion"; -import { X, MapPin, Calendar, Leaf, CheckCircle2, Factory, Presentation, Image as ImageIcon } from "lucide-react"; +import { X, MapPin, Calendar, Leaf, CheckCircle2, Factory, Presentation, Image as ImageIcon, ArrowRight } from "lucide-react"; import Image from "next/image"; import { useEffect, useState } from "react"; -import { useTranslations } from "next-intl"; +import { useTranslations } from "next-intl"; +import { Link } from "@/i18n/routing"; +import { trackEvent } from "@/lib/analytics/gtag"; export interface CaseStudyData { id: string; @@ -298,13 +300,35 @@ export default function CaseStudyModal({ isOpen, onClose, data }: ModalProps) {
{t("systemStatus")} - + {/* 🔥 AQUÍ ESTABA EL ERROR: Simplificamos la lógica 🔥 */} {isEvent ? (isUpcoming ? t("scheduled") : t("concluded")) : t("operational")}
+ {/* Bridge to the full case study inside its application page. + Only for real installations whose `application` maps to an + Application slug (not events or the HQ node). */} + {!isEvent && !isHQ && data.application && data.application !== "hq" && data.application !== "event" && ( + { + trackEvent({ name: "case_study_viewed", params: { nodeId: data.id, application: data.application } }); + onClose(); + }} + className="group mb-10 flex items-center justify-between gap-4 w-full bg-[#0066CC] dark:bg-[#00F0FF] text-white dark:text-black px-6 py-4 rounded-2xl font-medium hover:bg-[#0052a3] dark:hover:bg-[#00F0FF]/80 transition-colors shadow-lg" + > + + + {data.application.replace(/-/g, " ")} + + {t("viewFullCase")} + + + + )} + {data.projectOverview ? (