Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"dependencies": {
"@lucide/svelte": "^0.561.0",
"@sanity/client": "^7.13.2",
"qr-code-styling": "^1.9.2",
"vercel": "^50.0.1"
}
}
1 change: 1 addition & 0 deletions src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
--color-ecsess-950: #031c15;

/* Black variants for UI elements */
--color-ecsess-white: #ffffff;
--color-ecsess-black: #1f1f1f;
--color-ecsess-black-hover: #161917;
}
Expand Down
Binary file modified src/assets/ECSESS.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/ECSESS_old.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
182 changes: 182 additions & 0 deletions src/components/QRCode.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
<script>
import QRCodeStyling from 'qr-code-styling';
import ECSESSLogo from '../assets/ECSESS.png';

let { data = '', downloadSize = 1000, size = 300 } = $props();
let qrCodeContainer = $state(/** @type {HTMLDivElement | null} */ (null));
let qrCodeInstance = $state(/** @type {QRCodeStyling | null} */ (null));
let debounceTimer = $state(/** @type {any} */ (null));
let lastData = $state('');

function updateQRCode() {
if (!qrCodeContainer) return;

const trimmedData = data.trim();

// Skip if data hasn't actually changed
if (trimmedData === lastData) return;

lastData = trimmedData;

// Clean up previous instance
if (qrCodeInstance) {
qrCodeContainer.innerHTML = '';
qrCodeInstance = null;
}

// Only create QR code if data is provided
if (trimmedData) {
// Create new QR code instance
const instance = new QRCodeStyling({
width: size,
height: size,
Comment on lines +31 to +32
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 'size' prop is used in the updateQRCode function (lines 31-32) but changes to this prop won't trigger QR code regeneration due to the reactive dependency issue in the $effect. If the parent component changes the 'size' prop, the QR code will remain at its old size until 'data' changes or the component remounts. This creates an inconsistent state where the prop value doesn't match the rendered output.

Copilot uses AI. Check for mistakes.
type: 'svg',
data: trimmedData,
margin: 0,
qrOptions: {
typeNumber: 0,
mode: 'Byte',
errorCorrectionLevel: 'M'
},
imageOptions: {
hideBackgroundDots: true,
imageSize: 0.4,
margin: 8,
crossOrigin: 'anonymous'
},
dotsOptions: {
color: '#3f6a3f', // ecsess-600
type: 'rounded',
gradient: {
type: 'radial',
colorStops: [
{
offset: 0,
color: '#8fb98a' // ecsess-300
},
{
offset: 1,
color: '#2d5a2d' // ecsess-700
}
]
}
},
backgroundOptions: {
color: '#ffffff' // ecsess-white
},
cornersSquareOptions: {
color: '#3f6a3f', // ecsess-600
type: 'extra-rounded'
},
cornersDotOptions: {
color: '#3f6a3f', // ecsess-600
type: 'dot'
},
image: ECSESSLogo
});

qrCodeInstance = instance;
instance.append(qrCodeContainer);
}
Comment on lines +21 to +80
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cleanup logic directly manipulates the DOM with 'qrCodeContainer.innerHTML = ""' (line 23). While this works, it bypasses the QRCodeStyling library's own cleanup mechanisms. The QRCodeStyling instance may have internal references or event listeners that won't be properly cleaned up this way. Consider checking if the library provides a destroy or cleanup method, and use that instead of directly clearing innerHTML to prevent potential memory leaks.

Suggested change
// Clean up previous instance
if (qrCodeInstance) {
qrCodeContainer.innerHTML = '';
qrCodeInstance = null;
}
// Only create QR code if data is provided
if (trimmedData) {
// Create new QR code instance
const instance = new QRCodeStyling({
width: size,
height: size,
type: 'svg',
data: trimmedData,
margin: 0,
qrOptions: {
typeNumber: 0,
mode: 'Byte',
errorCorrectionLevel: 'M'
},
imageOptions: {
hideBackgroundDots: true,
imageSize: 0.4,
margin: 8,
crossOrigin: 'anonymous'
},
dotsOptions: {
color: '#3f6a3f', // ecsess-600
type: 'rounded',
gradient: {
type: 'radial',
colorStops: [
{
offset: 0,
color: '#8fb98a' // ecsess-300
},
{
offset: 1,
color: '#2d5a2d' // ecsess-700
}
]
}
},
backgroundOptions: {
color: '#ffffff' // ecsess-white
},
cornersSquareOptions: {
color: '#3f6a3f', // ecsess-600
type: 'extra-rounded'
},
cornersDotOptions: {
color: '#3f6a3f', // ecsess-600
type: 'dot'
},
image: ECSESSLogo
});
qrCodeInstance = instance;
instance.append(qrCodeContainer);
}
// If no data is provided, clear the QR code content via the library API
if (!trimmedData) {
if (qrCodeInstance) {
qrCodeInstance.update({ data: '' });
}
return;
}
// Reuse existing instance when possible
if (qrCodeInstance) {
qrCodeInstance.update({
width: size,
height: size,
data: trimmedData,
image: ECSESSLogo
});
return;
}
// Create new QR code instance when none exists yet
const instance = new QRCodeStyling({
width: size,
height: size,
type: 'svg',
data: trimmedData,
margin: 0,
qrOptions: {
typeNumber: 0,
mode: 'Byte',
errorCorrectionLevel: 'M'
},
imageOptions: {
hideBackgroundDots: true,
imageSize: 0.4,
margin: 8,
crossOrigin: 'anonymous'
},
dotsOptions: {
color: '#3f6a3f', // ecsess-600
type: 'rounded',
gradient: {
type: 'radial',
colorStops: [
{
offset: 0,
color: '#8fb98a' // ecsess-300
},
{
offset: 1,
color: '#2d5a2d' // ecsess-700
}
]
}
},
backgroundOptions: {
color: '#ffffff' // ecsess-white
},
cornersSquareOptions: {
color: '#3f6a3f', // ecsess-600
type: 'extra-rounded'
},
cornersDotOptions: {
color: '#3f6a3f', // ecsess-600
type: 'dot'
},
image: ECSESSLogo
});
qrCodeInstance = instance;
instance.append(qrCodeContainer);

Copilot uses AI. Check for mistakes.
}

