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,42 @@
|
||||
import { notFound } from 'next/navigation'
|
||||
import { getPayload } from 'payload'
|
||||
import React from 'react'
|
||||
import { RichText } from '@payloadcms/richtext-lexical/react'
|
||||
|
||||
import config from '@/payload.config'
|
||||
import type { Page } from '@/payload-types'
|
||||
import '../styles.css'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
type Props = { params: Promise<{ slug: string }> }
|
||||
|
||||
export async function generateMetadata({ params }: Props) {
|
||||
const stranka = await najdiStranku((await params).slug)
|
||||
return { title: stranka ? `${stranka.nazev} — Reiner zbraně` : 'Stránka nenalezena' }
|
||||
}
|
||||
|
||||
/** Statické stránky z kolekce Pages — /kontakt, /o-nas a podobně. */
|
||||
export default async function StatickaStranka({ params }: Props) {
|
||||
const stranka = await najdiStranku((await params).slug)
|
||||
if (!stranka) notFound()
|
||||
|
||||
return (
|
||||
<article className="stranka">
|
||||
<h1>{stranka.nazev}</h1>
|
||||
{stranka.obsah && <RichText data={stranka.obsah} />}
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
async function najdiStranku(slug: string): Promise<Page | null> {
|
||||
const payload = await getPayload({ config: await config })
|
||||
|
||||
const { docs } = await payload.find({
|
||||
collection: 'pages',
|
||||
where: { slug: { equals: slug } },
|
||||
limit: 1,
|
||||
})
|
||||
|
||||
return docs[0] ?? null
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Endpoint pro Docker HEALTHCHECK.
|
||||
*
|
||||
* Záměrně nesahá do databáze — odpovídá na otázku „stojí server a obsluhuje
|
||||
* požadavky?“. Výpadek databáze pozná až monitoring obsahu, ne healthcheck;
|
||||
* kdyby ho shazoval, restartoval by Docker aplikaci kvůli něčemu, co restart
|
||||
* neopraví.
|
||||
*/
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export function GET() {
|
||||
return Response.json({ status: 'ok' })
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import './styles.css'
|
||||
|
||||
export const metadata = {
|
||||
description: 'Prodej zbraní, střeliva a doplňků — Martin Reiner.',
|
||||
title: 'Reiner — zbraně a střelivo',
|
||||
}
|
||||
|
||||
export default async function RootLayout(props: { children: React.ReactNode }) {
|
||||
const { children } = props
|
||||
|
||||
return (
|
||||
<html lang="cs">
|
||||
<body>
|
||||
<header className="hlavicka">
|
||||
<Link className="znacka" href="/">
|
||||
Reiner <span>zbraně a střelivo</span>
|
||||
</Link>
|
||||
<nav>
|
||||
<Link href="/">Nabídka</Link>
|
||||
<Link href="/kontakt">Kontakt</Link>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>{children}</main>
|
||||
|
||||
<footer className="paticka">
|
||||
<p>© {new Date().getFullYear()} Martin Reiner</p>
|
||||
<p>
|
||||
Prodej zbraní a střeliva pouze osobám s platným zbrojním průkazem podle zákona
|
||||
č. 14/2021 Sb.
|
||||
</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
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'
|
||||
|
||||
// Obsah se čte z CMS při každém požadavku — jinak by se úprava položky
|
||||
// projevila až po přestavění webu.
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
/** Výpis nabídky. Skryté položky se nezobrazují, prodané se řadí nakonec. */
|
||||
export default async function HomePage() {
|
||||
const payload = await getPayload({ config: await config })
|
||||
|
||||
const { docs: polozky } = await payload.find({
|
||||
collection: 'products',
|
||||
where: { skryto: { not_equals: true } },
|
||||
sort: ['stav', '-updatedAt'],
|
||||
depth: 1,
|
||||
limit: 100,
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="stranka">
|
||||
<h1>Nabídka</h1>
|
||||
|
||||
{polozky.length === 0 ? (
|
||||
<p className="prazdno">
|
||||
Zatím tu nic není. Položky se přidávají v <Link href="/admin">administraci</Link>.
|
||||
</p>
|
||||
) : (
|
||||
<ul className="mrizka">
|
||||
{polozky.map((polozka) => (
|
||||
<KartaPolozky key={polozka.id} polozka={polozka} />
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function KartaPolozky({ polozka }: { polozka: Product }) {
|
||||
const nahled = prvniObrazek(polozka)
|
||||
|
||||
return (
|
||||
<li className={`karta${polozka.stav === 'prodano' ? ' karta--prodano' : ''}`}>
|
||||
<Link href={`/nabidka/${polozka.slug}`}>
|
||||
<div className="karta__obrazek">
|
||||
{nahled ? (
|
||||
<Image
|
||||
alt={nahled.alt}
|
||||
src={nahled.url}
|
||||
width={nahled.width}
|
||||
height={nahled.height}
|
||||
sizes="(max-width: 700px) 100vw, 320px"
|
||||
/>
|
||||
) : (
|
||||
<div className="karta__bezobrazku">bez fotky</div>
|
||||
)}
|
||||
</div>
|
||||
<h2>{polozka.nazev}</h2>
|
||||
<p className="karta__cena">{formatujCenu(polozka.cena)}</p>
|
||||
<p className="karta__stav">{popisStavu(polozka.stav)}</p>
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Vytáhne z položky první obrázek v podobě použitelné pro `next/image`.
|
||||
* Vrací `null`, pokud položka fotku nemá nebo se nepodařilo načíst rozměry.
|
||||
*/
|
||||
function prvniObrazek(polozka: Product) {
|
||||
const prvni = polozka.obrazky?.[0]?.obrazek
|
||||
if (!prvni || typeof prvni === 'number') return null
|
||||
|
||||
const media = prvni as Media
|
||||
const varianta = media.sizes?.nahled
|
||||
const url = varianta?.url ?? media.url
|
||||
const width = varianta?.width ?? media.width
|
||||
const height = varianta?.height ?? media.height
|
||||
|
||||
if (!url || !width || !height) return null
|
||||
|
||||
return { url, width, height, alt: media.alt ?? polozka.nazev }
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
/*
|
||||
* Styly veřejné části. Záměrně jeden soubor bez frameworku — web má tři
|
||||
* typy stránek a Tailwind by sem přinesl build krok navíc bez užitku.
|
||||
*/
|
||||
|
||||
:root {
|
||||
--barva-text: #16181c;
|
||||
--barva-tlumena: #6b7280;
|
||||
--barva-pozadi: #ffffff;
|
||||
--barva-plocha: #f5f5f4;
|
||||
--barva-linka: #e2e0dd;
|
||||
--barva-akcent: #7a5c3e;
|
||||
--sirka: 1100px;
|
||||
--radius: 6px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--barva-text: #ececec;
|
||||
--barva-tlumena: #a1a1aa;
|
||||
--barva-pozadi: #141414;
|
||||
--barva-plocha: #1e1e1e;
|
||||
--barva-linka: #2e2e2e;
|
||||
--barva-akcent: #c9a227;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
|
||||
font-size: 17px;
|
||||
line-height: 1.6;
|
||||
color: var(--barva-text);
|
||||
background: var(--barva-pozadi);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* ---------- hlavička a patička ---------- */
|
||||
|
||||
.hlavicka {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
max-width: var(--sirka);
|
||||
margin: 0 auto;
|
||||
padding: 1.5rem 1.25rem;
|
||||
border-bottom: 1px solid var(--barva-linka);
|
||||
}
|
||||
|
||||
.znacka {
|
||||
font-size: 1.35rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.01em;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.znacka span {
|
||||
display: block;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--barva-tlumena);
|
||||
}
|
||||
|
||||
.hlavicka nav {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.hlavicka nav a {
|
||||
text-decoration: none;
|
||||
color: var(--barva-tlumena);
|
||||
}
|
||||
|
||||
.hlavicka nav a:hover {
|
||||
color: var(--barva-text);
|
||||
}
|
||||
|
||||
.paticka {
|
||||
max-width: var(--sirka);
|
||||
margin: 4rem auto 0;
|
||||
padding: 1.5rem 1.25rem 3rem;
|
||||
border-top: 1px solid var(--barva-linka);
|
||||
font-size: 0.85rem;
|
||||
color: var(--barva-tlumena);
|
||||
}
|
||||
|
||||
.paticka p {
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
/* ---------- obecná stránka ---------- */
|
||||
|
||||
.stranka {
|
||||
max-width: var(--sirka);
|
||||
margin: 0 auto;
|
||||
padding: 2rem 1.25rem 0;
|
||||
}
|
||||
|
||||
.stranka h1 {
|
||||
margin: 0 0 1.5rem;
|
||||
font-size: clamp(1.6rem, 4vw, 2.2rem);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.prazdno {
|
||||
color: var(--barva-tlumena);
|
||||
}
|
||||
|
||||
/* ---------- výpis nabídky ---------- */
|
||||
|
||||
.mrizka {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(min(100%, 260px), 1fr));
|
||||
gap: 1.75rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.karta a {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.karta__obrazek {
|
||||
aspect-ratio: 4 / 3;
|
||||
overflow: hidden;
|
||||
background: var(--barva-plocha);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.karta__obrazek img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.25s ease;
|
||||
}
|
||||
|
||||
.karta a:hover .karta__obrazek img {
|
||||
transform: scale(1.03);
|
||||
}
|
||||
|
||||
.karta__bezobrazku {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
height: 100%;
|
||||
font-size: 0.85rem;
|
||||
color: var(--barva-tlumena);
|
||||
}
|
||||
|
||||
.karta h2 {
|
||||
margin: 0.75rem 0 0.25rem;
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.karta__cena {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
color: var(--barva-akcent);
|
||||
}
|
||||
|
||||
.karta__stav {
|
||||
margin: 0.1rem 0 0;
|
||||
font-size: 0.85rem;
|
||||
color: var(--barva-tlumena);
|
||||
}
|
||||
|
||||
/* Prodané položky zůstávají ve výpisu, ale nekřičí. */
|
||||
.karta--prodano .karta__obrazek img {
|
||||
filter: grayscale(1);
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
/* ---------- detail položky ---------- */
|
||||
|
||||
.zpet {
|
||||
margin: 0 0 1rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.detail__cena {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
margin: -0.75rem 0 1.75rem;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 600;
|
||||
color: var(--barva-akcent);
|
||||
}
|
||||
|
||||
.znacka-stavu {
|
||||
padding: 0.15rem 0.6rem;
|
||||
border: 1px solid var(--barva-linka);
|
||||
border-radius: 999px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
color: var(--barva-tlumena);
|
||||
}
|
||||
|
||||
.znacka-stavu--prodano {
|
||||
border-color: transparent;
|
||||
background: var(--barva-plocha);
|
||||
}
|
||||
|
||||
.galerie {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.galerie img {
|
||||
width: 100%;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.parametry {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.parametry th,
|
||||
.parametry td {
|
||||
padding: 0.6rem 0;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
border-bottom: 1px solid var(--barva-linka);
|
||||
}
|
||||
|
||||
.parametry th {
|
||||
width: 40%;
|
||||
font-weight: 500;
|
||||
color: var(--barva-tlumena);
|
||||
}
|
||||
|
||||
.popis {
|
||||
max-width: 65ch;
|
||||
white-space: pre-line;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
import config from '@payload-config'
|
||||
import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views'
|
||||
import { importMap } from '../importMap'
|
||||
|
||||
type Args = {
|
||||
params: Promise<{
|
||||
segments: string[]
|
||||
}>
|
||||
searchParams: Promise<{
|
||||
[key: string]: string | string[]
|
||||
}>
|
||||
}
|
||||
|
||||
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
|
||||
generatePageMetadata({ config, params, searchParams })
|
||||
|
||||
const NotFound = ({ params, searchParams }: Args) =>
|
||||
NotFoundPage({ config, params, searchParams, importMap })
|
||||
|
||||
export default NotFound
|
||||
@@ -0,0 +1,24 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
import config from '@payload-config'
|
||||
import { RootPage, generatePageMetadata } from '@payloadcms/next/views'
|
||||
import { importMap } from '../importMap'
|
||||
|
||||
type Args = {
|
||||
params: Promise<{
|
||||
segments: string[]
|
||||
}>
|
||||
searchParams: Promise<{
|
||||
[key: string]: string | string[]
|
||||
}>
|
||||
}
|
||||
|
||||
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
|
||||
generatePageMetadata({ config, params, searchParams })
|
||||
|
||||
const Page = ({ params, searchParams }: Args) =>
|
||||
RootPage({ config, params, searchParams, importMap })
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,52 @@
|
||||
import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||
import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||
import { LexicalDiffComponent as LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||
import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { UploadFeatureClient as UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { BlockquoteFeatureClient as BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { RelationshipFeatureClient as RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { ChecklistFeatureClient as ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { OrderedListFeatureClient as OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { UnorderedListFeatureClient as UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { IndentFeatureClient as IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { AlignFeatureClient as AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { InlineCodeFeatureClient as InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { SuperscriptFeatureClient as SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { SubscriptFeatureClient as SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc'
|
||||
|
||||
/** @type import('payload').ImportMap */
|
||||
export const importMap = {
|
||||
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell": RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalField": RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
"@payloadcms/richtext-lexical/rsc#LexicalDiffComponent": LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
"@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient": InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient": HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#UploadFeatureClient": UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#BlockquoteFeatureClient": BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#RelationshipFeatureClient": RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#LinkFeatureClient": LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#ChecklistFeatureClient": ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#OrderedListFeatureClient": OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#UnorderedListFeatureClient": UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#IndentFeatureClient": IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#AlignFeatureClient": AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#ParagraphFeatureClient": ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#InlineCodeFeatureClient": InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#SuperscriptFeatureClient": SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#SubscriptFeatureClient": SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#StrikethroughFeatureClient": StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/next/rsc#CollectionCards": CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import config from '@payload-config'
|
||||
import '@payloadcms/next/css'
|
||||
import {
|
||||
REST_DELETE,
|
||||
REST_GET,
|
||||
REST_OPTIONS,
|
||||
REST_PATCH,
|
||||
REST_POST,
|
||||
REST_PUT,
|
||||
} from '@payloadcms/next/routes'
|
||||
|
||||
export const GET = REST_GET(config)
|
||||
export const POST = REST_POST(config)
|
||||
export const DELETE = REST_DELETE(config)
|
||||
export const PATCH = REST_PATCH(config)
|
||||
export const PUT = REST_PUT(config)
|
||||
export const OPTIONS = REST_OPTIONS(config)
|
||||
@@ -0,0 +1,7 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import config from '@payload-config'
|
||||
import '@payloadcms/next/css'
|
||||
import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes'
|
||||
|
||||
export const GET = GRAPHQL_PLAYGROUND_GET(config)
|
||||
@@ -0,0 +1,8 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import config from '@payload-config'
|
||||
import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes'
|
||||
|
||||
export const POST = GRAPHQL_POST(config)
|
||||
|
||||
export const OPTIONS = REST_OPTIONS(config)
|
||||
@@ -0,0 +1,31 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import config from '@payload-config'
|
||||
import '@payloadcms/next/css'
|
||||
import type { ServerFunctionClient } from 'payload'
|
||||
import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts'
|
||||
import React from 'react'
|
||||
|
||||
import { importMap } from './admin/importMap.js'
|
||||
import './custom.scss'
|
||||
|
||||
type Args = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const serverFunction: ServerFunctionClient = async function (args) {
|
||||
'use server'
|
||||
return handleServerFunctions({
|
||||
...args,
|
||||
config,
|
||||
importMap,
|
||||
})
|
||||
}
|
||||
|
||||
const Layout = ({ children }: Args) => (
|
||||
<RootLayout config={config} importMap={importMap} serverFunction={serverFunction}>
|
||||
{children}
|
||||
</RootLayout>
|
||||
)
|
||||
|
||||
export default Layout
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
/**
|
||||
* Nahrané obrázky. Soubory leží v `media/`, což je v produkci Docker volume —
|
||||
* proto přežijí redeploy (viz NAVRH-PLATFORMY.md, sekce 6).
|
||||
*/
|
||||
export const Media: CollectionConfig = {
|
||||
slug: 'media',
|
||||
labels: {
|
||||
singular: 'Obrázek',
|
||||
plural: 'Média',
|
||||
},
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
upload: {
|
||||
staticDir: 'media',
|
||||
mimeTypes: ['image/*'],
|
||||
// Zmenšeniny se generují při nahrání; výpis nabídky tahá `nahled`,
|
||||
// ať se na mobilu nestahuje pětimegový originál.
|
||||
imageSizes: [
|
||||
{ name: 'nahled', width: 600, height: 450, position: 'centre' },
|
||||
{ name: 'detail', width: 1400, height: undefined },
|
||||
],
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'alt',
|
||||
label: 'Popis pro čtečky (alt)',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
/** Statické stránky — o nás, kontakt, otevírací doba. */
|
||||
export const Pages: CollectionConfig = {
|
||||
slug: 'pages',
|
||||
labels: {
|
||||
singular: 'Stránka',
|
||||
plural: 'Stránky',
|
||||
},
|
||||
admin: {
|
||||
useAsTitle: 'nazev',
|
||||
defaultColumns: ['nazev', 'slug', 'updatedAt'],
|
||||
},
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'nazev',
|
||||
label: 'Název',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'slug',
|
||||
label: 'URL adresa',
|
||||
type: 'text',
|
||||
required: true,
|
||||
unique: true,
|
||||
index: true,
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
description: 'Např. `kontakt` → /kontakt',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'obsah',
|
||||
label: 'Obsah',
|
||||
type: 'richText',
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
/**
|
||||
* Položka nabídky — zbraň, střelivo, doplněk.
|
||||
*
|
||||
* Záměrně plochá struktura: obrázky, pár parametrů, cena. Parametry jsou
|
||||
* volný seznam dvojic název/hodnota, protože se u pušky, pistole a střeliva
|
||||
* liší a pevná pole by znamenala migraci při každém novém sortimentu.
|
||||
*/
|
||||
export const Products: CollectionConfig = {
|
||||
slug: 'products',
|
||||
labels: {
|
||||
singular: 'Položka',
|
||||
plural: 'Nabídka',
|
||||
},
|
||||
admin: {
|
||||
useAsTitle: 'nazev',
|
||||
defaultColumns: ['nazev', 'kategorie', 'cena', 'stav', 'updatedAt'],
|
||||
description: 'Zboží zobrazené na webu. Skryté položky se veřejně nezobrazí.',
|
||||
},
|
||||
access: {
|
||||
// Veřejně čitelné, měnit může jen přihlášený uživatel.
|
||||
read: () => true,
|
||||
},
|
||||
defaultSort: '-updatedAt',
|
||||
fields: [
|
||||
{
|
||||
name: 'nazev',
|
||||
label: 'Název',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'slug',
|
||||
label: 'URL adresa',
|
||||
type: 'text',
|
||||
unique: true,
|
||||
index: true,
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
description: 'Vyplní se samo z názvu, pokud necháte prázdné.',
|
||||
},
|
||||
hooks: {
|
||||
beforeValidate: [
|
||||
({ value, data }) => value || slugify(data?.nazev ?? ''),
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'kategorie',
|
||||
label: 'Kategorie',
|
||||
type: 'select',
|
||||
required: true,
|
||||
defaultValue: 'zbrane',
|
||||
options: [
|
||||
{ label: 'Zbraně', value: 'zbrane' },
|
||||
{ label: 'Střelivo', value: 'strelivo' },
|
||||
{ label: 'Doplňky', value: 'doplnky' },
|
||||
],
|
||||
admin: { position: 'sidebar' },
|
||||
},
|
||||
{
|
||||
name: 'cena',
|
||||
label: 'Cena (Kč)',
|
||||
type: 'number',
|
||||
required: true,
|
||||
min: 0,
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
step: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'stav',
|
||||
label: 'Dostupnost',
|
||||
type: 'select',
|
||||
required: true,
|
||||
defaultValue: 'skladem',
|
||||
options: [
|
||||
{ label: 'Skladem', value: 'skladem' },
|
||||
{ label: 'Na objednávku', value: 'na-objednavku' },
|
||||
{ label: 'Prodáno', value: 'prodano' },
|
||||
],
|
||||
admin: { position: 'sidebar' },
|
||||
},
|
||||
{
|
||||
name: 'skryto',
|
||||
label: 'Skrýt z webu',
|
||||
type: 'checkbox',
|
||||
defaultValue: false,
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
description: 'Rozpracovaná položka, kterou zatím nikdo nemá vidět.',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'obrazky',
|
||||
label: 'Obrázky',
|
||||
type: 'array',
|
||||
minRows: 1,
|
||||
labels: { singular: 'Obrázek', plural: 'Obrázky' },
|
||||
admin: {
|
||||
description: 'První obrázek se použije jako náhled ve výpisu.',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'obrazek',
|
||||
label: 'Soubor',
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'parametry',
|
||||
label: 'Parametry',
|
||||
type: 'array',
|
||||
labels: { singular: 'Parametr', plural: 'Parametry' },
|
||||
admin: {
|
||||
description: 'Např. Ráže → 9 mm Luger, Délka hlavně → 108 mm.',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
name: 'nazev',
|
||||
label: 'Název',
|
||||
type: 'text',
|
||||
required: true,
|
||||
admin: { width: '40%' },
|
||||
},
|
||||
{
|
||||
name: 'hodnota',
|
||||
label: 'Hodnota',
|
||||
type: 'text',
|
||||
required: true,
|
||||
admin: { width: '60%' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'popis',
|
||||
label: 'Popis',
|
||||
type: 'textarea',
|
||||
admin: {
|
||||
description: 'Nepovinný delší text pod parametry.',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
/** Diakritika pryč, mezery na pomlčky — `CZ 75 SP-01` → `cz-75-sp-01`. */
|
||||
function slugify(text: string): string {
|
||||
return text
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
/**
|
||||
* Účty do administrace. Web nemá zákaznické účty ani registraci —
|
||||
* existuje jediný účet majitele, založený při prvním spuštění.
|
||||
*/
|
||||
export const Users: CollectionConfig = {
|
||||
slug: 'users',
|
||||
labels: {
|
||||
singular: 'Uživatel',
|
||||
plural: 'Uživatelé',
|
||||
},
|
||||
admin: {
|
||||
useAsTitle: 'email',
|
||||
},
|
||||
auth: {
|
||||
// Ochrana proti hádání hesla — admin je na veřejné doméně.
|
||||
maxLoginAttempts: 5,
|
||||
lockTime: 10 * 60 * 1000, // 10 minut
|
||||
},
|
||||
access: {
|
||||
// Nikdo se nesmí zaregistrovat sám; účty zakládá jen přihlášený uživatel.
|
||||
create: ({ req }) => Boolean(req.user),
|
||||
read: ({ req }) => Boolean(req.user),
|
||||
update: ({ req }) => Boolean(req.user),
|
||||
delete: ({ req }) => Boolean(req.user),
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'jmeno',
|
||||
label: 'Jméno',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Product } from '@/payload-types'
|
||||
|
||||
/** `28900` → `28 900 Kč` (pevná mezera, ať se cena nezalomí). */
|
||||
export function formatujCenu(cena: number): string {
|
||||
return `${cena.toLocaleString('cs-CZ').replace(/\s/g, ' ')} Kč`
|
||||
}
|
||||
|
||||
const STAVY: Record<Product['stav'], string> = {
|
||||
skladem: 'Skladem',
|
||||
'na-objednavku': 'Na objednávku',
|
||||
prodano: 'Prodáno',
|
||||
}
|
||||
|
||||
export function popisStavu(stav: Product['stav']): string {
|
||||
return STAVY[stav]
|
||||
}
|
||||
@@ -0,0 +1,509 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* This file was automatically generated by Payload.
|
||||
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
|
||||
* and re-run `payload generate:types` to regenerate this file.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Supported timezones in IANA format.
|
||||
*
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "supportedTimezones".
|
||||
*/
|
||||
export type SupportedTimezones =
|
||||
| 'Pacific/Midway'
|
||||
| 'Pacific/Niue'
|
||||
| 'Pacific/Honolulu'
|
||||
| 'Pacific/Rarotonga'
|
||||
| 'America/Anchorage'
|
||||
| 'Pacific/Gambier'
|
||||
| 'America/Los_Angeles'
|
||||
| 'America/Tijuana'
|
||||
| 'America/Denver'
|
||||
| 'America/Phoenix'
|
||||
| 'America/Chicago'
|
||||
| 'America/Guatemala'
|
||||
| 'America/New_York'
|
||||
| 'America/Bogota'
|
||||
| 'America/Caracas'
|
||||
| 'America/Santiago'
|
||||
| 'America/Buenos_Aires'
|
||||
| 'America/Sao_Paulo'
|
||||
| 'Atlantic/South_Georgia'
|
||||
| 'Atlantic/Azores'
|
||||
| 'Atlantic/Cape_Verde'
|
||||
| 'Europe/London'
|
||||
| 'Europe/Berlin'
|
||||
| 'Africa/Lagos'
|
||||
| 'Europe/Athens'
|
||||
| 'Africa/Cairo'
|
||||
| 'Europe/Moscow'
|
||||
| 'Asia/Riyadh'
|
||||
| 'Asia/Dubai'
|
||||
| 'Asia/Baku'
|
||||
| 'Asia/Karachi'
|
||||
| 'Asia/Tashkent'
|
||||
| 'Asia/Calcutta'
|
||||
| 'Asia/Dhaka'
|
||||
| 'Asia/Almaty'
|
||||
| 'Asia/Jakarta'
|
||||
| 'Asia/Bangkok'
|
||||
| 'Asia/Shanghai'
|
||||
| 'Asia/Singapore'
|
||||
| 'Asia/Tokyo'
|
||||
| 'Asia/Seoul'
|
||||
| 'Australia/Brisbane'
|
||||
| 'Australia/Sydney'
|
||||
| 'Pacific/Guam'
|
||||
| 'Pacific/Noumea'
|
||||
| 'Pacific/Auckland'
|
||||
| 'Pacific/Fiji';
|
||||
|
||||
export interface Config {
|
||||
auth: {
|
||||
users: UserAuthOperations;
|
||||
};
|
||||
blocks: {};
|
||||
collections: {
|
||||
products: Product;
|
||||
media: Media;
|
||||
pages: Page;
|
||||
users: User;
|
||||
'payload-kv': PayloadKv;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
};
|
||||
collectionsJoins: {};
|
||||
collectionsSelect: {
|
||||
products: ProductsSelect<false> | ProductsSelect<true>;
|
||||
media: MediaSelect<false> | MediaSelect<true>;
|
||||
pages: PagesSelect<false> | PagesSelect<true>;
|
||||
users: UsersSelect<false> | UsersSelect<true>;
|
||||
'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>;
|
||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||
};
|
||||
db: {
|
||||
defaultIDType: number;
|
||||
};
|
||||
fallbackLocale: null;
|
||||
globals: {};
|
||||
globalsSelect: {};
|
||||
locale: null;
|
||||
widgets: {
|
||||
collections: CollectionsWidget;
|
||||
};
|
||||
user: User;
|
||||
jobs: {
|
||||
tasks: unknown;
|
||||
workflows: unknown;
|
||||
};
|
||||
}
|
||||
export interface UserAuthOperations {
|
||||
forgotPassword: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
login: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
registerFirstUser: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
unlock: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Zboží zobrazené na webu. Skryté položky se veřejně nezobrazí.
|
||||
*
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "products".
|
||||
*/
|
||||
export interface Product {
|
||||
id: number;
|
||||
nazev: string;
|
||||
/**
|
||||
* Vyplní se samo z názvu, pokud necháte prázdné.
|
||||
*/
|
||||
slug?: string | null;
|
||||
kategorie: 'zbrane' | 'strelivo' | 'doplnky';
|
||||
cena: number;
|
||||
stav: 'skladem' | 'na-objednavku' | 'prodano';
|
||||
/**
|
||||
* Rozpracovaná položka, kterou zatím nikdo nemá vidět.
|
||||
*/
|
||||
skryto?: boolean | null;
|
||||
/**
|
||||
* První obrázek se použije jako náhled ve výpisu.
|
||||
*/
|
||||
obrazky?:
|
||||
| {
|
||||
obrazek: number | Media;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
/**
|
||||
* Např. Ráže → 9 mm Luger, Délka hlavně → 108 mm.
|
||||
*/
|
||||
parametry?:
|
||||
| {
|
||||
nazev: string;
|
||||
hodnota: string;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
/**
|
||||
* Nepovinný delší text pod parametry.
|
||||
*/
|
||||
popis?: string | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "media".
|
||||
*/
|
||||
export interface Media {
|
||||
id: number;
|
||||
alt: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
url?: string | null;
|
||||
thumbnailURL?: string | null;
|
||||
filename?: string | null;
|
||||
mimeType?: string | null;
|
||||
filesize?: number | null;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
focalX?: number | null;
|
||||
focalY?: number | null;
|
||||
sizes?: {
|
||||
nahled?: {
|
||||
url?: string | null;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
mimeType?: string | null;
|
||||
filesize?: number | null;
|
||||
filename?: string | null;
|
||||
};
|
||||
detail?: {
|
||||
url?: string | null;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
mimeType?: string | null;
|
||||
filesize?: number | null;
|
||||
filename?: string | null;
|
||||
};
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "pages".
|
||||
*/
|
||||
export interface Page {
|
||||
id: number;
|
||||
nazev: string;
|
||||
/**
|
||||
* Např. `kontakt` → /kontakt
|
||||
*/
|
||||
slug: string;
|
||||
obsah?: {
|
||||
root: {
|
||||
type: string;
|
||||
children: {
|
||||
type: any;
|
||||
version: number;
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
direction: ('ltr' | 'rtl') | null;
|
||||
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||
indent: number;
|
||||
version: number;
|
||||
};
|
||||
[k: string]: unknown;
|
||||
} | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: number;
|
||||
jmeno?: string | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
resetPasswordToken?: string | null;
|
||||
resetPasswordExpiration?: string | null;
|
||||
salt?: string | null;
|
||||
hash?: string | null;
|
||||
loginAttempts?: number | null;
|
||||
lockUntil?: string | null;
|
||||
sessions?:
|
||||
| {
|
||||
id: string;
|
||||
createdAt?: string | null;
|
||||
expiresAt: string;
|
||||
}[]
|
||||
| null;
|
||||
password?: string | null;
|
||||
collection: 'users';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-kv".
|
||||
*/
|
||||
export interface PayloadKv {
|
||||
id: number;
|
||||
key: string;
|
||||
data:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents".
|
||||
*/
|
||||
export interface PayloadLockedDocument {
|
||||
id: number;
|
||||
document?:
|
||||
| ({
|
||||
relationTo: 'products';
|
||||
value: number | Product;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'media';
|
||||
value: number | Media;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'pages';
|
||||
value: number | Page;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'users';
|
||||
value: number | User;
|
||||
} | null);
|
||||
globalSlug?: string | null;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: number | User;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences".
|
||||
*/
|
||||
export interface PayloadPreference {
|
||||
id: number;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: number | User;
|
||||
};
|
||||
key?: string | null;
|
||||
value?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-migrations".
|
||||
*/
|
||||
export interface PayloadMigration {
|
||||
id: number;
|
||||
name?: string | null;
|
||||
batch?: number | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "products_select".
|
||||
*/
|
||||
export interface ProductsSelect<T extends boolean = true> {
|
||||
nazev?: T;
|
||||
slug?: T;
|
||||
kategorie?: T;
|
||||
cena?: T;
|
||||
stav?: T;
|
||||
skryto?: T;
|
||||
obrazky?:
|
||||
| T
|
||||
| {
|
||||
obrazek?: T;
|
||||
id?: T;
|
||||
};
|
||||
parametry?:
|
||||
| T
|
||||
| {
|
||||
nazev?: T;
|
||||
hodnota?: T;
|
||||
id?: T;
|
||||
};
|
||||
popis?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "media_select".
|
||||
*/
|
||||
export interface MediaSelect<T extends boolean = true> {
|
||||
alt?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
url?: T;
|
||||
thumbnailURL?: T;
|
||||
filename?: T;
|
||||
mimeType?: T;
|
||||
filesize?: T;
|
||||
width?: T;
|
||||
height?: T;
|
||||
focalX?: T;
|
||||
focalY?: T;
|
||||
sizes?:
|
||||
| T
|
||||
| {
|
||||
nahled?:
|
||||
| T
|
||||
| {
|
||||
url?: T;
|
||||
width?: T;
|
||||
height?: T;
|
||||
mimeType?: T;
|
||||
filesize?: T;
|
||||
filename?: T;
|
||||
};
|
||||
detail?:
|
||||
| T
|
||||
| {
|
||||
url?: T;
|
||||
width?: T;
|
||||
height?: T;
|
||||
mimeType?: T;
|
||||
filesize?: T;
|
||||
filename?: T;
|
||||
};
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "pages_select".
|
||||
*/
|
||||
export interface PagesSelect<T extends boolean = true> {
|
||||
nazev?: T;
|
||||
slug?: T;
|
||||
obsah?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users_select".
|
||||
*/
|
||||
export interface UsersSelect<T extends boolean = true> {
|
||||
jmeno?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
email?: T;
|
||||
resetPasswordToken?: T;
|
||||
resetPasswordExpiration?: T;
|
||||
salt?: T;
|
||||
hash?: T;
|
||||
loginAttempts?: T;
|
||||
lockUntil?: T;
|
||||
sessions?:
|
||||
| T
|
||||
| {
|
||||
id?: T;
|
||||
createdAt?: T;
|
||||
expiresAt?: T;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-kv_select".
|
||||
*/
|
||||
export interface PayloadKvSelect<T extends boolean = true> {
|
||||
key?: T;
|
||||
data?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents_select".
|
||||
*/
|
||||
export interface PayloadLockedDocumentsSelect<T extends boolean = true> {
|
||||
document?: T;
|
||||
globalSlug?: T;
|
||||
user?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences_select".
|
||||
*/
|
||||
export interface PayloadPreferencesSelect<T extends boolean = true> {
|
||||
user?: T;
|
||||
key?: T;
|
||||
value?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-migrations_select".
|
||||
*/
|
||||
export interface PayloadMigrationsSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
batch?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "collections_widget".
|
||||
*/
|
||||
export interface CollectionsWidget {
|
||||
data?: {
|
||||
[k: string]: unknown;
|
||||
};
|
||||
width: 'full';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "auth".
|
||||
*/
|
||||
export interface Auth {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
|
||||
declare module 'payload' {
|
||||
export interface GeneratedTypes extends Config {}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { sqliteAdapter } from '@payloadcms/db-sqlite'
|
||||
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
import path from 'path'
|
||||
import { buildConfig } from 'payload'
|
||||
import { fileURLToPath } from 'url'
|
||||
import sharp from 'sharp'
|
||||
|
||||
import { Media } from './collections/Media'
|
||||
import { Pages } from './collections/Pages'
|
||||
import { Products } from './collections/Products'
|
||||
import { Users } from './collections/Users'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
/**
|
||||
* Payload odmítá zápisy z jiného původu, než je `serverURL` — jinak by cizí
|
||||
* stránka mohla zneužít přihlašovací cookie. Adresa ze `serverURL` platí vždy;
|
||||
* tohle je pro případy, kdy se admin otevírá i odjinud (třeba po IP v LAN).
|
||||
* Formát: CSRF_ORIGINS=http://10.0.0.110:3000,http://notebook:3000
|
||||
*/
|
||||
const dalsiPovoleneOrigins = (process.env.CSRF_ORIGINS ?? '')
|
||||
.split(',')
|
||||
.map((o) => o.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
export default buildConfig({
|
||||
admin: {
|
||||
user: Users.slug,
|
||||
importMap: {
|
||||
baseDir: path.resolve(dirname),
|
||||
},
|
||||
meta: {
|
||||
titleSuffix: ' — Reiner zbraně',
|
||||
},
|
||||
},
|
||||
collections: [Products, Media, Pages, Users],
|
||||
editor: lexicalEditor(),
|
||||
secret: process.env.PAYLOAD_SECRET || '',
|
||||
serverURL: process.env.NEXT_PUBLIC_SERVER_URL,
|
||||
csrf: dalsiPovoleneOrigins,
|
||||
typescript: {
|
||||
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
||||
},
|
||||
db: sqliteAdapter({
|
||||
client: {
|
||||
url: process.env.DATABASE_URI || 'file:./data/reiner.db',
|
||||
},
|
||||
// Migrace se generují do `migrations/` a commitují — produkce je pak
|
||||
// jen přehraje, nikdy neodvozuje schéma za běhu.
|
||||
// Výjimka jsou testy: tam se schéma odvodí z konfigurace, aby každý běh
|
||||
// startoval nad prázdnou databází bez přehrávání celé historie migrací.
|
||||
push: process.env.NODE_ENV === 'test',
|
||||
migrationDir: path.resolve(dirname, '../migrations'),
|
||||
}),
|
||||
sharp,
|
||||
plugins: [],
|
||||
})
|
||||
Reference in New Issue
Block a user