fix(ai): guard datasheet/gallery/videos against non-array values
Deploy to VPS / deploy (push) Has been cancelled

EquipmentConfigurator and CaseStudyViewer crashed with
"e.datasheet.find is not a function" when DB nodes had
malformed JSON in specificDatasheetJson. Both components now
normalize datasheet, gallery, and videos to safe arrays via
Array.isArray before any array method calls.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-07 09:12:55 -05:00
parent 7278d5d00f
commit 792dd6794b
2 changed files with 25 additions and 17 deletions
+18 -13
View File
@@ -56,6 +56,11 @@ export default function CaseStudyViewer({ data }: { data: CaseStudyData }) {
if (!data.found) return null; if (!data.found) return null;
// Defensive: ensure datasheet/gallery/videos are always arrays
const datasheet = Array.isArray(data.datasheet) ? data.datasheet : [];
const gallery = Array.isArray(data.gallery) ? data.gallery : [];
const videos = Array.isArray(data.videos) ? data.videos : [];
const accent = ACCENTS[data.industry] || ACCENTS.textile; const accent = ACCENTS[data.industry] || ACCENTS.textile;
const nodeSlug = nodeToSlug(data.title); const nodeSlug = nodeToSlug(data.title);
const coverSrc = data.mediaFileName ? `/cases/${nodeSlug}/${data.mediaFileName}` : null; const coverSrc = data.mediaFileName ? `/cases/${nodeSlug}/${data.mediaFileName}` : null;
@@ -99,14 +104,14 @@ export default function CaseStudyViewer({ data }: { data: CaseStudyData }) {
{/* Media indicators */} {/* Media indicators */}
<div className="absolute bottom-3 right-3 flex items-center gap-1.5 z-10"> <div className="absolute bottom-3 right-3 flex items-center gap-1.5 z-10">
{data.gallery.length > 0 && ( {gallery.length > 0 && (
<div className="bg-black/40 backdrop-blur-md text-white text-[9px] px-2 py-0.5 rounded-full flex items-center gap-1"> <div className="bg-black/40 backdrop-blur-md text-white text-[9px] px-2 py-0.5 rounded-full flex items-center gap-1">
<ImageIcon size={9} /> {data.gallery.length} <ImageIcon size={9} /> {gallery.length}
</div> </div>
)} )}
{data.videos.length > 0 && ( {videos.length > 0 && (
<div className="bg-black/40 backdrop-blur-md text-white text-[9px] px-2 py-0.5 rounded-full flex items-center gap-1"> <div className="bg-black/40 backdrop-blur-md text-white text-[9px] px-2 py-0.5 rounded-full flex items-center gap-1">
<Play size={9} /> {data.videos.length} <Play size={9} /> {videos.length}
</div> </div>
)} )}
</div> </div>
@@ -133,8 +138,8 @@ export default function CaseStudyViewer({ data }: { data: CaseStudyData }) {
)} )}
<Metric icon={Clock} label="Performance" value={data.stats} /> <Metric icon={Clock} label="Performance" value={data.stats} />
<Metric icon={MapPin} label="Region" value={data.location.split(",").pop()?.trim() || data.location} /> <Metric icon={MapPin} label="Region" value={data.location.split(",").pop()?.trim() || data.location} />
{data.datasheet.length > 0 && ( {datasheet.length > 0 && (
<Metric icon={FileText} label="Specs" value={`${data.datasheet.length} parameters`} /> <Metric icon={FileText} label="Specs" value={`${datasheet.length} parameters`} />
)} )}
</div> </div>
@@ -166,21 +171,21 @@ export default function CaseStudyViewer({ data }: { data: CaseStudyData }) {
)} )}
{/* Equipment Datasheet (from specificDatasheetJson) */} {/* Equipment Datasheet (from specificDatasheetJson) */}
{data.datasheet.length > 0 && ( {datasheet.length > 0 && (
<div className="bg-white/40 dark:bg-white/[0.03] border border-white/60 dark:border-white/[0.06] rounded-xl p-3 mb-3 transition-colors"> <div className="bg-white/40 dark:bg-white/[0.03] border border-white/60 dark:border-white/[0.06] rounded-xl p-3 mb-3 transition-colors">
<span className="text-[9px] text-[#86868B] dark:text-[#A1A1A6] uppercase tracking-wider font-semibold block mb-2"> <span className="text-[9px] text-[#86868B] dark:text-[#A1A1A6] uppercase tracking-wider font-semibold block mb-2">
Equipment Specifications Equipment Specifications
</span> </span>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
{data.datasheet.slice(0, 6).map((spec, i) => ( {datasheet.slice(0, 6).map((spec, i) => (
<div key={i} className="flex items-center justify-between py-1 border-b border-black/[0.03] dark:border-white/[0.04] last:border-0"> <div key={i} className="flex items-center justify-between py-1 border-b border-black/[0.03] dark:border-white/[0.04] last:border-0">
<span className="text-[10px] text-[#86868B] dark:text-[#A1A1A6]">{spec.label}</span> <span className="text-[10px] text-[#86868B] dark:text-[#A1A1A6]">{spec.label}</span>
<span className="text-[10px] font-medium text-[#1D1D1F] dark:text-[#F5F5F7]">{spec.value}</span> <span className="text-[10px] font-medium text-[#1D1D1F] dark:text-[#F5F5F7]">{spec.value}</span>
</div> </div>
))} ))}
{data.datasheet.length > 6 && ( {datasheet.length > 6 && (
<span className="text-[9px] text-[#86868B] dark:text-[#A1A1A6] text-center pt-1"> <span className="text-[9px] text-[#86868B] dark:text-[#A1A1A6] text-center pt-1">
+{data.datasheet.length - 6} more specs in full view +{datasheet.length - 6} more specs in full view
</span> </span>
)} )}
</div> </div>
@@ -188,17 +193,17 @@ export default function CaseStudyViewer({ data }: { data: CaseStudyData }) {
)} )}
{/* Gallery Preview */} {/* Gallery Preview */}
{data.gallery.length > 0 && ( {gallery.length > 0 && (
<div className="mb-3"> <div className="mb-3">
<button <button
onClick={() => setShowGallery(!showGallery)} onClick={() => setShowGallery(!showGallery)}
className="text-[10px] text-[#0066CC] dark:text-[#4DA6FF] font-medium flex items-center gap-1 mb-2" className="text-[10px] text-[#0066CC] dark:text-[#4DA6FF] font-medium flex items-center gap-1 mb-2"
> >
<ImageIcon size={10} /> {showGallery ? 'Hide' : 'Show'} Gallery ({data.gallery.length} images) <ImageIcon size={10} /> {showGallery ? 'Hide' : 'Show'} Gallery ({gallery.length} images)
</button> </button>
{showGallery && ( {showGallery && (
<div className="grid grid-cols-3 gap-1.5 rounded-lg overflow-hidden"> <div className="grid grid-cols-3 gap-1.5 rounded-lg overflow-hidden">
{data.gallery.slice(0, 6).map((img, i) => ( {gallery.slice(0, 6).map((img, i) => (
<div key={i} className="relative aspect-square bg-[#F5F5F7] dark:bg-[#1D1D1F] rounded-md overflow-hidden"> <div key={i} className="relative aspect-square bg-[#F5F5F7] dark:bg-[#1D1D1F] rounded-md overflow-hidden">
<Image src={`/cases/${nodeSlug}/${img}`} alt={`Gallery ${i + 1}`} fill className="object-cover" sizes="120px" /> <Image src={`/cases/${nodeSlug}/${img}`} alt={`Gallery ${i + 1}`} fill className="object-cover" sizes="120px" />
</div> </div>
+7 -4
View File
@@ -49,9 +49,12 @@ export default function EquipmentConfigurator({ data }: { data: EquipmentData })
const coverSrc = data.mediaFileName ? `/cases/${nodeSlug}/${data.mediaFileName}` : null; const coverSrc = data.mediaFileName ? `/cases/${nodeSlug}/${data.mediaFileName}` : null;
const appLabel = data.application.replace(/-/g, " ").replace(/\b\w/g, c => c.toUpperCase()); const appLabel = data.application.replace(/-/g, " ").replace(/\b\w/g, c => c.toUpperCase());
// Defensive: ensure datasheet is always an array (DB may store malformed JSON)
const datasheet = Array.isArray(data.datasheet) ? data.datasheet : [];
// Find key specs for header pills (power, frequency, model — from datasheet) // Find key specs for header pills (power, frequency, model — from datasheet)
const findSpec = (keywords: string[]) => { const findSpec = (keywords: string[]) => {
return data.datasheet.find(s => return datasheet.find(s =>
keywords.some(kw => s.label.toLowerCase().includes(kw)) keywords.some(kw => s.label.toLowerCase().includes(kw))
)?.value; )?.value;
}; };
@@ -61,8 +64,8 @@ export default function EquipmentConfigurator({ data }: { data: EquipmentData })
const modelSpec = findSpec(['model', 'modelo', 'type', 'tipo', 'series']); const modelSpec = findSpec(['model', 'modelo', 'type', 'tipo', 'series']);
// Split datasheet into primary (first 4) and extended // Split datasheet into primary (first 4) and extended
const primarySpecs = data.datasheet.slice(0, 4); const primarySpecs = datasheet.slice(0, 4);
const extendedSpecs = data.datasheet.slice(4); const extendedSpecs = datasheet.slice(4);
return ( return (
<motion.div <motion.div
@@ -175,7 +178,7 @@ export default function EquipmentConfigurator({ data }: { data: EquipmentData })
> >
<span className="flex items-center gap-1.5"> <span className="flex items-center gap-1.5">
<Settings2 size={12} /> <Settings2 size={12} />
All Specifications ({data.datasheet.length}) All Specifications ({datasheet.length})
</span> </span>
<ChevronDown size={14} className={`transition-transform duration-300 ${showAllSpecs ? "rotate-180" : ""}`} /> <ChevronDown size={14} className={`transition-transform duration-300 ${showAllSpecs ? "rotate-180" : ""}`} />
</button> </button>