$effect(() => {
// Clear any pending debounce
if (debounceTimer !== null) {
clearTimeout(debounceTimer);
}

// Only update if container is available
if (qrCodeContainer) {
// Debounce updates for smooth typing experience
debounceTimer = setTimeout(() => {
updateQRCode();
debounceTimer = null;
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 'debounceTimer' is being set to null after the timeout callback executes (line 94), but this assignment happens inside the callback and won't update the reactive state properly to cancel the cleanup. If the component unmounts or the effect re-runs before the timeout completes, the cleanup function (lines 99-103) will still clear the timeout, but the state update on line 94 may create a timing issue. Consider removing the assignment on line 94 since the cleanup function already handles clearing the timeout, or ensure the debounceTimer state is managed more consistently.

Suggested change
debounceTimer = null;

Copilot uses AI. Check for mistakes.
}, 300);
}

// Cleanup function
return () => {
if (debounceTimer !== null) {
clearTimeout(debounceTimer);
}
};
});
Comment on lines +83 to +104
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The $effect reactive statement doesn't explicitly track the 'data' prop or 'size' prop as dependencies. In Svelte 5, $effect automatically tracks state accessed within it, but since the effect only checks 'qrCodeContainer' and calls 'updateQRCode()', changes to the 'data' or 'size' props won't trigger the debounced update. The 'updateQRCode' function accesses these props but it's called inside a setTimeout callback, which breaks the reactive dependency tracking. This means when the parent component updates the 'data' or 'size' props, the QR code won't regenerate unless the effect happens to re-run for another reason. Consider directly accessing these props in the effect or restructuring to ensure proper reactivity.

Copilot uses AI. Check for mistakes.

export function download(format = 'png') {
if (!lastData || !lastData.trim() || !qrCodeInstance) return;

// Create a high-resolution version with extra margin for download
const marginSize = Math.floor(downloadSize * 0.04); // 4% margin

const downloadInstance = new QRCodeStyling({
width: downloadSize,
height: downloadSize,
type: 'svg',
data: lastData,
margin: marginSize, // Add extra padding/margin
qrOptions: {
typeNumber: 0,
mode: 'Byte',
errorCorrectionLevel: 'M'
},
imageOptions: {
hideBackgroundDots: true,
imageSize: 0.4,
margin: 8,
crossOrigin: 'anonymous'
},
dotsOptions: {
color: '#3f6a3f', // ecsess-600
type: 'rounded',
gradient: {
type: 'radial',
colorStops: [
{
offset: 0,
color: '#8fb98a' // ecsess-300
},
{
offset: 1,
color: '#2d5a2d' // ecsess-700
}
]
}
},
backgroundOptions: {
color: '#ffffff' // ecsess-white
},
cornersSquareOptions: {
color: '#3f6a3f', // ecsess-600
type: 'extra-rounded'
},
cornersDotOptions: {
color: '#3f6a3f', // ecsess-600
type: 'dot'
},
image: ECSESSLogo
});
Comment on lines +30 to +158
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's significant code duplication between the QR code configuration in 'updateQRCode()' (lines 30-76) and the 'download()' function (lines 112-158). The configuration objects are nearly identical with only minor differences in width, height, and margin. This duplication makes the code harder to maintain - if styling needs to change, it must be updated in two places. Consider extracting a shared configuration function that accepts size and margin parameters, reducing duplication and improving maintainability.

Copilot uses AI. Check for mistakes.

// Create a temporary container for the high-res QR code
const tempContainer = document.createElement('div');
tempContainer.style.width = `${downloadSize + marginSize}px`;
tempContainer.style.height = `${downloadSize + marginSize}px`;
tempContainer.style.position = 'absolute';
tempContainer.style.left = '-9999px';
document.body.appendChild(tempContainer);

downloadInstance.append(tempContainer);

// Wait for the QR code to render, then download
setTimeout(() => {
downloadInstance.download({
name: 'qrcode',
extension: /** @type {'png' | 'svg' | 'jpeg' | 'webp'} */ (format)
});
// Clean up temporary container
document.body.removeChild(tempContainer);
}, 100);
Comment on lines +170 to +178
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The download function creates and removes a temporary DOM element but uses a fixed timeout of 100ms (line 171) to wait for rendering. This is a magic number without explanation, and the arbitrary delay could be too short on slower devices (causing incomplete renders) or unnecessarily long on faster ones. Consider using a more reliable approach such as waiting for a load event or callback from the library, or at minimum, add a comment explaining why 100ms was chosen and whether it's been tested across different environments.

Suggested change
// Wait for the QR code to render, then download
setTimeout(() => {
downloadInstance.download({
name: 'qrcode',
extension: /** @type {'png' | 'svg' | 'jpeg' | 'webp'} */ (format)
});
// Clean up temporary container
document.body.removeChild(tempContainer);
}, 100);
// Wait for the browser to process layout/paint after appending, then download.
// Using requestAnimationFrame avoids relying on an arbitrary timeout duration.
requestAnimationFrame(() => {
requestAnimationFrame(() => {
downloadInstance.download({
name: 'qrcode',
extension: /** @type {'png' | 'svg' | 'jpeg' | 'webp'} */ (format)
});
// Clean up temporary container
if (tempContainer.parentNode === document.body) {
document.body.removeChild(tempContainer);
}
});
});

Copilot uses AI. Check for mistakes.
}
</script>

