Added card types
This commit is contained in:
31
src/lib/cards/esncard_2026.ts
Normal file
31
src/lib/cards/esncard_2026.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { Card } from './types';
|
||||
|
||||
// User-configurable settings for PDF generation
|
||||
export const ESNCard2026: Card = {
|
||||
name: 'ESNcard 2026',
|
||||
image: '/cards/2026.webp',
|
||||
textCard: {
|
||||
width: 50, // mm
|
||||
height: 35 // mm
|
||||
},
|
||||
photoCard: {
|
||||
width: 32, // mm
|
||||
height: 45 // mm
|
||||
},
|
||||
photo: {
|
||||
width: 28, // mm
|
||||
height: 38 // mm
|
||||
},
|
||||
textFields: {
|
||||
name: { x: 3, y: 5, size: 9 },
|
||||
nationality: { x: 3, y: 14, size: 9 },
|
||||
birthday: { x: 33, y: 14, size: 9 },
|
||||
studiesAt: { x: 3, y: 23, size: 9 },
|
||||
esnSection: { x: 3, y: 32, size: 9 },
|
||||
validityStart: { x: 33, y: 32, size: 9 }
|
||||
},
|
||||
photoFields: {
|
||||
photo: { x: 2, y: 2, width: 28, height: 38 },
|
||||
name: { x: 2, y: 42, size: 7 }
|
||||
}
|
||||
};
|
||||
31
src/lib/cards/esncard_anniversary.ts
Normal file
31
src/lib/cards/esncard_anniversary.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { Card } from './types';
|
||||
|
||||
// User-configurable settings for PDF generation
|
||||
export const ESNCardAnniversary: Card = {
|
||||
name: 'ESNcard Anniversary',
|
||||
image: '/cards/esncard_anniversary.png',
|
||||
textCard: {
|
||||
width: 45, // mm
|
||||
height: 30 // mm
|
||||
},
|
||||
photoCard: {
|
||||
width: 29, // mm
|
||||
height: 41 // mm
|
||||
},
|
||||
photo: {
|
||||
width: 26, // mm
|
||||
height: 36 // mm
|
||||
},
|
||||
textFields: {
|
||||
name: { x: 2, y: 4, size: 8 },
|
||||
nationality: { x: 2, y: 12, size: 8 },
|
||||
birthday: { x: 30, y: 12, size: 8 },
|
||||
studiesAt: { x: 2, y: 20, size: 8 },
|
||||
esnSection: { x: 2, y: 28, size: 8 },
|
||||
validityStart: { x: 30, y: 28, size: 8 }
|
||||
},
|
||||
photoFields: {
|
||||
photo: { x: 2, y: 2, width: 26, height: 36 },
|
||||
name: { x: 2, y: 40, size: 6 }
|
||||
}
|
||||
};
|
||||
5
src/lib/cards/index.ts
Normal file
5
src/lib/cards/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { ESNCardAnniversary } from './esncard_anniversary';
|
||||
import { ESNCard2026 } from './esncard_2026';
|
||||
import type { Card } from './types';
|
||||
|
||||
export const cardTypes: Card[] = [ESNCardAnniversary, ESNCard2026];
|
||||
46
src/lib/cards/types/index.ts
Normal file
46
src/lib/cards/types/index.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
export interface CardDimensions {
|
||||
width: number; // mm
|
||||
height: number; // mm
|
||||
}
|
||||
|
||||
export interface PhotoDimensions {
|
||||
width: number; // mm
|
||||
height: number; // mm
|
||||
}
|
||||
|
||||
export interface TextPosition {
|
||||
x: number; // mm, relative to cell top-left
|
||||
y: number; // mm, relative to cell top-left
|
||||
size: number; // font size in points
|
||||
}
|
||||
|
||||
export interface PhotoPosition {
|
||||
x: number; // mm, relative to cell top-left
|
||||
y: number; // mm, relative to cell top-left
|
||||
width: number; // mm
|
||||
height: number; // mm
|
||||
}
|
||||
|
||||
export interface TextFieldLayout {
|
||||
name: TextPosition;
|
||||
nationality: TextPosition;
|
||||
birthday: TextPosition;
|
||||
studiesAt: TextPosition;
|
||||
esnSection: TextPosition;
|
||||
validityStart: TextPosition;
|
||||
}
|
||||
|
||||
export interface PhotoFieldLayout {
|
||||
photo: PhotoPosition;
|
||||
name: TextPosition;
|
||||
}
|
||||
|
||||
export interface Card {
|
||||
name: string;
|
||||
image: string;
|
||||
textCard: CardDimensions;
|
||||
photoCard: CardDimensions;
|
||||
photo: PhotoDimensions;
|
||||
textFields: TextFieldLayout;
|
||||
photoFields: PhotoFieldLayout;
|
||||
}
|
||||
@@ -1,59 +1,71 @@
|
||||
<script lang="ts">
|
||||
import { currentStep } from '$lib/stores.js';
|
||||
import StepAuth from './wizard/StepAuth.svelte';
|
||||
import StepSheetSearch from './wizard/StepSheetSearch.svelte';
|
||||
import StepColumnMap from './wizard/StepColumnMap.svelte';
|
||||
import StepRowFilter from './wizard/StepRowFilter.svelte';
|
||||
import StepCardDetails from './wizard/StepCardDetails.svelte';
|
||||
import StepGallery from './wizard/StepGallery.svelte';
|
||||
import StepGenerate from './wizard/StepGenerate.svelte';
|
||||
import { currentStep, steps as stepNames, currentStepName } from '$lib/stores';
|
||||
import Splash from './Splash.svelte';
|
||||
import StepAuth from './wizard/StepAuth.svelte';
|
||||
import StepSheetSearch from './wizard/StepSheetSearch.svelte';
|
||||
import StepColumnMap from './wizard/StepColumnMap.svelte';
|
||||
import StepRowFilter from './wizard/StepRowFilter.svelte';
|
||||
import StepCardDetails from './wizard/StepCardDetails.svelte';
|
||||
import StepCardSelect from './wizard/StepCardSelect.svelte';
|
||||
import StepGallery from './wizard/StepGallery.svelte';
|
||||
import StepGenerate from './wizard/StepGenerate.svelte';
|
||||
|
||||
const steps = [
|
||||
StepAuth,
|
||||
StepSheetSearch,
|
||||
StepColumnMap,
|
||||
StepRowFilter,
|
||||
StepCardDetails,
|
||||
StepGallery,
|
||||
StepGenerate
|
||||
];
|
||||
const stepComponents = {
|
||||
splash: Splash,
|
||||
auth: StepAuth,
|
||||
search: StepSheetSearch,
|
||||
mapping: StepColumnMap,
|
||||
validation: StepRowFilter,
|
||||
'card-details': StepCardDetails,
|
||||
'card-select': StepCardSelect,
|
||||
gallery: StepGallery,
|
||||
generate: StepGenerate
|
||||
};
|
||||
|
||||
const stepTitles = [
|
||||
'Authenticate',
|
||||
'Select Sheet',
|
||||
'Map Columns',
|
||||
'Filter Rows',
|
||||
'Enter Card Details',
|
||||
'Preview Gallery',
|
||||
'Generate Cards'
|
||||
];
|
||||
const stepTitles = {
|
||||
splash: 'Welcome',
|
||||
auth: 'Authenticate',
|
||||
search: 'Select Sheet',
|
||||
mapping: 'Map Columns',
|
||||
validation: 'Filter Rows',
|
||||
'card-details': 'Enter Card Details',
|
||||
'card-select': 'Select Card Type',
|
||||
gallery: 'Preview Gallery',
|
||||
generate: 'Generate Cards'
|
||||
};
|
||||
|
||||
let currentTitle = $derived(stepTitles[$currentStepName]);
|
||||
let currentComponent = $derived(stepComponents[$currentStepName]);
|
||||
let currentStepIndex = $derived(stepNames.indexOf($currentStepName));
|
||||
</script>
|
||||
|
||||
<div class="bg-gray-100 min-h-screen p-4">
|
||||
<div class="container mx-auto max-w-4xl pb-10">
|
||||
<!-- Progress indicator -->
|
||||
<div class="bg-white rounded-lg shadow-sm p-6 mb-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h1 class="text-2xl font-bold text-gray-800">
|
||||
{stepTitles[$currentStep - 1]}
|
||||
</h1>
|
||||
<span class="text-sm text-gray-500">
|
||||
Step {$currentStep} of {steps.length}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Progress bar -->
|
||||
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
class="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
||||
style="width: {($currentStep / steps.length) * 100}%"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container mx-auto max-w-4xl pb-10">
|
||||
{#if $currentStepName !== 'splash'}
|
||||
<!-- Progress indicator -->
|
||||
<div class="bg-white rounded-lg shadow-sm p-6 mb-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h1 class="text-2xl font-bold text-gray-800">
|
||||
{currentTitle}
|
||||
</h1>
|
||||
<span class="text-sm text-gray-500">
|
||||
Step {currentStepIndex} of {stepNames.length - 1}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Step content -->
|
||||
<div class="bg-white rounded-lg shadow-sm">
|
||||
<svelte:component this={steps[$currentStep - 1]} />
|
||||
</div>
|
||||
</div>
|
||||
<!-- Progress bar -->
|
||||
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
class="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
||||
style="width: {(currentStepIndex / (stepNames.length - 1)) * 100}%"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Step content -->
|
||||
<div class="bg-white rounded-lg shadow-sm">
|
||||
<svelte:component this={currentComponent} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
{currentStep}
|
||||
onForward={handleContinue}
|
||||
textBack="Back to Row Selection"
|
||||
textForwardEnabled="Continue to Photo Review"
|
||||
textForwardEnabled="Continue to Card Selection"
|
||||
textForwardDisabled="Please fill out all fields"
|
||||
/>
|
||||
</div>
|
||||
|
||||
79
src/lib/components/wizard/StepCardSelect.svelte
Normal file
79
src/lib/components/wizard/StepCardSelect.svelte
Normal file
@@ -0,0 +1,79 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { currentStep, selectedCard } from '$lib/stores';
|
||||
import { cardTypes } from '$lib/cards';
|
||||
import type { Card } from '$lib/cards/types';
|
||||
import Navigator from './subcomponents/Navigator.svelte';
|
||||
|
||||
let selected: Card | null = $state(null);
|
||||
|
||||
onMount(() => {
|
||||
const savedCardName = localStorage.getItem('selectedCardName');
|
||||
if (savedCardName) {
|
||||
const foundCard = cardTypes.find((c) => c.name === savedCardName);
|
||||
if (foundCard) {
|
||||
selected = foundCard;
|
||||
selectedCard.set(foundCard);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function selectCard(card: Card) {
|
||||
selected = card;
|
||||
selectedCard.set(card);
|
||||
localStorage.setItem('selectedCardName', card.name);
|
||||
}
|
||||
|
||||
function onNext() {
|
||||
if (selected) {
|
||||
currentStep.set($currentStep + 1);
|
||||
}
|
||||
}
|
||||
|
||||
function onBack() {
|
||||
currentStep.set($currentStep - 1);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="p-6">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<div class="mb-6">
|
||||
<h2 class="text-xl font-semibold text-gray-900 mb-2">Select Card Type</h2>
|
||||
<p class="text-sm text-gray-700 mb-4">
|
||||
Choose the type of card you want to generate. This will determine the layout and dimensions of the final PDFs.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Card Type Selector -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 mb-8">
|
||||
{#each cardTypes as card (card.name)}
|
||||
<button
|
||||
class="relative rounded-lg border-2 p-2 transition-all duration-200"
|
||||
class:border-blue-600={selected?.name === card.name}
|
||||
class:border-gray-200={selected?.name !== card.name}
|
||||
class:shadow-lg={selected?.name === card.name}
|
||||
onclick={() => selectCard(card)}
|
||||
>
|
||||
<img src={card.image} alt={card.name} class="w-full h-auto rounded-md mb-2" />
|
||||
<p class="text-sm font-medium text-center text-gray-800">{card.name}</p>
|
||||
{#if selected?.name === card.name}
|
||||
<div
|
||||
class="absolute top-2 right-2 bg-blue-600 text-white rounded-full w-6 h-6 flex items-center justify-center"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 13l4 4L19 7"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<Navigator onForward={onNext} onBack={onBack} nextDisabled={!selected} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,8 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { env } from '$env/dynamic/public';
|
||||
import { PHOTO_DIMENSIONS } from '$lib/pdfSettings';
|
||||
import { columnMapping, sheetData, currentStep, pictures, cropRects } from '$lib/stores';
|
||||
import { columnMapping, sheetData, currentStep, pictures, cropRects, selectedCard } from '$lib/stores';
|
||||
import { downloadDriveImage, isGoogleDriveUrl, createImageObjectUrl, ensureToken } from '$lib/google';
|
||||
import Navigator from './subcomponents/Navigator.svelte';
|
||||
import PhotoCard from './subcomponents/PhotoCard.svelte';
|
||||
@@ -340,8 +339,9 @@ async function createPreviewBlob(original: Blob, maxSide = 1200, quality = 0.85)
|
||||
const faceCenterX = (x1 + (x2 - x1) / 2) * scaleX;
|
||||
const faceCenterY = (y1 + (y2 - y1) / 2) * scaleY;
|
||||
|
||||
// Use the photo card aspect ratio from PDF settings (width / height)
|
||||
const cropRatio = PHOTO_DIMENSIONS.width / PHOTO_DIMENSIONS.height;
|
||||
// Use the photo card aspect ratio from the selected card
|
||||
const photoDimensions = $selectedCard!.photo;
|
||||
const cropRatio = photoDimensions.width / photoDimensions.height;
|
||||
const offsetX = parseFloat(env.PUBLIC_FACE_OFFSET_X || '0.0');
|
||||
const offsetY = parseFloat(env.PUBLIC_FACE_OFFSET_Y || '0.0');
|
||||
const cropScale = parseFloat(env.PUBLIC_CROP_SCALE || '2.5');
|
||||
@@ -581,6 +581,7 @@ async function createPreviewBlob(original: Blob, maxSide = 1200, quality = 0.85)
|
||||
{#each photos as photo, index}
|
||||
<PhotoCard
|
||||
{photo}
|
||||
photoDimensions={$selectedCard!.photo}
|
||||
onCropUpdated={(e) => handleCropUpdate(index, e)}
|
||||
onRetry={() => retryPhoto(index)}
|
||||
/>
|
||||
@@ -593,7 +594,7 @@ async function createPreviewBlob(original: Blob, maxSide = 1200, quality = 0.85)
|
||||
<Navigator
|
||||
canProceed={canProceed()}
|
||||
{currentStep}
|
||||
textBack="Back to Card Details"
|
||||
textBack="Back to Card Selection"
|
||||
textForwardDisabled="Waiting from photos"
|
||||
textForwardEnabled={`Generate ${photos.filter((p) => p.status === 'success' && p.cropData).length} Cards`}
|
||||
/>
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { sheetData, currentStep, pictures, cropRects, cardDetails } from '$lib/stores';
|
||||
import { PDFDocument, StandardFonts, rgb } from 'pdf-lib';
|
||||
import { sheetData, currentStep, pictures, cropRects, cardDetails, selectedCard } from '$lib/stores';
|
||||
import type { Card } from '$lib/cards/types';
|
||||
import { PDFDocument, StandardFonts, rgb, type PDFPage } from 'pdf-lib';
|
||||
import * as fontkit from 'fontkit';
|
||||
import { clear } from 'idb-keyval';
|
||||
import {
|
||||
BORDER_CONFIG,
|
||||
TEXT_CONFIG,
|
||||
calculateGrid,
|
||||
getAbsolutePositionPt,
|
||||
getAbsolutePhotoDimensionsPt,
|
||||
getImageBlob,
|
||||
MM_TO_PT
|
||||
} from '$lib/pdfLayout';
|
||||
import {
|
||||
PAGE_SETTINGS,
|
||||
TEXT_CARD_DIMENSIONS,
|
||||
PHOTO_CARD_DIMENSIONS,
|
||||
TEXT_FIELD_LAYOUT,
|
||||
PHOTO_FIELD_LAYOUT
|
||||
} from '$lib/pdfSettings';
|
||||
BORDER_CONFIG,
|
||||
TEXT_CONFIG,
|
||||
calculateGrid,
|
||||
getAbsolutePositionPt,
|
||||
getAbsolutePhotoDimensionsPt,
|
||||
getImageBlob,
|
||||
MM_TO_PT
|
||||
} from '$lib/pdfLayout';
|
||||
import { PAGE_SETTINGS } from '$lib/pdfSettings';
|
||||
import type { PageSettings } from '$lib/pdfSettings';
|
||||
import Navigator from './subcomponents/Navigator.svelte';
|
||||
|
||||
type FileGenerationState = 'idle' | 'generating' | 'done' | 'error';
|
||||
|
||||
@@ -70,13 +67,10 @@
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
handleGenerateAll();
|
||||
// Add event listener for page unload
|
||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||
|
||||
// Start generation automatically when the component mounts
|
||||
handleGenerate('esncards_text.pdf');
|
||||
handleGenerate('esncards_photos.pdf');
|
||||
|
||||
// Cleanup function when component unmounts
|
||||
return () => {
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
@@ -84,6 +78,22 @@
|
||||
};
|
||||
});
|
||||
|
||||
async function handleGenerateAll() {
|
||||
if (!$selectedCard) return;
|
||||
|
||||
// Reset file states and revoke old URLs
|
||||
files.forEach((f) => {
|
||||
if (f.url) URL.revokeObjectURL(f.url);
|
||||
});
|
||||
files = JSON.parse(JSON.stringify(initialFiles));
|
||||
|
||||
// Generate both
|
||||
await handleGenerate('esncards_text.pdf');
|
||||
await handleGenerate('esncards_photos.pdf');
|
||||
}
|
||||
|
||||
const generationStarted = $derived(files.some((f) => f.state !== 'idle'));
|
||||
|
||||
// Load Roboto font
|
||||
async function loadRobotoFont() {
|
||||
try {
|
||||
@@ -110,15 +120,15 @@
|
||||
}
|
||||
|
||||
// Draw a very wide 'H' (10 cm length) at the top and left margins as registration marks
|
||||
function drawHMarks(page: any, font: any) {
|
||||
function drawHMarks(page: PDFPage, font: any, pageSettings: PageSettings) {
|
||||
const color = rgb(0, 0, 0); // pure black
|
||||
const lengthPt = 100 * MM_TO_PT; // 10 cm
|
||||
const tickLenPt = 2 * MM_TO_PT; // 2 mm tick
|
||||
const strokePt = 0.7; // visual thickness
|
||||
|
||||
// Top margin center
|
||||
const centerTopX = (PAGE_SETTINGS.pageWidth / 2) * MM_TO_PT;
|
||||
const centerTopY = (PAGE_SETTINGS.pageHeight - PAGE_SETTINGS.margin / 2) * MM_TO_PT;
|
||||
const centerTopX = (pageSettings.pageWidth / 2) * MM_TO_PT;
|
||||
const centerTopY = (pageSettings.pageHeight - pageSettings.margin / 2) * MM_TO_PT;
|
||||
const leftTopX = centerTopX - lengthPt / 2;
|
||||
const rightTopX = centerTopX + lengthPt / 2;
|
||||
|
||||
@@ -147,7 +157,6 @@
|
||||
color
|
||||
});
|
||||
|
||||
|
||||
// Label under the top bar, centered
|
||||
const label = 'Print gauge - if not 10 cm long, the page is not printed correctly!';
|
||||
const labelSize = 7;
|
||||
@@ -163,8 +172,8 @@
|
||||
});
|
||||
|
||||
// Left margin center (vertical bar)
|
||||
const centerLeftX = (PAGE_SETTINGS.margin / 2) * MM_TO_PT;
|
||||
const centerLeftY = (PAGE_SETTINGS.pageHeight / 2) * MM_TO_PT;
|
||||
const centerLeftX = (pageSettings.margin / 2) * MM_TO_PT;
|
||||
const centerLeftY = (pageSettings.pageHeight / 2) * MM_TO_PT;
|
||||
|
||||
// Vertical bar (left)
|
||||
page.drawRectangle({
|
||||
@@ -328,13 +337,6 @@
|
||||
const timestamp = getTimestamp();
|
||||
const baseName = fileName.replace('.pdf', '');
|
||||
fileToUpdate.downloadName = `${baseName}_${timestamp}.pdf`;
|
||||
|
||||
// Check if both PDFs are done, then clear sensitive data
|
||||
const allDone = files.every((f) => f.state === 'done' || f.state === 'error');
|
||||
if (allDone) {
|
||||
console.log('All PDFs generated, clearing sensitive data...');
|
||||
await clearSensitiveData();
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(`PDF generation failed for ${fileName}:`, error);
|
||||
fileToUpdate.state = 'error';
|
||||
@@ -343,6 +345,9 @@
|
||||
}
|
||||
|
||||
async function generateTextPDF() {
|
||||
const card = $selectedCard;
|
||||
if (!card) throw new Error('No card type selected');
|
||||
|
||||
const pdfDoc = await PDFDocument.create();
|
||||
pdfDoc.registerFontkit(fontkit);
|
||||
|
||||
@@ -355,8 +360,8 @@
|
||||
PAGE_SETTINGS.pageWidth,
|
||||
PAGE_SETTINGS.pageHeight,
|
||||
PAGE_SETTINGS.margin,
|
||||
TEXT_CARD_DIMENSIONS.width,
|
||||
TEXT_CARD_DIMENSIONS.height
|
||||
card.textCard.width,
|
||||
card.textCard.height
|
||||
);
|
||||
const pageDimsPt = {
|
||||
width: PAGE_SETTINGS.pageWidth * MM_TO_PT,
|
||||
@@ -364,7 +369,7 @@
|
||||
};
|
||||
|
||||
let page = pdfDoc.addPage([pageDimsPt.width, pageDimsPt.height]);
|
||||
drawHMarks(page, font);
|
||||
drawHMarks(page, font, PAGE_SETTINGS);
|
||||
let currentRow = 0;
|
||||
let currentCol = 0;
|
||||
|
||||
@@ -392,14 +397,12 @@
|
||||
const birthdayFmt = formatDateDDMMYY(birthday);
|
||||
const validityStartFmt = formatDateDDMMYY(validityStart);
|
||||
|
||||
console.log(birthday, validityStart)
|
||||
|
||||
// Row 1: Name
|
||||
const namePos = getAbsolutePositionPt(
|
||||
cellX_mm,
|
||||
cellY_mm,
|
||||
PAGE_SETTINGS.pageHeight,
|
||||
TEXT_FIELD_LAYOUT.name
|
||||
card.textFields.name
|
||||
);
|
||||
page.drawText(`${name} ${surname}`, {
|
||||
...namePos,
|
||||
@@ -412,7 +415,7 @@
|
||||
cellX_mm,
|
||||
cellY_mm,
|
||||
PAGE_SETTINGS.pageHeight,
|
||||
TEXT_FIELD_LAYOUT.nationality
|
||||
card.textFields.nationality
|
||||
);
|
||||
page.drawText(`${nationality}`, {
|
||||
...natPos,
|
||||
@@ -425,16 +428,16 @@
|
||||
cellX_mm,
|
||||
cellY_mm,
|
||||
PAGE_SETTINGS.pageHeight,
|
||||
TEXT_FIELD_LAYOUT.birthday
|
||||
card.textFields.birthday
|
||||
);
|
||||
// Row 3: Studies at
|
||||
const studiesPos = getAbsolutePositionPt(
|
||||
cellX_mm,
|
||||
cellY_mm,
|
||||
PAGE_SETTINGS.pageHeight,
|
||||
TEXT_FIELD_LAYOUT.studiesAt
|
||||
card.textFields.studiesAt
|
||||
);
|
||||
page.drawText(`${studiesAt}` , {
|
||||
page.drawText(`${studiesAt}`, {
|
||||
...studiesPos,
|
||||
font,
|
||||
color: rgb(TEXT_CONFIG.color.r, TEXT_CONFIG.color.g, TEXT_CONFIG.color.b)
|
||||
@@ -445,9 +448,9 @@
|
||||
cellX_mm,
|
||||
cellY_mm,
|
||||
PAGE_SETTINGS.pageHeight,
|
||||
TEXT_FIELD_LAYOUT.esnSection
|
||||
card.textFields.esnSection
|
||||
);
|
||||
page.drawText(`${esnSection}` , {
|
||||
page.drawText(`${esnSection}`, {
|
||||
...sectionPos,
|
||||
font,
|
||||
color: rgb(TEXT_CONFIG.color.r, TEXT_CONFIG.color.g, TEXT_CONFIG.color.b)
|
||||
@@ -458,9 +461,9 @@
|
||||
cellX_mm,
|
||||
cellY_mm,
|
||||
PAGE_SETTINGS.pageHeight,
|
||||
TEXT_FIELD_LAYOUT.validityStart
|
||||
card.textFields.validityStart
|
||||
);
|
||||
page.drawText(`${validityStartFmt}` , {
|
||||
page.drawText(`${validityStartFmt}`, {
|
||||
...validPos,
|
||||
font,
|
||||
color: rgb(TEXT_CONFIG.color.r, TEXT_CONFIG.color.g, TEXT_CONFIG.color.b)
|
||||
@@ -488,7 +491,7 @@
|
||||
currentRow++;
|
||||
if (currentRow >= gridLayout.rows) {
|
||||
page = pdfDoc.addPage([pageDimsPt.width, pageDimsPt.height]);
|
||||
drawHMarks(page, font);
|
||||
drawHMarks(page, font, PAGE_SETTINGS);
|
||||
currentRow = 0;
|
||||
}
|
||||
}
|
||||
@@ -498,6 +501,9 @@
|
||||
}
|
||||
|
||||
async function generatePhotoPDF() {
|
||||
const card = $selectedCard;
|
||||
if (!card) throw new Error('No card type selected');
|
||||
|
||||
const pdfDoc = await PDFDocument.create();
|
||||
pdfDoc.registerFontkit(fontkit);
|
||||
|
||||
@@ -510,8 +516,8 @@
|
||||
PAGE_SETTINGS.pageWidth,
|
||||
PAGE_SETTINGS.pageHeight,
|
||||
PAGE_SETTINGS.margin,
|
||||
PHOTO_CARD_DIMENSIONS.width,
|
||||
PHOTO_CARD_DIMENSIONS.height
|
||||
card.photoCard.width,
|
||||
card.photoCard.height
|
||||
);
|
||||
const pageDimsPt = {
|
||||
width: PAGE_SETTINGS.pageWidth * MM_TO_PT,
|
||||
@@ -519,7 +525,7 @@
|
||||
};
|
||||
|
||||
let page = pdfDoc.addPage([pageDimsPt.width, pageDimsPt.height]);
|
||||
drawHMarks(page, font);
|
||||
drawHMarks(page, font, PAGE_SETTINGS);
|
||||
let currentRow = 0;
|
||||
let currentCol = 0;
|
||||
|
||||
@@ -537,7 +543,7 @@
|
||||
cellX_mm,
|
||||
cellY_mm,
|
||||
PAGE_SETTINGS.pageHeight,
|
||||
PHOTO_FIELD_LAYOUT.photo
|
||||
card.photoFields.photo
|
||||
);
|
||||
|
||||
const pictureUrl = row.pictureUrl;
|
||||
@@ -607,7 +613,7 @@
|
||||
cellX_mm,
|
||||
cellY_mm,
|
||||
PAGE_SETTINGS.pageHeight,
|
||||
PHOTO_FIELD_LAYOUT.name
|
||||
card.photoFields.name
|
||||
);
|
||||
page.drawText(`${name} ${surname}`, {
|
||||
...namePos,
|
||||
@@ -622,7 +628,7 @@
|
||||
currentRow++;
|
||||
if (currentRow >= gridLayout.rows) {
|
||||
page = pdfDoc.addPage([pageDimsPt.width, pageDimsPt.height]);
|
||||
drawHMarks(page, font);
|
||||
drawHMarks(page, font, PAGE_SETTINGS);
|
||||
currentRow = 0;
|
||||
}
|
||||
}
|
||||
@@ -667,148 +673,127 @@
|
||||
<div class="p-6">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<div class="mb-6">
|
||||
<h2 class="text-xl font-semibold text-gray-900 mb-2">
|
||||
Generating PDFs
|
||||
</h2>
|
||||
|
||||
<h2 class="text-xl font-semibold text-gray-900 mb-2">Generating PDFs...</h2>
|
||||
<p class="text-sm text-gray-700 mb-4">
|
||||
Create two PDF documents: one with text data and one with photos.
|
||||
Your PDF documents are being created. Please wait a moment.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Summary -->
|
||||
<div class="bg-gray-50 border border-gray-200 rounded-lg p-4 mb-4">
|
||||
<h3 class="text-sm font-medium text-gray-700 mb-3">Generation Summary</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold text-gray-900">
|
||||
{$sheetData.filter((row) => row._valid).length}
|
||||
{#if files.some((f) => f.state === 'generating')}
|
||||
<div class="mb-6 rounded-lg border border-blue-200 bg-blue-50 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div
|
||||
class="mr-3 h-5 w-5 animate-spin rounded-full border-2 border-blue-600 border-t-transparent"
|
||||
></div>
|
||||
<span class="text-sm text-blue-800"> Processing... </span>
|
||||
</div>
|
||||
<div class="text-gray-600">Records to Process</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold text-blue-600">{files.length}</div>
|
||||
<div class="text-gray-600">PDFs to Generate</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold text-green-600">
|
||||
{files.filter((f) => f.state === 'done').length}
|
||||
</div>
|
||||
<div class="text-gray-600">Files Ready</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Generated Files -->
|
||||
<div class="bg-white border border-gray-200 rounded-lg overflow-hidden mb-6">
|
||||
<div class="p-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900">Available Downloads</h3>
|
||||
</div>
|
||||
{#if generationStarted}
|
||||
<!-- Generated Files -->
|
||||
<div class="bg-white border border-gray-200 rounded-lg overflow-hidden mb-6">
|
||||
<div class="p-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900">Available Downloads</h3>
|
||||
</div>
|
||||
|
||||
<div class="divide-y divide-gray-200">
|
||||
{#each files as file (file.name)}
|
||||
<div class="p-4 flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
{#if file.displayName === 'Text PDF'}
|
||||
<svg
|
||||
class="w-8 h-8 text-red-600 mr-3"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||
<polyline points="14 2 14 8 20 8"></polyline>
|
||||
<line x1="12" y1="18" x2="12" y2="12"></line>
|
||||
<line x1="9" y1="12" x2="15" y2="12"></line>
|
||||
</svg>
|
||||
{:else if file.displayName === 'Photos PDF'}
|
||||
<svg
|
||||
class="w-8 h-8 text-red-600 mr-3"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||
<polyline points="14 2 14 8 20 8"></polyline>
|
||||
<circle cx="12" cy="13" r="2"></circle>
|
||||
<path d="M15 17.5c-1.5-1-4.5-1-6 0"></path>
|
||||
</svg>
|
||||
{/if}
|
||||
<div>
|
||||
<h4 class="text-sm font-medium text-gray-900">{file.displayName}</h4>
|
||||
{#if file.state === 'done' && file.size}
|
||||
<p class="text-xs text-gray-500">{formatFileSize(file.size)}</p>
|
||||
{:else if file.state === 'error'}
|
||||
<p class="text-xs text-red-500">Error: {file.error}</p>
|
||||
<div class="divide-y divide-gray-200">
|
||||
{#each files as file (file.name)}
|
||||
<div class="p-4 flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
{#if file.displayName === 'Text PDF'}
|
||||
<svg
|
||||
class="w-8 h-8 text-red-600 mr-3"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||
<polyline points="14 2 14 8 20 8"></polyline>
|
||||
<line x1="12" y1="18" x2="12" y2="12"></line>
|
||||
<line x1="9" y1="12" x2="15" y2="12"></line>
|
||||
</svg>
|
||||
{:else if file.displayName === 'Photos PDF'}
|
||||
<svg
|
||||
class="w-8 h-8 text-red-600 mr-3"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||
<polyline points="14 2 14 8 20 8"></polyline>
|
||||
<circle cx="12" cy="13" r="2"></circle>
|
||||
<path d="M15 17.5c-1.5-1-4.5-1-6 0"></path>
|
||||
</svg>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if file.state === 'idle'}
|
||||
<button
|
||||
onclick={() => handleGenerate(file.name)}
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700"
|
||||
>
|
||||
Generate
|
||||
</button>
|
||||
{:else if file.state === 'generating'}
|
||||
<button
|
||||
disabled
|
||||
class="px-4 py-2 bg-gray-400 text-white rounded-md text-sm font-medium cursor-wait"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<div
|
||||
class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2"
|
||||
></div>
|
||||
Generating...
|
||||
<div>
|
||||
<h4 class="text-sm font-medium text-gray-900">{file.displayName}</h4>
|
||||
{#if file.state === 'done' && file.size}
|
||||
<p class="text-xs text-gray-500">{formatFileSize(file.size)}</p>
|
||||
{:else if file.state === 'error'}
|
||||
<p class="text-xs text-red-500">Error: {file.error}</p>
|
||||
{:else if file.state === 'generating'}
|
||||
<p class="text-xs text-gray-500">Generating...</p>
|
||||
{:else if file.state === 'idle'}
|
||||
<p class="text-xs text-gray-500">Waiting...</p>
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
{:else if file.state === 'done'}
|
||||
<button
|
||||
onclick={() => downloadFile(file)}
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700"
|
||||
>
|
||||
Download
|
||||
</button>
|
||||
{:else if file.state === 'error'}
|
||||
<button
|
||||
onclick={() => handleGenerate(file.name)}
|
||||
class="px-4 py-2 bg-red-600 text-white rounded-md text-sm font-medium hover:bg-red-700"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if file.state === 'idle'}
|
||||
<div class="px-4 py-2 text-gray-500 text-sm">Waiting...</div>
|
||||
{:else if file.state === 'generating'}
|
||||
<button
|
||||
disabled
|
||||
aria-label="Generating..."
|
||||
class="px-4 py-2 bg-gray-400 text-white rounded-md text-sm font-medium cursor-wait"
|
||||
>
|
||||
<div class="flex items-center justify-center">
|
||||
<div
|
||||
class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"
|
||||
></div>
|
||||
</div>
|
||||
</button>
|
||||
{:else if file.state === 'done'}
|
||||
<button
|
||||
onclick={() => downloadFile(file)}
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700"
|
||||
>
|
||||
Download
|
||||
</button>
|
||||
{:else if file.state === 'error'}
|
||||
<button
|
||||
onclick={() => handleGenerate(file.name)}
|
||||
class="px-4 py-2 bg-red-600 text-white rounded-md text-sm font-medium hover:bg-red-700"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Navigation -->
|
||||
<div class="flex justify-between">
|
||||
<button
|
||||
onclick={() => currentStep.set(6)}
|
||||
class="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg font-medium hover:bg-gray-300"
|
||||
>
|
||||
← Back to Gallery
|
||||
</button>
|
||||
|
||||
{#if files.some((f) => f.state === 'done' || f.state === 'error')}
|
||||
<button
|
||||
onclick={resetAndStartOver}
|
||||
class="px-4 py-2 bg-green-600 text-white rounded-lg font-medium hover:bg-green-700"
|
||||
>
|
||||
Start Over
|
||||
</button>
|
||||
{/if}
|
||||
<div class="mt-10">
|
||||
<Navigator
|
||||
{currentStep}
|
||||
onForward={resetAndStartOver}
|
||||
canProceed={files.some((f) => f.state === 'done' || f.state === 'error')}
|
||||
textBack="Back to Gallery"
|
||||
textForwardEnabled="Start Over"
|
||||
textForwardDisabled="Generate PDFs to Continue"
|
||||
hideForwardUntilProceedable={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,56 +1,61 @@
|
||||
<script lang="ts">
|
||||
let {
|
||||
canProceed,
|
||||
canProceed = true,
|
||||
currentStep,
|
||||
textBack,
|
||||
textForwardDisabled,
|
||||
textForwardEnabled,
|
||||
textBack = 'Back',
|
||||
textForwardDisabled = 'Next',
|
||||
textForwardEnabled = 'Next',
|
||||
onBack,
|
||||
onForward
|
||||
onForward,
|
||||
nextDisabled = false
|
||||
} = $props<{
|
||||
canProceed: boolean;
|
||||
currentStep: any;
|
||||
textBack: string;
|
||||
textForwardDisabled: string;
|
||||
textForwardEnabled: string;
|
||||
onBack?: () => void | null;
|
||||
onForward?: () => void | null;
|
||||
canProceed?: boolean;
|
||||
currentStep?: any;
|
||||
textBack?: string;
|
||||
textForwardDisabled?: string;
|
||||
textForwardEnabled?: string;
|
||||
onBack?: () => void;
|
||||
onForward?: () => void;
|
||||
nextDisabled?: boolean;
|
||||
}>();
|
||||
|
||||
async function handleBack() {
|
||||
if (onBack) {
|
||||
// Allow custom back logic if provided
|
||||
await onBack();
|
||||
} else if (currentStep) {
|
||||
currentStep.set($currentStep - 1);
|
||||
}
|
||||
currentStep.set($currentStep - 1);
|
||||
}
|
||||
|
||||
async function handleForward() {
|
||||
if (onForward) {
|
||||
// Allow custom forward logic if provided
|
||||
await onForward();
|
||||
}
|
||||
currentStep.set($currentStep + 1);
|
||||
if (currentStep) {
|
||||
currentStep.set($currentStep + 1);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:justify-between">
|
||||
<button
|
||||
onclick={handleBack}
|
||||
class="flex w-full items-center justify-center gap-2 rounded-lg bg-gray-200 px-4 py-2 font-medium text-gray-700 hover:bg-gray-300 sm:w-auto"
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
<span>{textBack}</span>
|
||||
</button>
|
||||
{#if onBack || currentStep}
|
||||
<button
|
||||
onclick={handleBack}
|
||||
class="flex w-full items-center justify-center gap-2 rounded-lg bg-gray-200 px-4 py-2 font-medium text-gray-700 hover:bg-gray-300 sm:w-auto"
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
<span>{textBack}</span>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
onclick={handleForward}
|
||||
disabled={!canProceed}
|
||||
disabled={!canProceed || nextDisabled}
|
||||
class="flex w-full items-center justify-center gap-2 rounded-lg bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700 disabled:cursor-not-allowed disabled:bg-gray-400 sm:w-auto"
|
||||
>
|
||||
<span>{canProceed ? textForwardEnabled : textForwardDisabled}</span>
|
||||
<span>{canProceed && !nextDisabled ? textForwardEnabled : textForwardDisabled}</span>
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import type { PhotoDimensions } from '$lib/cards/types';
|
||||
import PhotoCrop from './PhotoCrop.svelte';
|
||||
|
||||
let { photo, onCropUpdated, onRetry } = $props<{
|
||||
let { photo, onCropUpdated, onRetry, photoDimensions } = $props<{
|
||||
photo: {
|
||||
name: string;
|
||||
url: string;
|
||||
@@ -13,6 +14,7 @@
|
||||
};
|
||||
onCropUpdated: (detail: any) => void;
|
||||
onRetry: () => void;
|
||||
photoDimensions: PhotoDimensions;
|
||||
}>();
|
||||
|
||||
let showCropper = $state(false);
|
||||
@@ -145,6 +147,7 @@
|
||||
imageUrl={photo.objectUrl}
|
||||
personName={photo.name}
|
||||
initialCropData={photo.cropData}
|
||||
{photoDimensions}
|
||||
onClose={() => (showCropper = false)}
|
||||
onCropUpdated={handleCropUpdated}
|
||||
/>
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { PHOTO_DIMENSIONS } from '$lib/pdfSettings';
|
||||
import type { PhotoDimensions } from '$lib/cards/types';
|
||||
|
||||
let { imageUrl, personName, initialCropData, onCropUpdated, onClose } = $props<{
|
||||
let {
|
||||
imageUrl,
|
||||
personName,
|
||||
initialCropData,
|
||||
onCropUpdated,
|
||||
onClose,
|
||||
photoDimensions
|
||||
} = $props<{
|
||||
imageUrl: string;
|
||||
personName: string;
|
||||
initialCropData?: { x: number; y: number; width: number; height: number };
|
||||
@@ -10,6 +17,7 @@
|
||||
cropData: { x: number; y: number; width: number; height: number };
|
||||
}) => void;
|
||||
onClose: () => void;
|
||||
photoDimensions: PhotoDimensions;
|
||||
}>();
|
||||
|
||||
let canvas: HTMLCanvasElement;
|
||||
@@ -35,8 +43,8 @@
|
||||
let canvasWidth = 600;
|
||||
let canvasHeight = 400;
|
||||
|
||||
// Use the photo card aspect ratio from PDF settings (width / height)
|
||||
const cropRatio = PHOTO_DIMENSIONS.width / PHOTO_DIMENSIONS.height;
|
||||
// Use the photo card aspect ratio from the selected card's dimensions
|
||||
const cropRatio = photoDimensions.width / photoDimensions.height;
|
||||
|
||||
onMount(() => {
|
||||
ctx = canvas.getContext('2d')!;
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
// PDF Layout Configuration Module
|
||||
// Centralized configuration for PDF generation layouts, using millimeters.
|
||||
|
||||
import {
|
||||
PHOTO_DIMENSIONS,
|
||||
TEXT_FIELD_LAYOUT,
|
||||
PHOTO_FIELD_LAYOUT
|
||||
} from './pdfSettings';
|
||||
import { get } from 'idb-keyval';
|
||||
|
||||
// Conversion factor from millimeters to points (1 inch = 72 points, 1 inch = 25.4 mm)
|
||||
|
||||
@@ -6,111 +6,9 @@ export interface PageSettings {
|
||||
margin: number; // mm
|
||||
}
|
||||
|
||||
export interface CardDimensions {
|
||||
width: number; // mm
|
||||
height: number; // mm
|
||||
}
|
||||
|
||||
// A4 Page dimensions in millimeters
|
||||
export const PAGE_SETTINGS: PageSettings = {
|
||||
pageWidth: 210,
|
||||
pageHeight: 297,
|
||||
margin: 15
|
||||
};
|
||||
|
||||
// Dimensions for a single card in the text PDF.
|
||||
// These dimensions will be used to calculate how many cards can fit on a page.
|
||||
export const TEXT_CARD_DIMENSIONS: CardDimensions = {
|
||||
width: 45,
|
||||
height: 30
|
||||
};
|
||||
|
||||
// Dimensions for a single card in the photo PDF.
|
||||
export const PHOTO_CARD_DIMENSIONS: CardDimensions = {
|
||||
width: 29,
|
||||
height: 41
|
||||
};
|
||||
|
||||
// Photo dimensions within the photo card
|
||||
export const PHOTO_DIMENSIONS = {
|
||||
width: 26, // mm
|
||||
height: 36 // mm
|
||||
};
|
||||
|
||||
export interface TextPosition {
|
||||
x: number; // mm, relative to cell top-left
|
||||
y: number; // mm, relative to cell top-left
|
||||
size: number; // font size in points
|
||||
}
|
||||
|
||||
export interface PhotoPosition {
|
||||
x: number; // mm, relative to cell top-left
|
||||
y: number; // mm, relative to cell top-left
|
||||
width: number; // mm
|
||||
height: number; // mm
|
||||
}
|
||||
|
||||
export interface TextFieldLayout {
|
||||
name: TextPosition;
|
||||
nationality: TextPosition;
|
||||
birthday: TextPosition;
|
||||
studiesAt: TextPosition;
|
||||
esnSection: TextPosition;
|
||||
validityStart: TextPosition;
|
||||
}
|
||||
|
||||
export interface PhotoFieldLayout {
|
||||
photo: PhotoPosition;
|
||||
name: TextPosition;
|
||||
}
|
||||
|
||||
const FONT_SIZE = 8; // pt
|
||||
|
||||
// Text PDF Field Positions (in mm, relative to cell top-left)
|
||||
export const TEXT_FIELD_LAYOUT: TextFieldLayout = {
|
||||
name: {
|
||||
x: 2,
|
||||
y: 4,
|
||||
size: FONT_SIZE
|
||||
},
|
||||
nationality: {
|
||||
x: 2,
|
||||
y: 12,
|
||||
size: FONT_SIZE
|
||||
},
|
||||
birthday: {
|
||||
x: 30,
|
||||
y: 12,
|
||||
size: FONT_SIZE
|
||||
},
|
||||
studiesAt: {
|
||||
x: 2,
|
||||
y: 20,
|
||||
size: FONT_SIZE
|
||||
},
|
||||
esnSection: {
|
||||
x: 2,
|
||||
y: 28,
|
||||
size: FONT_SIZE
|
||||
},
|
||||
validityStart: {
|
||||
x: 30,
|
||||
y: 28,
|
||||
size: FONT_SIZE
|
||||
}
|
||||
};
|
||||
|
||||
// Photo PDF Field Positions (in mm, relative to cell top-left)
|
||||
export const PHOTO_FIELD_LAYOUT: PhotoFieldLayout = {
|
||||
photo: {
|
||||
x: 2,
|
||||
y: 2,
|
||||
width: PHOTO_DIMENSIONS.width,
|
||||
height: PHOTO_DIMENSIONS.height
|
||||
},
|
||||
name: {
|
||||
x: 2,
|
||||
y: PHOTO_DIMENSIONS.height + 4,
|
||||
size: 6
|
||||
}
|
||||
};
|
||||
|
||||
@@ -95,6 +95,10 @@ export const selectedSheet = writable<SheetInfoType>({ id: '', name: '', webView
|
||||
// Card details for generation
|
||||
export const cardDetails = writable<CardDetailsType | null>(null);
|
||||
|
||||
// Selected card type for generation
|
||||
import type { Card } from '$lib/cards/types';
|
||||
export const selectedCard = writable<Card | null>(null);
|
||||
|
||||
// Wizard state management
|
||||
export const currentStep = writable<number>(0);
|
||||
|
||||
@@ -104,6 +108,8 @@ export const steps = [
|
||||
'search',
|
||||
'mapping',
|
||||
'validation',
|
||||
'card-details',
|
||||
'card-select',
|
||||
'gallery',
|
||||
'generate'
|
||||
] as const;
|
||||
|
||||
BIN
static/cards/2026.webp
Normal file
BIN
static/cards/2026.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
BIN
static/cards/esncard_anniversary.png
Normal file
BIN
static/cards/esncard_anniversary.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 426 KiB |
Reference in New Issue
Block a user