nordabiz/scripts/create_presentation.js
Maciej Pienczyn 110d971dca
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
feat: migrate prod docs to OVH VPS + UTC→Warsaw timezone in all templates
Production moved from on-prem VM 249 (10.22.68.249) to OVH VPS
(57.128.200.27, inpi-vps-waw01). Updated ALL documentation, slash
commands, memory files, architecture docs, and deploy procedures.

Added |local_time Jinja filter (UTC→Europe/Warsaw) and converted
155 .strftime() calls across 71 templates so timestamps display
in Polish timezone regardless of server timezone.

Also includes: created_by_id tracking, abort import fix, ICS
calendar fix for missing end times, Pros Poland data cleanup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 13:41:53 +02:00

1113 lines
36 KiB
JavaScript

const pptxgen = require("pptxgenjs");
const React = require("react");
const ReactDOMServer = require("react-dom/server");
const sharp = require("sharp");
const {
FaBuilding, FaRobot, FaComments, FaCalendarAlt, FaAtom, FaShieldAlt,
FaRocket, FaQuestion, FaSearch, FaUsers, FaBullhorn, FaLock,
FaChartLine, FaHandshake, FaIndustry, FaGlobe, FaEnvelope, FaBell,
FaMobileAlt, FaNewspaper, FaUserShield, FaMoneyBillWave, FaStar,
FaCheckCircle, FaLightbulb, FaMapMarkedAlt, FaFileAlt
} = require("react-icons/fa");
function renderIconSvg(IconComponent, color = "#000000", size = 256) {
return ReactDOMServer.renderToStaticMarkup(
React.createElement(IconComponent, { color, size: String(size) })
);
}
async function iconToBase64Png(IconComponent, color, size = 256) {
const svg = renderIconSvg(IconComponent, color, size);
const pngBuffer = await sharp(Buffer.from(svg)).png().toBuffer();
return "image/png;base64," + pngBuffer.toString("base64");
}
// Color palette
const C = {
darkBg: "0F172A",
navyBg: "1E293B",
primary: "0891B2",
primaryLight: "22D3EE",
accent: "06B6D4",
lightBg: "F1F5F9",
white: "FFFFFF",
cardBg: "FFFFFF",
textDark: "1E293B",
textMuted: "64748B",
textLight: "CBD5E1",
green: "10B981",
amber: "F59E0B",
red: "EF4444",
purple: "8B5CF6",
indigo: "6366F1",
tealDark: "065F46",
};
const FONT_TITLE = "Georgia";
const FONT_BODY = "Calibri";
const makeShadow = () => ({ type: "outer", blur: 6, offset: 2, angle: 135, color: "000000", opacity: 0.12 });
function addSlideNumber(slide, num, total) {
slide.addText(`${num} / ${total}`, {
x: 8.8, y: 5.2, w: 1, h: 0.3,
fontSize: 9, fontFace: FONT_BODY, color: C.textLight, align: "right"
});
}
function addSectionHeader(slide, icon, title, subtitle, num, total) {
// Dark left panel
slide.addShape(slide._slideLayout ? "rect" : "rect", {});
// We'll just build the full slide manually
}
async function createPresentation() {
const pres = new pptxgen();
pres.layout = "LAYOUT_16x9";
pres.author = "Maciej Pienczyn";
pres.title = "NordaBiznes.pl — Prezentacja portalu";
const TOTAL = 14;
// Pre-render icons
const icons = {};
const iconMap = {
building: [FaBuilding, C.white],
robot: [FaRobot, C.white],
comments: [FaComments, C.white],
calendar: [FaCalendarAlt, C.white],
atom: [FaAtom, C.white],
shield: [FaShieldAlt, C.white],
rocket: [FaRocket, C.white],
question: [FaQuestion, C.white],
search: [FaSearch, C.primary],
users: [FaUsers, C.primary],
bullhorn: [FaBullhorn, C.primary],
lock: [FaLock, C.primary],
chart: [FaChartLine, C.primary],
handshake: [FaHandshake, C.primary],
industry: [FaIndustry, C.primary],
globe: [FaGlobe, C.primary],
envelope: [FaEnvelope, C.primary],
bell: [FaBell, C.primary],
mobile: [FaMobileAlt, C.primary],
newspaper: [FaNewspaper, C.primary],
userShield: [FaUserShield, C.primary],
money: [FaMoneyBillWave, C.primary],
star: [FaStar, C.amber],
check: [FaCheckCircle, C.green],
lightbulb: [FaLightbulb, C.amber],
map: [FaMapMarkedAlt, C.primary],
file: [FaFileAlt, C.primary],
// Dark bg versions
buildingDark: [FaBuilding, C.primaryLight],
robotDark: [FaRobot, C.primaryLight],
commentsDark: [FaComments, C.primaryLight],
calendarDark: [FaCalendarAlt, C.primaryLight],
atomDark: [FaAtom, C.primaryLight],
industryDark: [FaIndustry, C.primaryLight],
shieldDark: [FaShieldAlt, C.primaryLight],
rocketDark: [FaRocket, C.primaryLight],
checkWhite: [FaCheckCircle, C.white],
globeWhite: [FaGlobe, C.white],
searchWhite: [FaSearch, C.white],
lightbulbWhite: [FaLightbulb, C.white],
handshakeWhite: [FaHandshake, C.white],
};
for (const [key, [comp, color]] of Object.entries(iconMap)) {
icons[key] = await iconToBase64Png(comp, `#${color}`, 256);
}
// Helper: colored icon circle
function addIconCircle(slide, iconKey, x, y, size, bgColor) {
slide.addShape(pres.shapes.OVAL, {
x, y, w: size, h: size,
fill: { color: bgColor }
});
const pad = size * 0.25;
slide.addImage({
data: icons[iconKey],
x: x + pad, y: y + pad,
w: size - pad * 2, h: size - pad * 2
});
}
// Helper: content card with left accent
function addCard(slide, x, y, w, h, accentColor) {
slide.addShape(pres.shapes.RECTANGLE, {
x, y, w, h,
fill: { color: C.white },
shadow: makeShadow()
});
slide.addShape(pres.shapes.RECTANGLE, {
x, y, w: 0.06, h,
fill: { color: accentColor || C.primary }
});
}
// Logo paths
const nordaLogoPath = "/Users/maciejpi/claude/projects/active/nordabiz/static/img/logo-email.png";
const inpiLogoPath = "/tmp/inpi-logo.png";
// =============================================
// SLIDE 1: TYTUŁ
// =============================================
let s = pres.addSlide();
s.background = { color: C.darkBg };
// Accent line top
s.addShape(pres.shapes.RECTANGLE, {
x: 0, y: 0, w: 10, h: 0.06,
fill: { color: C.primary }
});
// Norda compass logo — right side, large
s.addImage({
path: nordaLogoPath,
x: 7.5, y: 1.2, w: 2.0, h: 2.0,
transparency: 15
});
s.addText("NordaBiznes.pl", {
x: 0.8, y: 1.2, w: 6.5, h: 1.2,
fontSize: 48, fontFace: FONT_TITLE, color: C.white, bold: true, margin: 0
});
s.addText("Platforma członków Izby NORDA", {
x: 0.8, y: 2.3, w: 6.5, h: 0.6,
fontSize: 22, fontFace: FONT_BODY, color: C.primaryLight, margin: 0
});
s.addShape(pres.shapes.RECTANGLE, {
x: 0.8, y: 3.2, w: 2, h: 0.04,
fill: { color: C.primary }
});
s.addText("Prezentacja dla członków Izby", {
x: 0.8, y: 3.6, w: 5, h: 0.4,
fontSize: 14, fontFace: FONT_BODY, color: C.textLight, margin: 0
});
s.addText("9 kwietnia 2026 | Urząd Miasta Wejherowo", {
x: 0.8, y: 4.0, w: 5, h: 0.4,
fontSize: 14, fontFace: FONT_BODY, color: C.textLight, margin: 0
});
s.addText("Maciej Pienczyn | InPi sp. z o.o.", {
x: 0.8, y: 4.6, w: 5, h: 0.4,
fontSize: 14, fontFace: FONT_BODY, color: C.textLight, margin: 0
});
// =============================================
// SLIDE 2: AGENDA
// =============================================
s = pres.addSlide();
s.background = { color: C.lightBg };
s.addText("Co dziś pokażemy", {
x: 0.8, y: 0.4, w: 8.4, h: 0.7,
fontSize: 32, fontFace: FONT_TITLE, color: C.textDark, bold: true, margin: 0
});
s.addShape(pres.shapes.RECTANGLE, {
x: 0.8, y: 1.05, w: 1.2, h: 0.04, fill: { color: C.primary }
});
const agendaItems = [
["building", "Twoja firma w Izbie", "Profil, wizytówka, wyszukiwanie"],
["robot", "NordaGPT", "Asystent AI, który zna wszystkich"],
["comments", "Komunikacja", "Wiadomości, forum, ogłoszenia B2B"],
["calendar", "Kalendarz", "Wydarzenia, zapisy, przypomnienia"],
["atom", "PEJ i Kaszubia", "Elektrownia jądrowa, inwestycje"],
["shield", "Prywatność", "Kto widzi co, bezpieczeństwo"],
];
const startY = 1.4;
for (let i = 0; i < agendaItems.length; i++) {
const [icon, title, desc] = agendaItems[i];
const row = i;
const yPos = startY + row * 0.65;
addIconCircle(s, icon, 0.8, yPos + 0.05, 0.45, C.primary);
s.addText(title, {
x: 1.45, y: yPos, w: 3, h: 0.35,
fontSize: 15, fontFace: FONT_BODY, color: C.textDark, bold: true, margin: 0, valign: "middle"
});
s.addText(desc, {
x: 1.45, y: yPos + 0.3, w: 5, h: 0.3,
fontSize: 12, fontFace: FONT_BODY, color: C.textMuted, margin: 0
});
}
// Right side - stats
addCard(s, 6.5, 1.4, 3, 3.5, C.primary);
s.addText("Portal w liczbach", {
x: 6.8, y: 1.6, w: 2.5, h: 0.35,
fontSize: 14, fontFace: FONT_BODY, color: C.textDark, bold: true, margin: 0
});
const stats = [
["150+", "firm członkowskich"],
["38", "uczestników spotkania"],
["57", "wydań od startu"],
["17", "kategorii branżowych"],
["6", "poziomów dostępu"],
];
for (let i = 0; i < stats.length; i++) {
s.addText(stats[i][0], {
x: 6.8, y: 2.15 + i * 0.52, w: 0.8, h: 0.4,
fontSize: 20, fontFace: FONT_TITLE, color: C.primary, bold: true, margin: 0, valign: "middle"
});
s.addText(stats[i][1], {
x: 7.7, y: 2.15 + i * 0.52, w: 1.6, h: 0.4,
fontSize: 12, fontFace: FONT_BODY, color: C.textMuted, margin: 0, valign: "middle"
});
}
addSlideNumber(s, 2, TOTAL);
// =============================================
// SLIDE 3: PO CO POWSTAŁ PORTAL
// =============================================
s = pres.addSlide();
s.background = { color: C.darkBg };
s.addShape(pres.shapes.RECTANGLE, {
x: 0, y: 0, w: 10, h: 0.06, fill: { color: C.primary }
});
s.addText("Po co powstał NordaBiznes?", {
x: 0.8, y: 0.4, w: 8.4, h: 0.7,
fontSize: 32, fontFace: FONT_TITLE, color: C.white, bold: true, margin: 0
});
const whyItems = [
[icons.handshakeWhite, "Łączyć firmy", "Znajdź partnera biznesowego w Izbie\nbez szukania numeru, maila czy pośredników"],
[icons.searchWhite, "Pokazywać", "Twoja firma widoczna dla wszystkich\nczłonków — wizytówka, usługi, dane kontaktowe"],
[icons.globeWhite, "Informować", "Aktualności, wydarzenia, PEJ,\ninwestycje na Kaszubach — wszystko w jednym"],
[icons.lightbulbWhite, "Wspierać AI", "NordaGPT zna firmy w Izbie\ni pomaga znaleźć właściwego partnera"],
];
for (let i = 0; i < whyItems.length; i++) {
const col = i % 2;
const row = Math.floor(i / 2);
const x = 0.8 + col * 4.5;
const y = 1.5 + row * 1.8;
s.addShape(pres.shapes.RECTANGLE, {
x, y, w: 4, h: 1.5,
fill: { color: C.navyBg },
shadow: makeShadow()
});
s.addShape(pres.shapes.RECTANGLE, {
x, y, w: 0.06, h: 1.5,
fill: { color: C.primary }
});
s.addImage({ data: whyItems[i][0], x: x + 0.3, y: y + 0.25, w: 0.4, h: 0.4 });
s.addText(whyItems[i][1], {
x: x + 0.9, y: y + 0.15, w: 2.8, h: 0.4,
fontSize: 16, fontFace: FONT_BODY, color: C.white, bold: true, margin: 0
});
s.addText(whyItems[i][2], {
x: x + 0.9, y: y + 0.55, w: 2.8, h: 0.8,
fontSize: 12, fontFace: FONT_BODY, color: C.textLight, margin: 0
});
}
addSlideNumber(s, 3, TOTAL);
// =============================================
// SLIDE 4: PROFIL FIRMY
// =============================================
s = pres.addSlide();
s.background = { color: C.lightBg };
s.addText("Twoja firma na portalu", {
x: 0.8, y: 0.4, w: 8.4, h: 0.7,
fontSize: 32, fontFace: FONT_TITLE, color: C.textDark, bold: true, margin: 0
});
s.addShape(pres.shapes.RECTANGLE, {
x: 0.8, y: 1.05, w: 1.2, h: 0.04, fill: { color: C.primary }
});
// Left column - what's on the profile
addCard(s, 0.8, 1.4, 4.2, 3.6, C.primary);
s.addText("Co jest na profilu?", {
x: 1.1, y: 1.55, w: 3.5, h: 0.35,
fontSize: 16, fontFace: FONT_BODY, color: C.textDark, bold: true, margin: 0
});
const profileItems = [
"Opis firmy, usługi, technologie",
"Dane kontaktowe (telefon, email, adres)",
"Strony internetowe (do 5)",
"Social media (FB, IG, LinkedIn, YT)",
"Godziny otwarcia (status na żywo)",
"Dane z rejestrów: KRS / CEIDG",
"Rekomendacje od innych firm",
"Zarząd, wspólnicy, kapitał",
];
s.addText(profileItems.map((item, i) => ({
text: item,
options: { bullet: true, breakLine: i < profileItems.length - 1, fontSize: 12, color: C.textDark }
})), {
x: 1.1, y: 2.0, w: 3.6, h: 2.8,
fontFace: FONT_BODY, paraSpaceAfter: 4, margin: 0
});
// Right column - what you can do
addCard(s, 5.4, 1.4, 4.2, 3.6, C.green);
s.addText("Co możesz zrobić?", {
x: 5.7, y: 1.55, w: 3.5, h: 0.35,
fontSize: 16, fontFace: FONT_BODY, color: C.textDark, bold: true, margin: 0
});
const actionItems = [
"Edytuj profil swojej firmy",
"Zarządzaj zespołem (role, uprawnienia)",
"Kontroluj co jest widoczne publicznie",
"Zaproś pracowników do portalu",
"Sprawdź wskaźnik kompletności",
"Pobierz dane z urzędowych rejestrów",
"Zbieraj AI o swojej firmie z internetu",
"Przeglądaj statystyki odwiedzin",
];
s.addText(actionItems.map((item, i) => ({
text: item,
options: { bullet: true, breakLine: i < actionItems.length - 1, fontSize: 12, color: C.textDark }
})), {
x: 5.7, y: 2.0, w: 3.6, h: 2.8,
fontFace: FONT_BODY, paraSpaceAfter: 4, margin: 0
});
addSlideNumber(s, 4, TOTAL);
// =============================================
// SLIDE 5: WYSZUKIWARKA I KATEGORIE
// =============================================
s = pres.addSlide();
s.background = { color: C.lightBg };
s.addText("Szukaj i znajdź", {
x: 0.8, y: 0.4, w: 8.4, h: 0.7,
fontSize: 32, fontFace: FONT_TITLE, color: C.textDark, bold: true, margin: 0
});
s.addShape(pres.shapes.RECTANGLE, {
x: 0.8, y: 1.05, w: 1.2, h: 0.04, fill: { color: C.primary }
});
s.addText("Wyszukiwarka rozumie literówki, synonimy i skróty", {
x: 0.8, y: 1.3, w: 8, h: 0.4,
fontSize: 14, fontFace: FONT_BODY, color: C.textMuted, margin: 0
});
// Category cards
const categories = [
["IT i technologie", "Software, hosting,\nautomatyzacja", C.indigo],
["Budownictwo", "Projekty, wykonawstwo,\ninstalacje", C.amber],
["Usługi", "Finanse, prawo,\nkonsulting, HR", C.green],
["Produkcja", "Przemysł, CNC,\nmetalurgia", C.red],
];
for (let i = 0; i < categories.length; i++) {
const x = 0.8 + i * 2.25;
const [name, desc, color] = categories[i];
s.addShape(pres.shapes.RECTANGLE, {
x, y: 1.9, w: 2.0, h: 1.4,
fill: { color: C.white }, shadow: makeShadow()
});
s.addShape(pres.shapes.RECTANGLE, {
x, y: 1.9, w: 2.0, h: 0.06,
fill: { color }
});
s.addText(name, {
x: x + 0.15, y: 2.1, w: 1.7, h: 0.35,
fontSize: 13, fontFace: FONT_BODY, color: C.textDark, bold: true, margin: 0
});
s.addText(desc, {
x: x + 0.15, y: 2.5, w: 1.7, h: 0.6,
fontSize: 11, fontFace: FONT_BODY, color: C.textMuted, margin: 0
});
}
// Bottom - search features
addCard(s, 0.8, 3.6, 8.4, 1.5, C.primary);
s.addText("Jak szukać?", {
x: 1.1, y: 3.75, w: 3, h: 0.35,
fontSize: 15, fontFace: FONT_BODY, color: C.textDark, bold: true, margin: 0
});
const searchFeatures = [
["Po nazwie firmy", "Wpisz fragment nazwy — znajdzie nawet z literówką"],
["Po NIP lub REGON", "Dokładne wyszukiwanie po numerach rejestrowych"],
["Po kategorii", "Filtruj po branży i podkategorii"],
["Przez NordaGPT", "Opisz czego szukasz swoimi słowami"],
];
for (let i = 0; i < searchFeatures.length; i++) {
const col = i % 2;
const row = Math.floor(i / 2);
const x = 1.1 + col * 4.1;
const y = 4.15 + row * 0.45;
s.addImage({ data: icons.check, x, y: y + 0.02, w: 0.2, h: 0.2 });
s.addText([
{ text: searchFeatures[i][0] + " ", options: { bold: true, fontSize: 11 } },
{ text: searchFeatures[i][1], options: { fontSize: 11, color: C.textMuted } }
], {
x: x + 0.3, y, w: 3.5, h: 0.35,
fontFace: FONT_BODY, color: C.textDark, margin: 0, valign: "middle"
});
}
addSlideNumber(s, 5, TOTAL);
// =============================================
// SLIDE 6: NORDAGPT
// =============================================
s = pres.addSlide();
s.background = { color: C.darkBg };
s.addShape(pres.shapes.RECTANGLE, {
x: 0, y: 0, w: 10, h: 0.06, fill: { color: C.primary }
});
addIconCircle(s, "robot", 0.8, 0.35, 0.55, C.primary);
s.addText("NordaGPT — asystent AI Izby", {
x: 1.5, y: 0.35, w: 7, h: 0.6,
fontSize: 30, fontFace: FONT_TITLE, color: C.white, bold: true, margin: 0, valign: "middle"
});
// What NordaGPT knows
s.addText("Co wie NordaGPT?", {
x: 0.8, y: 1.2, w: 4, h: 0.4,
fontSize: 16, fontFace: FONT_BODY, color: C.primaryLight, bold: true, margin: 0
});
const gptKnows = [
"Wszystkie firmy członkowskie — usługi, dane",
"Kto jest kim w Zarządzie i Radzie Izby",
"Kalendarz wydarzeń i aktualności",
"Projekt elektrowni jądrowej (PEJ)",
"Inwestycje na Kaszubach (ZOPK)",
"Ogłoszenia B2B i tematy na forum",
];
s.addText(gptKnows.map((item, i) => ({
text: item,
options: { bullet: true, breakLine: i < gptKnows.length - 1, fontSize: 13, color: C.white }
})), {
x: 0.8, y: 1.7, w: 4.2, h: 2.5,
fontFace: FONT_BODY, paraSpaceAfter: 5, margin: 0
});
// Right - example questions
s.addText("Przykładowe pytania:", {
x: 5.4, y: 1.2, w: 4, h: 0.4,
fontSize: 16, fontFace: FONT_BODY, color: C.primaryLight, bold: true, margin: 0
});
const examples = [
'"Szukam firmy od instalacji elektrycznych"',
'"Kto w Izbie zajmuje się budownictwem?"',
'"Jakie są najbliższe wydarzenia?"',
'"Co nowego w sprawie elektrowni jądrowej?"',
'"Firma od IT z doświadczeniem w offshore"',
];
for (let i = 0; i < examples.length; i++) {
s.addShape(pres.shapes.RECTANGLE, {
x: 5.4, y: 1.7 + i * 0.5, w: 4.2, h: 0.4,
fill: { color: C.navyBg }
});
s.addText(examples[i], {
x: 5.6, y: 1.7 + i * 0.5, w: 3.8, h: 0.4,
fontSize: 11, fontFace: FONT_BODY, color: C.textLight, italic: true, margin: 0, valign: "middle"
});
}
// Bottom note about AI
s.addShape(pres.shapes.RECTANGLE, {
x: 0.8, y: 4.5, w: 8.4, h: 0.7,
fill: { color: "172554" }
});
s.addText([
{ text: "Odpowiedzi na żywo ", options: { fontSize: 13, color: C.white, bold: true } },
{ text: " | ", options: { fontSize: 13, color: C.primaryLight } },
{ text: "Propozycje kolejnych pytań ", options: { fontSize: 13, color: C.white, bold: true } },
{ text: " | ", options: { fontSize: 13, color: C.primaryLight } },
{ text: "Klikalne linki do firm", options: { fontSize: 13, color: C.white, bold: true } },
], {
x: 1.0, y: 4.5, w: 8, h: 0.7,
fontFace: FONT_BODY, align: "center", valign: "middle", margin: 0
});
addSlideNumber(s, 6, TOTAL);
// =============================================
// SLIDE 7: KOMUNIKACJA
// =============================================
s = pres.addSlide();
s.background = { color: C.lightBg };
s.addText("Komunikacja w portalu", {
x: 0.8, y: 0.4, w: 8.4, h: 0.7,
fontSize: 32, fontFace: FONT_TITLE, color: C.textDark, bold: true, margin: 0
});
s.addShape(pres.shapes.RECTANGLE, {
x: 0.8, y: 1.05, w: 1.2, h: 0.04, fill: { color: C.primary }
});
// Three columns
const commCols = [
{
title: "Wiadomości prywatne",
color: C.primary,
items: [
"Napisz do każdego członka Izby",
"Bąbelki jak w WhatsApp / Teams",
"Formatowanie tekstu, linki",
"Załączniki: PDF, zdjęcia",
"Potwierdzenie odczytania",
"Wiadomości grupowe",
]
},
{
title: "Forum dyskusyjne",
color: C.green,
items: [
"Tematy z kategoriami",
"Odpowiadaj, reaguj emoji",
"Śledź tematy (powiadomienia)",
"Wstawiaj zdjęcia i linki",
"Oznaczaj @użytkowników",
"Najlepsza odpowiedź",
]
},
{
title: "Tablica B2B",
color: C.amber,
items: [
"Ogłoszenia z ofertami usług",
"\"Jestem zainteresowany\"",
"Publiczne pytania / odpowiedzi",
"Kto widział ogłoszenie?",
"Kategorie: Partnerstwo, Okazja...",
"Wysyłaj wiadomość z ogłoszenia",
]
},
];
for (let c = 0; c < commCols.length; c++) {
const x = 0.8 + c * 3.1;
const col = commCols[c];
addCard(s, x, 1.35, 2.8, 3.7, col.color);
s.addText(col.title, {
x: x + 0.25, y: 1.5, w: 2.3, h: 0.35,
fontSize: 14, fontFace: FONT_BODY, color: C.textDark, bold: true, margin: 0
});
s.addText(col.items.map((item, i) => ({
text: item,
options: { bullet: true, breakLine: i < col.items.length - 1, fontSize: 11, color: C.textDark }
})), {
x: x + 0.25, y: 2.0, w: 2.3, h: 2.8,
fontFace: FONT_BODY, paraSpaceAfter: 4, margin: 0
});
}
addSlideNumber(s, 7, TOTAL);
// =============================================
// SLIDE 8: KALENDARZ
// =============================================
s = pres.addSlide();
s.background = { color: C.lightBg };
s.addText("Kalendarz wydarzeń", {
x: 0.8, y: 0.4, w: 8.4, h: 0.7,
fontSize: 32, fontFace: FONT_TITLE, color: C.textDark, bold: true, margin: 0
});
s.addShape(pres.shapes.RECTANGLE, {
x: 0.8, y: 1.05, w: 1.2, h: 0.04, fill: { color: C.primary }
});
// Features grid 2x3
const calFeatures = [
[icons.calendar, "Widok miesięczny", "Przejrzysta siatka z kolorowymi\noznaczeniami typów wydarzeń"],
[icons.check, "Zapis jednym kliknięciem", "Zielony przycisk — zapisz się\nlub zrezygnuj w każdej chwili"],
[icons.users, "Lista uczestników", "Zobacz kto idzie — klikalne\nplakietki z linkami do profili"],
[icons.bell, "Przypomnienia", "Email dzień przed wydarzeniem,\npowiadomienie w portalu"],
[icons.mobile, "Synchronizacja", "Eksport do iPhone (iCal)\ni Google Calendar"],
[icons.map, "Lokalizacja", "Adresy prowadzą do Google Maps,\nwszystko klikalne"],
];
for (let i = 0; i < calFeatures.length; i++) {
const col = i % 3;
const row = Math.floor(i / 3);
const x = 0.8 + col * 3.1;
const y = 1.4 + row * 1.9;
addCard(s, x, y, 2.8, 1.6, C.primary);
s.addImage({ data: calFeatures[i][0], x: x + 0.25, y: y + 0.2, w: 0.35, h: 0.35 });
s.addText(calFeatures[i][1], {
x: x + 0.25, y: y + 0.65, w: 2.3, h: 0.3,
fontSize: 13, fontFace: FONT_BODY, color: C.textDark, bold: true, margin: 0
});
s.addText(calFeatures[i][2], {
x: x + 0.25, y: y + 0.95, w: 2.3, h: 0.5,
fontSize: 11, fontFace: FONT_BODY, color: C.textMuted, margin: 0
});
}
addSlideNumber(s, 8, TOTAL);
// =============================================
// SLIDE 9: PEJ
// =============================================
s = pres.addSlide();
s.background = { color: C.darkBg };
s.addShape(pres.shapes.RECTANGLE, {
x: 0, y: 0, w: 10, h: 0.06, fill: { color: C.amber }
});
addIconCircle(s, "atom", 0.8, 0.35, 0.55, C.amber);
s.addText("PEJ — Elektrownia Jądrowa", {
x: 1.5, y: 0.35, w: 7, h: 0.6,
fontSize: 30, fontFace: FONT_TITLE, color: C.white, bold: true, margin: 0, valign: "middle"
});
s.addText("Dedykowana przestrzeń dla firm zainteresowanych największą inwestycją w regionie", {
x: 0.8, y: 1.1, w: 8.4, h: 0.4,
fontSize: 14, fontFace: FONT_BODY, color: C.textLight, margin: 0
});
// Two columns
// Left - what's there
s.addShape(pres.shapes.RECTANGLE, {
x: 0.8, y: 1.7, w: 4.2, h: 3.2,
fill: { color: C.navyBg }, shadow: makeShadow()
});
s.addShape(pres.shapes.RECTANGLE, {
x: 0.8, y: 1.7, w: 0.06, h: 3.2, fill: { color: C.amber }
});
s.addText("Co znajdziesz?", {
x: 1.1, y: 1.85, w: 3.5, h: 0.35,
fontSize: 15, fontFace: FONT_BODY, color: C.amber, bold: true, margin: 0
});
const pejItems = [
"Aktualności o elektrowni jądrowej (66+ artykułów)",
"Local Content — lista firm Izby dopasowanych do projektu",
"Wskaźnik dopasowania firmy do przetargów PEJ",
"Oś czasu kamieni milowych budowy",
"Kontakty: PEJ, Bechtel, PAA, Ministerstwo",
"Link do platformy zakupowej i rejestracji dostawców",
];
s.addText(pejItems.map((item, i) => ({
text: item,
options: { bullet: true, breakLine: i < pejItems.length - 1, fontSize: 12, color: C.white }
})), {
x: 1.1, y: 2.3, w: 3.6, h: 2.4,
fontFace: FONT_BODY, paraSpaceAfter: 4, margin: 0
});
// Right - Local Content
s.addShape(pres.shapes.RECTANGLE, {
x: 5.4, y: 1.7, w: 4.2, h: 3.2,
fill: { color: C.navyBg }, shadow: makeShadow()
});
s.addShape(pres.shapes.RECTANGLE, {
x: 5.4, y: 1.7, w: 0.06, h: 3.2, fill: { color: C.green }
});
s.addText("Local Content — Twoja szansa", {
x: 5.7, y: 1.85, w: 3.5, h: 0.35,
fontSize: 15, fontFace: FONT_BODY, color: C.green, bold: true, margin: 0
});
const lcItems = [
"System dopasowuje Twoją firmę do projektu PEJ",
"Widoczne: typ współpracy, branża, opis",
"Filtruj po: kategorii, typie powiązania",
"Potencjalny dostawca / partner / beneficjent",
"Im pełniejszy profil, tym lepsze dopasowanie",
"NordaGPT odpowiada na pytania o PEJ",
];
s.addText(lcItems.map((item, i) => ({
text: item,
options: { bullet: true, breakLine: i < lcItems.length - 1, fontSize: 12, color: C.white }
})), {
x: 5.7, y: 2.3, w: 3.6, h: 2.4,
fontFace: FONT_BODY, paraSpaceAfter: 4, margin: 0
});
addSlideNumber(s, 9, TOTAL);
// =============================================
// SLIDE 10: KASZUBIA / ZOPK
// =============================================
s = pres.addSlide();
s.background = { color: C.lightBg };
s.addText("Kaszubia — Inwestycje regionalne", {
x: 0.8, y: 0.4, w: 8.4, h: 0.7,
fontSize: 30, fontFace: FONT_TITLE, color: C.textDark, bold: true, margin: 0
});
s.addText("Zielony Okręg Przemysłowy Kaszubia — monitoring szans biznesowych", {
x: 0.8, y: 1.0, w: 8.4, h: 0.4,
fontSize: 14, fontFace: FONT_BODY, color: C.textMuted, margin: 0
});
// Project cards - use darker shades for white text readability
const projects = [
["Energia", "Elektrownia jądrowa,\noffshore, OZE", "B45309"],
["Infrastruktura", "Drogi, kolej,\nport Gdynia", C.indigo],
["Przemysł", "Kongsberg, CNC,\nmetalurgia", C.red],
["Turystyka", "Żarnowiecki Ring,\nEnergy Velo", "047857"],
];
for (let i = 0; i < projects.length; i++) {
const x = 0.8 + i * 2.25;
const [name, desc, color] = projects[i];
s.addShape(pres.shapes.RECTANGLE, {
x, y: 1.6, w: 2.0, h: 1.2,
fill: { color }, shadow: makeShadow()
});
s.addText(name, {
x: x + 0.15, y: 1.7, w: 1.7, h: 0.35,
fontSize: 15, fontFace: FONT_BODY, color: C.white, bold: true, margin: 0
});
s.addText(desc, {
x: x + 0.15, y: 2.1, w: 1.7, h: 0.6,
fontSize: 11, fontFace: FONT_BODY, color: C.white, margin: 0
});
}
// Bottom - what's tracked
addCard(s, 0.8, 3.1, 8.4, 2.1, C.primary);
s.addText("Co monitorujemy?", {
x: 1.1, y: 3.25, w: 4, h: 0.35,
fontSize: 15, fontFace: FONT_BODY, color: C.textDark, bold: true, margin: 0
});
const zopkItems = [
[icons.newspaper, "Aktualności prasowe", "Automatyczne zbieranie i filtrowanie wiadomości przez AI"],
[icons.chart, "Kamienie milowe", "Oś czasu z postępem kluczowych projektów w regionie"],
[icons.handshake, "Dopasowanie firm", "System wskazuje które firmy Izby pasują do przetargów"],
[icons.lightbulb, "Baza wiedzy AI", "NordaGPT odpowiada na pytania o inwestycje kaszubskie"],
];
for (let i = 0; i < zopkItems.length; i++) {
const col = i % 2;
const row = Math.floor(i / 2);
const x = 1.1 + col * 4.1;
const y = 3.7 + row * 0.65;
s.addImage({ data: zopkItems[i][0], x, y: y + 0.05, w: 0.28, h: 0.28 });
s.addText([
{ text: zopkItems[i][1] + " ", options: { bold: true, fontSize: 12 } },
{ text: zopkItems[i][2], options: { fontSize: 11, color: C.textMuted } }
], {
x: x + 0.4, y, w: 3.5, h: 0.5,
fontFace: FONT_BODY, color: C.textDark, margin: 0, valign: "middle"
});
}
addSlideNumber(s, 10, TOTAL);
// =============================================
// SLIDE 11: POWIADOMIENIA
// =============================================
s = pres.addSlide();
s.background = { color: C.lightBg };
s.addText("Powiadomienia i aplikacja mobilna", {
x: 0.8, y: 0.4, w: 8.4, h: 0.7,
fontSize: 30, fontFace: FONT_TITLE, color: C.textDark, bold: true, margin: 0
});
s.addShape(pres.shapes.RECTANGLE, {
x: 0.8, y: 1.05, w: 1.2, h: 0.04, fill: { color: C.primary }
});
// Notification channels
const notifCols = [
{
title: "W portalu",
icon: "bell",
color: C.primary,
items: [
"Dzwoneczek z licznikiem",
"Nowe wiadomości",
"Odpowiedzi na forum",
"Zapis na wydarzenie",
"Badge \"Nowe\" na treściach",
]
},
{
title: "Email",
icon: "envelope",
color: C.green,
items: [
"Nowa wiadomość prywatna",
"Odpowiedź na śledzony temat",
"Przypomnienie o wydarzeniu",
"Wniosek członkowski",
"Link do rezygnacji",
]
},
{
title: "Telefon",
icon: "mobile",
color: C.purple,
items: [
"Zainstaluj jak aplikację",
"Powiadomienia na iPhone i Android",
"Pełna funkcjonalność",
"Działa bez internetu",
"Ikona na pulpicie telefonu",
]
},
];
for (let c = 0; c < notifCols.length; c++) {
const x = 0.8 + c * 3.1;
const col = notifCols[c];
addCard(s, x, 1.35, 2.8, 3.5, col.color);
addIconCircle(s, col.icon, x + 0.25, 1.5, 0.4, col.color);
s.addText(col.title, {
x: x + 0.8, y: 1.5, w: 1.7, h: 0.4,
fontSize: 15, fontFace: FONT_BODY, color: C.textDark, bold: true, margin: 0, valign: "middle"
});
s.addText(col.items.map((item, i) => ({
text: item,
options: { bullet: true, breakLine: i < col.items.length - 1, fontSize: 11, color: C.textDark }
})), {
x: x + 0.25, y: 2.1, w: 2.3, h: 2.5,
fontFace: FONT_BODY, paraSpaceAfter: 5, margin: 0
});
}
addSlideNumber(s, 11, TOTAL);
// =============================================
// SLIDE 12: PRYWATNOŚĆ I BEZPIECZEŃSTWO
// =============================================
s = pres.addSlide();
s.background = { color: C.darkBg };
s.addShape(pres.shapes.RECTANGLE, {
x: 0, y: 0, w: 10, h: 0.06, fill: { color: C.green }
});
addIconCircle(s, "shield", 0.8, 0.35, 0.55, C.green);
s.addText("Prywatność i bezpieczeństwo", {
x: 1.5, y: 0.35, w: 7, h: 0.6,
fontSize: 30, fontFace: FONT_TITLE, color: C.white, bold: true, margin: 0, valign: "middle"
});
// Privacy features
const privacyItems = [
["Twoje dane pod kontrolą", "Decydujesz co jest widoczne — telefon, email,\nsekcje profilu. Ukrywasz jednym kliknięciem.", C.green],
["6 poziomów dostępu", "Gość → Użytkownik → Członek → Pracownik →\nKierownik → Administrator. Każdy widzi tylko to, co powinien.", C.primary],
["Tylko dla członków Izby", "NordaGPT, wiadomości, B2B, dane kontaktowe\n— dostępne wyłącznie po zalogowaniu.", C.amber],
["Ochrona danych osobowych", "PESEL, numery kart, IBAN — automatycznie\nukrywane. Blokowanie niechcianych kontaktów.", C.purple],
];
for (let i = 0; i < privacyItems.length; i++) {
const col = i % 2;
const row = Math.floor(i / 2);
const x = 0.8 + col * 4.5;
const y = 1.3 + row * 1.8;
s.addShape(pres.shapes.RECTANGLE, {
x, y, w: 4.0, h: 1.5,
fill: { color: C.navyBg }, shadow: makeShadow()
});
s.addShape(pres.shapes.RECTANGLE, {
x, y, w: 0.06, h: 1.5,
fill: { color: privacyItems[i][2] }
});
s.addText(privacyItems[i][0], {
x: x + 0.25, y: y + 0.15, w: 3.5, h: 0.35,
fontSize: 15, fontFace: FONT_BODY, color: C.white, bold: true, margin: 0
});
s.addText(privacyItems[i][1], {
x: x + 0.25, y: y + 0.55, w: 3.5, h: 0.8,
fontSize: 12, fontFace: FONT_BODY, color: C.textLight, margin: 0
});
}
addSlideNumber(s, 12, TOTAL);
// =============================================
// SLIDE 13: KOSZTY I PRZYSZŁOŚĆ
// =============================================
s = pres.addSlide();
s.background = { color: C.lightBg };
s.addText("Koszty, ograniczenia i przyszłość", {
x: 0.8, y: 0.4, w: 8.4, h: 0.7,
fontSize: 30, fontFace: FONT_TITLE, color: C.textDark, bold: true, margin: 0
});
s.addShape(pres.shapes.RECTANGLE, {
x: 0.8, y: 1.05, w: 1.2, h: 0.04, fill: { color: C.primary }
});
// Left - what's included
addCard(s, 0.8, 1.35, 4.2, 2.0, C.green);
s.addText("Co jest w cenie członkostwa?", {
x: 1.1, y: 1.5, w: 3.5, h: 0.35,
fontSize: 14, fontFace: FONT_BODY, color: C.textDark, bold: true, margin: 0
});
const freeItems = [
"Profil firmy, katalog, wyszukiwarka",
"Forum, ogłoszenia B2B, aktualności",
"Kalendarz wydarzeń z zapisami",
"Wiadomości prywatne i grupowe",
"Powiadomienia email i push (PWA)",
];
s.addText(freeItems.map((item, i) => ({
text: item,
options: { bullet: true, breakLine: i < freeItems.length - 1, fontSize: 12, color: C.textDark }
})), {
x: 1.1, y: 1.9, w: 3.6, h: 1.3,
fontFace: FONT_BODY, paraSpaceAfter: 3, margin: 0
});
// Right - AI limits
addCard(s, 5.4, 1.35, 4.2, 2.0, C.amber);
s.addText("Ograniczenia AI (NordaGPT)", {
x: 5.7, y: 1.5, w: 3.5, h: 0.35,
fontSize: 14, fontFace: FONT_BODY, color: C.textDark, bold: true, margin: 0
});
const aiLimits = [
"Silnik AI: Google Gemini (darmowy tier)",
"Limit dzienny na użytkownika",
"Limit zbiorczy na cały portal",
"Odpowiedzi w 2 trybach: szybki / zaawansowany",
"W przyszłości: płatne plany = więcej AI",
];
s.addText(aiLimits.map((item, i) => ({
text: item,
options: { bullet: true, breakLine: i < aiLimits.length - 1, fontSize: 12, color: C.textDark }
})), {
x: 5.7, y: 1.9, w: 3.6, h: 1.3,
fontFace: FONT_BODY, paraSpaceAfter: 3, margin: 0
});
// Bottom - future plans
addCard(s, 0.8, 3.6, 8.4, 1.7, C.indigo);
s.addText("Co planujemy?", {
x: 1.1, y: 3.75, w: 4, h: 0.35,
fontSize: 15, fontFace: FONT_BODY, color: C.textDark, bold: true, margin: 0
});
const futureItems = [
[icons.robot, "NordaGPT 2.0", "Zaawansowany doradca — analiza rynku, rekomendacje, raporty"],
[icons.handshake, "Marketplace usług", "Giełda kompetencji — członkowie oferują usługi sobie nawzajem"],
[icons.chart, "Raporty branżowe", "Automatyczna analiza trendów i szans rynkowych przez AI"],
[icons.globe, "Integracje", "WhatsApp Business, SMS — komunikacja bez granic"],
];
for (let i = 0; i < futureItems.length; i++) {
const col = i % 2;
const row = Math.floor(i / 2);
const x = 1.1 + col * 4.1;
const y = 4.2 + row * 0.5;
s.addImage({ data: futureItems[i][0], x, y: y + 0.05, w: 0.25, h: 0.25 });
s.addText([
{ text: futureItems[i][1] + " ", options: { bold: true, fontSize: 12 } },
{ text: futureItems[i][2], options: { fontSize: 11, color: C.textMuted } }
], {
x: x + 0.35, y, w: 3.5, h: 0.4,
fontFace: FONT_BODY, color: C.textDark, margin: 0, valign: "middle"
});
}
addSlideNumber(s, 13, TOTAL);
// =============================================
// SLIDE 14: PYTANIA
// =============================================
s = pres.addSlide();
s.background = { color: C.darkBg };
s.addShape(pres.shapes.RECTANGLE, {
x: 0, y: 0, w: 10, h: 0.06, fill: { color: C.primary }
});
s.addText("Pytania?", {
x: 0.8, y: 1.0, w: 8.4, h: 1.0,
fontSize: 48, fontFace: FONT_TITLE, color: C.white, bold: true, align: "center", margin: 0
});
s.addShape(pres.shapes.RECTANGLE, {
x: 4.2, y: 2.2, w: 1.6, h: 0.04, fill: { color: C.primary }
});
s.addText("Zaloguj się i sprawdź", {
x: 0.8, y: 2.6, w: 8.4, h: 0.6,
fontSize: 22, fontFace: FONT_BODY, color: C.primaryLight, align: "center", margin: 0
});
// Norda compass next to URL
s.addImage({
path: nordaLogoPath,
x: 3.4, y: 3.05, w: 0.7, h: 0.7
});
s.addText("nordabiznes.pl", {
x: 4.2, y: 3.1, w: 4, h: 0.6,
fontSize: 28, fontFace: FONT_TITLE, color: C.white, bold: true, margin: 0, valign: "middle"
});
// Bottom contact bar
s.addShape(pres.shapes.RECTANGLE, {
x: 1.5, y: 4.1, w: 7, h: 1.0,
fill: { color: C.navyBg }
});
s.addText([
{ text: "Maciej Pienczyn", options: { bold: true, fontSize: 13, color: C.white } },
{ text: " | maciej.pienczyn@inpi.pl | Izba Gospodarcza NORDA", options: { fontSize: 12, color: C.textLight } }
], {
x: 1.5, y: 4.1, w: 7, h: 0.55,
fontFace: FONT_BODY, align: "center", valign: "middle", margin: 0
});
// INPI logo + "Stworzone przez" — bottom of contact bar
s.addImage({
path: inpiLogoPath,
x: 3.65, y: 4.62, w: 0.55, h: 0.31
});
s.addText("Stworzone przez", {
x: 2.2, y: 4.62, w: 1.4, h: 0.31,
fontSize: 9, fontFace: FONT_BODY, color: C.textMuted, align: "right", valign: "middle", margin: 0
});
s.addText("sp. z o.o.", {
x: 4.25, y: 4.62, w: 1, h: 0.31,
fontSize: 9, fontFace: FONT_BODY, color: C.textMuted, valign: "middle", margin: 0
});
// Save
const outputPath = "/Users/maciejpi/Desktop/NordaBiznes_Prezentacja_2026-04-09.pptx";
await pres.writeFile({ fileName: outputPath });
console.log("Presentation saved to:", outputPath);
}
createPresentation().catch(err => {
console.error("Error:", err);
process.exit(1);
});