Files
flux-srl/src/components/ui/CaseStudyModal.tsx
T
davidherran 7ae5685ca9
Deploy to VPS / deploy (push) Has been cancelled
fixes News
2026-04-21 09:44:26 -05:00

343 lines
16 KiB
TypeScript

"use client";
import { motion, AnimatePresence } from "framer-motion";
import { X, MapPin, Calendar, Leaf, CheckCircle2, Factory, Presentation, Image as ImageIcon } from "lucide-react";
import Image from "next/image";
import { useEffect, useState } from "react";
import { useTranslations } from "next-intl";
export interface CaseStudyData {
id: string;
title: string;
location: string;
nodeType: string;
application: string;
stats: string;
mediaFileName?: string | null;
projectOverview?: string | null;
energySavings?: string | null;
galleryJson?: string | null;
eventDate?: string | null;
}
interface ModalProps {
isOpen: boolean;
onClose: () => void;
data: CaseStudyData | null;
}
function nodeToSlug(title: string): string {
return title.toLowerCase().trim().replace(/[^\w\s-]/g, '').replace(/[\s_-]+/g, '-').replace(/^-+|-+$/g, '');
}
const renderMarkdown = (text: string) => {
if (!text) return null;
const lines = text.split('\n');
const elements: React.ReactNode[] = [];
let listItems: React.ReactNode[] = [];
let isOrderedList = false;
let inTable = false;
let tableHeaders: string[] = [];
let tableRows: string[][] = [];
const pushTable = () => {
if (inTable) {
elements.push(
<div key={`table-${elements.length}`} className="my-8 w-full overflow-x-auto pb-4 [scrollbar-width:none]">
<table className="w-full text-left border-collapse min-w-[500px] shadow-lg rounded-2xl overflow-hidden border border-black/5 dark:border-white/5">
<thead>
<tr className="bg-[#F5F5F7] dark:bg-[#1D1D1F]">
{tableHeaders.map((th, i) => (
<th key={i} className={`p-4 border-b border-black/5 dark:border-white/10 text-xs uppercase tracking-widest font-semibold ${i === tableHeaders.length - 1 ? 'text-[#0066CC] dark:text-[#4DA6FF] bg-[#0066CC]/5 dark:bg-[#4DA6FF]/5' : 'text-[#1D1D1F] dark:text-white'}`}>
{parseInline(th)}
</th>
))}
</tr>
</thead>
<tbody className="bg-white dark:bg-[#0A0A0C]">
{tableRows.map((row, rIdx) => (
<tr key={rIdx} className="hover:bg-black/5 dark:hover:bg-white/[0.02] transition-colors group">
{row.map((cell, cIdx) => (
<td key={cIdx} className={`p-4 border-b border-black/5 dark:border-white/5 text-sm ${cIdx === 0 ? 'text-[#86868B] dark:text-[#A1A1A6] font-medium' : cIdx === row.length - 1 ? 'text-[#1D1D1F] dark:text-white font-semibold bg-[#0066CC]/5 dark:bg-[#4DA6FF]/5 group-hover:bg-[#0066CC]/10 dark:group-hover:bg-[#4DA6FF]/10 transition-colors' : 'text-[#1D1D1F]/80 dark:text-white/80'}`}>
{parseInline(cell)}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
inTable = false;
tableHeaders = [];
tableRows = [];
}
};
const pushList = () => {
if (listItems.length > 0) {
elements.push(
isOrderedList ? (
<ol key={`ol-${elements.length}`} className="list-decimal ml-6 mb-6 text-[#86868B] dark:text-[#A1A1A6] space-y-2 text-base font-light marker:text-[#0066CC] dark:marker:text-[#4DA6FF]">
{listItems}
</ol>
) : (
<ul key={`ul-${elements.length}`} className="list-disc ml-6 mb-6 text-[#86868B] dark:text-[#A1A1A6] space-y-2 text-base font-light marker:text-[#0066CC] dark:marker:text-[#4DA6FF]">
{listItems}
</ul>
)
);
listItems = [];
}
};
const parseInline = (str: string) => {
const boldRegex = /\*\*(.*?)\*\*/g;
const italicRegex = /\*(.*?)\*/g;
let parts = str.split(boldRegex);
return parts.map((part, i) => {
if (i % 2 === 1) return <strong key={i} className="font-medium text-[#1D1D1F] dark:text-white">{part}</strong>;
let subParts = part.split(italicRegex);
return subParts.map((subPart, j) => {
if (j % 2 === 1) return <em key={`${i}-${j}`} className="italic text-[#1D1D1F]/90 dark:text-white/90">{subPart}</em>;
return subPart;
});
});
};
lines.forEach((line, idx) => {
const trimmed = line.trim();
if (!trimmed) {
pushList(); pushTable();
return;
}
if (trimmed.startsWith('|') && trimmed.endsWith('|')) {
pushList();
const cells = trimmed.split('|').filter((_, i, arr) => i !== 0 && i !== arr.length - 1).map(c => c.trim());
if (!inTable) { inTable = true; tableHeaders = cells; }
else if (cells.every(c => c.match(/^[-:]+$/))) { }
else { tableRows.push(cells); }
return;
} else {
pushTable();
}
const imgMatch = trimmed.match(/^!\[(.*?)\]\((.*?)\)$/);
if (imgMatch) {
pushList(); pushTable();
elements.push(
<div key={`img-${idx}`} className="relative w-full my-8 rounded-2xl overflow-hidden border border-black/10 dark:border-white/10 shadow-lg bg-[#F5F5F7] dark:bg-[#1D1D1F]">
<img src={imgMatch[2]} alt={imgMatch[1]} className="w-full h-auto object-cover hover:scale-105 transition-transform duration-700" loading="lazy" />
</div>
);
return;
}
const h3Match = trimmed.match(/^###\s*(.*)/);
if (h3Match) { pushList(); pushTable(); elements.push(<h3 key={idx} className="text-xl mt-8 mb-3 font-medium text-[#0066CC] dark:text-[#4DA6FF]">{parseInline(h3Match[1])}</h3>); return; }
const h2Match = trimmed.match(/^##\s*(.*)/);
if (h2Match) { pushList(); pushTable(); elements.push(<h2 key={idx} className="text-2xl mt-10 mb-4 font-light text-[#1D1D1F] dark:text-white tracking-tight">{parseInline(h2Match[1])}</h2>); return; }
const h1Match = trimmed.match(/^#\s*(.*)/);
if (h1Match) { pushList(); pushTable(); elements.push(<h1 key={idx} className="text-3xl mt-10 mb-5 font-light text-[#1D1D1F] dark:text-white tracking-tight">{parseInline(h1Match[1])}</h1>); return; }
const quoteMatch = trimmed.match(/^>\s*(.*)/);
if (quoteMatch) {
pushList(); pushTable();
elements.push(
<blockquote key={idx} className="border-l-4 border-[#0066CC] dark:border-[#4DA6FF] pl-5 py-2 my-6 text-lg font-light italic text-[#1D1D1F]/70 dark:text-[#A1A1A6] bg-[#0066CC]/5 dark:bg-[#4DA6FF]/5 rounded-r-xl">
{parseInline(quoteMatch[1])}
</blockquote>
);
return;
}
const ulMatch = trimmed.match(/^[-*]\s+(.*)/);
if (ulMatch) { isOrderedList = false; listItems.push(<li key={idx} className="leading-relaxed pl-2">{parseInline(ulMatch[1])}</li>); return; }
const olMatch = trimmed.match(/^\d+\.\s*(.*)/);
if (olMatch) { isOrderedList = true; listItems.push(<li key={idx} className="leading-relaxed pl-2">{parseInline(olMatch[1])}</li>); return; }
pushList();
elements.push(<p key={idx} className="text-[#86868B] dark:text-[#A1A1A6] font-light leading-relaxed mb-4 text-base">{parseInline(trimmed)}</p>);
});
pushList();
pushTable();
return <>{elements}</>;
};
export default function CaseStudyModal({ isOpen, onClose, data }: ModalProps) {
const [gallery, setGallery] = useState<string[]>([]);
const t = useTranslations("CaseStudyModal");
useEffect(() => {
if (isOpen) document.body.style.overflow = "hidden";
else document.body.style.overflow = "unset";
return () => { document.body.style.overflow = "unset"; };
}, [isOpen]);
useEffect(() => {
if (data?.galleryJson) {
try {
setGallery(JSON.parse(data.galleryJson));
} catch (e) {
setGallery([]);
}
} else {
setGallery([]);
}
}, [data]);
if (!data) return null;
const isEvent = data.nodeType === "event";
const isHQ = data.nodeType === "hq";
const nodeSlug = nodeToSlug(data.title);
const coverImage = data.mediaFileName ? `/cases/${nodeSlug}/${data.mediaFileName}` : null;
let formattedDate = null;
let isUpcoming = false;
if (data.eventDate) {
const eventD = new Date(data.eventDate);
formattedDate = eventD.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
isUpcoming = eventD > new Date();
}
return (
<AnimatePresence>
{isOpen && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 sm:p-6 md:p-12">
<motion.div
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
onClick={onClose}
className="absolute inset-0 bg-black/60 backdrop-blur-md"
/>
<motion.div
initial={{ opacity: 0, y: 40, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 20, scale: 0.95 }}
transition={{ type: "spring", damping: 25, stiffness: 300 }}
className="relative w-full max-w-4xl bg-white dark:bg-[#0A0A0C] border border-black/10 dark:border-white/10 rounded-[2rem] md:rounded-[3rem] shadow-2xl overflow-hidden flex flex-col max-h-[90vh]"
>
<button
onClick={onClose}
className="absolute top-6 right-6 z-50 w-10 h-10 bg-black/50 hover:bg-black/80 backdrop-blur-md text-white rounded-full flex items-center justify-center transition-colors border border-white/20"
>
<X size={20} />
</button>
<div className="flex-1 overflow-y-auto [scrollbar-width:none]">
<div className="relative w-full h-64 md:h-96 bg-[#1D1D1F] overflow-hidden">
{coverImage ? (
<Image src={coverImage} alt={data.title} fill className="object-cover" />
) : (
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(0,102,204,0.4)_0%,transparent_100%)] flex items-center justify-center">
<Factory size={64} className="text-white/10" />
</div>
)}
<div className="absolute inset-0 bg-gradient-to-t from-white via-white/20 dark:from-[#0A0A0C] dark:via-[#0A0A0C]/20 to-transparent" />
<div className="absolute bottom-6 left-6 md:left-10 flex items-center gap-2">
<div className="bg-[#0066CC] text-white px-3 py-1.5 rounded-full text-[10px] font-bold uppercase tracking-widest flex items-center gap-1.5 shadow-lg">
{isEvent ? <Presentation size={12} /> : isHQ ? <MapPin size={12} /> : <Factory size={12} />}
{isEvent ? t("typeEvent") : isHQ ? t("typeHQ") : t("typeInstall")}
</div>
<div className="bg-white/90 dark:bg-black/80 backdrop-blur-md text-[#1D1D1F] dark:text-[#F5F5F7] border border-black/5 dark:border-white/10 px-3 py-1.5 rounded-full text-[10px] font-bold uppercase tracking-widest shadow-lg">
{data.application.replace("-", " ")}
</div>
</div>
</div>
<div className="p-6 md:p-10 lg:p-12 relative -mt-4 bg-white dark:bg-[#0A0A0C] rounded-t-[2rem] md:rounded-t-[3rem] z-10">
<div className="mb-10">
<h2 className="text-4xl md:text-5xl font-light text-[#1D1D1F] dark:text-[#F5F5F7] tracking-tight mb-4">
{data.title}
</h2>
<div className="flex flex-wrap items-center gap-4 md:gap-8 text-sm text-[#86868B]">
<span className="flex items-center gap-1.5"><MapPin size={16} /> {data.location}</span>
{formattedDate && (
<span className={`flex items-center gap-1.5 ${isUpcoming ? 'text-[#0066CC] dark:text-[#4DA6FF] font-medium' : ''}`}>
<Calendar size={16} /> {formattedDate} {isUpcoming && "(Upcoming)"}
</span>
)}
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 mb-10">
<div className="bg-[#F5F5F7] dark:bg-[#1D1D1F] p-5 rounded-2xl border border-transparent dark:border-white/5">
<span className="text-[10px] uppercase tracking-widest text-[#86868B] block mb-1">
{isEvent ? t("keyHighlight") : t("keyMetric")}
</span>
<span className="text-lg md:text-xl font-medium text-[#1D1D1F] dark:text-white leading-tight">
{data.stats}
</span>
</div>
{data.energySavings && (
<div className="bg-[#0066CC]/5 dark:bg-[#4DA6FF]/10 p-5 rounded-2xl border border-[#0066CC]/10 dark:border-[#4DA6FF]/20">
<span className="text-[10px] uppercase tracking-widest text-[#0066CC] dark:text-[#4DA6FF] block mb-1 flex items-center gap-1">
{isEvent ? <MapPin size={10} /> : <Leaf size={10} />}
{isEvent ? t("locationStand") : t("energyImpact")}
</span>
<span className="text-lg md:text-xl font-medium text-[#0066CC] dark:text-[#4DA6FF] leading-tight">
{data.energySavings}
</span>
</div>
)}
<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} />
{/* 🔥 AQUÍ ESTABA EL ERROR: Simplificamos la lógica 🔥 */}
{isEvent ? (isUpcoming ? t("scheduled") : t("concluded")) : t("operational")}
</span>
</div>
</div>
{data.projectOverview ? (
<div className="max-w-none mb-12">
<h3 className="text-2xl font-light mb-6 text-[#1D1D1F] dark:text-white">
{isEvent ? t("eventOverview") : t("projectChronicle")}
</h3>
{renderMarkdown(data.projectOverview)}
</div>
) : (
<div className="py-12 text-center border-2 border-dashed border-black/5 dark:border-white/5 rounded-2xl mb-12">
<p className="text-[#86868B] text-sm uppercase tracking-widest">{t("pendingData")}</p>
</div>
)}
{gallery.length > 0 && (
<div>
<h3 className="text-2xl font-light mb-6 text-[#1D1D1F] dark:text-white flex items-center gap-2">
<ImageIcon size={20} className="text-[#86868B]" /> {t("mediaGallery")}
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{gallery.map((imgSrc, idx) => (
<div key={idx} className={`relative rounded-2xl overflow-hidden bg-[#1D1D1F] border border-black/10 dark:border-white/10 ${idx === 0 && gallery.length % 2 !== 0 ? 'sm:col-span-2 h-64 md:h-80' : 'h-48 md:h-64'}`}>
<Image src={`/cases/${nodeSlug}/${imgSrc}`} alt={`Gallery image ${idx + 1}`} fill className="object-cover hover:scale-105 transition-transform duration-700" />
</div>
))}
</div>
</div>
)}
</div>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
);
}