Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f80e72c1d | |||
| d9334f558d | |||
| cb436d31d0 |
180
lib/pdf-page.tsx
180
lib/pdf-page.tsx
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { Document, Page, View, Text, Image, StyleSheet, Font, Link } from '@react-pdf/renderer';
|
||||
import { Document, Page, View, Text, StyleSheet, Font, Link } from '@react-pdf/renderer';
|
||||
|
||||
// Register fonts (using system fonts for now, can be customized)
|
||||
Font.register({
|
||||
@@ -10,120 +10,120 @@ Font.register({
|
||||
],
|
||||
});
|
||||
|
||||
// ─── Brand Tokens ────────────────────────────────────────
|
||||
|
||||
// ─── Brand Tokens (matching datasheet) ──────────────────────────────────
|
||||
const C = {
|
||||
navy: '#001a4d',
|
||||
navyDeep: '#000d26',
|
||||
green: '#4da612',
|
||||
greenLight: '#e8f5d8',
|
||||
white: '#FFFFFF',
|
||||
offWhite: '#f8f9fa',
|
||||
gray100: '#f3f4f6',
|
||||
gray200: '#e5e7eb',
|
||||
gray300: '#d1d5db',
|
||||
gray400: '#9ca3af',
|
||||
gray600: '#4b5563',
|
||||
gray900: '#111827',
|
||||
};
|
||||
|
||||
const MARGIN = 56;
|
||||
const HEADER_H = 52;
|
||||
const FOOTER_H = 48;
|
||||
const BODY_TOP = HEADER_H + 20;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
page: {
|
||||
color: C.gray900,
|
||||
lineHeight: 1.5,
|
||||
backgroundColor: C.white,
|
||||
paddingTop: BODY_TOP,
|
||||
paddingBottom: FOOTER_H + 40,
|
||||
paddingHorizontal: MARGIN,
|
||||
paddingTop: 0,
|
||||
paddingBottom: 80,
|
||||
fontFamily: 'Helvetica',
|
||||
},
|
||||
header: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: HEADER_H,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-end',
|
||||
|
||||
// Hero-style header
|
||||
hero: {
|
||||
backgroundColor: C.white,
|
||||
paddingTop: 24,
|
||||
paddingBottom: 0,
|
||||
paddingHorizontal: MARGIN,
|
||||
paddingBottom: 12,
|
||||
marginBottom: 20,
|
||||
position: 'relative',
|
||||
borderBottomWidth: 0,
|
||||
},
|
||||
logoText: {
|
||||
fontSize: 16,
|
||||
fontWeight: 700,
|
||||
color: C.navy,
|
||||
},
|
||||
docTitleLabel: {
|
||||
fontSize: 7,
|
||||
fontWeight: 700,
|
||||
color: C.gray400,
|
||||
letterSpacing: 1.2,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
footer: {
|
||||
position: 'absolute',
|
||||
bottom: 20,
|
||||
left: MARGIN,
|
||||
right: MARGIN,
|
||||
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
borderTopWidth: 0.5,
|
||||
borderTopColor: C.gray200,
|
||||
paddingTop: 8,
|
||||
marginBottom: 16,
|
||||
},
|
||||
footerText: {
|
||||
fontSize: 7,
|
||||
color: C.gray400,
|
||||
letterSpacing: 0.8,
|
||||
|
||||
logoText: {
|
||||
fontSize: 22,
|
||||
fontWeight: 700,
|
||||
color: C.navyDeep,
|
||||
letterSpacing: 2,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
|
||||
docTitle: {
|
||||
fontSize: 8,
|
||||
fontWeight: 700,
|
||||
color: C.green,
|
||||
letterSpacing: 2,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
|
||||
// Content Area
|
||||
content: {
|
||||
paddingHorizontal: MARGIN,
|
||||
},
|
||||
|
||||
pageTitle: {
|
||||
fontSize: 24,
|
||||
fontWeight: 700,
|
||||
color: C.navyDeep,
|
||||
marginBottom: 16,
|
||||
marginBottom: 8,
|
||||
marginTop: 10,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: -0.5,
|
||||
},
|
||||
|
||||
accentBar: {
|
||||
width: 40,
|
||||
height: 3,
|
||||
width: 30,
|
||||
height: 2,
|
||||
backgroundColor: C.green,
|
||||
marginBottom: 24,
|
||||
marginBottom: 20,
|
||||
borderRadius: 1,
|
||||
},
|
||||
|
||||
// Lexical Elements
|
||||
paragraph: {
|
||||
fontSize: 10,
|
||||
color: C.gray600,
|
||||
lineHeight: 1.6,
|
||||
lineHeight: 1.7,
|
||||
marginBottom: 12,
|
||||
},
|
||||
heading1: {
|
||||
fontSize: 18,
|
||||
fontSize: 16,
|
||||
fontWeight: 700,
|
||||
color: C.navyDeep,
|
||||
marginTop: 20,
|
||||
marginBottom: 10,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
heading2: {
|
||||
fontSize: 12,
|
||||
fontWeight: 700,
|
||||
color: C.navyDeep,
|
||||
marginTop: 16,
|
||||
marginBottom: 8,
|
||||
},
|
||||
heading2: {
|
||||
fontSize: 14,
|
||||
fontWeight: 700,
|
||||
color: C.navyDeep,
|
||||
marginTop: 14,
|
||||
marginBottom: 6,
|
||||
},
|
||||
heading3: {
|
||||
fontSize: 12,
|
||||
fontSize: 10,
|
||||
fontWeight: 700,
|
||||
color: C.navyDeep,
|
||||
marginTop: 12,
|
||||
marginBottom: 4,
|
||||
marginBottom: 6,
|
||||
},
|
||||
list: {
|
||||
marginBottom: 12,
|
||||
@@ -137,12 +137,13 @@ const styles = StyleSheet.create({
|
||||
width: 12,
|
||||
fontSize: 10,
|
||||
color: C.green,
|
||||
fontWeight: 700,
|
||||
},
|
||||
listItemContent: {
|
||||
flex: 1,
|
||||
fontSize: 10,
|
||||
color: C.gray600,
|
||||
lineHeight: 1.6,
|
||||
lineHeight: 1.7,
|
||||
},
|
||||
link: {
|
||||
color: C.green,
|
||||
@@ -154,7 +155,37 @@ const styles = StyleSheet.create({
|
||||
color: C.navyDeep,
|
||||
},
|
||||
textItalic: {
|
||||
fontStyle: 'italic', // Not actually working without proper font set, but we will fallback
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
|
||||
// Footer — matches brochure style
|
||||
footer: {
|
||||
position: 'absolute',
|
||||
bottom: 28,
|
||||
left: MARGIN,
|
||||
right: MARGIN,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingTop: 12,
|
||||
borderTopWidth: 2,
|
||||
borderTopColor: C.green,
|
||||
},
|
||||
|
||||
footerText: {
|
||||
fontSize: 7,
|
||||
color: C.gray400,
|
||||
fontWeight: 400,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.8,
|
||||
},
|
||||
|
||||
footerBrand: {
|
||||
fontSize: 9,
|
||||
fontWeight: 700,
|
||||
color: C.navyDeep,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 1.5,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -232,11 +263,10 @@ const renderLexicalNode = (node: any, idx: number): React.ReactNode => {
|
||||
}
|
||||
|
||||
case 'linebreak': {
|
||||
// React-PDF doesn't handle `<br/>`, but a newline char usually works inside `<Text>`.
|
||||
return <Text key={idx}>{'\n'}</Text>;
|
||||
}
|
||||
|
||||
// Ignore payload blocks recursively to avoid crashing, as pages should mainly use rich text
|
||||
// Ignore payload blocks recursively to avoid crashing
|
||||
case 'block':
|
||||
return null;
|
||||
|
||||
@@ -267,20 +297,30 @@ export const PDFPage: React.FC<PDFPageProps> = ({ page, locale = 'de' }) => {
|
||||
return (
|
||||
<Document>
|
||||
<Page size="A4" style={styles.page}>
|
||||
<View style={styles.header} fixed>
|
||||
<Text style={styles.logoText}>KLZ</Text>
|
||||
<Text style={styles.docTitleLabel}>{locale === 'en' ? 'Document' : 'Dokument'}</Text>
|
||||
{/* Hero Header */}
|
||||
<View style={styles.hero} fixed>
|
||||
<View style={styles.header}>
|
||||
<View>
|
||||
<Text style={styles.logoText}>KLZ</Text>
|
||||
</View>
|
||||
<Text style={styles.docTitle}>{locale === 'en' ? 'Document' : 'Dokument'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text style={styles.pageTitle}>{page.title}</Text>
|
||||
<View style={styles.accentBar} />
|
||||
<View style={styles.content}>
|
||||
<Text style={styles.pageTitle}>{page.title}</Text>
|
||||
<View style={styles.accentBar} />
|
||||
|
||||
<View>
|
||||
{page.content?.root?.children?.map((node: any, i: number) => renderLexicalNode(node, i))}
|
||||
<View>
|
||||
{page.content?.root?.children?.map((node: any, i: number) =>
|
||||
renderLexicalNode(node, i),
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Minimal footer */}
|
||||
<View style={styles.footer} fixed>
|
||||
<Text style={styles.footerText}>KLZ CABLES</Text>
|
||||
<Text style={styles.footerBrand}>KLZ CABLES</Text>
|
||||
<Text style={styles.footerText}>{dateStr}</Text>
|
||||
</View>
|
||||
</Page>
|
||||
|
||||
@@ -71,6 +71,8 @@ export default buildConfig({
|
||||
},
|
||||
db: postgresAdapter({
|
||||
prodMigrations: migrations,
|
||||
migrationDir:
|
||||
process.env.NODE_ENV === 'production' ? undefined : path.resolve(dirname, 'src/migrations'),
|
||||
pool: {
|
||||
connectionString:
|
||||
process.env.DATABASE_URI ||
|
||||
|
||||
52
src/migrations/20260305_215000_products_featured_image.ts
Normal file
52
src/migrations/20260305_215000_products_featured_image.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres';
|
||||
|
||||
export async function up({ db }: MigrateUpArgs): Promise<void> {
|
||||
// Add featured_image_id to products and _products_v
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "products" ADD COLUMN IF NOT EXISTS "featured_image_id" integer;
|
||||
`);
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "_products_v" ADD COLUMN IF NOT EXISTS "version_featured_image_id" integer;
|
||||
`);
|
||||
|
||||
// Add foreign key constraints
|
||||
await db.execute(sql`
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "products" ADD CONSTRAINT "products_featured_image_id_media_id_fk" FOREIGN KEY ("featured_image_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action;
|
||||
EXCEPTION WHEN duplicate_object THEN null; END $$;
|
||||
`);
|
||||
await db.execute(sql`
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "_products_v" ADD CONSTRAINT "_products_v_version_featured_image_id_media_id_fk" FOREIGN KEY ("version_featured_image_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action;
|
||||
EXCEPTION WHEN duplicate_object THEN null; END $$;
|
||||
`);
|
||||
|
||||
// Add indexes
|
||||
await db.execute(sql`
|
||||
CREATE INDEX IF NOT EXISTS "products_featured_image_idx" ON "products" USING btree ("featured_image_id");
|
||||
`);
|
||||
await db.execute(sql`
|
||||
CREATE INDEX IF NOT EXISTS "_products_v_version_version_featured_image_idx" ON "_products_v" USING btree ("version_featured_image_id");
|
||||
`);
|
||||
}
|
||||
|
||||
export async function down({ db }: MigrateDownArgs): Promise<void> {
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "products" DROP CONSTRAINT IF EXISTS "products_featured_image_id_media_id_fk";
|
||||
`);
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "_products_v" DROP CONSTRAINT IF EXISTS "_products_v_version_featured_image_id_media_id_fk";
|
||||
`);
|
||||
await db.execute(sql`
|
||||
DROP INDEX IF EXISTS "products_featured_image_idx";
|
||||
`);
|
||||
await db.execute(sql`
|
||||
DROP INDEX IF EXISTS "_products_v_version_version_featured_image_idx";
|
||||
`);
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "products" DROP COLUMN IF EXISTS "featured_image_id";
|
||||
`);
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "_products_v" DROP COLUMN IF EXISTS "version_featured_image_id";
|
||||
`);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import * as migration_20260223_195005_products_collection from './20260223_19500
|
||||
import * as migration_20260223_195151_remove_sku_unique from './20260223_195151_remove_sku_unique';
|
||||
import * as migration_20260225_003500_add_pages_collection from './20260225_003500_add_pages_collection';
|
||||
import * as migration_20260225_175000_native_localization from './20260225_175000_native_localization';
|
||||
import * as migration_20260305_215000_products_featured_image from './20260305_215000_products_featured_image';
|
||||
|
||||
export const migrations = [
|
||||
{
|
||||
@@ -24,4 +25,9 @@ export const migrations = [
|
||||
down: migration_20260225_175000_native_localization.down,
|
||||
name: '20260225_175000_native_localization',
|
||||
},
|
||||
{
|
||||
up: migration_20260305_215000_products_featured_image.up,
|
||||
down: migration_20260305_215000_products_featured_image.down,
|
||||
name: '20260305_215000_products_featured_image',
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user