feat(map): connect Global Map cases to their full application case pages
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-<id>" hash, auto-expands the
matching case in the "Proven Solutions" wall, and smooth-scrolls to it.
Each case row now carries id="case-<id>" + 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) <noreply@anthropic.com>
This commit is contained in:
@@ -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<string | null>(null);
|
||||
|
||||
// Deep-link from the Global Map: a "#case-<id>" 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<string[]>([]);
|
||||
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 (
|
||||
<div key={node.id} className="bg-white/80 dark:bg-[#111]/90 backdrop-blur-2xl border border-black/10 dark:border-white/10 rounded-2xl md:rounded-[2.5rem] overflow-hidden transition-all duration-500 shadow-xl">
|
||||
<div key={node.id} id={`case-${node.id}`} className="scroll-mt-28 bg-white/80 dark:bg-[#111]/90 backdrop-blur-2xl border border-black/10 dark:border-white/10 rounded-2xl md:rounded-[2.5rem] overflow-hidden transition-all duration-500 shadow-xl target:ring-2 target:ring-[#0066CC] dark:target:ring-[#00F0FF]">
|
||||
<div onClick={() => 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">
|
||||
<div className="flex items-center gap-5 flex-1">
|
||||
<div className="w-16 h-16 md:w-20 md:h-20 rounded-2xl md:rounded-3xl bg-black/5 dark:bg-black/50 border border-black/10 dark:border-white/5 flex items-center justify-center shrink-0 overflow-hidden relative shadow-inner">
|
||||
|
||||
@@ -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) {
|
||||
<div className="col-span-2 md:col-span-1 bg-[#F5F5F7] dark:bg-[#1D1D1F] p-5 rounded-2xl border border-transparent dark:border-white/5 flex flex-col justify-center">
|
||||
<span className="text-[10px] uppercase tracking-widest text-[#86868B] block mb-1">{t("systemStatus")}</span>
|
||||
<span className="flex items-center gap-2 text-sm font-medium text-emerald-600 dark:text-emerald-400">
|
||||
<CheckCircle2 size={16} />
|
||||
<CheckCircle2 size={16} />
|
||||
{/* 🔥 AQUÍ ESTABA EL ERROR: Simplificamos la lógica 🔥 */}
|
||||
{isEvent ? (isUpcoming ? t("scheduled") : t("concluded")) : t("operational")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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" && (
|
||||
<Link
|
||||
href={`/applications/${data.application}#case-${data.id}`}
|
||||
onClick={() => {
|
||||
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"
|
||||
>
|
||||
<span className="flex flex-col text-left">
|
||||
<span className="text-[10px] uppercase tracking-widest opacity-70">
|
||||
{data.application.replace(/-/g, " ")}
|
||||
</span>
|
||||
<span className="text-base">{t("viewFullCase")}</span>
|
||||
</span>
|
||||
<ArrowRight size={20} className="group-hover:translate-x-1 transition-transform shrink-0" />
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{data.projectOverview ? (
|
||||
<div className="max-w-none mb-12">
|
||||
<h3 className="text-2xl font-light mb-6 text-[#1D1D1F] dark:text-white">
|
||||
|
||||
Reference in New Issue
Block a user