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;
// 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 nodeSlug = nodeToSlug(data.title);
const coverSrc = data.mediaFileName ? `/cases/${nodeSlug}/${data.mediaFileName}` : null;
@@ -99,14 +104,14 @@ export default function CaseStudyViewer({ data }: { data: CaseStudyData }) {
{/* Media indicators */}
<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">
<ImageIcon size={9} /> {data.gallery.length}
<ImageIcon size={9} /> {gallery.length}
</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">
<Play size={9} /> {data.videos.length}
<Play size={9} /> {videos.length}
</div>
)}
</div>
@@ -133,8 +138,8 @@ export default function CaseStudyViewer({ data }: { data: CaseStudyData }) {
)}
<Metric icon={Clock} label="Performance" value={data.stats} />
<Metric icon={MapPin} label="Region" value={data.location.split(",").pop()?.trim() || data.location} />
{data.datasheet.length > 0 && (
<Metric icon={FileText} label="Specs" value={`${data.datasheet.length} parameters`} />
{datasheet.length > 0 && (
<Metric icon={FileText} label="Specs" value={`${datasheet.length} parameters`} />
)}
</div>
@@ -166,21 +171,21 @@ export default function CaseStudyViewer({ data }: { data: CaseStudyData }) {
)}
{/* 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">
<span className="text-[9px] text-[#86868B] dark:text-[#A1A1A6] uppercase tracking-wider font-semibold block mb-2">
Equipment Specifications
</span>
<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">
<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>
</div>
))}
{data.datasheet.length > 6 && (
{datasheet.length > 6 && (
<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>
)}
</div>
@@ -188,17 +193,17 @@ export default function CaseStudyViewer({ data }: { data: CaseStudyData }) {
)}
{/* Gallery Preview */}
{data.gallery.length > 0 && (
{gallery.length > 0 && (
<div className="mb-3">
<button
onClick={() => setShowGallery(!showGallery)}
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>
{showGallery && (
<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">
<Image src={`/cases/${nodeSlug}/${img}`} alt={`Gallery ${i + 1}`} fill className="object-cover" sizes="120px" />
</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 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)
const findSpec = (keywords: string[]) => {
return data.datasheet.find(s =>
return datasheet.find(s =>
keywords.some(kw => s.label.toLowerCase().includes(kw))
)?.value;
};
@@ -61,8 +64,8 @@ export default function EquipmentConfigurator({ data }: { data: EquipmentData })
const modelSpec = findSpec(['model', 'modelo', 'type', 'tipo', 'series']);
// Split datasheet into primary (first 4) and extended
const primarySpecs = data.datasheet.slice(0, 4);
const extendedSpecs = data.datasheet.slice(4);
const primarySpecs = datasheet.slice(0, 4);
const extendedSpecs = datasheet.slice(4);
return (
<motion.div
@@ -175,7 +178,7 @@ export default function EquipmentConfigurator({ data }: { data: EquipmentData })
>
<span className="flex items-center gap-1.5">
<Settings2 size={12} />
All Specifications ({data.datasheet.length})
All Specifications ({datasheet.length})
</span>
<ChevronDown size={14} className={`transition-transform duration-300 ${showAllSpecs ? "rotate-180" : ""}`} />
</button>