Scaffold webu na Payload CMS 3 + Next.js nad SQLite
Kostra prezentace s vlastní administrací pro Martina Reinera. Obsah: - kolekce Products (obrázky, volné parametry, cena, dostupnost, skrytí), Media se zmenšeninami, Pages, Users bez veřejné registrace - veřejná část: výpis nabídky, detail položky, statické stránky - /api/health pro Docker HEALTHCHECK Provoz: - Dockerfile, docker-compose.yml (vývoj) a docker-compose.prod.yml (nasazení z Gitea registry) - CI v Gitea Actions: lint -> testy -> build -> push image - úvodní migrace databáze Stránky jsou force-dynamic, aby se úprava položky projevila hned; schéma se za běhu nedomýšlí (push: false), migrace se commitují. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
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 { 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={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
|
||||
}
|
||||
Reference in New Issue
Block a user