Proper sizing in the layout
This commit is contained in:
5
.github/copilot-instructions.md
vendored
5
.github/copilot-instructions.md
vendored
@@ -7,7 +7,7 @@
|
||||
- Pass fucntions as props instead od dispatching events
|
||||
- Mixing old (on:click) and new syntaxes for event handling is not allowed. Use only the onclick syntax
|
||||
- when setting state entity, simply od variable = newValue, do not use setState or similar methods like $state.
|
||||
- USe $props instead of export let!
|
||||
- USe $props instead of "export let"!
|
||||
- Use styling from ".github/styling.md" for any UI components.
|
||||
- Refer to the ".github/core-instructions.md" for the overall structure of the application.
|
||||
- Generate ".github/done.md" file to see what is done and what is not. Check it when you start and finish a task.
|
||||
@@ -15,4 +15,5 @@
|
||||
- Avoid unncessary iterations. If problems is mostly solved, stop.
|
||||
- Split big components into subcomponents. Always create smaller subcomponents for better context management later.
|
||||
- Do not do what you're not being asked. Stick to scope of my request.
|
||||
- Do not edit stores.ts ! Unless is explicitly allow you to.
|
||||
- Do not edit stores.ts ! Unless is explicitly allow you to.
|
||||
- Focus only on files that are relevant. Do not venture to fix other things.
|
||||
@@ -3,31 +3,38 @@
|
||||
import Navigator from './subcomponents/Navigator.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let homeSection = $state('');
|
||||
let esnSection = $state('');
|
||||
let studiesAt = $state('');
|
||||
let validityStart = $state('');
|
||||
|
||||
onMount(() => {
|
||||
validityStart = new Date().toISOString().split('T')[0];
|
||||
|
||||
try {
|
||||
const savedHomeSection = localStorage.getItem('homeSection');
|
||||
if (savedHomeSection) {
|
||||
homeSection = savedHomeSection;
|
||||
const savedesnSection = localStorage.getItem('esnSection');
|
||||
if (savedesnSection) {
|
||||
esnSection = savedesnSection;
|
||||
}
|
||||
const savedStudiesAt = localStorage.getItem('studiesAt');
|
||||
if (savedStudiesAt) {
|
||||
studiesAt = savedStudiesAt;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to access localStorage on mount:', error);
|
||||
}
|
||||
});
|
||||
|
||||
let canProceed = $derived(homeSection.trim() !== '' && validityStart.trim() !== '');
|
||||
let canProceed = $derived(esnSection.trim() !== '' && studiesAt.trim() !== '' && validityStart.trim() !== '');
|
||||
|
||||
function handleContinue() {
|
||||
try {
|
||||
localStorage.setItem('homeSection', homeSection);
|
||||
localStorage.setItem('esnSection', esnSection);
|
||||
localStorage.setItem('studiesAt', studiesAt);
|
||||
} catch (error) {
|
||||
console.error('Failed to save to localStorage:', error);
|
||||
}
|
||||
$cardDetails = { homeSection, validityStart };
|
||||
// Include new field; spread in case store has more fields defined elsewhere
|
||||
$cardDetails = { ...$cardDetails, esnSection, studiesAt, validityStart } as any;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -41,18 +48,31 @@
|
||||
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<label for="homeSection" class="mb-2 block text-sm font-medium text-gray-700">
|
||||
Home Section
|
||||
<label for="esnSection" class="mb-2 block text-sm font-medium text-gray-700">
|
||||
ESN Section
|
||||
</label>
|
||||
<input
|
||||
id="homeSection"
|
||||
id="esnSection"
|
||||
type="text"
|
||||
bind:value={homeSection}
|
||||
bind:value={esnSection}
|
||||
placeholder="e.g., ESN VUT Brno"
|
||||
class="w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-gray-900 focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="studiesAt" class="mb-2 block text-sm font-medium text-gray-700">
|
||||
Studies At
|
||||
</label>
|
||||
<input
|
||||
id="studiesAt"
|
||||
type="text"
|
||||
bind:value={studiesAt}
|
||||
placeholder="e.g., Brno University of Technology"
|
||||
class="w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-gray-900 focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="validityStart" class="mb-2 block text-sm font-medium text-gray-700">
|
||||
Card Validity Start Date
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
}
|
||||
|
||||
// Initialize queues with more conservative concurrency
|
||||
downloadQueue = new PQueue({ concurrency: 3 }); // Reduced from 5
|
||||
downloadQueue = new PQueue({ concurrency: 4 }); // Reduced from 5
|
||||
faceDetectionQueue = new PQueue({ concurrency: 1 }); // Keep at 1 for memory safety
|
||||
|
||||
// When both queues are idle, we're done
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { sheetData, currentStep, pictures, cropRects } from '$lib/stores';
|
||||
import { sheetData, currentStep, pictures, cropRects, cardDetails } from '$lib/stores';
|
||||
import { PDFDocument, StandardFonts, rgb } 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
|
||||
import {
|
||||
BORDER_CONFIG,
|
||||
TEXT_CONFIG,
|
||||
calculateGrid,
|
||||
getAbsolutePositionPt,
|
||||
getAbsolutePhotoDimensionsPt,
|
||||
getImageBlob,
|
||||
MM_TO_PT
|
||||
} from '$lib/pdfLayout';
|
||||
import {
|
||||
PAGE_SETTINGS,
|
||||
@@ -109,6 +109,138 @@
|
||||
return `${year}-${month}-${day}-${hours}-${minutes}`;
|
||||
}
|
||||
|
||||
// Draw a very wide 'H' (10 cm length) at the top and left margins as registration marks
|
||||
function drawHMarks(page: any, font: any) {
|
||||
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 leftTopX = centerTopX - lengthPt / 2;
|
||||
const rightTopX = centerTopX + lengthPt / 2;
|
||||
|
||||
// Horizontal bar (top)
|
||||
page.drawRectangle({
|
||||
x: leftTopX,
|
||||
y: centerTopY - strokePt / 2,
|
||||
width: lengthPt,
|
||||
height: strokePt,
|
||||
color
|
||||
});
|
||||
// Left vertical tick (top)
|
||||
page.drawRectangle({
|
||||
x: leftTopX - strokePt / 2,
|
||||
y: centerTopY - tickLenPt / 2,
|
||||
width: strokePt,
|
||||
height: tickLenPt,
|
||||
color
|
||||
});
|
||||
// Right vertical tick (top)
|
||||
page.drawRectangle({
|
||||
x: rightTopX - strokePt / 2,
|
||||
y: centerTopY - tickLenPt / 2,
|
||||
width: strokePt,
|
||||
height: tickLenPt,
|
||||
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;
|
||||
const labelWidth = font.widthOfTextAtSize(label, labelSize);
|
||||
const labelX = centerTopX - labelWidth / 2; // center horizontally
|
||||
const labelY = centerTopY - 3 * MM_TO_PT; // ~3mm below the bar
|
||||
page.drawText(label, {
|
||||
x: labelX,
|
||||
y: labelY,
|
||||
size: labelSize,
|
||||
font,
|
||||
color
|
||||
});
|
||||
|
||||
// Left margin center (vertical bar)
|
||||
const centerLeftX = (PAGE_SETTINGS.margin / 2) * MM_TO_PT;
|
||||
const centerLeftY = (PAGE_SETTINGS.pageHeight / 2) * MM_TO_PT;
|
||||
|
||||
// Vertical bar (left)
|
||||
page.drawRectangle({
|
||||
x: centerLeftX - strokePt / 2,
|
||||
y: centerLeftY - lengthPt / 2,
|
||||
width: strokePt,
|
||||
height: lengthPt,
|
||||
color
|
||||
});
|
||||
// Top horizontal tick (left)
|
||||
page.drawRectangle({
|
||||
x: centerLeftX - tickLenPt / 2,
|
||||
y: centerLeftY + lengthPt / 2 - strokePt / 2,
|
||||
width: tickLenPt,
|
||||
height: strokePt,
|
||||
color
|
||||
});
|
||||
// Bottom horizontal tick (left)
|
||||
page.drawRectangle({
|
||||
x: centerLeftX - tickLenPt / 2,
|
||||
y: centerLeftY - lengthPt / 2 - strokePt / 2,
|
||||
width: tickLenPt,
|
||||
height: strokePt,
|
||||
color
|
||||
});
|
||||
}
|
||||
|
||||
// Format a date-like string into "DD MM YY" with basic heuristics for common inputs
|
||||
function formatDateDDMMYY(value: string): string {
|
||||
if (!value) return '';
|
||||
const trimmed = value.trim();
|
||||
|
||||
// ISO: YYYY-MM-DD or YYYY/MM/DD
|
||||
let m = trimmed.match(/^(\d{4})[\/\-](\d{2})[\/\-](\d{2})$/);
|
||||
if (m) {
|
||||
const [, y, mo, d] = m;
|
||||
return `${d} ${mo} ${y.slice(-2)}`;
|
||||
}
|
||||
|
||||
// DMY with separators: DD.MM.YYYY or DD/MM/YYYY or DD-MM-YYYY
|
||||
m = trimmed.match(/^(\d{2})[.\/-](\d{2})[.\/-](\d{4})$/);
|
||||
if (m) {
|
||||
const [, d, mo, y] = m;
|
||||
return `${d} ${mo} ${y.slice(-2)}`;
|
||||
}
|
||||
|
||||
// DMY two-digit year: DD.MM.YY or DD/MM/YY or DD-MM-YY
|
||||
m = trimmed.match(/^(\d{2})[.\/-](\d{2})[.\/-](\d{2})$/);
|
||||
if (m) {
|
||||
const [, d, mo, y2] = m;
|
||||
return `${d} ${mo} ${y2}`;
|
||||
}
|
||||
|
||||
// Try native Date parsing as a last resort
|
||||
const parsed = new Date(trimmed);
|
||||
if (!isNaN(parsed.getTime())) {
|
||||
const dd = String(parsed.getDate()).padStart(2, '0');
|
||||
const mm = String(parsed.getMonth() + 1).padStart(2, '0');
|
||||
const yy = String(parsed.getFullYear()).slice(-2);
|
||||
return `${dd} ${mm} ${yy}`;
|
||||
}
|
||||
|
||||
// Handle plain numeric serials (e.g., Google/Excel serial days since 1899-12-30)
|
||||
if (/^\d{5,}$/.test(trimmed)) {
|
||||
const days = parseInt(trimmed, 10);
|
||||
const base = new Date(Date.UTC(1899, 11, 30));
|
||||
base.setUTCDate(base.getUTCDate() + days);
|
||||
const dd = String(base.getUTCDate()).padStart(2, '0');
|
||||
const mm = String(base.getUTCMonth() + 1).padStart(2, '0');
|
||||
const yy = String(base.getUTCFullYear()).slice(-2);
|
||||
return `${dd} ${mm} ${yy}`;
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
// Crop image using canvas
|
||||
async function cropImage(
|
||||
imageBlob: Blob,
|
||||
@@ -177,7 +309,12 @@
|
||||
const pdfBytes =
|
||||
fileName === 'esncards_text.pdf' ? await generateTextPDF() : await generatePhotoPDF();
|
||||
|
||||
const blob = new Blob([pdfBytes], { type: 'application/pdf' });
|
||||
// Convert Uint8Array to ArrayBuffer slice to satisfy BlobPart typing
|
||||
const arrayBuffer = pdfBytes.buffer.slice(
|
||||
pdfBytes.byteOffset,
|
||||
pdfBytes.byteOffset + pdfBytes.byteLength
|
||||
);
|
||||
const blob = new Blob([arrayBuffer as ArrayBuffer], { type: 'application/pdf' });
|
||||
|
||||
// Revoke old URL if it exists
|
||||
if (fileToUpdate.url) {
|
||||
@@ -227,10 +364,15 @@
|
||||
};
|
||||
|
||||
let page = pdfDoc.addPage([pageDimsPt.width, pageDimsPt.height]);
|
||||
drawHMarks(page, font);
|
||||
let currentRow = 0;
|
||||
let currentCol = 0;
|
||||
|
||||
const validRows = $sheetData.filter((row) => row._valid);
|
||||
const details = $cardDetails;
|
||||
const studiesAtAll = details?.studiesAt ?? '';
|
||||
const esnSectionAll = details?.esnSection ?? '';
|
||||
const validityStartAll = details?.validityStart ?? '';
|
||||
|
||||
for (let i = 0; i < validRows.length; i++) {
|
||||
const row = validRows[i];
|
||||
@@ -244,8 +386,13 @@
|
||||
const surname = row.surname;
|
||||
const nationality = row.nationality;
|
||||
const birthday = row.birthday;
|
||||
const studiesAt = studiesAtAll;
|
||||
const esnSection = esnSectionAll;
|
||||
const validityStart = validityStartAll;
|
||||
const birthdayFmt = formatDateDDMMYY(birthday);
|
||||
const validityStartFmt = formatDateDDMMYY(validityStart);
|
||||
|
||||
// Draw name
|
||||
// Row 1: Name
|
||||
const namePos = getAbsolutePositionPt(
|
||||
cellX_mm,
|
||||
cellY_mm,
|
||||
@@ -258,27 +405,65 @@
|
||||
color: rgb(TEXT_CONFIG.color.r, TEXT_CONFIG.color.g, TEXT_CONFIG.color.b)
|
||||
});
|
||||
|
||||
// Draw nationality
|
||||
// Row 2 left: Nationality
|
||||
const natPos = getAbsolutePositionPt(
|
||||
cellX_mm,
|
||||
cellY_mm,
|
||||
PAGE_SETTINGS.pageHeight,
|
||||
TEXT_FIELD_LAYOUT.nationality
|
||||
);
|
||||
page.drawText(`Nationality: ${nationality}`, {
|
||||
page.drawText(`${nationality}`, {
|
||||
...natPos,
|
||||
font,
|
||||
color: rgb(TEXT_CONFIG.color.r, TEXT_CONFIG.color.g, TEXT_CONFIG.color.b)
|
||||
});
|
||||
|
||||
// Draw birthday
|
||||
// Row 2 right: Date of birth
|
||||
const bdayPos = getAbsolutePositionPt(
|
||||
cellX_mm,
|
||||
cellY_mm,
|
||||
PAGE_SETTINGS.pageHeight,
|
||||
TEXT_FIELD_LAYOUT.birthday
|
||||
);
|
||||
page.drawText(`Birthday: ${birthday}`, {
|
||||
// Row 3: Studies at
|
||||
const studiesPos = getAbsolutePositionPt(
|
||||
cellX_mm,
|
||||
cellY_mm,
|
||||
PAGE_SETTINGS.pageHeight,
|
||||
TEXT_FIELD_LAYOUT.studiesAt
|
||||
);
|
||||
page.drawText(`${studiesAt}` , {
|
||||
...studiesPos,
|
||||
font,
|
||||
color: rgb(TEXT_CONFIG.color.r, TEXT_CONFIG.color.g, TEXT_CONFIG.color.b)
|
||||
});
|
||||
|
||||
// Row 4 left: ESN Section
|
||||
const sectionPos = getAbsolutePositionPt(
|
||||
cellX_mm,
|
||||
cellY_mm,
|
||||
PAGE_SETTINGS.pageHeight,
|
||||
TEXT_FIELD_LAYOUT.esnSection
|
||||
);
|
||||
page.drawText(`${esnSection}` , {
|
||||
...sectionPos,
|
||||
font,
|
||||
color: rgb(TEXT_CONFIG.color.r, TEXT_CONFIG.color.g, TEXT_CONFIG.color.b)
|
||||
});
|
||||
|
||||
// Row 4 right: Valid from
|
||||
const validPos = getAbsolutePositionPt(
|
||||
cellX_mm,
|
||||
cellY_mm,
|
||||
PAGE_SETTINGS.pageHeight,
|
||||
TEXT_FIELD_LAYOUT.validityStart
|
||||
);
|
||||
page.drawText(`${validityStartFmt}` , {
|
||||
...validPos,
|
||||
font,
|
||||
color: rgb(TEXT_CONFIG.color.r, TEXT_CONFIG.color.g, TEXT_CONFIG.color.b)
|
||||
});
|
||||
page.drawText(`${birthdayFmt}`, {
|
||||
...bdayPos,
|
||||
font,
|
||||
color: rgb(TEXT_CONFIG.color.r, TEXT_CONFIG.color.g, TEXT_CONFIG.color.b)
|
||||
@@ -301,6 +486,7 @@
|
||||
currentRow++;
|
||||
if (currentRow >= gridLayout.rows) {
|
||||
page = pdfDoc.addPage([pageDimsPt.width, pageDimsPt.height]);
|
||||
drawHMarks(page, font);
|
||||
currentRow = 0;
|
||||
}
|
||||
}
|
||||
@@ -331,6 +517,7 @@
|
||||
};
|
||||
|
||||
let page = pdfDoc.addPage([pageDimsPt.width, pageDimsPt.height]);
|
||||
drawHMarks(page, font);
|
||||
let currentRow = 0;
|
||||
let currentCol = 0;
|
||||
|
||||
@@ -433,6 +620,7 @@
|
||||
currentRow++;
|
||||
if (currentRow >= gridLayout.rows) {
|
||||
page = pdfDoc.addPage([pageDimsPt.width, pageDimsPt.height]);
|
||||
drawHMarks(page, font);
|
||||
currentRow = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,20 +101,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Function to toggle all valid rows
|
||||
// Function to toggle select-all: selects first 200 eligible items in current view
|
||||
function toggleSelectAll(event: Event) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const shouldCheck = target.checked;
|
||||
|
||||
rows.forEach((row) => {
|
||||
if (row._valid && !row.alreadyPrinted) {
|
||||
if (shouldCheck && selectedCount < ROW_LIMIT) {
|
||||
row._checked = true;
|
||||
} else {
|
||||
row._checked = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
// Determine eligible rows in the current display order
|
||||
const eligible = displayData.filter((r) => r._valid && !r.alreadyPrinted);
|
||||
const firstBatch = eligible.slice(0, ROW_LIMIT);
|
||||
|
||||
if (shouldCheck) {
|
||||
// Check only the first batch, uncheck the rest
|
||||
rows.forEach((row) => (row._checked = false));
|
||||
firstBatch.forEach((row) => (row._checked = true));
|
||||
} else {
|
||||
// Uncheck all
|
||||
rows.forEach((row) => (row._checked = false));
|
||||
}
|
||||
}
|
||||
|
||||
// Function to handle sorting
|
||||
@@ -148,11 +151,12 @@
|
||||
});
|
||||
});
|
||||
|
||||
// Derived state to determine if the "Select All" checkbox should be checked
|
||||
// Derived state: master checkbox reflects if first 200 eligible items in current view are selected
|
||||
const allValidRowsSelected = $derived.by(() => {
|
||||
const validRows = rows.filter((row) => row._valid && !row.alreadyPrinted);
|
||||
if (validRows.length === 0) return false;
|
||||
return validRows.every((row) => row._checked);
|
||||
const eligible = displayData.filter((r) => r._valid && !r.alreadyPrinted);
|
||||
const firstBatch = eligible.slice(0, ROW_LIMIT);
|
||||
if (firstBatch.length === 0) return false;
|
||||
return firstBatch.every((row) => row._checked);
|
||||
});
|
||||
|
||||
const selectedCount = $derived(rows.filter((row) => row._checked).length);
|
||||
@@ -169,6 +173,7 @@
|
||||
<p class="text-sm text-gray-700">
|
||||
Review your data and select which rows to include. Select a batch of max 200 items by using the top checkbox.
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 mt-1">Note: Processing of cards is allowed only in batches of 200.</p>
|
||||
<p class="text-sm text-gray-700">
|
||||
Already printed or invalid data is marked in the status column.
|
||||
</p>
|
||||
|
||||
@@ -15,25 +15,25 @@ export interface CardDimensions {
|
||||
export const PAGE_SETTINGS: PageSettings = {
|
||||
pageWidth: 210,
|
||||
pageHeight: 297,
|
||||
margin: 10
|
||||
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: 63,
|
||||
height: 40
|
||||
width: 45,
|
||||
height: 30
|
||||
};
|
||||
|
||||
// Dimensions for a single card in the photo PDF.
|
||||
export const PHOTO_CARD_DIMENSIONS: CardDimensions = {
|
||||
width: 25,
|
||||
height: 35
|
||||
width: 27,
|
||||
height: 39
|
||||
};
|
||||
|
||||
// Photo dimensions within the photo card
|
||||
export const PHOTO_DIMENSIONS = {
|
||||
width: 20, // mm
|
||||
width: 25, // mm
|
||||
height: 35 // mm
|
||||
};
|
||||
|
||||
@@ -54,6 +54,9 @@ export interface TextFieldLayout {
|
||||
name: TextPosition;
|
||||
nationality: TextPosition;
|
||||
birthday: TextPosition;
|
||||
studiesAt: TextPosition;
|
||||
esnSection: TextPosition;
|
||||
validityStart: TextPosition;
|
||||
}
|
||||
|
||||
export interface PhotoFieldLayout {
|
||||
@@ -61,36 +64,53 @@ export interface PhotoFieldLayout {
|
||||
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,
|
||||
x: 3,
|
||||
y: 5,
|
||||
size: 10 // font size in points
|
||||
size: FONT_SIZE // font size in points
|
||||
},
|
||||
nationality: {
|
||||
x: 2,
|
||||
y: 10,
|
||||
size: 10
|
||||
x: 3,
|
||||
y: 12,
|
||||
size: FONT_SIZE
|
||||
},
|
||||
birthday: {
|
||||
x: 2,
|
||||
y: 15,
|
||||
size: 10
|
||||
x: 30,
|
||||
y: 12,
|
||||
size: FONT_SIZE
|
||||
},
|
||||
studiesAt: {
|
||||
x: 3,
|
||||
y: 20,
|
||||
size: FONT_SIZE
|
||||
},
|
||||
esnSection: {
|
||||
x: 3,
|
||||
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, // 2mm from left of cell
|
||||
y: 2, // 2mm from top of cell
|
||||
x: 2,
|
||||
y: 2,
|
||||
width: PHOTO_DIMENSIONS.width,
|
||||
height: PHOTO_DIMENSIONS.height
|
||||
},
|
||||
name: {
|
||||
x: 2, // 2mm from left of cell
|
||||
y: PHOTO_DIMENSIONS.height + 0, // Below the photo + 5mm gap
|
||||
size: 5 // font size in points
|
||||
x: 2,
|
||||
y: PHOTO_DIMENSIONS.height + 4,
|
||||
size: FONT_SIZE
|
||||
}
|
||||
};
|
||||
|
||||
@@ -61,7 +61,8 @@ export interface SheetInfoType {
|
||||
|
||||
// Card details type
|
||||
export interface CardDetailsType {
|
||||
homeSection: string;
|
||||
esnSection: string;
|
||||
studiesAt: string;
|
||||
validityStart: string;
|
||||
}
|
||||
|
||||
|
||||
1
src/routes/.layout.server.ts
Normal file
1
src/routes/.layout.server.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const ssr = false;
|
||||
Reference in New Issue
Block a user