Skip to content

Conversation

@bbarni2020
Copy link
Collaborator

No description provided.

Copilot AI review requested due to automatic review settings December 8, 2025 21:06
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a promotional "swag modal" to the homepage that displays when users click a "Free swag!" button. The modal showcases two stickers and a 3D-rendered keychain preview using Three.js, encouraging users to submit their first CAD project. The PR also simplifies the FAQ section by removing several questions.

Key Changes:

  • Added interactive modal with sticker images and 3D keychain preview using Three.js OBJ loader
  • Implemented OrbitControls for interactive 3D model viewing
  • Streamlined FAQ section from 8 questions to 4 questions
  • Added new asset imports for keyring model and sticker images

Reviewed changes

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.

File Description
src/routes/+page.svelte Added modal dialog with 3D renderer, simplified FAQ section, integrated new swag promotion UI
src/lib/assets/sticker2.png Binary image asset added for modal sticker display
Comments suppressed due to low confidence (4)

src/routes/+page.svelte:471

  • The modal dialog is missing a proper focus trap implementation. When the modal opens, focus should be trapped within it for accessibility. Without this, keyboard users can tab outside the modal to the background content, which violates WCAG 2.1 guidelines.
	<div
			class="fixed inset-0 z-40 flex items-center justify-center bg-black bg-opacity-70 p-4"
			role="dialog"
			aria-modal="true"
			tabindex="0"
			onclick={(e) => {
				if (e.target === e.currentTarget) {
					showStickersSection = false;
				}
			}}
			onkeydown={(e) => e.key === 'Escape' && (showStickersSection = false)}
	>
		<div
				class="relative max-h-[95vh] w-full max-w-5xl overflow-y-auto rounded-lg border-3 border-primary-900 bg-primary-950 p-8 shadow-2xl"
				role="document"
				tabindex="-1"
		>
			<button
				class="button sm absolute top-4 right-4 z-10 border-2 border-primary-900 bg-primary-800 outline-primary-50 hover:bg-primary-700"
				onclick={() => (showStickersSection = false)}
				aria-label="Close dialog"
			>
				Close
			</button>

			<div class="mx-auto max-w-4xl">
				<div class="mb-8 text-center">
					<h2 class="mb-2 text-2xl font-bold sm:text-3xl">
						Free swag with your first submission
					</h2>
					<p class="text-lg font-medium text-primary-300">
						Ship a project, get exclusive Construct goodies
					</p>
				</div>

				<div class="grid gap-6 sm:grid-cols-2">
					<div class="themed-box p-6">
						<div class="mb-4 flex h-56 items-center justify-center gap-3 overflow-hidden rounded-lg border-2 border-primary-900 bg-primary-900">
							<img
								src={sticker1Image}
								alt="Construct sticker 1"
								class="h-40 w-40 animate-[spin_20s_linear_infinite] object-contain"
								style="animation-direction: normal;"
							/>
							<img
								src={sticker2Image}
								alt="Construct sticker 2"
								class="h-40 w-40 animate-[spin_20s_linear_infinite] object-contain"
								style="animation-direction: reverse;"
							/>
						</div>
						<div class="text-center">
							<h3 class="mb-2 text-xl font-bold">Sticker Pack</h3>
							<p class="text-sm text-primary-300">
								Sticker 1 and Sticker 2—both included
							</p>
						</div>
					</div>

					<div class="themed-box p-6">
						<div class="mb-4 flex h-56 items-center justify-center overflow-hidden rounded-lg border-2 border-primary-900 bg-primary-900">
							<canvas class="h-full w-full" width="200" height="200" id="keyring-canvas"></canvas>
						</div>
						<div class="text-center">
							<h3 class="mb-2 text-xl font-bold">3D Keychain</h3>
							<p class="text-sm text-primary-300">
								Custom 3D printed keychain
							</p>
						</div>
					</div>
				</div>

				<div class="themed-box mt-6 p-6 text-center">
					<p class="font-medium">
						<strong>How it works:</strong> Submit your first CAD project and we'll mail these to you—completely free!
					</p>
				</div>
			</div>
		</div>
	</div>
{/if}

