Payload skládá URL uploadů absolutně podle serverURL. next/image to vyhodnotí jako externí zdroj a bez images.remotePatterns odmítne s HTTP 400 "url parameter is not allowed" — samotný soubor se přitom servíruje správně, takže se to projevilo až s nahraným obrázkem. Do next/image proto posíláme jen cestu (helper cestaKSouboru). Soubor leží na stejném originu jako web, takže je to korektní, a navíc na tom nezávisí doména: remotePatterns se zapékají při buildu, ale tentýž image běží na stagingu i v produkci pod jiným jménem. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
105 lines
2.9 KiB
TypeScript
105 lines
2.9 KiB
TypeScript
import Image from 'next/image'
|
|
import Link from 'next/link'
|
|
import { notFound } from 'next/navigation'
|
|
import { getPayload } from 'payload'
|
|
import React from 'react'
|
|
|
|
import config from '@/payload.config'
|
|
import type { Media, Product } from '@/payload-types'
|
|
import { cestaKSouboru, formatujCenu, popisStavu } from '@/lib/format'
|
|
import '../../styles.css'
|
|
|
|
export const dynamic = 'force-dynamic'
|
|
|
|
type Props = { params: Promise<{ slug: string }> }
|
|
|
|
export async function generateMetadata({ params }: Props) {
|
|
const polozka = await najdiPolozku((await params).slug)
|
|
if (!polozka) return { title: 'Položka nenalezena' }
|
|
|
|
return {
|
|
title: `${polozka.nazev} — Reiner zbraně`,
|
|
description: polozka.popis ?? undefined,
|
|
}
|
|
}
|
|
|
|
export default async function DetailPolozky({ params }: Props) {
|
|
const polozka = await najdiPolozku((await params).slug)
|
|
if (!polozka) notFound()
|
|
|
|
const obrazky = (polozka.obrazky ?? [])
|
|
.map((radek) => radek.obrazek)
|
|
.filter((o): o is Media => typeof o === 'object' && o !== null)
|
|
|
|
return (
|
|
<article className="stranka detail">
|
|
<p className="zpet">
|
|
<Link href="/">← Zpět na nabídku</Link>
|
|
</p>
|
|
|
|
<h1>{polozka.nazev}</h1>
|
|
|
|
<p className="detail__cena">
|
|
{formatujCenu(polozka.cena)}
|
|
<span className={`znacka-stavu znacka-stavu--${polozka.stav}`}>
|
|
{popisStavu(polozka.stav)}
|
|
</span>
|
|
</p>
|
|
|
|
{obrazky.length > 0 && (
|
|
<div className="galerie">
|
|
{obrazky.map((obrazek) => {
|
|
const varianta = obrazek.sizes?.detail
|
|
const url = varianta?.url ?? obrazek.url
|
|
const width = varianta?.width ?? obrazek.width
|
|
const height = varianta?.height ?? obrazek.height
|
|
if (!url || !width || !height) return null
|
|
|
|
return (
|
|
<Image
|
|
key={obrazek.id}
|
|
alt={obrazek.alt ?? polozka.nazev}
|
|
src={cestaKSouboru(url)}
|
|
width={width}
|
|
height={height}
|
|
sizes="(max-width: 900px) 100vw, 800px"
|
|
/>
|
|
)
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
{polozka.parametry && polozka.parametry.length > 0 && (
|
|
<table className="parametry">
|
|
<tbody>
|
|
{polozka.parametry.map((parametr) => (
|
|
<tr key={parametr.id}>
|
|
<th scope="row">{parametr.nazev}</th>
|
|
<td>{parametr.hodnota}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
)}
|
|
|
|
{polozka.popis && <div className="popis">{polozka.popis}</div>}
|
|
</article>
|
|
)
|
|
}
|
|
|
|
async function najdiPolozku(slug: string): Promise<Product | null> {
|
|
const payload = await getPayload({ config: await config })
|
|
|
|
const { docs } = await payload.find({
|
|
collection: 'products',
|
|
where: {
|
|
slug: { equals: slug },
|
|
skryto: { not_equals: true },
|
|
},
|
|
depth: 1,
|
|
limit: 1,
|
|
})
|
|
|
|
return docs[0] ?? null
|
|
}
|