The two AI tool cards rendered inside the chat — CaseStudyViewer (the
"Show me proven installations" card) and EquipmentConfigurator (the
"Show equipment specs" card) — were composing image URLs without the
node-slug segment:
/cases/<filename> ← what the cards were emitting (404)
/cases/<nodeSlug>/<filename> ← what the public site actually serves
Result: cover images and gallery thumbnails inside chat cards came back
as broken-image icons, while the same files rendered fine on
/en/applications/<slug> and inside the CaseStudyModal (those
already used the correct path).
Fix: both components now derive the slug from data.title with the same
nodeToSlug() the public pages use, and prefix it on every /cases/
URL — cover and gallery thumbnails alike.
CHANGES (2 files)
- src/components/ai/CaseStudyViewer.tsx
- Added local nodeToSlug() helper (mirrors ApplicationClient + assetFolders)
- coverSrc: /cases/${nodeSlug}/${mediaFileName}
- gallery image: /cases/${nodeSlug}/${img}
- src/components/ai/EquipmentConfigurator.tsx
- Added local nodeToSlug() helper
- coverSrc: /cases/${nodeSlug}/${mediaFileName}
No backend / API / DB changes. Pure client-side path correction.
This commit is contained in:
@@ -5,6 +5,13 @@ import { MapPin, Factory, Zap, Clock, ChevronDown, ArrowRight, Globe2, Image as
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
|
// Slugify the node title the same way ApplicationClient + assetFolders do —
|
||||||
|
// keeps image paths consistent with the on-disk layout
|
||||||
|
// (/cases/<nodeSlug>/<file>).
|
||||||
|
function nodeToSlug(title: string): string {
|
||||||
|
return title.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
// ── Interface matches GlobalNode shape from Prisma ──
|
// ── Interface matches GlobalNode shape from Prisma ──
|
||||||
interface CaseStudyData {
|
interface CaseStudyData {
|
||||||
found: boolean;
|
found: boolean;
|
||||||
@@ -50,7 +57,8 @@ export default function CaseStudyViewer({ data }: { data: CaseStudyData }) {
|
|||||||
if (!data.found) return null;
|
if (!data.found) return null;
|
||||||
|
|
||||||
const accent = ACCENTS[data.industry] || ACCENTS.textile;
|
const accent = ACCENTS[data.industry] || ACCENTS.textile;
|
||||||
const coverSrc = data.mediaFileName ? `/cases/${data.mediaFileName}` : null;
|
const nodeSlug = nodeToSlug(data.title);
|
||||||
|
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());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -192,7 +200,7 @@ export default function CaseStudyViewer({ data }: { data: CaseStudyData }) {
|
|||||||
<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) => (
|
{data.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/${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>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,12 @@ import { Cpu, ArrowRight, Settings2, ChevronDown, MapPin, Factory, Zap, Box } fr
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
|
// Same slugger as the public-facing pages so cover images resolve to the
|
||||||
|
// /cases/<nodeSlug>/<file> layout that's actually on disk.
|
||||||
|
function nodeToSlug(title: string): string {
|
||||||
|
return title.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
// ── Interface matches GlobalNode + specificDatasheetJson from Prisma ──
|
// ── Interface matches GlobalNode + specificDatasheetJson from Prisma ──
|
||||||
interface EquipmentData {
|
interface EquipmentData {
|
||||||
found: boolean;
|
found: boolean;
|
||||||
@@ -39,7 +45,8 @@ export default function EquipmentConfigurator({ data }: { data: EquipmentData })
|
|||||||
|
|
||||||
if (!data.found) return null;
|
if (!data.found) return null;
|
||||||
|
|
||||||
const coverSrc = data.mediaFileName ? `/cases/${data.mediaFileName}` : null;
|
const nodeSlug = nodeToSlug(data.title);
|
||||||
|
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());
|
||||||
|
|
||||||
// Find key specs for header pills (power, frequency, model — from datasheet)
|
// Find key specs for header pills (power, frequency, model — from datasheet)
|
||||||
|
|||||||
Reference in New Issue
Block a user