-
Notifications
You must be signed in to change notification settings - Fork 3
Add swag modal with stickers and 3D keychain preview #27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this 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
setTimeoutwith 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'sbind:thisdirective 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 (showStickersSectionbecomes 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
keyringInitializedflag 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 whenshowStickersSectionbecomes false.
keyringInitialized = true;
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 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" |
Copilot
AI
Dec 8, 2025
There was a problem hiding this comment.
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.
No description provided.