<div bind:this={qrCodeContainer} class="max-h-sm flex max-w-sm items-center justify-center"></div>
6 changes: 5 additions & 1 deletion src/routes/council/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ const councilQuery = `{
}`;

export const load = async ({ url }) => {
const { members, councilGoofyPic }: { members: CouncilMember[], councilGoofyPic: { url: string } } = await getFromCMS(councilQuery);
const {
members,
councilGoofyPic
}: { members: CouncilMember[]; councilGoofyPic: { url: string } } =
await getFromCMS(councilQuery);

return {
members: members,
Expand Down
57 changes: 57 additions & 0 deletions src/routes/internal/qrcode/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<script>
import QRCode from 'components/QRCode.svelte';
import Button from 'components/Button.svelte';
import SeoMetaTags from 'components/layout/SeoMetaTags.svelte';
import Section from 'components/layout/Section.svelte';

let inputText = $state('');
let qrCodeRef = $state(/** @type {any} */ (null));

function handleExport() {
if (qrCodeRef) {
qrCodeRef.download('png');
}
}
</script>

<SeoMetaTags title="QR Code Generator - ECSESS" />

<Section from="from-ecsess-black" to="to-ecsess-black" via="via-ecsess-800" direction="to-b">
<p class="page-title">ECSESS QR Code Generator</p>

<div class="mx-auto flex w-full max-w-4xl flex-col gap-6">
<!-- Input Section -->
<div class="flex flex-col gap-4">
<label for="qr-input" class="text-ecsess-100 text-lg font-semibold">
Enter text or URL:
</label>
<input
id="qr-input"
type="text"
bind:value={inputText}
placeholder="Enter text or URL to encode..."
class="border-ecsess-400 text-ecsess-900 placeholder:text-ecsess-400 focus:border-ecsess-500 focus:ring-ecsess-500/20 w-full rounded-md border-2 bg-white px-4 py-3 focus:ring-2 focus:outline-none"
/>
</div>

<!-- Preview Section -->
{#if inputText}
<div class="flex flex-col items-center gap-4">
<h2 class="text-ecsess-100 text-xl font-bold">Preview</h2>
<div class="flex justify-center rounded-lg bg-white p-2 shadow-lg md:p-4">
<div class="mx-auto max-h-75 w-full max-w-75">
<QRCode bind:this={qrCodeRef} data={inputText} size={300} downloadSize={1000} />
</div>
</div>
<Button onclick={handleExport}>
<span>Download QR Code as PNG</span>
</Button>
<span>1000x1000px</span>
</div>
Comment on lines +38 to +50
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition checks if 'inputText' is truthy to show the QR code preview, but the QRCode component's updateQRCode function uses 'data.trim()' to determine if it should generate a QR code. This creates a mismatch: if inputText contains only whitespace characters, the preview section will render (line 38), but the QRCode component won't generate anything (because trim() returns empty string). This results in an empty preview with a download button that won't work. Consider using 'inputText.trim()' in the condition on line 38 to match the QRCode component's behavior.

Copilot uses AI. Check for mistakes.
{:else}
<div class="flex items-center justify-center">
<p class="text-ecsess-200 text-center">Enter text or URL above to generate a QR code</p>
</div>
{/if}
</div>
</Section>
Loading