This commit is contained in:
@@ -0,0 +1,338 @@
|
||||
"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;
|
||||
}
|
||||
|
||||
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 coverImage = data.mediaFileName ? `/cases/${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/${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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user