feat: introduce new PDF layouts and modules, enhance shared UI components, and add wording guidelines.
This commit is contained in:
@@ -12,6 +12,7 @@ Die Grundlage für jede Website:
|
||||
• Grundstruktur & Design-Vorlage
|
||||
• technisches SEO-Basics
|
||||
• Analytics (mit automatischem Mail-Report)
|
||||
• Testing, Staging, Production Umgebung
|
||||
• Livegang
|
||||
|
||||
Enthält keine Seiten, Inhalte oder Funktionen.
|
||||
@@ -125,7 +126,7 @@ Datensatz anpassen
|
||||
|
||||
Hosting & Betrieb
|
||||
|
||||
120 € / Monat
|
||||
12 Monate = 1440 €
|
||||
|
||||
Sichert:
|
||||
• Webhosting & Verfügbarkeit
|
||||
|
||||
56
docs/WORDING.md
Normal file
56
docs/WORDING.md
Normal file
@@ -0,0 +1,56 @@
|
||||
1. Aktiv statt passiv
|
||||
|
||||
Sätze werden aktiv formuliert.
|
||||
Keine unpersönlichen Konstruktionen, kein „es wird“, „man sollte“, „könnte“.
|
||||
|
||||
2. Kurz und eindeutig
|
||||
|
||||
Sätze sind so kurz wie möglich, so lang wie nötig.
|
||||
Ein Gedanke pro Satz. Keine Schachtelsätze.
|
||||
|
||||
3. Keine Weichmacher
|
||||
|
||||
Keine Wörter wie:
|
||||
• eventuell
|
||||
• möglicherweise
|
||||
• grundsätzlich
|
||||
• in der Regel
|
||||
• normalerweise
|
||||
|
||||
Wenn etwas gilt, wird es gesagt. Wenn nicht, wird es ausgeschlossen.
|
||||
|
||||
4. Keine Marketingbegriffe
|
||||
|
||||
Keine Buzzwords, Superlative oder leeren Versprechen.
|
||||
Keine emotional aufgeladenen Begriffe. Keine Werbesprache.
|
||||
|
||||
5. Konkrete Aussagen
|
||||
|
||||
Keine abstrakten Formulierungen.
|
||||
Aussagen beziehen sich auf konkrete Ergebnisse, Zustände oder Abläufe.
|
||||
|
||||
6. Ich-Form
|
||||
|
||||
Kommunikation erfolgt konsequent in der Ich-Form.
|
||||
Kein „wir“, kein „unser Team“, keine künstliche Vergrößerung.
|
||||
|
||||
7. Keine Rechtfertigungen
|
||||
|
||||
Keine erklärenden Absicherungen im Satz.
|
||||
Aussagen stehen für sich und werden nicht relativiert.
|
||||
|
||||
8. Neutraler Ton
|
||||
|
||||
Keine Umgangssprache.
|
||||
Keine Ironie.
|
||||
Keine Emojis.
|
||||
|
||||
9. Verbindliche Sprache
|
||||
|
||||
Keine offenen Enden ohne Grund.
|
||||
Wenn etwas nicht garantiert wird, wird das klar benannt – ohne Abschwächung.
|
||||
|
||||
10. Technisch präzise, sprachlich einfach
|
||||
|
||||
Technische Inhalte werden präzise beschrieben, sprachlich jedoch simpel gehalten.
|
||||
Kein unnötiger Jargon.
|
||||
@@ -89,7 +89,19 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
// 2. AI Prompting
|
||||
// 2. Distill Crawl Context (Context Filtering)
|
||||
let distilledCrawl = '';
|
||||
if (crawlContext) {
|
||||
const cachedDistilled = await cache.get<string>(`distilled_${targetUrl}`);
|
||||
if (cachedDistilled && !clearCache) {
|
||||
distilledCrawl = cachedDistilled;
|
||||
} else {
|
||||
distilledCrawl = await distillCrawlContext(crawlContext, OPENROUTER_KEY);
|
||||
await cache.set(`distilled_${targetUrl}`, distilledCrawl, 86400);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. AI Prompting
|
||||
console.log('🤖 Consultating Gemini 3 Flash...');
|
||||
const cachedAi = !clearCache ? await cache.get<any>(finalCacheKey) : null;
|
||||
let formState: any;
|
||||
@@ -108,7 +120,7 @@ async function main() {
|
||||
console.log('📦 Using cached AI response.');
|
||||
formState = cachedAi;
|
||||
} else {
|
||||
const result = await getAiEstimation(briefing, crawlContext, comments, OPENROUTER_KEY, principles, techStandards, tone);
|
||||
const result = await getAiEstimation(briefing, distilledCrawl, comments, OPENROUTER_KEY, principles, techStandards, tone);
|
||||
formState = result.state;
|
||||
usage = result.usage;
|
||||
await cache.set(finalCacheKey, formState);
|
||||
@@ -146,6 +158,32 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
async function distillCrawlContext(rawCrawl: string, apiKey: string): Promise<string> {
|
||||
if (!rawCrawl || rawCrawl.trim().length === 0) return "Keine Crawl-Daten vorhanden.";
|
||||
|
||||
console.log(' ↳ Distilling Crawl Context (Noise Filtering)...');
|
||||
const systemPrompt = `
|
||||
You are a context distiller. Your goal is to strip away HTML noise, legal footers, and generic fluff from a website crawl.
|
||||
Extract the "Company DNA" in 5-8 bullet points (GERMAN).
|
||||
|
||||
### FOCUS ON:
|
||||
1. Core Business / Services.
|
||||
2. Unique Selling Points (USPs).
|
||||
3. Target Audience (if clear).
|
||||
4. Tech Stack or industry-specific equipment mentioned.
|
||||
5. Brand tone (e.g. "industrial", "friendly", "technical").
|
||||
|
||||
### OUTPUT:
|
||||
Return ONLY the bullet points. No intro/outro.
|
||||
`;
|
||||
const resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', {
|
||||
model: 'google/gemini-3-flash-preview',
|
||||
messages: [{ role: 'system', content: systemPrompt }, { role: 'user', content: `RAW_CRAWL:\n${rawCrawl.substring(0, 30000)}` }],
|
||||
}, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } });
|
||||
|
||||
return resp.data.choices[0].message.content;
|
||||
}
|
||||
|
||||
async function performCrawl(url: string): Promise<string> {
|
||||
const pages: { url: string, content: string, type: string }[] = [];
|
||||
const origin = new URL(url).origin;
|
||||
@@ -205,7 +243,7 @@ async function performCrawl(url: string): Promise<string> {
|
||||
return summary + pages.map(p => `--- PAGE: ${p.url} ---\n${p.content}`).join('\n\n');
|
||||
}
|
||||
|
||||
async function getAiEstimation(briefing: string, crawlContext: string, comments: string | null, apiKey: string, principles: string, techStandards: string, tone: string) {
|
||||
async function getAiEstimation(briefing: string, distilledCrawl: string, comments: string | null, apiKey: string, principles: string, techStandards: string, tone: string) {
|
||||
let usage = { prompt: 0, completion: 0, cost: 0 };
|
||||
const addUsage = (data: any) => {
|
||||
if (data?.usage) {
|
||||
@@ -222,22 +260,25 @@ async function getAiEstimation(briefing: string, crawlContext: string, comments:
|
||||
}
|
||||
};
|
||||
|
||||
// 1. PASS 1: Fact Extraction
|
||||
console.log(' ↳ Pass 1: Fact Extraction...');
|
||||
// 1. PASS 1: Fact Extraction (Briefing Sensor)
|
||||
console.log(' ↳ Pass 1: Fact Extraction (Briefing Sensor)...');
|
||||
const pass1SystemPrompt = `
|
||||
You are a precision extraction engine. Analyze the briefing and extract ONLY the raw facts.
|
||||
You are a precision sensor. Analyze the BRIEFING and extract ONLY the raw facts.
|
||||
Tone: Literal, non-interpretive.
|
||||
Output language: GERMAN (Strict).
|
||||
|
||||
### MISSION:
|
||||
Focus 100% on the BRIEFING text provided by the user. Use the DISTILLED_CRAWL only as background context for terms or company details. If there is a conflict, the BRIEFING is the absolute source of truth.
|
||||
|
||||
### OBJECTIVES:
|
||||
- Extract companyName (Strictly the name, no descriptors).
|
||||
- Extract companyAddress (Full address if found).
|
||||
- Extract personName (Primary contact if found).
|
||||
- Extract **websiteTopic**: This MUST be a single, short branch name (e.g., "Kabeltiefbau", "Logistik", "Anwaltskanzlei"). ABSOLUTELY NO SENTENCES. If the briefing says "Group-Homepage for X", extract ONLY "X".
|
||||
- Extract **websiteTopic**: This MUST be a single, short branch name (e.g., "Kabeltiefbau", "Logistik", "Anwaltskanzlei"). ABSOLUTELY NO SENTENCES.
|
||||
- Map to internal IDs for selectedPages, features, functions, apiSystems, assets.
|
||||
- Identify if isRelaunch is true (briefing mentions existing site or URL).
|
||||
- For all textual values (deadline, websiteTopic, targetAudience etc.): USE GERMAN.
|
||||
- **multilang**: ONLY if the briefing mentions multiple target languages (e.g., DE/EN). If only one language is mentioned, do NOT use multilang.
|
||||
- **multilang**: ONLY if the briefing mentions multiple target languages (e.g., DE/EN).
|
||||
- **maps**: If "Google Maps" or location maps are mentioned or implicit (Contact page).
|
||||
- **CRITICAL**: Do NOT include "social" in apiSystems unless the user explicitly wants to SYNC/POST content to social media. "Existing social media links" are NOT apiSystems.
|
||||
- **CRITICAL**: "Video Player", "Cookie Banner", "Animations" are NOT features. They are visual/base. Do NOT map them to features.
|
||||
@@ -267,7 +308,7 @@ Output language: GERMAN (Strict).
|
||||
"employeeCount": string
|
||||
}
|
||||
`;
|
||||
const pass1UserPrompt = `BRIEFING:\n${briefing}\n\nCOMMENTS:\n${comments}\n\nCRAWL:\n${crawlContext}`;
|
||||
const pass1UserPrompt = `BRIEFING (TRUTH SOURCE):\n${briefing}\n\nCOMMENTS:\n${comments}\n\nDISTILLED_CRAWL (CONTEXT ONLY):\n${distilledCrawl}`;
|
||||
const p1Resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', {
|
||||
model: 'google/gemini-3-flash-preview',
|
||||
messages: [{ role: 'system', content: pass1SystemPrompt }, { role: 'user', content: pass1UserPrompt }],
|
||||
@@ -308,8 +349,8 @@ ${JSON.stringify(facts, null, 2)}
|
||||
addUsage(p2Resp.data);
|
||||
const details = JSON.parse(p2Resp.data.choices[0].message.content);
|
||||
|
||||
// 3. PASS 3: Strategic Content
|
||||
console.log(' ↳ Pass 3: Strategic Content...');
|
||||
// 3. PASS 3: Strategic Content (Bespoke Strategy)
|
||||
console.log(' ↳ Pass 3: Strategic Content (Bespoke Strategy)...');
|
||||
const pass3SystemPrompt = `
|
||||
You are a high-end Digital Architect. Analyze the BRIEFING.
|
||||
ABSOLUTE RULE: OUTPUT MUST BE 100% GERMAN.
|
||||
@@ -319,14 +360,16 @@ ${tone}
|
||||
|
||||
### OBJECTIVE:
|
||||
1. **briefingSummary**: Summarize the project's essence for the CUSTOMER.
|
||||
- FOLLOW PRINCIPLE 1 & 5: Clear, direct, no marketing fluff, no "partnership talk".
|
||||
- Focus purely on the CUSTOMER'S goal: What are they building, why does it matter to their business, and what is the outcome?
|
||||
- FOLLOW PRINCIPLE 1 & 5: Clear, direct, no marketing fluff.
|
||||
- **MIRROR TEST**: Capture unique customer "hooks" or personality (e.g., if they mention an Instagram account or a specific hobby/preference that influences the project vibe).
|
||||
- Focus 100% on the BRIEFING (TRUTH SOURCE). Ignore the CRAWL context for this narrative.
|
||||
- Keep it 2-3 professional, direct sentences.
|
||||
2. **designVision**: A solid, grounded, and high-quality description of the look & feel.
|
||||
- FOLLOW PRINCIPLE 1 & 3: Fact-based, professional, high density of information.
|
||||
- **BESPOKE ELEMENTS**: If the client mentions specific layout ideas (e.g., "Timeline", "Grid", "Industrial Tiles"), incorporate these as strategic decisions.
|
||||
- **NO ARROGANCE**: Eliminate all "high-end", "world-class", "dominance" language. Be humble and precise.
|
||||
- **SIMPLE & CLEAR**: Use simple German. No buzzwords. "Solid Industrial Design" instead of "Technocratic Sovereignty".
|
||||
- 3-4 sentences of deep analysis.
|
||||
- 3-5 sentences of deep analysis.
|
||||
|
||||
### OUTPUT FORMAT (Strict JSON):
|
||||
{
|
||||
@@ -336,7 +379,7 @@ ${tone}
|
||||
`;
|
||||
const p3Resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', {
|
||||
model: 'google/gemini-3-flash-preview',
|
||||
messages: [{ role: 'system', content: pass3SystemPrompt }, { role: 'user', content: briefing }],
|
||||
messages: [{ role: 'system', content: pass3SystemPrompt }, { role: 'user', content: `BRIEFING (TRUTH SOURCE):\n${briefing}` }],
|
||||
response_format: { type: 'json_object' }
|
||||
}, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } });
|
||||
addUsage(p3Resp.data);
|
||||
@@ -365,7 +408,7 @@ ${JSON.stringify({ facts, strategy }, null, 2)}
|
||||
`;
|
||||
const p4Resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', {
|
||||
model: 'google/gemini-3-flash-preview',
|
||||
messages: [{ role: 'system', content: pass4SystemPrompt }, { role: 'user', content: briefing }],
|
||||
messages: [{ role: 'system', content: pass4SystemPrompt }, { role: 'user', content: `BRIEFING (TRUTH SOURCE):\n${briefing}\n\nDISTILLED_CRAWL (CONTEXT):\n${distilledCrawl}` }],
|
||||
response_format: { type: 'json_object' }
|
||||
}, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } });
|
||||
addUsage(p4Resp.data);
|
||||
@@ -388,26 +431,29 @@ Each position in the quote must be perfectly justified and detailed.
|
||||
- **Schnittstellen (API)**: REAL Data Syncs (CRM, ERP). DO NOT include Tracking, Google Maps, or simple Video embedding here. Basic embedding is "Basis Website Setup".
|
||||
- **Sorglos-Betrieb (Hosting)**: Hosting & Maintenance.
|
||||
|
||||
### RULES FOR positionDescriptions:
|
||||
### RULES FOR positionDescriptions (STRICT):
|
||||
1. **ZERO GENERALIZATION**: Do NOT say "Verschiedene Funktionen".
|
||||
2. **ITEMIZED SYNTHESIS**: Mention EVERY component selected in Pass 1.
|
||||
3. **BREVITY & DENSITY**: Max 1-2 short sentences. Focus on TASKS not RESULTS.
|
||||
4. **STYLE**: Direct, engineering-grade, no fluff.
|
||||
5. **LANGUAGE**: 100% GERMAN.
|
||||
6. **SPECIFIC - PAGES**: For "Individuelle Seiten", list the pages as a comma-separated list (e.g. "Umfasst: Startseite, Über uns, Leistungen, Kontakt, Impressum").
|
||||
7. **SPECIFIC - API**: Video Uploads, Google Maps, and Tracking are NOT APIs. If video/maps are standard embedding, do NOT put them in "Schnittstellen".
|
||||
8. **SPECIFIC - HOSTING**: Always append: "Inkl. 20GB Speicher. Auto-Erweiterung +10€/10GB."
|
||||
9. **SPECIFIC - LOGIC**: Describe the ACTUAL logic.
|
||||
- BAD: "Erweiterte Formulare", "Logikfunktionen"
|
||||
- GOOD: "Anfrage-Strecken mit Validierung", "Filterung nach Kategorie", "Mehrsprachigkeits-Routing".
|
||||
2. **ITEMIZED SYNTHESIS**: Mention EVERY component selected in Pass 1.
|
||||
3. **HARD SPECIFICS**: Preserve technical details from the briefing (e.g., "110 kV", "HDD-Bohrtechnik", specific industry standards).
|
||||
4. **BREVITY & DENSITY**: Max 1-2 short sentences. Focus on TASKS not RESULTS.
|
||||
5. **STYLE**: Direct, engineering-grade, no fluff.
|
||||
6. **LANGUAGE**: 100% GERMAN.
|
||||
7. **SPECIFIC - PAGES**: For "Individuelle Seiten", list the pages as a comma-separated list.
|
||||
8. **SPECIFIC - API**: Video Uploads, Google Maps, and Tracking are NOT APIs.
|
||||
9. **SPECIFIC - HOSTING**: Always append: "Inkl. 20GB Speicher. Auto-Erweiterung +10€/10GB."
|
||||
10. **SPECIFIC - LOGIC**: Describe the ACTUAL logic. NEVER use generic terms like "Erweiterte Formulare" or "Individuelle Formular-Logik".
|
||||
11. **STRICT KEYS**: Keys MUST be EXACTLY: "Basis Website Setup", "Individuelle Seiten", "System-Module", "Logik-Funktionen", "Schnittstellen (API)", "Inhaltsverwaltung (CMS)", "Sorglos-Betrieb (Hosting)".
|
||||
|
||||
### FORBIDDEN PHRASES (STRICT BLOCKLIST):
|
||||
- "Erweiterte Formulare" (INSTEAD USE: "Komplexe Anfrage-Logik" or "Valide Formular-Systeme")
|
||||
- "Verschiedene Funktionen"
|
||||
- "Allgemeine Logik"
|
||||
- "Optimierte Darstellung"
|
||||
### EXAMPLES (FEW-SHOT):
|
||||
- **BAD**: "Individuelle Seiten für die Unternehmensdarstellung."
|
||||
- **GOOD**: "Erstellung der Seiten: Startseite (Video-Hero), Über uns (Timeline), Leistungen (110kV Montage), Kontakt."
|
||||
- **BAD**: "Anbindung von externen Systemen."
|
||||
- **GOOD**: "Native Integration von Google Maps zur Standortermittlung inkl. individueller Marker-Logik."
|
||||
|
||||
10. **NO "MARKETING LINGO"**: Never say "avoids branding" or "maximizes performance". Say "Implements HTML5 Video Player". ALWAYS DESCRIBE THE TASK.
|
||||
### FORBIDDEN PHRASES:
|
||||
- "Erweiterte Formulare", "Verschiedene Funktionen", "Allgemeine Logik", "Optimierte Darstellung", "Individuelle Formular-Logik".
|
||||
|
||||
11. **NO "MARKETING LINGO"**: Never say "avoids branding" or "maximizes performance". Say "Implements HTML5 Video Player". ALWAYS DESCRIBE THE TASK.
|
||||
|
||||
### DATA CONTEXT:
|
||||
${JSON.stringify({ facts, details, strategy, ia }, null, 2)}
|
||||
@@ -425,48 +471,60 @@ ${JSON.stringify({ facts, details, strategy, ia }, null, 2)}
|
||||
addUsage(p5Resp.data);
|
||||
const positionsData = JSON.parse(p5Resp.data.choices[0].message.content);
|
||||
|
||||
// 6. PASS 6: Reflection & Hardening
|
||||
console.log(' ↳ Pass 6: Reflection & Nuance Check...');
|
||||
// 6. PASS 6: The Industrial Critic
|
||||
console.log(' ↳ Pass 6: The Industrial Critic (Quality Gate)...');
|
||||
const pass6SystemPrompt = `
|
||||
You are a senior supervisor. Compare the CURRENT_STATE against the RAW_BRIEFING.
|
||||
Your goal is to catch missed nuances, specific customer wishes, and technical details.
|
||||
You are the "Industrial Critic". Your goal is to catch quality regressions and ensure the document is bespoke, technical, and professional.
|
||||
Analyze the CURRENT_STATE against the BRIEFING_TRUTH.
|
||||
|
||||
### CHECKLIST:
|
||||
1. **SPECIFICS**: Did we miss names, technical terms (kV, HDD, etc.), or specific vendor refs?
|
||||
2. **CONSISTENCY**: Do the positionDescriptions match the counts of features/functions in Pass 1?
|
||||
3. **DEADLINE**: Is there a specific month? (e.g. April/Mai). If yes, set "deadline" field.
|
||||
4. **LANGUAGE**: ABSOLUTE RULE: EVERYTHING MUST BE GERMAN.
|
||||
5. **CONFLICT CHECK**: If 'languagesList' has only 1 item, REMOVE 'multilang' from 'functions'.
|
||||
6. Refactor 'dontKnows' into a 'gridDontKnows' object for missing technical facts.
|
||||
### CRITICAL ERROR CHECKLIST (FAIL IF FOUND):
|
||||
1. **Placeholder Leakage**: Catch "null", "undefined", or generic strings like "Verschiedene Funktionen", "Erweiterte Formulare", "Individuelle Formular-Logik".
|
||||
2. **Detail Loss**: The user mentioned specific terms (e.g., "110 kV", "HDD", "Timeline", "Instagram"). Are they present in the positionDescriptions or sitemap? If not, ADD THEM.
|
||||
3. **Consistency**: Ensure the count of pages in "Individuelle Seiten" matches the sitemap pages.
|
||||
4. **Deadlines**: Ensure relative dates (e.g., "April / Mai") are resolved to the year 2026.
|
||||
5. **Tone Drift**: Remove any marketing "fluff" or "sales-y" language. Maintain the "Industrial Design" persona.
|
||||
|
||||
### CURRENT_STATE:
|
||||
### MISSION:
|
||||
Return updated fields ONLY. Specifically focus on hardening 'positionDescriptions', 'sitemap', and 'briefingSummary'.
|
||||
|
||||
### DATA CONTEXT:
|
||||
${JSON.stringify({ facts, strategy, ia, positionsData }, null, 2)}
|
||||
`;
|
||||
const p6Resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', {
|
||||
model: 'google/gemini-3-flash-preview',
|
||||
messages: [{ role: 'system', content: pass6SystemPrompt }, { role: 'user', content: `RAW_BRIEFING:\n${briefing}\n\nEnhance the state. Return ONLY the delta or the corrected fields.` }],
|
||||
messages: [{ role: 'system', content: pass6SystemPrompt }, { role: 'user', content: `BRIEFING_TRUTH:\n${briefing}` }],
|
||||
response_format: { type: 'json_object' }
|
||||
}, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } });
|
||||
addUsage(p6Resp.data);
|
||||
const reflection = JSON.parse(p6Resp.data.choices[0].message.content);
|
||||
|
||||
let finalState = {
|
||||
// 6. Reflection Merge Utility
|
||||
const mergeReflection = (state: any, reflection: any) => {
|
||||
let result = { ...state };
|
||||
const unwrap = (obj: any): any => {
|
||||
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return obj;
|
||||
if (obj["0"]) return unwrap(obj["0"]);
|
||||
if (obj.state) return unwrap(obj.state);
|
||||
if (obj.facts) return unwrap(obj.facts);
|
||||
if (obj.strategy) return unwrap(obj.strategy);
|
||||
if (obj.ia) return unwrap(obj.ia);
|
||||
if (obj.positionsData) return unwrap(obj.positionsData);
|
||||
return obj;
|
||||
};
|
||||
|
||||
const cleanedReflection = unwrap(reflection);
|
||||
return { ...result, ...cleanedReflection };
|
||||
};
|
||||
|
||||
let finalState = mergeReflection({
|
||||
...initialState,
|
||||
...facts,
|
||||
...strategy,
|
||||
...ia,
|
||||
...positionsData,
|
||||
...reflection,
|
||||
statusQuo: facts.isRelaunch ? 'Relaunch' : 'Neuentwicklung'
|
||||
};
|
||||
...positionsData
|
||||
}, reflection);
|
||||
|
||||
// Flatten if AI nested everything under "0", "state" or "state.0"
|
||||
if (finalState["0"]) finalState = { ...finalState, ...finalState["0"] };
|
||||
if ((finalState as any).state) {
|
||||
const nestedState = (finalState as any).state;
|
||||
finalState = { ...finalState, ...nestedState };
|
||||
if (nestedState["0"]) finalState = { ...finalState, ...nestedState["0"] };
|
||||
}
|
||||
finalState.statusQuo = facts.isRelaunch ? 'Relaunch' : 'Neuentwicklung';
|
||||
|
||||
// Normalization Layer: Map hallucinated German keys back to internal keys
|
||||
const normalizationMap: Record<string, string> = {
|
||||
|
||||
@@ -15,8 +15,8 @@ const __dirname = path.dirname(__filename);
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const isInteractive = args.includes('--interactive') || args.includes('-I');
|
||||
const isEstimationOnly = args.includes('--estimation') || args.includes('-E');
|
||||
const inputPath = args.find((_, i) => args[i - 1] === '--input' || args[i - 1] === '-i');
|
||||
const outputPath = args.find((_, i) => args[i - 1] === '--output' || args[i - 1] === '-o');
|
||||
|
||||
let state = { ...initialState };
|
||||
|
||||
@@ -39,7 +39,7 @@ async function main() {
|
||||
const monthlyPrice = calculateMonthly(state);
|
||||
const totalPagesCount = (state.selectedPages?.length || 0) + (state.otherPages?.length || 0) + (state.otherPagesCount || 0);
|
||||
|
||||
const finalOutputPath = outputPath || generateDefaultPath(state);
|
||||
const finalOutputPath = generateDefaultPath(state);
|
||||
const outputDir = path.dirname(finalOutputPath);
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
@@ -66,7 +66,9 @@ async function main() {
|
||||
createElement(CombinedQuotePDF as any, {
|
||||
estimationProps,
|
||||
techDetails: getTechDetails(),
|
||||
principles: getPrinciples()
|
||||
principles: getPrinciples(),
|
||||
mode: isEstimationOnly ? 'estimation' : 'full',
|
||||
showAgbs: !isEstimationOnly // AGBS only for full quotes
|
||||
}) as any,
|
||||
finalOutputPath
|
||||
);
|
||||
@@ -144,8 +146,10 @@ function generateDefaultPath(state: any) {
|
||||
const now = new Date();
|
||||
const month = now.toISOString().slice(0, 7);
|
||||
const day = now.toISOString().slice(0, 10);
|
||||
// Add seconds and minutes for 100% unique names without collision
|
||||
const time = now.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit' }).replace(/:/g, '-');
|
||||
const company = (state.companyName || state.name || 'Unknown').replace(/[^a-z0-9]/gi, '_');
|
||||
return path.join(process.cwd(), 'out', 'estimations', month, `${day}_${company}_${state.projectType}.pdf`);
|
||||
return path.join(process.cwd(), 'out', 'estimations', month, `${day}_${time}_${company}_${state.projectType}.pdf`);
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
StyleSheet as PDFStyleSheet,
|
||||
} from '@react-pdf/renderer';
|
||||
import { pdfStyles, Header, Footer, FoldingMarks, DocumentTitle } from './pdf/SharedUI';
|
||||
import { SimpleLayout } from './pdf/SimpleLayout';
|
||||
|
||||
const localStyles = PDFStyleSheet.create({
|
||||
sectionContainer: {
|
||||
@@ -52,9 +53,10 @@ interface AgbsPDFProps {
|
||||
state: any;
|
||||
headerIcon?: string;
|
||||
footerLogo?: string;
|
||||
mode?: 'estimation' | 'full';
|
||||
}
|
||||
|
||||
export const AgbsPDF = ({ state, headerIcon, footerLogo }: AgbsPDFProps) => {
|
||||
export const AgbsPDF = ({ state, headerIcon, footerLogo, mode = 'full' }: AgbsPDFProps) => {
|
||||
const date = new Date().toLocaleDateString('de-DE', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
@@ -74,59 +76,79 @@ export const AgbsPDF = ({ state, headerIcon, footerLogo }: AgbsPDFProps) => {
|
||||
iban: "DE50 1001 1001 2620 4328 65"
|
||||
};
|
||||
|
||||
const content = (
|
||||
<>
|
||||
<DocumentTitle title="Allgemeine Geschäftsbedingungen" subLines={[`Stand: ${date}`]} />
|
||||
<PDFView style={localStyles.sectionContainer}>
|
||||
<AGBSection index="01" title="Geltungsbereich">
|
||||
Diese Allgemeinen Geschäftsbedingungen gelten für alle Verträge zwischen Marc Mintel (nachfolgend „Auftragnehmer“) und dem jeweiligen Kunden (nachfolgend „Auftraggeber“). Abweichende oder ergänzende Bedingungen des Auftraggebers werden nicht Vertragsbestandteil, auch wenn ihrer Geltung nicht ausdrücklich widersprochen wird.
|
||||
</AGBSection>
|
||||
|
||||
<AGBSection index="02" title="Vertragsgegenstand">
|
||||
Der Auftragnehmer erbringt Dienstleistungen im Bereich: Webentwicklung, technische Umsetzung digitaler Systeme, Funktionen, Schnittstellen und Automatisierungen sowie Hosting, Betrieb und Wartung, sofern ausdrücklich vereinbard. Der Auftragnehmer schuldet ausschließlich die vereinbarte technische Leistung, nicht jedoch einen wirtschaftlichen Erfolg, bestimmte Umsätze, Conversions, Reichweiten, Suchmaschinen-Rankings oder rechtliche Ergebnisse.
|
||||
</AGBSection>
|
||||
|
||||
<AGBSection index="03" title="Mitwirkungspflichten des Auftraggebers">
|
||||
Der Auftraggeber verpflichtet sich, alle zur Leistungserbringung erforderlichen Inhalte, Informationen, Zugänge und Entscheidungen rechtzeitig, vollständig und korrekt bereitzustellen. Hierzu zählen insbesondere Texte, Bilder, Videos, Produktdaten, Freigaben, Feedback, Zugangsdaten sowie rechtlich erforderliche Inhalte (z. B. Impressum, DSGVO). Verzögerungen oder Unterlassungen führen zu Verschiebungen aller Termine ohne Schadensersatzanspruch.
|
||||
</AGBSection>
|
||||
|
||||
<AGBSection index="04" title="Ausführungs- und Bearbeitungszeiten">
|
||||
Angegebene Bearbeitungszeiten sind unverbindliche Schätzungen, keine garantierten Fristen. Fixe Termine oder Deadlines gelten nur, wenn sie ausdrücklich schriftlich als verbindlich vereinbart wurden.
|
||||
</AGBSection>
|
||||
|
||||
<AGBSection index="05" title="Abnahme">
|
||||
Die Leistung gilt als abgenommen, wenn der Auftraggeber sie produktiv nutzt oder innerhalb von 7 Tagen nach Bereitstellung keine wesentlichen Mängel angezeigt werden. Optische Abweichungen, Geschmacksfragen oder subjektive Einschätzungen stellen keine Mängel dar.
|
||||
</AGBSection>
|
||||
|
||||
<AGBSection index="06" title="Haftung">
|
||||
Der Auftragnehmer haftet nur für Schäden, die auf vorsätzlicher oder grob fahrlässiger Pflichtverletzung beruhen. Eine Haftung für entgangenen Gewinn, Umsatzausfälle, Datenverlust, Betriebsunterbrechungen, mittelbare oder Folgeschäden ist ausgeschlossen, soweit gesetzlich zulässig.
|
||||
</AGBSection>
|
||||
|
||||
<AGBSection index="07" title="Verfügbarkeit & Betrieb">
|
||||
Bei vereinbartem Hosting oder Betrieb schuldet der Auftragnehmer keine permanente Verfügbarkeit. Wartungsarbeiten, Updates, Sicherheitsmaßnahmen oder externe Störungen können zu zeitweisen Einschränkungen führen und begründen keine Haftungsansprüche.
|
||||
</AGBSection>
|
||||
|
||||
<AGBSection index="07a" title="Betriebs- und Pflegeleistung">
|
||||
Die Betriebs- und Pflegeleistung umfasst ausschließlich die Sicherstellung des technischen Betriebs, Wartung, Updates, Fehlerbehebung der bestehenden Systeme sowie Pflege bestehender Datensätze ohne Strukturänderung. Nicht Bestandteil sind die Erstellung neuer Inhalte (Blogartikel, News, Produkte), redaktionelle Tätigkeiten, strategische Planung oder der Aufbau neuer Features/Datenmodelle. Leistungen darüber hinaus gelten als Neuentwicklung.
|
||||
</AGBSection>
|
||||
|
||||
<AGBSection index="08" title="Drittanbieter & externe Systeme">
|
||||
Der Auftragnehmer übernimmt keine Verantwortung für Leistungen, Ausfälle oder Änderungen externer Dienste, APIs, Schnittstellen oder Plattformen Dritter. Eine Funktionsfähigkeit kann nur im Rahmen der jeweils aktuellen externen Schnittstellen gewährleistet werden.
|
||||
</AGBSection>
|
||||
|
||||
<AGBSection index="09" title="Inhalte & Rechtliches">
|
||||
Der Auftraggeber ist allein verantwortlich für Inhalte, rechtliche Konformität (DSGVO, Urheberrecht etc.) sowie bereitgestellte Daten. Der Auftragnehmer übernimmt keine rechtliche Prüfung.
|
||||
</AGBSection>
|
||||
|
||||
<AGBSection index="10" title="Vergütung & Zahlungsverzug">
|
||||
Alle Preise netto zzgl. MwSt. Rechnungen sind innerhalb von 7 Tagen fällig. Bei Zahlungsverzug ist der Auftragnehmer berechtigt, Leistungen auszusetzen, Systeme offline zu nehmen oder laufende Arbeiten zu stoppen.
|
||||
</AGBSection>
|
||||
|
||||
<AGBSection index="11" title="Kündigung laufender Leistungen">
|
||||
Laufende Leistungen (z. B. Hosting & Betrieb) können mit einer Frist von 4 Wochen zum Monatsende gekündigt werden, sofern nichts anderes vereinbart ist.
|
||||
</AGBSection>
|
||||
|
||||
<AGBSection index="12" title="Schlussbestimmungen">
|
||||
Es gilt das Recht der Bundesrepublik Deutschland. Gerichtsstand ist der Sitz des Auftragnehmers. Sollte eine Bestimmung unwirksam sein, bleibt die Wirksamkeit der übrigen Regelungen unberührt.
|
||||
</AGBSection>
|
||||
</PDFView>
|
||||
</>
|
||||
);
|
||||
|
||||
if (mode === 'full') {
|
||||
return (
|
||||
<SimpleLayout companyData={companyData} bankData={bankData} footerLogo={footerLogo} icon={headerIcon} pageNumber="10" showPageNumber={false}>
|
||||
{content}
|
||||
</SimpleLayout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PDFPage size="A4" style={pdfStyles.page}><FoldingMarks /><Header icon={headerIcon} showAddress={false} /><DocumentTitle title="Allgemeine Geschäftsbedingungen" subLines={[`Stand: ${date}`]} /><PDFView style={localStyles.sectionContainer}>
|
||||
<AGBSection index="01" title="Geltungsbereich">
|
||||
Diese Allgemeinen Geschäftsbedingungen gelten für alle Verträge zwischen Marc Mintel (nachfolgend „Auftragnehmer“) und dem jeweiligen Kunden (nachfolgend „Auftraggeber“). Abweichende oder ergänzende Bedingungen des Auftraggebers werden nicht Vertragsbestandteil, auch wenn ihrer Geltung nicht ausdrücklich widersprochen wird.
|
||||
</AGBSection>
|
||||
|
||||
<AGBSection index="02" title="Vertragsgegenstand">
|
||||
Der Auftragnehmer erbringt Dienstleistungen im Bereich: Webentwicklung, technische Umsetzung digitaler Systeme, Funktionen, Schnittstellen und Automatisierungen sowie Hosting, Betrieb und Wartung, sofern ausdrücklich vereinbart. Der Auftragnehmer schuldet ausschließlich die vereinbarte technische Leistung, nicht jedoch einen wirtschaftlichen Erfolg, bestimmte Umsätze, Conversions, Reichweiten, Suchmaschinen-Rankings oder rechtliche Ergebnisse.
|
||||
</AGBSection>
|
||||
|
||||
<AGBSection index="03" title="Mitwirkungspflichten des Auftraggebers">
|
||||
Der Auftraggeber verpflichtet sich, alle zur Leistungserbringung erforderlichen Inhalte, Informationen, Zugänge und Entscheidungen rechtzeitig, vollständig und korrekt bereitzustellen. Hierzu zählen insbesondere Texte, Bilder, Videos, Produktdaten, Freigaben, Feedback, Zugangsdaten sowie rechtlich erforderliche Inhalte (z. B. Impressum, DSGVO). Verzögerungen oder Unterlassungen führen zu Verschiebungen aller Termine ohne Schadensersatzanspruch.
|
||||
</AGBSection>
|
||||
|
||||
<AGBSection index="04" title="Ausführungs- und Bearbeitungszeiten">
|
||||
Angegebene Bearbeitungszeiten sind unverbindliche Schätzungen, keine garantierten Fristen. Fixe Termine oder Deadlines gelten nur, wenn sie ausdrücklich schriftlich als verbindlich vereinbart wurden.
|
||||
</AGBSection>
|
||||
|
||||
<AGBSection index="05" title="Abnahme">
|
||||
Die Leistung gilt als abgenommen, wenn der Auftraggeber sie produktiv nutzt oder innerhalb von 7 Tagen nach Bereitstellung keine wesentlichen Mängel angezeigt werden. Optische Abweichungen, Geschmacksfragen oder subjektive Einschätzungen stellen keine Mängel dar.
|
||||
</AGBSection>
|
||||
|
||||
<AGBSection index="06" title="Haftung">
|
||||
Der Auftragnehmer haftet nur für Schäden, die auf vorsätzlicher oder grob fahrlässiger Pflichtverletzung beruhen. Eine Haftung für entgangenen Gewinn, Umsatzausfälle, Datenverlust, Betriebsunterbrechungen, mittelbare oder Folgeschäden ist ausgeschlossen, soweit gesetzlich zulässig.
|
||||
</AGBSection>
|
||||
|
||||
<AGBSection index="07" title="Verfügbarkeit & Betrieb">
|
||||
Bei vereinbartem Hosting oder Betrieb schuldet der Auftragnehmer keine permanente Verfügbarkeit. Wartungsarbeiten, Updates, Sicherheitsmaßnahmen oder externe Störungen können zu zeitweisen Einschränkungen führen und begründen keine Haftungsansprüche.
|
||||
</AGBSection>
|
||||
|
||||
<AGBSection index="07a" title="Betriebs- und Pflegeleistung">
|
||||
Die Betriebs- und Pflegeleistung umfasst ausschließlich die Sicherstellung des technischen Betriebs, Wartung, Updates, Fehlerbehebung der bestehenden Systeme sowie Pflege bestehender Datensätze ohne Strukturänderung. Nicht Bestandteil sind die Erstellung neuer Inhalte (Blogartikel, News, Produkte), redaktionelle Tätigkeiten, strategische Planung oder der Aufbau neuer Features/Datenmodelle. Leistungen darüber hinaus gelten als Neuentwicklung.
|
||||
</AGBSection>
|
||||
|
||||
<AGBSection index="08" title="Drittanbieter & externe Systeme">
|
||||
Der Auftragnehmer übernimmt keine Verantwortung für Leistungen, Ausfälle oder Änderungen externer Dienste, APIs, Schnittstellen oder Plattformen Dritter. Eine Funktionsfähigkeit kann nur im Rahmen der jeweils aktuellen externen Schnittstellen gewährleistet werden.
|
||||
</AGBSection>
|
||||
|
||||
<AGBSection index="09" title="Inhalte & Rechtliches">
|
||||
Der Auftraggeber ist allein verantwortlich für Inhalte, rechtliche Konformität (DSGVO, Urheberrecht etc.) sowie bereitgestellte Daten. Der Auftragnehmer übernimmt keine rechtliche Prüfung.
|
||||
</AGBSection>
|
||||
|
||||
<AGBSection index="10" title="Vergütung & Zahlungsverzug">
|
||||
Alle Preise netto zzgl. MwSt. Rechnungen sind innerhalb von 7 Tagen fällig. Bei Zahlungsverzug ist der Auftragnehmer berechtigt, Leistungen auszusetzen, Systeme offline zu nehmen oder laufende Arbeiten zu stoppen.
|
||||
</AGBSection>
|
||||
|
||||
<AGBSection index="11" title="Kündigung laufender Leistungen">
|
||||
Laufende Leistungen (z. B. Hosting & Betrieb) können mit einer Frist von 4 Wochen zum Monatsende gekündigt werden, sofern nichts anderes vereinbart ist.
|
||||
</AGBSection>
|
||||
|
||||
<AGBSection index="12" title="Schlussbestimmungen">
|
||||
Es gilt das Recht der Bundesrepublik Deutschland. Gerichtsstand ist der Sitz des Auftragnehmers. Sollte eine Bestimmung unwirksam sein, bleibt die Wirksamkeit der übrigen Regelungen unberührt.
|
||||
</AGBSection>
|
||||
</PDFView><Footer logo={footerLogo} companyData={companyData} bankData={bankData} showDetails={false} /></PDFPage>
|
||||
<PDFPage size="A4" style={pdfStyles.page}>
|
||||
<FoldingMarks />
|
||||
<Header icon={headerIcon} showAddress={false} />
|
||||
{content}
|
||||
<Footer logo={footerLogo} companyData={companyData} bankData={bankData} showDetails={false} showPageNumber={false} />
|
||||
</PDFPage>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,8 +12,13 @@ interface CombinedProps {
|
||||
principles?: any[];
|
||||
}
|
||||
|
||||
export const CombinedQuotePDF = ({ estimationProps, showAgbs = true, techDetails, principles }: CombinedProps) => {
|
||||
export const CombinedQuotePDF = ({ estimationProps, showAgbs = true, techDetails, principles, mode = 'full' }: CombinedProps & { mode?: 'estimation' | 'full' }) => {
|
||||
return (
|
||||
<PDFDocument title={`Mintel - ${estimationProps.state.companyName || estimationProps.state.name}`}><EstimationPDF {...estimationProps} techDetails={techDetails} principles={principles} />{showAgbs && (<AgbsPDF state={estimationProps.state} headerIcon={estimationProps.headerIcon} footerLogo={estimationProps.footerLogo} />)}</PDFDocument>
|
||||
<PDFDocument title={`Mintel - ${estimationProps.state.companyName || estimationProps.state.name}`}>
|
||||
<EstimationPDF {...estimationProps} mode={mode} techDetails={techDetails} principles={principles} />
|
||||
{showAgbs && (
|
||||
<AgbsPDF mode={mode} state={estimationProps.state} headerIcon={estimationProps.headerIcon} footerLogo={estimationProps.footerLogo} />
|
||||
)}
|
||||
</PDFDocument>
|
||||
);
|
||||
};
|
||||
|
||||
File diff suppressed because one or more lines are too long
55
src/components/pdf/DINLayout.tsx
Normal file
55
src/components/pdf/DINLayout.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { Page as PDFPage } from '@react-pdf/renderer';
|
||||
import { FoldingMarks, Header, Footer, pdfStyles } from './SharedUI';
|
||||
|
||||
interface DINLayoutProps {
|
||||
children: React.ReactNode;
|
||||
sender?: string;
|
||||
recipient?: {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
address?: string;
|
||||
phone?: string;
|
||||
email?: string;
|
||||
taxId?: string;
|
||||
};
|
||||
icon?: string;
|
||||
footerLogo?: string;
|
||||
companyData: any;
|
||||
bankData: any;
|
||||
showAddress?: boolean;
|
||||
showFooterDetails?: boolean;
|
||||
}
|
||||
|
||||
export const DINLayout = ({
|
||||
children,
|
||||
sender,
|
||||
recipient,
|
||||
icon,
|
||||
footerLogo,
|
||||
companyData,
|
||||
bankData,
|
||||
showAddress = true,
|
||||
showFooterDetails = true
|
||||
}: DINLayoutProps) => {
|
||||
return (
|
||||
<PDFPage size="A4" style={pdfStyles.page}>
|
||||
<FoldingMarks />
|
||||
<Header
|
||||
sender={sender}
|
||||
recipient={recipient}
|
||||
icon={icon}
|
||||
showAddress={showAddress}
|
||||
/>
|
||||
{children}
|
||||
<Footer
|
||||
logo={footerLogo}
|
||||
companyData={companyData}
|
||||
bankData={bankData}
|
||||
showDetails={showFooterDetails}
|
||||
/>
|
||||
</PDFPage>
|
||||
);
|
||||
};
|
||||
@@ -13,7 +13,7 @@ export const pdfStyles = PDFStyleSheet.create({
|
||||
paddingTop: 45, // DIN 5008
|
||||
paddingLeft: 70, // ~25mm
|
||||
paddingRight: 57, // ~20mm
|
||||
paddingBottom: 48,
|
||||
paddingBottom: 80, // Safe buffer for absolute footer
|
||||
backgroundColor: '#ffffff',
|
||||
fontFamily: 'Helvetica',
|
||||
fontSize: 10,
|
||||
@@ -129,13 +129,98 @@ export const pdfStyles = PDFStyleSheet.create({
|
||||
width: 10,
|
||||
borderTopWidth: 0.5,
|
||||
borderTopColor: '#cbd5e1',
|
||||
}
|
||||
},
|
||||
// Atoms
|
||||
industrialListItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
marginBottom: 6,
|
||||
},
|
||||
industrialBulletBox: {
|
||||
width: 6,
|
||||
height: 6,
|
||||
backgroundColor: '#0f172a',
|
||||
marginRight: 8,
|
||||
marginTop: 5,
|
||||
},
|
||||
industrialTitle: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
color: '#0f172a',
|
||||
marginBottom: 6,
|
||||
letterSpacing: -0.5,
|
||||
},
|
||||
industrialSubtitle: {
|
||||
fontSize: 8,
|
||||
fontWeight: 'bold',
|
||||
color: '#94a3b8',
|
||||
textTransform: 'uppercase',
|
||||
marginBottom: 16,
|
||||
letterSpacing: 2,
|
||||
},
|
||||
industrialTextLead: {
|
||||
fontSize: 10,
|
||||
color: '#334155',
|
||||
lineHeight: 1.6,
|
||||
marginBottom: 12,
|
||||
},
|
||||
industrialText: {
|
||||
fontSize: 9,
|
||||
color: '#64748b',
|
||||
lineHeight: 1.6,
|
||||
marginBottom: 8,
|
||||
},
|
||||
industrialCard: {
|
||||
padding: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: '#e2e8f0',
|
||||
marginBottom: 12,
|
||||
},
|
||||
industrialCardTitle: {
|
||||
fontSize: 10,
|
||||
fontWeight: 'bold',
|
||||
color: '#0f172a',
|
||||
marginBottom: 4,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
darkBox: {
|
||||
marginTop: 32,
|
||||
padding: 24,
|
||||
backgroundColor: '#0f172a',
|
||||
color: '#ffffff',
|
||||
},
|
||||
darkTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
color: '#ffffff',
|
||||
marginBottom: 8,
|
||||
},
|
||||
darkText: {
|
||||
fontSize: 9,
|
||||
color: '#94a3b8',
|
||||
lineHeight: 1.6,
|
||||
},
|
||||
});
|
||||
|
||||
export const IndustrialListItem = ({ children }: { children: React.ReactNode }) => (
|
||||
<PDFView style={pdfStyles.industrialListItem}>
|
||||
<PDFView style={pdfStyles.industrialBulletBox} />
|
||||
{children}
|
||||
</PDFView>
|
||||
);
|
||||
|
||||
export const IndustrialCard = ({ title, children, style = {} }: { title: string; children: React.ReactNode; style?: any }) => (
|
||||
<PDFView style={[pdfStyles.industrialCard, style]}>
|
||||
<PDFText style={pdfStyles.industrialCardTitle}>{title}</PDFText>
|
||||
{children}
|
||||
</PDFView>
|
||||
);
|
||||
|
||||
export const FoldingMarks = () => (<><PDFView style={[pdfStyles.foldingMark, { top: 297.6 }]} fixed /><PDFView style={[pdfStyles.foldingMark, { top: 420.9, width: 15 }]} fixed /><PDFView style={[pdfStyles.foldingMark, { top: 595.3 }]} fixed /></>);
|
||||
|
||||
export const Footer = ({ logo, companyData, bankData, showDetails = true }: { logo?: string; companyData: any; bankData: any; showDetails?: boolean }) => (
|
||||
<PDFView style={pdfStyles.footer}><PDFView style={pdfStyles.footerColumn}>{logo ? (<PDFImage src={logo} style={pdfStyles.footerLogo} />) : (<PDFText style={{ fontSize: 12, fontWeight: 'bold', marginBottom: 8 }}>marc mintel</PDFText>)}</PDFView>{showDetails && (<><PDFView style={pdfStyles.footerColumn}><PDFText style={pdfStyles.footerText}><PDFText style={pdfStyles.footerLabel}>{companyData.name}</PDFText>{"\n"}{companyData.address1}{"\n"}{companyData.address2}{"\n"}UST: {companyData.ustId}</PDFText></PDFView><PDFView style={[pdfStyles.footerColumn, { alignItems: 'flex-end' }]}><PDFText style={[pdfStyles.footerText, { textAlign: 'right' }]}><PDFText style={pdfStyles.footerLabel}>{bankData.name}</PDFText>{"\n"}{bankData.bic}{"\n"}{bankData.iban}</PDFText><PDFText style={pdfStyles.pageNumber} render={({ pageNumber, totalPages }) => `${pageNumber} / ${totalPages}`} fixed /></PDFView></>)}{!showDetails && (<PDFView style={[pdfStyles.footerColumn, { alignItems: 'flex-end' }]}><PDFText style={pdfStyles.pageNumber} render={({ pageNumber, totalPages }) => `${pageNumber} / ${totalPages}`} fixed /></PDFView>)}</PDFView>
|
||||
export const Footer = ({ logo, companyData, bankData, showDetails = true, showPageNumber = true }: { logo?: string; companyData: any; bankData: any; showDetails?: boolean; showPageNumber?: boolean }) => (
|
||||
<PDFView style={pdfStyles.footer}><PDFView style={pdfStyles.footerColumn}>{logo ? (<PDFImage src={logo} style={pdfStyles.footerLogo} />) : (<PDFText style={{ fontSize: 12, fontWeight: 'bold', marginBottom: 8 }}>marc mintel</PDFText>)}</PDFView>{showDetails && (<><PDFView style={pdfStyles.footerColumn}><PDFText style={pdfStyles.footerText}><PDFText style={pdfStyles.footerLabel}>{companyData.name}</PDFText>{"\n"}{companyData.address1}{"\n"}{companyData.address2}{"\n"}UST: {companyData.ustId}</PDFText></PDFView><PDFView style={[pdfStyles.footerColumn, { alignItems: 'flex-end' }]}>{showPageNumber && <PDFText style={pdfStyles.pageNumber} render={({ pageNumber, totalPages }) => `${pageNumber} / ${totalPages}`} fixed />}</PDFView></>)}{!showDetails && (<PDFView style={[pdfStyles.footerColumn, { alignItems: 'flex-end' }]}>{showPageNumber && <PDFText style={pdfStyles.pageNumber} render={({ pageNumber, totalPages }) => `${pageNumber} / ${totalPages}`} fixed />}</PDFView>)}</PDFView>
|
||||
);
|
||||
|
||||
export const Header = ({ sender, recipient, icon, showAddress = true }: { sender?: string; recipient?: { title: string; subtitle?: string; email?: string; address?: string; phone?: string; taxId?: string }; icon?: string; showAddress?: boolean; }) => (
|
||||
|
||||
66
src/components/pdf/SimpleLayout.tsx
Normal file
66
src/components/pdf/SimpleLayout.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { Page as PDFPage, View as PDFView, Text as PDFText, StyleSheet } from '@react-pdf/renderer';
|
||||
import { Header, Footer, pdfStyles } from './SharedUI';
|
||||
|
||||
const simpleStyles = StyleSheet.create({
|
||||
industrialPage: {
|
||||
padding: 40,
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
industrialNumber: {
|
||||
fontSize: 60,
|
||||
fontWeight: 'bold',
|
||||
color: '#f1f5f9',
|
||||
position: 'absolute',
|
||||
top: -10,
|
||||
right: 0,
|
||||
zIndex: -1,
|
||||
},
|
||||
industrialSection: {
|
||||
marginTop: 32,
|
||||
paddingTop: 24,
|
||||
flexDirection: 'row',
|
||||
position: 'relative',
|
||||
},
|
||||
});
|
||||
|
||||
interface SimpleLayoutProps {
|
||||
children: React.ReactNode;
|
||||
pageNumber?: string;
|
||||
icon?: string;
|
||||
footerLogo?: string;
|
||||
companyData: any;
|
||||
bankData: any;
|
||||
showPageNumber?: boolean;
|
||||
}
|
||||
|
||||
export const SimpleLayout = ({
|
||||
children,
|
||||
pageNumber,
|
||||
icon,
|
||||
footerLogo,
|
||||
companyData,
|
||||
bankData,
|
||||
showPageNumber = true
|
||||
}: SimpleLayoutProps) => {
|
||||
return (
|
||||
<PDFPage size="A4" style={[pdfStyles.page, simpleStyles.industrialPage]}>
|
||||
<Header icon={icon} showAddress={false} />
|
||||
{pageNumber && <PDFText style={simpleStyles.industrialNumber}>{pageNumber}</PDFText>}
|
||||
<PDFView style={simpleStyles.industrialSection}>
|
||||
<PDFView style={{ width: '100%' }}>
|
||||
{children}
|
||||
</PDFView>
|
||||
</PDFView>
|
||||
<Footer
|
||||
logo={footerLogo}
|
||||
companyData={companyData}
|
||||
bankData={bankData}
|
||||
showDetails={false}
|
||||
showPageNumber={showPageNumber}
|
||||
/>
|
||||
</PDFPage>
|
||||
);
|
||||
};
|
||||
99
src/components/pdf/modules/BrandingModules.tsx
Normal file
99
src/components/pdf/modules/BrandingModules.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { View as PDFView, Text as PDFText, StyleSheet } from '@react-pdf/renderer';
|
||||
import { IndustrialListItem, IndustrialCard } from '../SharedUI';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
industrialTitle: { fontSize: 24, fontWeight: 'bold', color: '#0f172a', marginBottom: 6, letterSpacing: -0.5 },
|
||||
industrialSubtitle: { fontSize: 8, fontWeight: 'bold', color: '#94a3b8', textTransform: 'uppercase', marginBottom: 16, letterSpacing: 2 },
|
||||
industrialTextLead: { fontSize: 10, color: '#334155', lineHeight: 1.6, marginBottom: 12 },
|
||||
industrialText: { fontSize: 9, color: '#64748b', lineHeight: 1.6, marginBottom: 8 },
|
||||
industrialGrid2: { flexDirection: 'row', gap: 32 },
|
||||
industrialCol: { width: '48%' },
|
||||
darkBox: { marginTop: 32, padding: 24, backgroundColor: '#0f172a', color: '#ffffff' },
|
||||
darkTitle: { fontSize: 18, fontWeight: 'bold', color: '#ffffff', marginBottom: 8 },
|
||||
darkText: { fontSize: 9, color: '#94a3b8', lineHeight: 1.6 },
|
||||
industrialBulletBox: {
|
||||
width: 6,
|
||||
height: 6,
|
||||
backgroundColor: '#0f172a',
|
||||
marginRight: 8,
|
||||
marginTop: 5,
|
||||
},
|
||||
});
|
||||
|
||||
export const AboutModule = () => (
|
||||
<>
|
||||
<PDFText style={styles.industrialTitle}>Über mich</PDFText>
|
||||
<PDFText style={styles.industrialSubtitle}>Direkt. Sauber. Verantwortlich.</PDFText>
|
||||
<PDFView style={[styles.industrialGrid2, { marginTop: 20 }]}>
|
||||
<PDFView style={styles.industrialCol}>
|
||||
<PDFText style={styles.industrialTextLead}>Ich baue Websites und Systeme seit über 15 Jahren. Nicht weil ich Websites so liebe – sondern weil ich es hasse, wenn Dinge nicht funktionieren.</PDFText>
|
||||
<PDFText style={styles.industrialText}>In diesen Jahren habe ich Agenturen von innen gesehen, Konzerne erlebt und Startups aufgebaut. Ich habe gelernt, dass das Problem selten die Technik ist – sondern die Zuständigkeit.</PDFText>
|
||||
<PDFView style={{ marginTop: 16 }}>
|
||||
<PDFText style={[styles.industrialText, { fontWeight: 'bold' }]}>Meine Arbeitsweise:</PDFText>
|
||||
<IndustrialListItem><PDFText style={styles.industrialText}>schnell & stabil</PDFText></IndustrialListItem>
|
||||
<IndustrialListItem><PDFText style={styles.industrialText}>"boring" (im besten Sinne)</PDFText></IndustrialListItem>
|
||||
<IndustrialListItem><PDFText style={styles.industrialText}>erweiterbar & wartungsarm</PDFText></IndustrialListItem>
|
||||
<IndustrialListItem><PDFText style={styles.industrialText}>unabhängig von Agenturen</PDFText></IndustrialListItem>
|
||||
</PDFView>
|
||||
</PDFView>
|
||||
<PDFView style={styles.industrialCol}>
|
||||
<IndustrialCard title="WAS SIE BEKOMMEN">
|
||||
<IndustrialListItem><PDFText style={styles.industrialText}>Eine Person. Eine Verantwortung.</PDFText></IndustrialListItem>
|
||||
<IndustrialListItem><PDFText style={styles.industrialText}>Direkte Kommunikation ohne Stille Post.</PDFText></IndustrialListItem>
|
||||
<IndustrialListItem><PDFText style={styles.industrialText}>Code, der Ihnen gehört.</PDFText></IndustrialListItem>
|
||||
</IndustrialCard>
|
||||
<IndustrialCard title="WAS SIE NICHT BEKOMMEN" style={{ backgroundColor: '#ffffff', borderColor: '#cbd5e1' }}>
|
||||
<PDFText style={[styles.industrialText, { color: '#94a3b8', textDecoration: 'line-through' }]}>Projektmanager</PDFText>
|
||||
<PDFText style={[styles.industrialText, { color: '#94a3b8', textDecoration: 'line-through' }]}>Ticket-Systeme</PDFText>
|
||||
<PDFText style={[styles.industrialText, { color: '#94a3b8', textDecoration: 'line-through' }]}>CMS-Drama & Update-Angst</PDFText>
|
||||
</IndustrialCard>
|
||||
</PDFView>
|
||||
</PDFView>
|
||||
</>
|
||||
);
|
||||
|
||||
export const CrossSellModule = ({ state }: any) => {
|
||||
const isWebsite = state.projectType === 'website';
|
||||
const title = isWebsite ? "Das könnte Sie auch interessieren" : "Websites & Ökosysteme";
|
||||
const subtitle = isWebsite ? "Kleine Helfer, die den Alltag entlasten" : "Digitale Visitenkarten mit Tiefe";
|
||||
|
||||
return (
|
||||
<>
|
||||
<PDFText style={styles.industrialTitle}>Das könnte Sie auch interessieren</PDFText>
|
||||
<PDFText style={styles.industrialSubtitle}>Kleine helfer, die den alltag entlasten</PDFText>
|
||||
<PDFView style={[styles.industrialGrid2, { marginTop: 12 }]}>
|
||||
{isWebsite ? (
|
||||
<>
|
||||
<PDFView style={styles.industrialCol}>
|
||||
<PDFText style={styles.industrialTextLead}>Zusätzlich zur digitalen Präsenz fressen in vielen Unternehmen wiederkehrende Aufgaben unzählige Stunden: Daten abtippen, Formulare ausfüllen, Angebote anpassen.</PDFText>
|
||||
<PDFText style={[styles.industrialText, { fontWeight: 'bold' }]}>Das ist keine wertschöpfende Arbeit. Das ist Routine.</PDFText>
|
||||
<PDFView style={styles.darkBox}>
|
||||
<PDFText style={styles.darkTitle}>Der schnelle Check</PDFText>
|
||||
<PDFText style={styles.darkText}>Wenn bei Ihnen gerade etwas "von Hand gemacht wird" oder "ewig dauert" – ich sage Ihnen in 1-2 Tagen, ob und wie schnell man das sinnvoll digitalisieren kann.</PDFText>
|
||||
</PDFView>
|
||||
</PDFView>
|
||||
<PDFView style={styles.industrialCol}>
|
||||
<IndustrialCard title="PDF-GENERATOREN">
|
||||
<PDFText style={styles.industrialText}>Kurze Eingaben via Formular → fertiges Angebot/Bericht. Von 60min auf 5min reduziert.</PDFText>
|
||||
</IndustrialCard>
|
||||
<IndustrialCard title="EXCEL-AUTOMATISIERUNG">
|
||||
<PDFText style={styles.industrialText}>Verkaufszahlen, Lagerbestände oder Kundenlisten automatisch synchronisieren und auswerten.</PDFText>
|
||||
</IndustrialCard>
|
||||
<IndustrialCard title="KI-SCANNER">
|
||||
<PDFText style={styles.industrialText}>Rechnungen oder handschriftliche Notizen fotografieren → Daten landen direkt strukturiert in der Tabelle.</PDFText>
|
||||
</IndustrialCard>
|
||||
</PDFView>
|
||||
</>
|
||||
) : (
|
||||
<PDFView style={{ width: '100%' }}>
|
||||
<PDFText style={styles.industrialTextLead}>Neben Automatisierung baue ich komplette digitale Ökosysteme für Unternehmen, die Wert auf Qualität legen.</PDFText>
|
||||
<PDFText style={styles.industrialText}>Keine Baukästen. Keine langsamen WordPress-Themes. Sondern maßgeschneiderte Systeme, die technisch und optisch in der ersten Liga spielen.</PDFText>
|
||||
</PDFView>
|
||||
)}
|
||||
</PDFView>
|
||||
</>
|
||||
);
|
||||
};
|
||||
42
src/components/pdf/modules/BriefingModule.tsx
Normal file
42
src/components/pdf/modules/BriefingModule.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { View as PDFView, Text as PDFText, StyleSheet } from '@react-pdf/renderer';
|
||||
import { DocumentTitle } from '../SharedUI';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
section: { marginBottom: 24 },
|
||||
sectionTitle: { fontSize: 10, fontWeight: 'bold', marginBottom: 8, color: '#0f172a' },
|
||||
configGrid: { flexDirection: 'row', flexWrap: 'wrap', gap: 6, marginTop: 6 },
|
||||
configItem: { width: '24%', marginBottom: 4 },
|
||||
configLabel: { fontSize: 5, color: '#94a3b8', textTransform: 'uppercase', marginBottom: 2 },
|
||||
configValue: { fontSize: 7, color: '#0f172a', fontWeight: 'bold' },
|
||||
visionText: { fontSize: 9, color: '#334155', lineHeight: 1.8, textAlign: 'justify' },
|
||||
});
|
||||
|
||||
export const BriefingModule = ({ state }: any) => (
|
||||
<>
|
||||
<DocumentTitle title="Projektdetails" />
|
||||
{state.briefingSummary && (
|
||||
<PDFView style={styles.section}>
|
||||
<PDFText style={styles.sectionTitle}>Briefing Analyse</PDFText>
|
||||
<PDFText style={{ fontSize: 8, color: '#334155', lineHeight: 1.6, textAlign: 'justify' }}>{state.briefingSummary}</PDFText>
|
||||
</PDFView>
|
||||
)}
|
||||
<PDFView style={styles.section}>
|
||||
<PDFText style={styles.sectionTitle}>Kern-Informationen</PDFText>
|
||||
<PDFView style={styles.configGrid}>
|
||||
<PDFView style={styles.configItem}><PDFText style={styles.configLabel}>Ansprechpartner</PDFText><PDFText style={styles.configValue}>{state.personName || "Ihr Team"}</PDFText></PDFView>
|
||||
<PDFView style={styles.configItem}><PDFText style={styles.configLabel}>Status Quo</PDFText><PDFText style={styles.configValue}>{state.statusQuo || (state.existingWebsite ? 'Relaunch' : 'Neuentwicklung')}</PDFText></PDFView>
|
||||
<PDFView style={styles.configItem}><PDFText style={styles.configLabel}>Mitarbeiter</PDFText><PDFText style={styles.configValue}>{state.employeeCount || "—"}</PDFText></PDFView>
|
||||
<PDFView style={styles.configItem}><PDFText style={styles.configLabel}>Deadline / Zeitplan</PDFText><PDFText style={styles.configValue}>{state.deadline || 'Flexibel'}</PDFText></PDFView>
|
||||
</PDFView>
|
||||
</PDFView>
|
||||
{state.designVision && (
|
||||
<PDFView style={[styles.section, { padding: 16, borderLeftWidth: 2, borderLeftColor: '#000000', backgroundColor: '#f9fafb' }]}>
|
||||
<PDFText style={[styles.sectionTitle, { color: '#000000' }]}>Strategische Vision</PDFText>
|
||||
<PDFText style={styles.visionText}>{state.designVision}</PDFText>
|
||||
</PDFView>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
100
src/components/pdf/modules/CommonModules.tsx
Normal file
100
src/components/pdf/modules/CommonModules.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { View as PDFView, Text as PDFText, StyleSheet, Image as PDFImage } from '@react-pdf/renderer';
|
||||
import { DocumentTitle } from '../SharedUI';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
section: { marginBottom: 24 },
|
||||
pricingGrid: { marginTop: 24 },
|
||||
pricingRow: { flexDirection: 'row', borderBottomWidth: 1, borderBottomColor: '#f1f5f9', paddingVertical: 10, alignItems: 'flex-start' },
|
||||
pricingTitle: { width: '30%', fontSize: 9, fontWeight: 'bold', color: '#0f172a' },
|
||||
pricingDesc: { width: '55%', fontSize: 8, color: '#64748b', lineHeight: 1.4 },
|
||||
pricingTag: { width: '15%', fontSize: 9, fontWeight: 'bold', textAlign: 'right' },
|
||||
configLabel: { fontSize: 5, color: '#94a3b8', textTransform: 'uppercase', marginBottom: 8 },
|
||||
});
|
||||
|
||||
const CHROME_ICON = '/Users/marcmintel/Projects/mintel.me/src/assets/browser/chrome.png'; // Fallback to a placeholder if not found
|
||||
const SAFARI_ICON = '/Users/marcmintel/Projects/mintel.me/src/assets/browser/safari.png';
|
||||
|
||||
export const techPageModule = ({ techDetails, headerIcon }: any) => (
|
||||
<>
|
||||
<DocumentTitle title="Technische Umsetzung" />
|
||||
<PDFView style={styles.section}>
|
||||
<PDFText style={{ fontSize: 8, color: '#64748b', lineHeight: 1.6, marginBottom: 16 }}>Ich entwickle Websites als moderne, performante Websysteme.</PDFText>
|
||||
<PDFView style={styles.pricingGrid}>
|
||||
{techDetails?.map((item: any, i: number) => (
|
||||
<PDFView key={i} style={styles.pricingRow}>
|
||||
<PDFText style={[styles.pricingTitle, { width: '35%' }]}>{item.t}</PDFText>
|
||||
<PDFText style={[styles.pricingDesc, { width: '65%' }]}>{item.d}</PDFText>
|
||||
</PDFView>
|
||||
))}
|
||||
</PDFView>
|
||||
</PDFView>
|
||||
</>
|
||||
);
|
||||
|
||||
export const TransparenzModule = ({ pricing }: any) => (
|
||||
<>
|
||||
<DocumentTitle title="Preis-Transparenz & Modell" />
|
||||
<PDFView style={styles.section}>
|
||||
<PDFText style={{ fontSize: 10, fontWeight: 'bold', color: '#000000', marginBottom: 8 }}>Grundlage für kalkulierbare Investitionen</PDFText>
|
||||
<PDFText style={{ fontSize: 8, color: '#64748b', lineHeight: 1.6, marginBottom: 24 }}>Digitaler Erfolg basiert auf Transparenz. Dieses Dokument dient als detaillierte Grundlage für Ihre Investitionsentscheidung. Es handelt sich um ein Festpreis-Modell für die beschriebenen Leistungen, um Ihnen maximale Planungssicherheit zu gewährleisten.</PDFText>
|
||||
</PDFView>
|
||||
<PDFView style={styles.section}>
|
||||
<PDFText style={{ fontSize: 9, color: '#0f172a', fontWeight: 'bold', marginBottom: 8 }}>Warum dieser modulare Ansatz?</PDFText>
|
||||
<PDFText style={{ fontSize: 8, color: '#64748b', lineHeight: 1.6, marginBottom: 20 }}>Meine Kalkulation basiert auf einem transparenten Festpreis-System. Sie zahlen nicht für Stunden, sondern für messbare Ergebnisse. Jeder Baustein ist darauf ausgelegt, Ihre digitale Präsenz zu stärken und den ROI Ihres Projekts zu maximieren.</PDFText>
|
||||
<PDFView style={styles.pricingGrid}>
|
||||
<PDFView style={styles.pricingRow}>
|
||||
<PDFText style={styles.pricingTitle}>1. Das Fundament</PDFText>
|
||||
<PDFText style={styles.pricingDesc}>Infrastruktur, technisches SEO, Analytics und ein skalierbares Setup. Der Grundstein für eine Website, die technisch perfekt performt.</PDFText>
|
||||
<PDFText style={styles.pricingTag}>{pricing.BASE_WEBSITE?.toLocaleString('de-DE')} €</PDFText>
|
||||
</PDFView>
|
||||
<PDFView style={styles.pricingRow}>
|
||||
<PDFText style={styles.pricingTitle}>2. Individuelle Erlebnisse</PDFText>
|
||||
<PDFText style={styles.pricingDesc}>Pro Seite entwickeln wir ein UI/UX-Konzept, das Besucher führt und überzeugt. Keine Standard-Templates, sondern maßgeschneidertes Design.</PDFText>
|
||||
<PDFText style={styles.pricingTag}>{pricing.PAGE?.toLocaleString('de-DE')} € / Stk</PDFText>
|
||||
</PDFView>
|
||||
<PDFView style={styles.pricingRow}>
|
||||
<PDFText style={styles.pricingTitle}>3. Smarte Content-Systeme</PDFText>
|
||||
<PDFText style={styles.pricingDesc}>Automatisieren Sie Ihre Kommunikation. Ob Blog, Job-Board oder Case Studies – wir bauen Systeme, die sich einfach pflegen lassen.</PDFText>
|
||||
<PDFText style={styles.pricingTag}>{pricing.FEATURE?.toLocaleString('de-DE')} € / Stk</PDFText>
|
||||
</PDFView>
|
||||
<PDFView style={styles.pricingRow}>
|
||||
<PDFText style={styles.pricingTitle}>4. Funktionalität</PDFText>
|
||||
<PDFText style={styles.pricingDesc}>Suche, Filter oder interaktive Formulare. Wir reduzieren Reibung und machen die Nutzung für Ihre Kunden zum Erlebnis.</PDFText>
|
||||
<PDFText style={styles.pricingTag}>{pricing.FUNCTION?.toLocaleString('de-DE')} € / Stk</PDFText>
|
||||
</PDFView>
|
||||
<PDFView style={styles.pricingRow}>
|
||||
<PDFText style={styles.pricingTitle}>5. High-End Inszenierung</PDFText>
|
||||
<PDFText style={styles.pricingDesc}>Video-Integrationen, Mikro-Animationen und immersive Effekte. So heben Sie sich im Markt ab und wirken als Premium-Marke.</PDFText>
|
||||
<PDFText style={styles.pricingTag}>{pricing.COMPLEX_INTERACTION?.toLocaleString('de-DE')} € / Stk</PDFText>
|
||||
</PDFView>
|
||||
<PDFView style={styles.pricingRow}>
|
||||
<PDFText style={styles.pricingTitle}>6. Autonomie & Vernetzung</PDFText>
|
||||
<PDFText style={styles.pricingDesc}>Nahtlose Anbindung an Headless CMS oder Ihre Bestands-APIs (CRM, ERP). Maximale Freiheit bei voller Kontrolle.</PDFText>
|
||||
<PDFText style={styles.pricingTag}>ab {pricing.API_INTEGRATION?.toLocaleString('de-DE')} €</PDFText>
|
||||
</PDFView>
|
||||
<PDFView style={styles.pricingRow}>
|
||||
<PDFText style={styles.pricingTitle}>7. Sorglos-Betrieb</PDFText>
|
||||
<PDFText style={styles.pricingDesc}>Full-Service Hosting, tägliche Backups und proaktive Sicherheitsupdates. Damit Sie sich auf Ihr Kerngeschäft konzentrieren können.</PDFText>
|
||||
<PDFText style={styles.pricingTag}>{pricing.HOSTING_MONTHLY?.toLocaleString('de-DE')} € / Mon</PDFText>
|
||||
</PDFView>
|
||||
</PDFView>
|
||||
</PDFView>
|
||||
</>
|
||||
);
|
||||
|
||||
export const PrinciplesModule = ({ principles }: any) => (
|
||||
<>
|
||||
<DocumentTitle title="Meine Prinzipien" />
|
||||
<PDFView style={styles.pricingGrid}>
|
||||
{principles?.map((item: any, i: number) => (
|
||||
<PDFView key={i} style={styles.pricingRow}>
|
||||
<PDFText style={[styles.pricingTitle, { width: '35%' }]}>{item.t}</PDFText>
|
||||
<PDFText style={[styles.pricingDesc, { width: '65%' }]}>{item.d}</PDFText>
|
||||
</PDFView>
|
||||
))}
|
||||
</PDFView>
|
||||
</>
|
||||
);
|
||||
55
src/components/pdf/modules/EstimationModule.tsx
Normal file
55
src/components/pdf/modules/EstimationModule.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { View as PDFView, Text as PDFText, StyleSheet } from '@react-pdf/renderer';
|
||||
import { DocumentTitle } from '../SharedUI';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
table: { marginTop: 12 },
|
||||
tableHeader: { flexDirection: 'row', paddingBottom: 8, borderBottomWidth: 1, borderBottomColor: '#000000', marginBottom: 12 },
|
||||
tableRow: { flexDirection: 'row', paddingVertical: 6, borderBottomWidth: 1, borderBottomColor: '#eeeeee', alignItems: 'flex-start' },
|
||||
colPos: { width: '8%' },
|
||||
colDesc: { width: '62%' },
|
||||
colQty: { width: '10%', textAlign: 'center' },
|
||||
colPrice: { width: '20%', textAlign: 'right' },
|
||||
headerText: { fontSize: 7, fontWeight: 'bold', textTransform: 'uppercase', letterSpacing: 1 },
|
||||
posText: { fontSize: 8, color: '#999999' },
|
||||
itemTitle: { fontSize: 10, fontWeight: 'bold', marginBottom: 4 },
|
||||
itemDesc: { fontSize: 8, color: '#666666', lineHeight: 1.4 },
|
||||
priceText: { fontSize: 10, fontWeight: 'bold' },
|
||||
summaryContainer: { borderTopWidth: 1, borderTopColor: '#000000', paddingTop: 8 },
|
||||
summaryRow: { flexDirection: 'row', justifyContent: 'flex-end', paddingVertical: 4, alignItems: 'baseline' },
|
||||
summaryLabel: { fontSize: 7, color: '#64748b', textTransform: 'uppercase', letterSpacing: 1, fontWeight: 'bold', marginRight: 12 },
|
||||
summaryValue: { fontSize: 9, fontWeight: 'bold', width: 100, textAlign: 'right' },
|
||||
totalRow: { flexDirection: 'row', justifyContent: 'flex-end', paddingTop: 12, marginTop: 8, borderTopWidth: 2, borderTopColor: '#000000', alignItems: 'baseline' },
|
||||
});
|
||||
|
||||
export const EstimationModule = ({ state, positions, totalPrice, date }: any) => (
|
||||
<>
|
||||
<DocumentTitle title="Kostenschätzung" subLines={[`Datum: ${date}`, `Projekt: ${state.projectType === 'website' ? 'Website' : 'Web App'}`]} />
|
||||
<PDFView style={styles.table}>
|
||||
<PDFView style={styles.tableHeader}>
|
||||
<PDFText style={[styles.headerText, styles.colPos]}>Pos</PDFText>
|
||||
<PDFText style={[styles.headerText, styles.colDesc]}>Beschreibung</PDFText>
|
||||
<PDFText style={[styles.headerText, styles.colQty]}>Menge</PDFText>
|
||||
<PDFText style={[styles.headerText, styles.colPrice]}>Betrag</PDFText>
|
||||
</PDFView>
|
||||
{positions.map((item: any, i: number) => (
|
||||
<PDFView key={i} style={styles.tableRow} wrap={false}>
|
||||
<PDFText style={[styles.posText, styles.colPos]}>{item.pos.toString().padStart(2, '0')}</PDFText>
|
||||
<PDFView style={styles.colDesc}>
|
||||
<PDFText style={styles.itemTitle}>{item.title}</PDFText>
|
||||
<PDFText style={styles.itemDesc}>{state.positionDescriptions?.[item.title] || item.desc}</PDFText>
|
||||
</PDFView>
|
||||
<PDFText style={[styles.posText, styles.colQty]}>{item.qty}</PDFText>
|
||||
<PDFText style={[styles.priceText, styles.colPrice]}>{item.price > 0 ? `${item.price.toLocaleString('de-DE')} €` : 'n. A.'}</PDFText>
|
||||
</PDFView>
|
||||
))}
|
||||
</PDFView>
|
||||
<PDFView style={styles.summaryContainer} wrap={false}>
|
||||
<PDFView style={styles.summaryRow}><PDFText style={styles.summaryLabel}>Zwischensumme (Netto)</PDFText><PDFText style={styles.summaryValue}>{totalPrice.toLocaleString('de-DE')} €</PDFText></PDFView>
|
||||
<PDFView style={styles.summaryRow}><PDFText style={styles.summaryLabel}>zzgl. 19% MwSt.</PDFText><PDFText style={styles.summaryValue}>{(totalPrice * 0.19).toLocaleString('de-DE')} €</PDFText></PDFView>
|
||||
<PDFView style={styles.totalRow}><PDFText style={styles.summaryLabel}>Gesamtsumme (Brutto)</PDFText><PDFText style={[styles.summaryValue, { fontSize: 14 }]}>{(totalPrice * 1.19).toLocaleString('de-DE')} €</PDFText></PDFView>
|
||||
</PDFView>
|
||||
</>
|
||||
);
|
||||
73
src/components/pdf/modules/FrontPageModule.tsx
Normal file
73
src/components/pdf/modules/FrontPageModule.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { View as PDFView, Text as PDFText, Image as PDFImage, StyleSheet } from '@react-pdf/renderer';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
titlePage: {
|
||||
padding: 60,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '90%',
|
||||
},
|
||||
titleBrandIcon: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
backgroundColor: '#000000',
|
||||
borderRadius: 16,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 40,
|
||||
},
|
||||
brandIconText: {
|
||||
fontSize: 40,
|
||||
color: '#ffffff',
|
||||
fontWeight: 'bold'
|
||||
},
|
||||
titleProjectName: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
color: '#0f172a',
|
||||
marginBottom: 16,
|
||||
textAlign: 'center',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 2,
|
||||
},
|
||||
titleCustomerName: {
|
||||
fontSize: 14,
|
||||
color: '#64748b',
|
||||
marginBottom: 40,
|
||||
textAlign: 'center',
|
||||
},
|
||||
titleDocumentType: {
|
||||
fontSize: 10,
|
||||
color: '#94a3b8',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 4,
|
||||
marginBottom: 8,
|
||||
},
|
||||
titleDivider: {
|
||||
width: 40,
|
||||
height: 2,
|
||||
backgroundColor: '#000000',
|
||||
marginBottom: 40,
|
||||
},
|
||||
titleDate: {
|
||||
fontSize: 9,
|
||||
color: '#94a3b8',
|
||||
marginTop: 'auto',
|
||||
},
|
||||
});
|
||||
|
||||
export const FrontPageModule = ({ state, headerIcon, date }: any) => (
|
||||
<PDFView style={styles.titlePage}>
|
||||
<PDFView style={styles.titleBrandIcon}>
|
||||
{headerIcon ? <PDFImage src={headerIcon} style={{ width: 40, height: 40 }} /> : <PDFText style={styles.brandIconText}>M</PDFText>}
|
||||
</PDFView>
|
||||
<PDFText style={styles.titleDocumentType}>Kostenschätzung & Konzept</PDFText>
|
||||
<PDFText style={styles.titleProjectName}>{state.projectType === 'website' ? 'Digitale Präsenz' : 'Digitale Applikation'}</PDFText>
|
||||
<PDFView style={styles.titleDivider} />
|
||||
<PDFText style={styles.titleCustomerName}>für {state.companyName || "Ihre Projektanfrage"}</PDFText>
|
||||
<PDFText style={styles.titleDate}>{date} | Marc Mintel</PDFText>
|
||||
</PDFView>
|
||||
);
|
||||
52
src/components/pdf/modules/SitemapModule.tsx
Normal file
52
src/components/pdf/modules/SitemapModule.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { View as PDFView, Text as PDFText, StyleSheet } from '@react-pdf/renderer';
|
||||
import { DocumentTitle } from '../SharedUI';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
section: { marginBottom: 24 },
|
||||
sitemapTree: { marginTop: 20 },
|
||||
sitemapRootNode: { flexDirection: 'row', alignItems: 'center', marginBottom: 12 },
|
||||
sitemapRootDot: { width: 6, height: 6, borderRadius: 3, backgroundColor: '#000000', marginRight: 10 },
|
||||
sitemapRootTitle: { fontSize: 10, fontWeight: 'bold', textTransform: 'uppercase', letterSpacing: 1 },
|
||||
sitemapMainLine: { position: 'absolute', left: 2, top: 20, bottom: 0, width: 0.5, backgroundColor: '#cbd5e1' },
|
||||
sitemapBranch: { marginLeft: 20, marginBottom: 12, position: 'relative' },
|
||||
sitemapNode: { flexDirection: 'row', alignItems: 'center', marginBottom: 4 },
|
||||
sitemapRootIcon: { width: 4, height: 4, backgroundColor: '#000000', marginRight: 8 },
|
||||
sitemapBranchTitle: { fontSize: 8, fontWeight: 'bold' },
|
||||
sitemapLeaf: { marginLeft: 12, borderLeftWidth: 0.5, borderLeftColor: '#cbd5e1', paddingLeft: 12, marginTop: 4 },
|
||||
sitemapLeafNode: { flexDirection: 'row', alignItems: 'flex-start', marginBottom: 6 },
|
||||
sitemapLeafPointer: { fontSize: 7, color: '#94a3b8', marginRight: 6 },
|
||||
sitemapLeafTitle: { fontSize: 7, fontWeight: 'bold' },
|
||||
sitemapLeafDesc: { fontSize: 6, color: '#64748b', lineHeight: 1.3, marginTop: 1 },
|
||||
});
|
||||
|
||||
export const SitemapModule = ({ state }: any) => (
|
||||
<>
|
||||
<DocumentTitle title="Seitenstruktur" />
|
||||
<PDFView style={styles.section}>
|
||||
<PDFText style={{ fontSize: 8, color: '#64748b', lineHeight: 1.6, marginBottom: 16 }}>Die folgende Struktur bildet das Fundament für die Benutzerführung und Informationsarchitektur Ihres Projekts.</PDFText>
|
||||
<PDFView style={styles.sitemapTree}>
|
||||
<PDFView style={styles.sitemapRootNode}><PDFView style={styles.sitemapRootDot} /><PDFText style={styles.sitemapRootTitle}>{state.websiteTopic || 'Digitales Ökosystem'}</PDFText></PDFView>
|
||||
<PDFView style={styles.sitemapMainLine} />
|
||||
{state.sitemap?.map((cat: any, i: number) => (
|
||||
<PDFView key={i} style={styles.sitemapBranch}>
|
||||
<PDFView style={styles.sitemapNode}><PDFView style={styles.sitemapRootIcon} /><PDFText style={styles.sitemapBranchTitle}>{cat.category}</PDFText></PDFView>
|
||||
<PDFView style={styles.sitemapLeaf}>
|
||||
{cat.pages.map((p: any, j: number) => (
|
||||
<PDFView key={j} style={styles.sitemapLeafNode}>
|
||||
<PDFText style={styles.sitemapLeafPointer}>└─</PDFText>
|
||||
<PDFView style={{ flex: 1 }}>
|
||||
<PDFText style={styles.sitemapLeafTitle}>{p.title}</PDFText>
|
||||
{p.desc && <PDFText style={styles.sitemapLeafDesc}>{p.desc}</PDFText>}
|
||||
</PDFView>
|
||||
</PDFView>
|
||||
))}
|
||||
</PDFView>
|
||||
</PDFView>
|
||||
))}
|
||||
</PDFView>
|
||||
</PDFView>
|
||||
</>
|
||||
);
|
||||
@@ -44,7 +44,7 @@ export function calculatePositions(state: FormState, pricing: any): Position[] {
|
||||
positions.push({
|
||||
pos: pos++,
|
||||
title: 'Logik-Funktionen',
|
||||
desc: `Erweiterte Funktionen: ${allFunctions.join(', ')}.`,
|
||||
desc: `Implementierung technischer Logik: ${allFunctions.join(', ')}.`,
|
||||
qty: allFunctions.length,
|
||||
price: allFunctions.length * pricing.FUNCTION
|
||||
});
|
||||
|
||||
@@ -102,7 +102,7 @@ export const FUNCTION_OPTIONS = [
|
||||
{ id: 'search', label: 'Suche', desc: 'Volltextsuche über alle Inhalte.' },
|
||||
{ id: 'filter', label: 'Filter-Systeme', desc: 'Kategorisierung und Sortierung.' },
|
||||
{ id: 'pdf', label: 'PDF-Export', desc: 'Automatisierte PDF-Erstellung.' },
|
||||
{ id: 'forms', label: 'Erweiterte Formulare', desc: 'Komplexe Abfragen & Logik.' },
|
||||
{ id: 'forms', label: 'Individuelle Formular-Logik', desc: 'Smarte Validierung & mehrstufige Prozesse.' },
|
||||
];
|
||||
|
||||
export const API_OPTIONS = [
|
||||
@@ -190,7 +190,7 @@ export const FUNCTION_LABELS: Record<string, string> = {
|
||||
search: 'Suche',
|
||||
filter: 'Filter-Systeme',
|
||||
pdf: 'PDF-Export',
|
||||
forms: 'Erweiterte Formulare',
|
||||
forms: 'Individuelle Formular-Logik',
|
||||
members: 'Mitgliederbereich',
|
||||
calendar: 'Event-Kalender',
|
||||
multilang: 'Mehrsprachigkeit',
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
{
|
||||
"requestsFinished": 15,
|
||||
"requestsFinished": 8,
|
||||
"requestsFailed": 0,
|
||||
"requestsRetries": 0,
|
||||
"requestsFailedPerMinute": 0,
|
||||
"requestsFinishedPerMinute": 37,
|
||||
"requestMinDurationMillis": 235,
|
||||
"requestMaxDurationMillis": 14424,
|
||||
"requestsFinishedPerMinute": 109,
|
||||
"requestMinDurationMillis": 572,
|
||||
"requestMaxDurationMillis": 2979,
|
||||
"requestTotalFailedDurationMillis": 0,
|
||||
"requestTotalFinishedDurationMillis": 139181,
|
||||
"crawlerStartedAt": "2026-02-03T14:54:00.683Z",
|
||||
"crawlerFinishedAt": "2026-02-03T14:54:25.093Z",
|
||||
"statsPersistedAt": "2026-02-03T14:54:25.093Z",
|
||||
"crawlerRuntimeMillis": 24424,
|
||||
"crawlerLastStartTimestamp": 1770130440669,
|
||||
"requestTotalFinishedDurationMillis": 15374,
|
||||
"crawlerStartedAt": "2026-02-03T17:52:01.328Z",
|
||||
"crawlerFinishedAt": "2026-02-03T17:52:05.723Z",
|
||||
"statsPersistedAt": "2026-02-03T17:52:05.723Z",
|
||||
"crawlerRuntimeMillis": 4407,
|
||||
"crawlerLastStartTimestamp": 1770141121316,
|
||||
"requestRetryHistogram": [
|
||||
15
|
||||
8
|
||||
],
|
||||
"statsId": 0,
|
||||
"requestAvgFailedDurationMillis": null,
|
||||
"requestAvgFinishedDurationMillis": 9279,
|
||||
"requestTotalDurationMillis": 139181,
|
||||
"requestsTotal": 15,
|
||||
"requestAvgFinishedDurationMillis": 1922,
|
||||
"requestTotalDurationMillis": 15374,
|
||||
"requestsTotal": 8,
|
||||
"requestsWithStatusCode": {},
|
||||
"errors": {},
|
||||
"retryErrors": {}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"usableSessionsCount": 15,
|
||||
"usableSessionsCount": 8,
|
||||
"retiredSessionsCount": 0,
|
||||
"sessions": [
|
||||
{
|
||||
"id": "session_taRd1uiomf",
|
||||
"id": "session_d5YGFP9eEW",
|
||||
"cookieJar": {
|
||||
"version": "tough-cookie@6.0.0",
|
||||
"storeType": "MemoryCookieStore",
|
||||
@@ -13,31 +13,29 @@
|
||||
"prefixSecurity": "silent",
|
||||
"cookies": [
|
||||
{
|
||||
"key": "__cf_bm",
|
||||
"value": "mYk4XWZ2CBN_.EBBdeMrfi84cPgoknkhpQtAnTVX2Uo-1770130441-1.0.1.1-6jC0lEWEqs8Rb6QTaJIhjcyr.qLdz4.CqkFcY5EpGtSQTc1jWkiE.TwSJOdnA.tOeShG2ESr.SYDBmx3Hf3LGTcMoYZBbPl20KUGwrLG1so",
|
||||
"expires": "2026-02-03T15:24:01.000Z",
|
||||
"domain": "klz-cables.com",
|
||||
"key": "8a164f127e89bfa6ad5b54e0547581b9",
|
||||
"value": "0v9stk94l2has4gq4jejmkd3op",
|
||||
"domain": "www.schleicher-gruppe.de",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"httpOnly": true,
|
||||
"hostOnly": false,
|
||||
"creation": "2026-02-03T14:54:01.228Z",
|
||||
"lastAccessed": "2026-02-03T14:54:01.228Z",
|
||||
"sameSite": "none"
|
||||
"hostOnly": true,
|
||||
"creation": "2026-02-03T17:52:02.702Z",
|
||||
"lastAccessed": "2026-02-03T17:52:02.702Z"
|
||||
}
|
||||
]
|
||||
},
|
||||
"userData": {},
|
||||
"maxErrorScore": 3,
|
||||
"errorScoreDecrement": 0.5,
|
||||
"expiresAt": "2026-02-03T15:44:00.728Z",
|
||||
"createdAt": "2026-02-03T14:54:00.728Z",
|
||||
"expiresAt": "2026-02-03T18:42:01.370Z",
|
||||
"createdAt": "2026-02-03T17:52:01.370Z",
|
||||
"usageCount": 1,
|
||||
"maxUsageCount": 50,
|
||||
"errorScore": 0
|
||||
},
|
||||
{
|
||||
"id": "session_QMujM58pcv",
|
||||
"id": "session_TkPjda1ECD",
|
||||
"cookieJar": {
|
||||
"version": "tough-cookie@6.0.0",
|
||||
"storeType": "MemoryCookieStore",
|
||||
@@ -47,31 +45,29 @@
|
||||
"prefixSecurity": "silent",
|
||||
"cookies": [
|
||||
{
|
||||
"key": "__cf_bm",
|
||||
"value": "cXQxC0rC1_FELL4.SmocwODxWbj7RcqzGYNq0.HMPpQ-1770130448-1.0.1.1-o9AVdZJ1haFB2wnE6hqwc1dpioKZ1O6YlyoindQgrk6_.WmBXTYxDRF1EnmHVgt4H1MklpxxFrbWWRqKo9e.hjBNRizafeKmDk.p87VSMjU",
|
||||
"expires": "2026-02-03T15:24:08.000Z",
|
||||
"domain": "klz-cables.com",
|
||||
"key": "8a164f127e89bfa6ad5b54e0547581b9",
|
||||
"value": "tbb2pveomkq9ua23o1u71bjgv0",
|
||||
"domain": "www.schleicher-gruppe.de",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"httpOnly": true,
|
||||
"hostOnly": false,
|
||||
"creation": "2026-02-03T14:54:08.894Z",
|
||||
"lastAccessed": "2026-02-03T14:54:08.894Z",
|
||||
"sameSite": "none"
|
||||
"hostOnly": true,
|
||||
"creation": "2026-02-03T17:52:03.289Z",
|
||||
"lastAccessed": "2026-02-03T17:52:03.289Z"
|
||||
}
|
||||
]
|
||||
},
|
||||
"userData": {},
|
||||
"maxErrorScore": 3,
|
||||
"errorScoreDecrement": 0.5,
|
||||
"expiresAt": "2026-02-03T15:44:01.248Z",
|
||||
"createdAt": "2026-02-03T14:54:01.248Z",
|
||||
"expiresAt": "2026-02-03T18:42:02.723Z",
|
||||
"createdAt": "2026-02-03T17:52:02.723Z",
|
||||
"usageCount": 1,
|
||||
"maxUsageCount": 50,
|
||||
"errorScore": 0
|
||||
},
|
||||
{
|
||||
"id": "session_gfvVX25QNS",
|
||||
"id": "session_yh43IUCayx",
|
||||
"cookieJar": {
|
||||
"version": "tough-cookie@6.0.0",
|
||||
"storeType": "MemoryCookieStore",
|
||||
@@ -81,31 +77,29 @@
|
||||
"prefixSecurity": "silent",
|
||||
"cookies": [
|
||||
{
|
||||
"key": "__cf_bm",
|
||||
"value": "D25Wq2dzQpzcJyTwi_FWdN1aBwnI5m0U8xoWj_NOndQ-1770130454-1.0.1.1-DjtNIhv7PtKqQeX9EUl16ETp9KFdBfS4X_osjyKbTtfYBqcY_hQqGxe63pbxaBGz0e.0K2qqt7NWxLfDUEHQlCppMnhV.FgAUhwutyIE4UI",
|
||||
"expires": "2026-02-03T15:24:14.000Z",
|
||||
"domain": "klz-cables.com",
|
||||
"key": "8a164f127e89bfa6ad5b54e0547581b9",
|
||||
"value": "fd753scoscg0t7e1s2to2f9v7i",
|
||||
"domain": "www.schleicher-gruppe.de",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"httpOnly": true,
|
||||
"hostOnly": false,
|
||||
"creation": "2026-02-03T14:54:14.926Z",
|
||||
"lastAccessed": "2026-02-03T14:54:14.926Z",
|
||||
"sameSite": "none"
|
||||
"hostOnly": true,
|
||||
"creation": "2026-02-03T17:52:04.196Z",
|
||||
"lastAccessed": "2026-02-03T17:52:04.196Z"
|
||||
}
|
||||
]
|
||||
},
|
||||
"userData": {},
|
||||
"maxErrorScore": 3,
|
||||
"errorScoreDecrement": 0.5,
|
||||
"expiresAt": "2026-02-03T15:44:01.250Z",
|
||||
"createdAt": "2026-02-03T14:54:01.250Z",
|
||||
"expiresAt": "2026-02-03T18:42:02.725Z",
|
||||
"createdAt": "2026-02-03T17:52:02.725Z",
|
||||
"usageCount": 1,
|
||||
"maxUsageCount": 50,
|
||||
"errorScore": 0
|
||||
},
|
||||
{
|
||||
"id": "session_j4eQuiyZiM",
|
||||
"id": "session_S0J5fDWVY3",
|
||||
"cookieJar": {
|
||||
"version": "tough-cookie@6.0.0",
|
||||
"storeType": "MemoryCookieStore",
|
||||
@@ -115,31 +109,29 @@
|
||||
"prefixSecurity": "silent",
|
||||
"cookies": [
|
||||
{
|
||||
"key": "__cf_bm",
|
||||
"value": "_p2YUDlKVdZUnvu4QImbteG0LjxS_YTtQ3ibO6mEUOE-1770130454-1.0.1.1-j7Vd4zElVsrznqt8lRVBPzq_wnwYYcn7PmADQPx0ee6eZd6PNVsuPjK.5VaWiTkrpPQMII86qYuJ1iLBKBLYwPDkGwFordaVnE5725k5Vn4",
|
||||
"expires": "2026-02-03T15:24:14.000Z",
|
||||
"domain": "klz-cables.com",
|
||||
"key": "8a164f127e89bfa6ad5b54e0547581b9",
|
||||
"value": "1q9rudf8c5vv73cqq40rlfekur",
|
||||
"domain": "www.schleicher-gruppe.de",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"httpOnly": true,
|
||||
"hostOnly": false,
|
||||
"creation": "2026-02-03T14:54:14.777Z",
|
||||
"lastAccessed": "2026-02-03T14:54:14.777Z",
|
||||
"sameSite": "none"
|
||||
"hostOnly": true,
|
||||
"creation": "2026-02-03T17:52:04.653Z",
|
||||
"lastAccessed": "2026-02-03T17:52:04.653Z"
|
||||
}
|
||||
]
|
||||
},
|
||||
"userData": {},
|
||||
"maxErrorScore": 3,
|
||||
"errorScoreDecrement": 0.5,
|
||||
"expiresAt": "2026-02-03T15:44:01.251Z",
|
||||
"createdAt": "2026-02-03T14:54:01.251Z",
|
||||
"expiresAt": "2026-02-03T18:42:02.726Z",
|
||||
"createdAt": "2026-02-03T17:52:02.726Z",
|
||||
"usageCount": 1,
|
||||
"maxUsageCount": 50,
|
||||
"errorScore": 0
|
||||
},
|
||||
{
|
||||
"id": "session_Wl1IquiyiZ",
|
||||
"id": "session_r79Eqvg1Uq",
|
||||
"cookieJar": {
|
||||
"version": "tough-cookie@6.0.0",
|
||||
"storeType": "MemoryCookieStore",
|
||||
@@ -149,31 +141,29 @@
|
||||
"prefixSecurity": "silent",
|
||||
"cookies": [
|
||||
{
|
||||
"key": "__cf_bm",
|
||||
"value": "LF7Sgjhlli5FJLD8pAzT3Gj6_QKS1_I3mYKgTv1yuzQ-1770130447-1.0.1.1-Erb.7NKXL7gEBvz.OaTUMATd1dAniD6k9CJ4mouxPXuW7tmSxdAZnZnfTpOztc9.8tW7mLOyv4h4zL0Fw0n37vMVzcefuiJZD30BpqLVrkY",
|
||||
"expires": "2026-02-03T15:24:07.000Z",
|
||||
"domain": "klz-cables.com",
|
||||
"key": "8a164f127e89bfa6ad5b54e0547581b9",
|
||||
"value": "f840og9pek9msq9draobvo2u03",
|
||||
"domain": "www.schleicher-gruppe.de",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"httpOnly": true,
|
||||
"hostOnly": false,
|
||||
"creation": "2026-02-03T14:54:07.145Z",
|
||||
"lastAccessed": "2026-02-03T14:54:07.145Z",
|
||||
"sameSite": "none"
|
||||
"hostOnly": true,
|
||||
"creation": "2026-02-03T17:52:04.787Z",
|
||||
"lastAccessed": "2026-02-03T17:52:04.787Z"
|
||||
}
|
||||
]
|
||||
},
|
||||
"userData": {},
|
||||
"maxErrorScore": 3,
|
||||
"errorScoreDecrement": 0.5,
|
||||
"expiresAt": "2026-02-03T15:44:01.252Z",
|
||||
"createdAt": "2026-02-03T14:54:01.252Z",
|
||||
"expiresAt": "2026-02-03T18:42:02.727Z",
|
||||
"createdAt": "2026-02-03T17:52:02.727Z",
|
||||
"usageCount": 1,
|
||||
"maxUsageCount": 50,
|
||||
"errorScore": 0
|
||||
},
|
||||
{
|
||||
"id": "session_lNsh0WgahX",
|
||||
"id": "session_TQDqR0N6bh",
|
||||
"cookieJar": {
|
||||
"version": "tough-cookie@6.0.0",
|
||||
"storeType": "MemoryCookieStore",
|
||||
@@ -183,31 +173,29 @@
|
||||
"prefixSecurity": "silent",
|
||||
"cookies": [
|
||||
{
|
||||
"key": "__cf_bm",
|
||||
"value": "kYEPgRhMKlHLCvF6WXtD6GLZRJE2CnYVG3aT5_XvsAs-1770130441-1.0.1.1-58IGOFymhk7rOOLcBmAamQltCTcruktYtu_JY2pjDhBYEjpUDilZDFaOShpXsqUCp1Hdu05YTknqaiDqa9qXWsCOlSPfOed_.6mNKL96i8o",
|
||||
"expires": "2026-02-03T15:24:01.000Z",
|
||||
"domain": "klz-cables.com",
|
||||
"key": "8a164f127e89bfa6ad5b54e0547581b9",
|
||||
"value": "ad2e6u92rpajjh5vesgdqfc05b",
|
||||
"domain": "www.schleicher-gruppe.de",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"httpOnly": true,
|
||||
"hostOnly": false,
|
||||
"creation": "2026-02-03T14:54:01.483Z",
|
||||
"lastAccessed": "2026-02-03T14:54:01.483Z",
|
||||
"sameSite": "none"
|
||||
"hostOnly": true,
|
||||
"creation": "2026-02-03T17:52:04.891Z",
|
||||
"lastAccessed": "2026-02-03T17:52:04.891Z"
|
||||
}
|
||||
]
|
||||
},
|
||||
"userData": {},
|
||||
"maxErrorScore": 3,
|
||||
"errorScoreDecrement": 0.5,
|
||||
"expiresAt": "2026-02-03T15:44:01.253Z",
|
||||
"createdAt": "2026-02-03T14:54:01.253Z",
|
||||
"expiresAt": "2026-02-03T18:42:02.728Z",
|
||||
"createdAt": "2026-02-03T17:52:02.728Z",
|
||||
"usageCount": 1,
|
||||
"maxUsageCount": 50,
|
||||
"errorScore": 0
|
||||
},
|
||||
{
|
||||
"id": "session_gRMxS3WSoM",
|
||||
"id": "session_GNNTPF8Zd1",
|
||||
"cookieJar": {
|
||||
"version": "tough-cookie@6.0.0",
|
||||
"storeType": "MemoryCookieStore",
|
||||
@@ -217,31 +205,29 @@
|
||||
"prefixSecurity": "silent",
|
||||
"cookies": [
|
||||
{
|
||||
"key": "__cf_bm",
|
||||
"value": "b0o7XflZwTYrTXYCWPld0nsrrGeZAItW_uqOy38G.mo-1770130455-1.0.1.1-Rhri9V2uqOHn9o.gfpCD1QhmJSJgdPldQQMBq1XppsHhYnI8KkWXhLK2rjTnI2UiAgPODwpk2RCdhTTpWNthRgEqdVTvF1mfYiYOk4r.eKU",
|
||||
"expires": "2026-02-03T15:24:15.000Z",
|
||||
"domain": "klz-cables.com",
|
||||
"key": "8a164f127e89bfa6ad5b54e0547581b9",
|
||||
"value": "063l7o872dac686isv37sdf3of",
|
||||
"domain": "www.schleicher-gruppe.de",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"httpOnly": true,
|
||||
"hostOnly": false,
|
||||
"creation": "2026-02-03T14:54:15.181Z",
|
||||
"lastAccessed": "2026-02-03T14:54:15.181Z",
|
||||
"sameSite": "none"
|
||||
"hostOnly": true,
|
||||
"creation": "2026-02-03T17:52:05.702Z",
|
||||
"lastAccessed": "2026-02-03T17:52:05.702Z"
|
||||
}
|
||||
]
|
||||
},
|
||||
"userData": {},
|
||||
"maxErrorScore": 3,
|
||||
"errorScoreDecrement": 0.5,
|
||||
"expiresAt": "2026-02-03T15:44:01.254Z",
|
||||
"createdAt": "2026-02-03T14:54:01.254Z",
|
||||
"expiresAt": "2026-02-03T18:42:02.729Z",
|
||||
"createdAt": "2026-02-03T17:52:02.729Z",
|
||||
"usageCount": 1,
|
||||
"maxUsageCount": 50,
|
||||
"errorScore": 0
|
||||
},
|
||||
{
|
||||
"id": "session_VpSB8LC4HR",
|
||||
"id": "session_iHMY60Kcw5",
|
||||
"cookieJar": {
|
||||
"version": "tough-cookie@6.0.0",
|
||||
"storeType": "MemoryCookieStore",
|
||||
@@ -251,263 +237,23 @@
|
||||
"prefixSecurity": "silent",
|
||||
"cookies": [
|
||||
{
|
||||
"key": "__cf_bm",
|
||||
"value": "FLbQAp3i_EnhdKRqIUTYTh3PTIV1Zd2VNXpUwfhPEK0-1770130450-1.0.1.1-VfTWGaK1O1qJX395cAf9u8AqNZBLqcNjAXYSYTxZ9se6eQHS8hvPT.7obeb7_enkEz9TGU1EqTqkfaJ4RpC.3NrxkmE0.GPuktPpCgiJFsA",
|
||||
"expires": "2026-02-03T15:24:10.000Z",
|
||||
"domain": "klz-cables.com",
|
||||
"key": "8a164f127e89bfa6ad5b54e0547581b9",
|
||||
"value": "bdkvmq3k59es68k3u9h2ed5l4s",
|
||||
"domain": "www.schleicher-gruppe.de",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"httpOnly": true,
|
||||
"hostOnly": false,
|
||||
"creation": "2026-02-03T14:54:10.230Z",
|
||||
"lastAccessed": "2026-02-03T14:54:10.230Z",
|
||||
"sameSite": "none"
|
||||
"hostOnly": true,
|
||||
"creation": "2026-02-03T17:52:05.563Z",
|
||||
"lastAccessed": "2026-02-03T17:52:05.563Z"
|
||||
}
|
||||
]
|
||||
},
|
||||
"userData": {},
|
||||
"maxErrorScore": 3,
|
||||
"errorScoreDecrement": 0.5,
|
||||
"expiresAt": "2026-02-03T15:44:01.255Z",
|
||||
"createdAt": "2026-02-03T14:54:01.255Z",
|
||||
"usageCount": 1,
|
||||
"maxUsageCount": 50,
|
||||
"errorScore": 0
|
||||
},
|
||||
{
|
||||
"id": "session_RpZSkPxfMB",
|
||||
"cookieJar": {
|
||||
"version": "tough-cookie@6.0.0",
|
||||
"storeType": "MemoryCookieStore",
|
||||
"rejectPublicSuffixes": true,
|
||||
"enableLooseMode": false,
|
||||
"allowSpecialUseDomain": true,
|
||||
"prefixSecurity": "silent",
|
||||
"cookies": [
|
||||
{
|
||||
"key": "__cf_bm",
|
||||
"value": "vzQFWJPaqJR7ZhmU._jz9xR_vecA0PS0rR1dT51EFto-1770130455-1.0.1.1-Ovxa_cWE2KZiZu9gPcab5mYc2B8F2AC9cONg2fYhowwD0IonRW0B4C5wMHFAxBAUfbZ27YwJ0Yk28g379EdIN5p_4Zku7xqXOzvtwHfp_1o",
|
||||
"expires": "2026-02-03T15:24:15.000Z",
|
||||
"domain": "klz-cables.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"httpOnly": true,
|
||||
"hostOnly": false,
|
||||
"creation": "2026-02-03T14:54:15.676Z",
|
||||
"lastAccessed": "2026-02-03T14:54:15.676Z",
|
||||
"sameSite": "none"
|
||||
}
|
||||
]
|
||||
},
|
||||
"userData": {},
|
||||
"maxErrorScore": 3,
|
||||
"errorScoreDecrement": 0.5,
|
||||
"expiresAt": "2026-02-03T15:44:01.256Z",
|
||||
"createdAt": "2026-02-03T14:54:01.256Z",
|
||||
"usageCount": 1,
|
||||
"maxUsageCount": 50,
|
||||
"errorScore": 0
|
||||
},
|
||||
{
|
||||
"id": "session_b8T90CbPVv",
|
||||
"cookieJar": {
|
||||
"version": "tough-cookie@6.0.0",
|
||||
"storeType": "MemoryCookieStore",
|
||||
"rejectPublicSuffixes": true,
|
||||
"enableLooseMode": false,
|
||||
"allowSpecialUseDomain": true,
|
||||
"prefixSecurity": "silent",
|
||||
"cookies": [
|
||||
{
|
||||
"key": "__cf_bm",
|
||||
"value": "Lmc6R7FJN8cusA4INEWvDIY1.07SiAERtLRJ6ZxCJx4-1770130455-1.0.1.1-HMu4QPLPTlR9vbqgZp.aRLMaKdTlv1RhhGU9DUIRdtl4S4Gu6fAqKQr8qWeHnphSr2J1RUBi7RwhKLlU0mY8hJukUCNrUybZGYbsTzXgm6U",
|
||||
"expires": "2026-02-03T15:24:15.000Z",
|
||||
"domain": "klz-cables.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"httpOnly": true,
|
||||
"hostOnly": false,
|
||||
"creation": "2026-02-03T14:54:15.617Z",
|
||||
"lastAccessed": "2026-02-03T14:54:15.617Z",
|
||||
"sameSite": "none"
|
||||
}
|
||||
]
|
||||
},
|
||||
"userData": {},
|
||||
"maxErrorScore": 3,
|
||||
"errorScoreDecrement": 0.5,
|
||||
"expiresAt": "2026-02-03T15:44:01.261Z",
|
||||
"createdAt": "2026-02-03T14:54:01.261Z",
|
||||
"usageCount": 1,
|
||||
"maxUsageCount": 50,
|
||||
"errorScore": 0
|
||||
},
|
||||
{
|
||||
"id": "session_kdWw2fLTqx",
|
||||
"cookieJar": {
|
||||
"version": "tough-cookie@6.0.0",
|
||||
"storeType": "MemoryCookieStore",
|
||||
"rejectPublicSuffixes": true,
|
||||
"enableLooseMode": false,
|
||||
"allowSpecialUseDomain": true,
|
||||
"prefixSecurity": "silent",
|
||||
"cookies": [
|
||||
{
|
||||
"key": "__cf_bm",
|
||||
"value": "bQdDA2de_60joMEzRdUsUFi_4WLgLPeBLirqRb7d_uE-1770130464-1.0.1.1-X0bi9u1khnVdIbXNcRDx.LTKr_BseH14RgLZ6JwYvJ9WgEK.2.jEErPM58DPBg8r3LKFs6TPAdKbKDWBBt5uCCuwISSDcip1nV9HBPy9Mg8",
|
||||
"expires": "2026-02-03T15:24:24.000Z",
|
||||
"domain": "klz-cables.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"httpOnly": true,
|
||||
"hostOnly": false,
|
||||
"creation": "2026-02-03T14:54:24.832Z",
|
||||
"lastAccessed": "2026-02-03T14:54:24.832Z",
|
||||
"sameSite": "none"
|
||||
}
|
||||
]
|
||||
},
|
||||
"userData": {},
|
||||
"maxErrorScore": 3,
|
||||
"errorScoreDecrement": 0.5,
|
||||
"expiresAt": "2026-02-03T15:44:15.632Z",
|
||||
"createdAt": "2026-02-03T14:54:15.632Z",
|
||||
"usageCount": 1,
|
||||
"maxUsageCount": 50,
|
||||
"errorScore": 0
|
||||
},
|
||||
{
|
||||
"id": "session_BWi2bOlkZc",
|
||||
"cookieJar": {
|
||||
"version": "tough-cookie@6.0.0",
|
||||
"storeType": "MemoryCookieStore",
|
||||
"rejectPublicSuffixes": true,
|
||||
"enableLooseMode": false,
|
||||
"allowSpecialUseDomain": true,
|
||||
"prefixSecurity": "silent",
|
||||
"cookies": [
|
||||
{
|
||||
"key": "__cf_bm",
|
||||
"value": "_ZTlV8p3fHgv2sQ1341UsoiaWEWjQIdHENu3DoCdSgk-1770130464-1.0.1.1-3K1XN01G4rfSBHqqkiuVrYRpLBt_d_tbOS6Jt1eqT2WwC2x7v2e1.92h0OfrR8_BM71iVOc4zhvGD55Y5PEYry23glfN8FtNvV1sOfjs1ZE",
|
||||
"expires": "2026-02-03T15:24:24.000Z",
|
||||
"domain": "klz-cables.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"httpOnly": true,
|
||||
"hostOnly": false,
|
||||
"creation": "2026-02-03T14:54:24.759Z",
|
||||
"lastAccessed": "2026-02-03T14:54:24.759Z",
|
||||
"sameSite": "none"
|
||||
}
|
||||
]
|
||||
},
|
||||
"userData": {},
|
||||
"maxErrorScore": 3,
|
||||
"errorScoreDecrement": 0.5,
|
||||
"expiresAt": "2026-02-03T15:44:15.634Z",
|
||||
"createdAt": "2026-02-03T14:54:15.634Z",
|
||||
"usageCount": 1,
|
||||
"maxUsageCount": 50,
|
||||
"errorScore": 0
|
||||
},
|
||||
{
|
||||
"id": "session_0jipwW4kaY",
|
||||
"cookieJar": {
|
||||
"version": "tough-cookie@6.0.0",
|
||||
"storeType": "MemoryCookieStore",
|
||||
"rejectPublicSuffixes": true,
|
||||
"enableLooseMode": false,
|
||||
"allowSpecialUseDomain": true,
|
||||
"prefixSecurity": "silent",
|
||||
"cookies": [
|
||||
{
|
||||
"key": "__cf_bm",
|
||||
"value": "C2yjMg1qBud.ZZQebFaSH1RV4BEgCtzo5yOvkasopVs-1770130464-1.0.1.1-9kPIfJ_AYDtxIcK59ZEYaaC6kfzIhphNRr_Cs8IR8dUHFYscGFOZdkj7TSg4fWm1R9YiS6U3CmOHBNwmcKEA6Wh7IS9IhYZZ0ZPJNQ41C_M",
|
||||
"expires": "2026-02-03T15:24:24.000Z",
|
||||
"domain": "klz-cables.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"httpOnly": true,
|
||||
"hostOnly": false,
|
||||
"creation": "2026-02-03T14:54:24.790Z",
|
||||
"lastAccessed": "2026-02-03T14:54:24.790Z",
|
||||
"sameSite": "none"
|
||||
}
|
||||
]
|
||||
},
|
||||
"userData": {},
|
||||
"maxErrorScore": 3,
|
||||
"errorScoreDecrement": 0.5,
|
||||
"expiresAt": "2026-02-03T15:44:15.634Z",
|
||||
"createdAt": "2026-02-03T14:54:15.634Z",
|
||||
"usageCount": 1,
|
||||
"maxUsageCount": 50,
|
||||
"errorScore": 0
|
||||
},
|
||||
{
|
||||
"id": "session_Eili27RzOp",
|
||||
"cookieJar": {
|
||||
"version": "tough-cookie@6.0.0",
|
||||
"storeType": "MemoryCookieStore",
|
||||
"rejectPublicSuffixes": true,
|
||||
"enableLooseMode": false,
|
||||
"allowSpecialUseDomain": true,
|
||||
"prefixSecurity": "silent",
|
||||
"cookies": [
|
||||
{
|
||||
"key": "__cf_bm",
|
||||
"value": "1e8PQFWIAE6NnE3Td2S_XAewywm4Lh94oxYUxHwvRZ4-1770130464-1.0.1.1-.owWHCyhXQIX1N_hvgr7RLwrxh1zhk_YkdhETwiDbWeykMKl6kWCQGHEIigQildrc8mo8qsVnSZjHtiEzXzkgWrnJYVsrwxBEcL2VzN..dY",
|
||||
"expires": "2026-02-03T15:24:24.000Z",
|
||||
"domain": "klz-cables.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"httpOnly": true,
|
||||
"hostOnly": false,
|
||||
"creation": "2026-02-03T14:54:24.666Z",
|
||||
"lastAccessed": "2026-02-03T14:54:24.666Z",
|
||||
"sameSite": "none"
|
||||
}
|
||||
]
|
||||
},
|
||||
"userData": {},
|
||||
"maxErrorScore": 3,
|
||||
"errorScoreDecrement": 0.5,
|
||||
"expiresAt": "2026-02-03T15:44:15.635Z",
|
||||
"createdAt": "2026-02-03T14:54:15.635Z",
|
||||
"usageCount": 1,
|
||||
"maxUsageCount": 50,
|
||||
"errorScore": 0
|
||||
},
|
||||
{
|
||||
"id": "session_0Dj2ncRL8O",
|
||||
"cookieJar": {
|
||||
"version": "tough-cookie@6.0.0",
|
||||
"storeType": "MemoryCookieStore",
|
||||
"rejectPublicSuffixes": true,
|
||||
"enableLooseMode": false,
|
||||
"allowSpecialUseDomain": true,
|
||||
"prefixSecurity": "silent",
|
||||
"cookies": [
|
||||
{
|
||||
"key": "__cf_bm",
|
||||
"value": "_N6mflXcsPrgYoeb4_jlX8bsGZw_e9gFRMZFTbAM.Fw-1770130465-1.0.1.1-5v0ZDOwXE4LjwRww18QrM.wcT_YUMPx6Z459jAJhrL9wPHGpCl1MLTmKXHjwsJBrdYb1DZ39vGrRWnmIAnSrIAcFkcEWGgRHpHbwoEHWCY8",
|
||||
"expires": "2026-02-03T15:24:25.000Z",
|
||||
"domain": "klz-cables.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"httpOnly": true,
|
||||
"hostOnly": false,
|
||||
"creation": "2026-02-03T14:54:25.073Z",
|
||||
"lastAccessed": "2026-02-03T14:54:25.073Z",
|
||||
"sameSite": "none"
|
||||
}
|
||||
]
|
||||
},
|
||||
"userData": {},
|
||||
"maxErrorScore": 3,
|
||||
"errorScoreDecrement": 0.5,
|
||||
"expiresAt": "2026-02-03T15:44:15.639Z",
|
||||
"createdAt": "2026-02-03T14:54:15.639Z",
|
||||
"expiresAt": "2026-02-03T18:42:02.733Z",
|
||||
"createdAt": "2026-02-03T17:52:02.733Z",
|
||||
"usageCount": 1,
|
||||
"maxUsageCount": 50,
|
||||
"errorScore": 0
|
||||
|
||||
Reference in New Issue
Block a user