src/routes/+page.svelte:374

  • The setTimeout with 100ms delay is a code smell that suggests a race condition. The code waits for the DOM element to be available, but this approach is fragile and can fail if rendering takes longer than 100ms. Consider using Svelte's bind:this directive or a proper lifecycle method to ensure the canvas element exists before initializing the 3D renderer.
		setTimeout(() => {
			let keyringCanvas = document.querySelector(`#keyring-canvas`);

			if (!keyringCanvas) {
				return;
			}

			keyringInitialized = true;

			const keyringRenderer = new THREE.WebGLRenderer({
				canvas: keyringCanvas,
				antialias: true,
				alpha: true
			});

			keyringRenderer.setClearColor(0xffffff, 0);
			keyringRenderer.setPixelRatio(window.devicePixelRatio);
			keyringRenderer.shadowMap.enabled = true;
			keyringRenderer.shadowMap.type = THREE.PCFSoftShadowMap;

			const keyringCamera = new THREE.PerspectiveCamera(40, 2, 1, 1000);
			keyringCamera.rotation.x = -45 * degree;

			let keyringControls = new OrbitControls(keyringCamera, keyringRenderer.domElement);
			keyringControls.target.set(0, 0, 0);
			keyringControls.rotateSpeed = 0.6;
			keyringControls.enablePan = false;
			keyringControls.dampingFactor = 0.1;
			keyringControls.enableDamping = true;
			keyringControls.autoRotate = true;
			keyringControls.autoRotateSpeed = 3;
			keyringControls.update();

			const keyringHemisphere = new THREE.HemisphereLight(0xffffff, 0xffffff, 4);
			keyringScene.add(keyringHemisphere);

			const keyringDirectional = new THREE.DirectionalLight(0xffffff, 1);
			keyringDirectional.castShadow = true;
			keyringDirectional.shadow.mapSize.width = 2048;
			keyringDirectional.shadow.mapSize.height = 2048;
			keyringScene.add(keyringDirectional);

			const keyringDirectional2 = new THREE.DirectionalLight(0xffffff, 1);
			keyringDirectional2.castShadow = true;
			keyringDirectional2.shadow.mapSize.width = 2048;
			keyringDirectional2.shadow.mapSize.height = 2048;
			keyringScene.add(keyringDirectional2);

			function resizeKeyringCanvasToDisplaySize() {
				const canvas = keyringRenderer.domElement;
				const width = canvas.clientWidth;
				const height = canvas.clientHeight;
				if (canvas.width !== width || canvas.height !== height) {
					keyringRenderer.setSize(width, height, false);
					keyringRenderer.setPixelRatio(window.devicePixelRatio);
					keyringCamera.aspect = width / height;
					keyringCamera.updateProjectionMatrix();
				}
			}

			function parseKeyringObject(object: THREE.Group<THREE.Object3DEventMap>) {
				object = object as THREE.Group<THREE.Object3DEventMap> & { children: THREE.Mesh[] };

				object.rotation.x = THREE.MathUtils.degToRad(-90);

				const aabb = new THREE.Box3().setFromObject(object);
				const center = aabb.getCenter(new THREE.Vector3());

				object.position.x += object.position.x - center.x;
				object.position.y += object.position.y - center.y;
				object.position.z += object.position.z - center.z;

				keyringControls.reset();

				var box = new THREE.Box3().setFromObject(object);
				const size = new THREE.Vector3();
				box.getSize(size);
				const largestDimension = Math.max(size.x, size.y, size.z);

				keyringCamera.position.z = largestDimension * 0.3;
				keyringCamera.position.y = largestDimension * 1.38;

				keyringDirectional.position.set(
					largestDimension * 2,
					largestDimension * 2,
					largestDimension * 2
				);
				keyringDirectional2.position.set(
					-largestDimension * 2,
					largestDimension * 2,
					-largestDimension * 2
				);

				keyringCamera.near = largestDimension * 0.001;
				keyringCamera.far = largestDimension * 10;
				keyringCamera.updateProjectionMatrix();

				const edgeLines: { lines: THREE.LineSegments; mesh: THREE.Mesh }[] = [];

				object.traverse(function (child) {
					child.castShadow = true;
					child.receiveShadow = true;

					const mesh = child as THREE.Mesh;

					const edges = new THREE.EdgesGeometry(mesh.geometry);
					const lines = new THREE.LineSegments(
						edges,
						new THREE.LineBasicMaterial({
							color: 0xf3dcc6,
							linewidth: 1,
							polygonOffset: true,
							polygonOffsetFactor: -1,
							polygonOffsetUnits: -1
						})
					);

					lines.position.copy(mesh.position);
					lines.rotation.copy(mesh.rotation);

					edgeLines.push({ lines, mesh });
				});

				edgeLines.forEach(({ lines, mesh }) => {
					mesh.add(lines);
				});

				keyringScene.add(object);
			}

			var objLoader = new OBJLoader();

			objLoader.load(
				keyringModel,
				parseKeyringObject,
				(xhr) => {
					console.log('Keyring: ' + (xhr.loaded / xhr.total) * 100 + '% loaded');
				},
				(error) => {
					console.error('Keyring error:', error);
				}
			);

			const animateKeyring = function () {
				requestAnimationFrame(animateKeyring);
				keyringControls.update();
				keyringRenderer.render(keyringScene, keyringCamera);
				resizeKeyringCanvasToDisplaySize();
			};
			animateKeyring();
		}, 100);

src/routes/+page.svelte:373

  • Memory leak: The animation loop started by animateKeyring() on line 373 is never stopped. When the modal closes (showStickersSection becomes false), the animation continues running in the background, consuming CPU and battery. The animation frame should be cancelled and the renderer disposed when the modal is closed.
			const animateKeyring = function () {
				requestAnimationFrame(animateKeyring);
				keyringControls.update();
				keyringRenderer.render(keyringScene, keyringCamera);
				resizeKeyringCanvasToDisplaySize();
			};
			animateKeyring();

src/routes/+page.svelte:231

  • The keyringInitialized flag is set but never reset. If the user closes and reopens the modal, the 3D scene won't be re-initialized even though the canvas element no longer exists. This will cause the scene to fail to render. The flag should be reset when showStickersSection becomes false.
			keyringInitialized = true;

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 431 to 437
alt="Construct sticker 1"
class="h-40 w-40 animate-[spin_20s_linear_infinite] object-contain"
style="animation-direction: normal;"
/>
<img
src={sticker2}
src={sticker2Image}
alt="Construct sticker 2"
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

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

Missing alt text context: The alt text "Construct sticker 1" and "Construct sticker 2" should be more descriptive to help screen reader users understand what the stickers represent. Consider adding more context about the design or purpose of each sticker.

Copilot uses AI. Check for mistakes.
@ArcaEge ArcaEge merged commit 8059c96 into hackclub:staging Dec 8, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants