Skip to content

Commit 9156bd5

Browse files
committed
Set title/description below QR code
1 parent a1cbc0d commit 9156bd5

File tree

1 file changed

+151
-75
lines changed

1 file changed

+151
-75
lines changed

src/routes/+page.svelte

Lines changed: 151 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,23 @@
88
import VCardForm from '$lib/qr_code_templates/VCardForm.svelte';
99
import CalendarEventForm from '$lib/qr_code_templates/CalendarEventForm.svelte';
1010
11-
let selectedModeValue = $state('wifi');
11+
// Constants for canvas rendering
12+
const CANVAS_QR_PADDING = 10; // Padding around the QR code graphic on the canvas
13+
const CANVAS_TITLE_FONT_SIZE = 20; // Font size for the title
14+
const CANVAS_TITLE_AREA_VERTICAL_PADDING = 5; // Vertical padding above and below the title text (each side)
1215
13-
// Calendar Event Details state variables are now moved to CalendarEventForm.svelte
16+
let selectedModeValue = $state('wifi');
17+
let qrTitle = $state('');
1418
let activeFormOutput = $state('');
1519
let activeFilenameHint = $state('');
1620
1721
let qrCodeDataURL = $state('');
18-
let size = $state(256);
22+
let size = $state(256); // This is the size of the QR code graphic itself
1923
let errorCorrectionLevel = $state('M');
2024
let isGenerating = $state(false);
2125
22-
let darkColor = $state('#000000'); // default black
23-
let lightColor = $state('#ffffff'); // default white
26+
let darkColor = $state('#000000');
27+
let lightColor = $state('#ffffff');
2428
2529
const modeOptions = [
2630
{ value: 'text', label: 'Text' },
@@ -33,78 +37,113 @@
3337
modeOptions.find((opt) => opt.value === selectedModeValue)?.label
3438
);
3539
36-
// All generate...String and getTextToEncode functions are removed
37-
// as their logic is now within their respective child components.
38-
// Child components will update `activeFormOutput`.
40+
// Derived values for UI display dimensions, reacting to `size` and `qrTitle`
41+
let titleUiExtraHeight = $derived(
42+
qrTitle.trim() ? CANVAS_TITLE_FONT_SIZE + CANVAS_TITLE_AREA_VERTICAL_PADDING * 2 : 0
43+
);
44+
// Width of the generated image (canvas)
45+
let qrImageActualWidth = $derived(size + CANVAS_QR_PADDING * 2);
46+
// Height of the generated image (canvas)
47+
let qrImageActualHeight = $derived(size + CANVAS_QR_PADDING * 2 + titleUiExtraHeight);
3948
40-
// Download QR code as image
4149
function downloadQRCode() {
4250
if (!qrCodeDataURL) return;
4351
4452
const link = document.createElement('a');
45-
// Make filename more unique to try and bust browser cache for downloads
4653
const timestamp = Date.now();
4754
let baseFilename = 'qrcode';
48-
if (activeFilenameHint) {
49-
baseFilename = activeFilenameHint.replace(/[^\\w-]/g, '_');
55+
56+
const titleTrimmed = qrTitle.trim();
57+
if (titleTrimmed) {
58+
baseFilename = titleTrimmed.replace(/[^-\w\s]/g, '').replace(/\s+/g, '_');
59+
} else if (activeFilenameHint) {
60+
baseFilename = activeFilenameHint.replace(/[^-\w\s]/g, '').replace(/\s+/g, '_');
5061
}
51-
// All mode-specific filename logic removed, activeFilenameHint handles this.
52-
// Append current size and timestamp to the filename
62+
5363
link.download = `${baseFilename}-${size}-${timestamp}.png`;
5464
link.href = qrCodeDataURL;
5565
link.click();
5666
}
5767
58-
// Auto-generate QR code when inputs change
5968
$effect(() => {
60-
// Capture reactive values from component state for this specific effect run
61-
const capturedModeValue = selectedModeValue;
62-
// WiFi state captures removed as WifiForm manages its own state
63-
// const capturedCustomText = customText; // Removed, TextForm output used directly
6469
const capturedSize = size;
6570
const capturedErrorCorrectionLevel = errorCorrectionLevel;
66-
// const capturedSimpleTestInput = simpleTestInput;
67-
68-
// console.log('Page - capturedSimpleTestInput:', capturedSimpleTestInput);
69-
// console.log('Page - capturedSize for slider:', capturedSize);
70-
// console.log('Page - activeFormOutput from child:', activeFormOutput);
71-
72-
// vCard details capture removed, VCardForm manages its own state
73-
// Calendar event details capture removed, CalendarEventForm manages its own state
74-
75-
// All form-specific data is now in activeFormOutput, provided by the active child component.
76-
// The getTextToEncode function is removed.
7771
const textToEncode = activeFormOutput;
72+
const capturedQrTitle = qrTitle;
73+
const capturedDarkColor = darkColor;
74+
const capturedLightColor = lightColor;
7875
79-
// Use an Immediately Invoked Async Function Expression (IIAFE)
80-
// to handle the asynchronous QR code generation.
8176
(async () => {
8277
if (!textToEncode.trim()) {
8378
qrCodeDataURL = '';
84-
// isGenerating should be false if we return early and no generation happens.
85-
// However, isGenerating is set to true only if we proceed.
8679
return;
8780
}
8881
8982
isGenerating = true;
9083
try {
91-
// QRCode is now imported statically at the top
92-
const options = {
93-
width: capturedSize, // Use captured size from the effect's scope
94-
margin: 2,
84+
const qrOptions = {
85+
width: capturedSize,
86+
margin: 0, // We handle padding on the canvas
9587
color: {
96-
dark: darkColor,
97-
light: lightColor
88+
dark: capturedDarkColor,
89+
light: '#00000000' // Transparent light color for QR, canvas bg will be lightColor
9890
},
99-
errorCorrectionLevel: capturedErrorCorrectionLevel // Use captured ECL
91+
errorCorrectionLevel: capturedErrorCorrectionLevel
10092
};
101-
// Assign to a temporary variable first to ensure the await completes
102-
// before updating the reactive state.
103-
const dataUrl = await QRCode.toDataURL(textToEncode, options);
104-
qrCodeDataURL = dataUrl;
93+
// Generate QR code as a data URL (this is the QR pattern only)
94+
const rawQrDataUrl = await QRCode.toDataURL(textToEncode, qrOptions);
95+
96+
// Draw QR and title onto a new canvas
97+
await new Promise((resolve, reject) => {
98+
const img = new Image();
99+
img.onload = () => {
100+
const canvas = document.createElement('canvas');
101+
const ctx = canvas.getContext('2d');
102+
const titleText = capturedQrTitle.trim();
103+
104+
const titleAreaHeightOnCanvas = titleText
105+
? CANVAS_TITLE_FONT_SIZE + CANVAS_TITLE_AREA_VERTICAL_PADDING * 2
106+
: 0;
107+
108+
canvas.width = capturedSize + CANVAS_QR_PADDING * 2;
109+
canvas.height = capturedSize + CANVAS_QR_PADDING * 2 + titleAreaHeightOnCanvas;
110+
111+
// Fill canvas background with the chosen light color
112+
ctx.fillStyle = capturedLightColor;
113+
ctx.fillRect(0, 0, canvas.width, canvas.height);
114+
115+
// Draw QR code image onto the canvas
116+
ctx.drawImage(img, CANVAS_QR_PADDING, CANVAS_QR_PADDING, capturedSize, capturedSize);
117+
118+
// If title is provided, draw it below the QR code
119+
if (titleText) {
120+
ctx.fillStyle = capturedDarkColor; // Title text color
121+
ctx.font = `${CANVAS_TITLE_FONT_SIZE}px Arial`; // Consider making font family configurable
122+
ctx.textAlign = 'center';
123+
ctx.textBaseline = 'top'; // Align the top of the text to the Y coordinate
124+
125+
const titleTextY =
126+
CANVAS_QR_PADDING + // Top padding for QR on canvas
127+
capturedSize + // QR graphic height
128+
CANVAS_TITLE_AREA_VERTICAL_PADDING; // Padding above title text
129+
130+
ctx.fillText(titleText, canvas.width / 2, titleTextY);
131+
}
132+
133+
// Update reactive state with the new Data URL from canvas
134+
qrCodeDataURL = canvas.toDataURL('image/png');
135+
resolve();
136+
};
137+
img.onerror = (errEvent) => {
138+
console.error('Error loading QR code image for canvas drawing:', errEvent);
139+
qrCodeDataURL = ''; // Clear QR code on error
140+
reject(new Error('Failed to load QR image onto canvas.'));
141+
};
142+
img.src = rawQrDataUrl;
143+
});
105144
} catch (error) {
106-
console.error('Error generating QR code:', error);
107-
qrCodeDataURL = ''; // Clear QR code on error
145+
console.error('Error in QR generation pipeline:', error);
146+
qrCodeDataURL = '';
108147
} finally {
109148
isGenerating = false;
110149
}
@@ -120,10 +159,9 @@
120159
<div class="w-full max-w-md space-y-6 lg:w-[350px] lg:flex-none">
121160
<!-- Mode Selector -->
122161
<div>
162+
<label for="template" class="mb-2 block text-sm font-medium text-blue-500">Template</label
163+
>
123164
<Select.Root type="single" bind:value={selectedModeValue}>
124-
<label for="template" class="mb-2 block text-sm font-medium text-blue-500"
125-
>Template</label
126-
>
127165
<Select.Trigger
128166
id="template"
129167
class="flex h-10 w-full items-center justify-between rounded-md bg-[#d9ff7a] px-4 text-sm font-medium shadow-sm transition-colors hover:bg-[#bede68]"
@@ -161,7 +199,21 @@
161199
</Select.Root>
162200
</div>
163201
164-
<!-- Input Fields -->
202+
<!-- QR Code Title Input -->
203+
<div>
204+
<label for="qrTitle" class="mb-2 block text-sm font-medium text-blue-500"
205+
>QR Code Title (Optional)</label
206+
>
207+
<input
208+
type="text"
209+
id="qrTitle"
210+
bind:value={qrTitle}
211+
class="block w-full rounded-md border-gray-600 bg-gray-700 p-2.5 text-sm text-white placeholder-gray-400 shadow-sm focus:border-blue-500 focus:ring-blue-500"
212+
placeholder="Enter title (displays on image)"
213+
/>
214+
</div>
215+
216+
<!-- Input Fields based on Mode -->
165217
{#if selectedModeValue === 'wifi'}
166218
<WifiForm
167219
bind:generatedString={activeFormOutput}
@@ -187,7 +239,7 @@
187239
<!-- Size Slider -->
188240
<div class="space-y-3">
189241
<div class="flex items-center justify-between">
190-
<label class="text-sm font-medium text-blue-600">Size</label>
242+
<label class="text-sm font-medium text-blue-600">QR Size</label>
191243
<span class="text-sm font-medium text-blue-400">{size}px</span>
192244
</div>
193245
<Slider.Root
@@ -211,67 +263,80 @@
211263
</div>
212264
</div>
213265
214-
<!-- Right Section -->
266+
<!-- Right Section: QR Code Display and Actions -->
215267
<div
216268
class="mx-auto flex w-full max-w-[584px] flex-col items-center justify-center space-y-6 lg:w-auto lg:flex-none"
217269
>
270+
<!-- QR Code Display Area: Loading, Image, or Placeholder -->
271+
<!-- The outer div's size is determined by qrImageActualWidth/Height + its own padding (p-4 = 1rem = 16px, so 32px total) -->
218272
{#if isGenerating}
219273
<div
220-
class="mx-auto flex w-full max-w-full items-center justify-center rounded-lg border border-black p-4"
221-
style="width: {size + 32}px; height: {size + 32}px;"
274+
class="mx-auto flex items-center justify-center rounded-lg border border-gray-500 p-4"
275+
style="width: {qrImageActualWidth + 32}px; height: {qrImageActualHeight + 32}px;"
222276
>
223277
<div
224-
class="h-8 w-8 animate-spin rounded-full border-2 border-gray-300 border-t-black"
278+
class="h-8 w-8 animate-spin rounded-full border-2 border-gray-300 border-t-blue-500"
225279
></div>
226280
</div>
227281
{:else if qrCodeDataURL}
228282
<div
229-
class="mx-auto w-full max-w-full rounded-lg border border-black p-4"
230-
style="width: {size + 32}px; height: {size + 32}px;"
283+
class="mx-auto rounded-lg border border-gray-600 bg-gray-800 p-4 shadow-lg"
284+
style="width: {qrImageActualWidth + 32}px; height: {qrImageActualHeight + 32}px;"
231285
>
232286
<img
233287
src={qrCodeDataURL}
234-
alt="QR Code"
235-
class="block h-auto max-w-full object-contain"
236-
style="width: {size}px; height: {size}px;"
288+
alt="Generated QR Code{qrTitle.trim() ? ' with title: ' + qrTitle.trim() : ''}"
289+
class="block"
290+
style="width: {qrImageActualWidth}px; height: {qrImageActualHeight}px;"
237291
/>
238292
</div>
239293
{:else}
240294
<div
241-
class="mx-auto flex w-full max-w-full items-center justify-center rounded-lg border border-dashed border-gray-300"
242-
style="width: {size + 32}px; height: {size + 32}px;"
295+
class="mx-auto flex flex-col items-center justify-center rounded-lg border border-dashed border-gray-700 p-4 text-center"
296+
style="width: {qrImageActualWidth + 32}px; height: {qrImageActualHeight + 32}px;"
243297
>
298+
<svg
299+
class="mb-2 h-12 w-12 text-gray-600"
300+
fill="none"
301+
viewBox="0 0 24 24"
302+
stroke="currentColor"
303+
stroke-width="1"
304+
>
305+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4" />
306+
<path stroke-linecap="round" stroke-linejoin="round" d="M7 7h10v10H7z" />
307+
</svg>
244308
<p class="text-sm text-gray-500">QR code will appear here</p>
309+
<p class="text-xs text-gray-600">Configure options to generate</p>
245310
</div>
246311
{/if}
247312
248-
{#if qrCodeDataURL}
313+
<!-- Color Pickers and Download Button (only if QR code is visible) -->
314+
{#if qrCodeDataURL && !isGenerating}
249315
<div
250-
class="qr-color-inputs"
251-
style="display: flex; gap: 1rem; margin-bottom: 1rem; align-items: center;"
316+
class="qr-color-inputs flex flex-wrap items-center justify-center gap-4"
317+
style="max-width: {qrImageActualWidth + 32}px;"
252318
>
253-
<label style="display: flex; align-items: center;" class="text-blue-500">
254-
<span style="margin-right: 0.5rem;">Dark color</span>
319+
<label class="flex items-center text-sm text-blue-500">
320+
<span class="mr-2">Dark Color:</span>
255321
<input
256322
type="color"
257323
bind:value={darkColor}
258-
class="border-magenta-500 rounded-sm border"
324+
class="h-8 w-8 cursor-pointer rounded border border-blue-600 bg-gray-700 p-0"
259325
/>
260326
</label>
261-
<label style="display: flex; align-items: center;" class="text-blue-500">
262-
<span style="margin-right: 0.5rem;">Light color</span>
327+
<label class="flex items-center text-sm text-blue-500">
328+
<span class="mr-2">Light Color:</span>
263329
<input
264330
type="color"
265-
class="border-magenta-500 m-0 rounded-sm border p-0"
266331
bind:value={lightColor}
332+
class="h-8 w-8 cursor-pointer rounded border border-blue-600 bg-gray-700 p-0"
267333
/>
268334
</label>
269335
</div>
270336
271337
<Button.Root
272338
onclick={downloadQRCode}
273-
disabled={isGenerating}
274-
class="h-10 cursor-pointer rounded-lg bg-[#d9ff7a] px-6 text-sm font-medium text-gray-800 transition-colors hover:bg-[#bede68] data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50"
339+
class="h-10 cursor-pointer rounded-lg bg-[#d9ff7a] px-6 text-sm font-medium text-gray-800 transition-colors hover:bg-[#bede68] data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50"
275340
>
276341
Download Image
277342
</Button.Root>
@@ -282,5 +347,16 @@
282347
</div>
283348
284349
<style>
285-
/* Empty style block to ensure proper CSS processing */
350+
/* Make color input clickable area cover the preview box better */
351+
input[type='color']::-webkit-color-swatch-wrapper {
352+
padding: 0;
353+
}
354+
input[type='color']::-webkit-color-swatch {
355+
border: none;
356+
border-radius: 0.25rem; /* Match rounded */
357+
}
358+
input[type='color']::-moz-color-swatch {
359+
border: none;
360+
border-radius: 0.25rem; /* Match rounded */
361+
}
286362
</style>

0 commit comments

Comments
 (0)