Skip to content
Open
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
3 changes: 0 additions & 3 deletions apps/site/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -216,9 +216,6 @@ const config = {
reactStrictMode: true,
images: { unoptimized: true },
transpilePackages: ["@prisma/eclipse"],
experimental: {
globalNotFound: true,
},
async headers() {
return [
{
Expand Down
58 changes: 12 additions & 46 deletions apps/site/src/app/not-found.tsx
Original file line number Diff line number Diff line change
@@ -1,57 +1,23 @@
import type { Metadata } from 'next';
import Link from 'next/link';
import GlitchParticles from "@/components/glitch-particles";
import type { Metadata } from "next";
import Link from "next/link";

export const metadata: Metadata = {
title: '404 - Page Not Found',
description: 'The page you were looking for could not be found.',
title: "404 - Page Not Found",
description: "The page you were looking for could not be found.",
};

export default function NotFound() {

return (
<main className="flex-1 w-full max-w-249 mx-auto px-4 py-8 z-1">
<h1 className="stretch-display text-4xl font-bold mb-2 landing-h1 text-center mt-9 font-sans-display">
Blog
<h1 className="absolute opacity-0 stretch-display text-4xl font-bold mb-2 landing-h1 text-center mt-9 font-sans-display">
Prisma Website | 404
</h1>
<div className="pt-6 pb-12 mt-10 mx-auto flex flex-col items-center justify-center">
<h2
style={{fontSize: 'clamp(4rem, 10vw, 9rem)'}}
className="
relative
mb-4
pointer-events-none
font-extrabold

before:content-[attr(data-text)]
before:absolute
before:top-0
before:left-[2px]
before:w-full
before:overflow-hidden
before:[text-shadow:-2px_0_red]
before:animate-[glitch-1_2s_infinite_linear_alternate-reverse]

after:content-[attr(data-text)]
after:absolute
after:top-0
after:-left-[2px]
after:w-full
after:overflow-hidden
after:[text-shadow:-2px_0_cyan]
after:animate-[glitch-2_1.5s_infinite_linear_alternate-reverse]

motion-reduce:before:animate-none
motion-reduce:after:animate-none"
data-text="404"
>
404
</h2>
<p className="text-xl font-semibold mb-4">
We could not find the page you were looking for
</p>
<Link href="/" className="text-xl hover:underline transition-colors">
Back to Blog
</Link>
<div className="relative mx-auto flex flex-col items-center justify-center -mt-10 md:-mt-30">
<GlitchParticles />
<p className="absolute top-[65%] text-foreground-neutral-weak text-2xl md:text-[46px] font-semibold mb-4 font-mono">
page not found
</p>
Comment on lines +13 to +20
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Make the overlay layers ignore pointer input.

Line 13’s invisible <h1> and Line 18’s absolute <p> both sit above the canvas. Since GlitchParticles listens on the canvas itself in apps/site/src/components/glitch-particles.tsx, Lines 243-248, those overlays can swallow hover/touch events and leave parts of the effect unresponsive. Add pointer-events-none to non-interactive overlays.

Minimal fix
-      <h1 className="absolute opacity-0 stretch-display text-4xl font-bold mb-2 landing-h1 text-center mt-9 font-sans-display">
+      <h1 className="pointer-events-none absolute opacity-0 stretch-display text-4xl font-bold mb-2 landing-h1 text-center mt-9 font-sans-display">
         Prisma Website | 404
       </h1>
@@
-        <p className="absolute top-[65%] text-foreground-neutral-weak text-2xl md:text-[46px] font-semibold mb-4 font-mono">
+        <p className="pointer-events-none absolute top-[65%] text-foreground-neutral-weak text-2xl md:text-[46px] font-semibold mb-4 font-mono">
           page not found
         </p>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/site/src/app/not-found.tsx` around lines 13 - 20, The invisible overlay
elements in not-found.tsx (the <h1> with classes including "opacity-0
stretch-display ..." and the absolute <p> with "top-[65%] ...") sit above the
GlitchParticles canvas and block pointer events; update those non-interactive
overlays to include the Tailwind class pointer-events-none so they no longer
swallow hover/touch input that GlitchParticles (component GlitchParticles in
apps/site/src/components/glitch-particles.tsx) needs to receive.

</div>
</main>
);
Expand Down
268 changes: 268 additions & 0 deletions apps/site/src/components/glitch-particles.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
"use client";

import { useEffect, useRef } from "react";

const SVG_PATH =
"M64.7997 131.22V37.8H64.9797L39.2397 75.42H120.96V104.58H-0.000312462V79.2L57.9597 1.44003H101.88V131.22H64.7997ZM190.03 132.84C180.07 132.84 171.31 131.28 163.75 128.16C156.19 124.92 149.95 120.42 145.03 114.66C140.11 108.78 136.39 101.76 133.87 93.6C131.35 85.44 130.09 76.32 130.09 66.24C130.09 51.36 132.55 39.06 137.47 29.34C142.51 19.5 149.53 12.18 158.53 7.38003C167.65 2.46003 178.21 2.59876e-05 190.21 2.59876e-05C200.29 2.59876e-05 209.05 1.62003 216.49 4.86003C223.93 7.98002 230.11 12.42 235.03 18.18C240.07 23.94 243.79 30.9 246.19 39.06C248.71 47.22 249.97 56.34 249.97 66.42C249.97 81.3 247.51 93.66 242.59 103.5C237.67 113.34 230.65 120.72 221.53 125.64C212.53 130.44 202.03 132.84 190.03 132.84ZM190.03 102.6C193.87 102.6 197.17 101.82 199.93 100.26C202.69 98.7 204.97 96.36 206.77 93.24C208.57 90.12 209.89 86.34 210.73 81.9C211.69 77.34 212.17 72.18 212.17 66.42C212.17 59.82 211.63 54.24 210.55 49.68C209.47 45.12 207.97 41.4 206.05 38.52C204.13 35.64 201.79 33.54 199.03 32.22C196.39 30.9 193.45 30.24 190.21 30.24C186.25 30.24 182.89 31.02 180.13 32.58C177.37 34.14 175.09 36.48 173.29 39.6C171.49 42.6 170.17 46.38 169.33 50.94C168.49 55.38 168.07 60.54 168.07 66.42C168.07 73.02 168.55 78.66 169.51 83.34C170.59 87.9 172.09 91.62 174.01 94.5C175.93 97.38 178.27 99.48 181.03 100.8C183.79 102 186.79 102.6 190.03 102.6ZM164.65 96.66V72.9L215.95 36.18V59.94L164.65 96.66ZM323.901 131.22V37.8H324.081L298.341 75.42H380.061V104.58H259.101V79.2L317.061 1.44003H360.981V131.22H323.901Z";

interface Particle {
homeX: number;
homeY: number;
x: number;
y: number;
vx: number;
vy: number;
size: number;
}

interface Glitch {
offsetX1: number;
offsetX2: number;
offsetY1: number;
offsetY2: number;
slices: number[];
showGlitch: boolean;
}

interface GlitchParticlesProps {
/** Background color of the container */
background?: string;
/** Width of the content area (canvas = this + padding * 2) */
contentWidth?: number;
/** Height of the content area (canvas = this + padding * 2) */
contentHeight?: number;
/** Space around the content */
padding?: number;
}

export default function GlitchParticles({
background = "transparent",
contentWidth = 427,
contentHeight = 178,
padding = 300,
}: GlitchParticlesProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);

useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;

const ctx = canvas.getContext("2d");
if (!ctx) return;

const canvasWidth = contentWidth + padding * 2;
const canvasHeight = contentHeight + padding * 2;
canvas.width = canvasWidth;
canvas.height = canvasHeight;

const particles: Particle[] = [];
const mouse = { x: 0, y: 0 };
let isPressed = false;
let glitch: Glitch = {
offsetX1: 0,
offsetX2: 0,
offsetY1: 0,
offsetY2: 0,
slices: [0, 0, 0, 0, 0],
showGlitch: true,
};
let rafId: number;

// — Init particles from SVG path —
const offscreen = document.createElement("canvas");
offscreen.width = contentWidth;
offscreen.height = contentHeight;
const offCtx = offscreen.getContext("2d")!;
const path2d = new Path2D(SVG_PATH);
offCtx.save();
offCtx.scale(contentWidth / 381, contentHeight / 133);
offCtx.fillStyle = "var(--background-default)";
offCtx.fill(path2d);
offCtx.restore();
Comment on lines +77 to +81
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

CSS variable won't resolve on offscreen canvas.

var(--background-default) doesn't work here because the offscreen canvas has no DOM context to inherit CSS custom properties from. The browser will fall back to a default (typically black), which happens to work for your alpha-channel sampling. But this is misleading code—consider using a concrete color like "#000" since you're only checking opacity anyway.

Suggested fix
   offCtx.save();
   offCtx.scale(contentWidth / 381, contentHeight / 133);
-  offCtx.fillStyle = "var(--background-default)";
+  offCtx.fillStyle = "#000"; // Color doesn't matter; we only sample alpha
   offCtx.fill(path2d);
   offCtx.restore();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/site/src/components/glitch-particles.tsx` around lines 77 - 81, The
offscreen canvas fill uses a CSS variable which doesn't resolve offscreen;
update the fill to a concrete color (e.g. "#000" or "rgba(0,0,0,1)") where
offCtx.fillStyle is set so alpha sampling remains correct—locate the block using
offCtx.scale(contentWidth / 381, contentHeight / 133); offCtx.fillStyle = ...;
offCtx.fill(path2d) and replace the CSS var assignment with a solid color
literal.


const imageData = offCtx.getImageData(0, 0, contentWidth, contentHeight);
const sampleGap = 4;
const particleSize = 6;

for (let y = 0; y < contentHeight; y += sampleGap) {
for (let x = 0; x < contentWidth; x += sampleGap) {
const i = (y * contentWidth + x) * 4;
if (imageData.data[i + 3] > 128) {
const px = x + padding;
const py = y + padding;
particles.push({
homeX: px,
homeY: py,
x: px,
y: py,
vx: 0,
vy: 0,
size: particleSize,
});
}
}
}

// — Glitch updater —
const glitchInterval = setInterval(() => {
const isHeavy = Math.random() > 0.07;
glitch = {
offsetX1: (Math.random() - 0.5) * (isHeavy ? 8 : 4),
offsetX2: (Math.random() - 0.5) * (isHeavy ? 8 : 4),
offsetY1: (Math.random() - 0.5) * 2,
offsetY2: (Math.random() - 0.5) * 2,
slices: [
(Math.random() - 0.5) * (isHeavy ? 16 : 6),
(Math.random() - 0.5) * (isHeavy ? 20 : 8),
(Math.random() - 0.5) * (isHeavy ? 16 : 6),
(Math.random() - 0.5) * (isHeavy ? 12 : 6),
(Math.random() - 0.5) * (isHeavy ? 16 : 5),
],
showGlitch: Math.random() > 0.008,
};
}, 50);
Comment on lines +83 to +123
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Honor reduced-motion on this route-level animation.

This effect starts automatically and runs indefinitely, but there’s no reduced-motion fallback or pause path. On a 404 page, that means motion-sensitive users always get the full glitch animation. Please short-circuit to a static render when prefers-reduced-motion: reduce is set.

One simple fallback
     for (let y = 0; y < contentHeight; y += sampleGap) {
       for (let x = 0; x < contentWidth; x += sampleGap) {
         const i = (y * contentWidth + x) * 4;
         if (imageData.data[i + 3] > 128) {
           const px = x + padding;
           const py = y + padding;
           particles.push({
             homeX: px,
             homeY: py,
             x: px,
             y: py,
             vx: 0,
             vy: 0,
             size: particleSize,
           });
         }
       }
     }
+
+    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
+      particles.forEach((p) => {
+        ctx.fillStyle = "#FFFFFF";
+        ctx.fillRect(p.x, p.y, p.size, p.size);
+      });
+      return;
+    }
 
     // — Glitch updater —
     const glitchInterval = setInterval(() => {

Also applies to: 125-203

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/site/src/components/glitch-particles.tsx` around lines 83 - 123, Detect
prefers-reduced-motion via window.matchMedia('(prefers-reduced-motion:
reduce)').matches at component init and, if true, do not start the glitch
setInterval or any animation RAF; instead set the glitch state to a
neutral/static object (offsetX1/offsetX2/offsetY1/offsetY2 = 0, slices =
[0,...], showGlitch = false) and skip any particle motion updates so the canvas
renders statically from the populated particles array. Update the code around
the glitchInterval creation (the glitch variable and setInterval block) to
early-return / conditionally skip creating the interval when reduced motion is
requested, and ensure cleanup still clears any interval and cancels RAF in the
component unmount logic.


// — Animation loop —
function animate() {
if (!ctx) return;
ctx.clearRect(0, 0, canvasWidth, canvasHeight);

const repelRadius = isPressed ? 200 : 80;
const repelStrength = isPressed ? 50 : 15;

particles.forEach((p) => {
const dx = p.x - mouse.x;
const dy = p.y - mouse.y;
const dist = Math.sqrt(dx * dx + dy * dy);

if (dist < repelRadius && dist > 0) {
const force = (repelRadius - dist) / repelRadius;
const angle = Math.atan2(dy, dx);
p.vx += Math.cos(angle) * force * repelStrength;
p.vy += Math.sin(angle) * force * repelStrength;
}

if (isPressed) {
p.vx += (Math.random() - 0.5) * 6;
p.vy += (Math.random() - 0.5) * 6;
}

p.vx += (p.homeX - p.x) * 0.08;
p.vy += (p.homeY - p.y) * 0.08;
p.vx *= 0.85;
p.vy *= 0.85;
p.x += p.vx;
p.y += p.vy;
});

if (!glitch.showGlitch) {
ctx.globalAlpha = 0.3;
particles.forEach((p) => {
ctx.fillStyle = "#E0E7FF";
ctx.fillRect(p.x, p.y, p.size, p.size);
});
ctx.globalAlpha = 1;
} else {
ctx.globalAlpha = 0.6;
particles.forEach((p) => {
const sliceIndex = Math.floor((p.homeY - padding) / 27);
const sliceOffset =
glitch.slices[Math.min(Math.max(sliceIndex, 0), 4)];
ctx.fillStyle = "#CCFBF1";
ctx.fillRect(
p.x + glitch.offsetX1 + sliceOffset * 0.3,
p.y + glitch.offsetY1,
p.size,
p.size,
);
});
particles.forEach((p) => {
const sliceIndex = Math.floor((p.homeY - padding) / 27);
const sliceOffset =
glitch.slices[Math.min(Math.max(sliceIndex, 0), 4)];
ctx.fillStyle = "#E0E7FF";
ctx.fillRect(
p.x + glitch.offsetX2 + sliceOffset * 0.3,
p.y + glitch.offsetY2,
p.size,
p.size,
);
});
ctx.globalAlpha = 1;
particles.forEach((p) => {
const sliceIndex = Math.floor((p.homeY - padding) / 27);
const sliceOffset =
glitch.slices[Math.min(Math.max(sliceIndex, 0), 4)];
ctx.fillStyle = "#FFFFFF";
ctx.fillRect(p.x + sliceOffset, p.y, p.size, p.size);
});
}

rafId = requestAnimationFrame(animate);
}

animate();

// — Event listeners —
const onMouseMove = (e: MouseEvent) => {
const rect = canvas.getBoundingClientRect();
// Scale mouse coords to canvas resolution
const scaleX = canvasWidth / rect.width;
const scaleY = canvasHeight / rect.height;
mouse.x = (e.clientX - rect.left) * scaleX;
mouse.y = (e.clientY - rect.top) * scaleY;
};
const onMouseDown = () => {
isPressed = true;
};
const onMouseUp = () => {
isPressed = false;
};

const onTouchMove = (e: TouchEvent) => {
e.preventDefault();
const rect = canvas.getBoundingClientRect();
const scaleX = canvasWidth / rect.width;
const scaleY = canvasHeight / rect.height;
const touch = e.touches[0];
mouse.x = (touch.clientX - rect.left) * scaleX;
mouse.y = (touch.clientY - rect.top) * scaleY;
};
const onTouchStart = (e: TouchEvent) => {
isPressed = true;
const rect = canvas.getBoundingClientRect();
const scaleX = canvasWidth / rect.width;
const scaleY = canvasHeight / rect.height;
const touch = e.touches[0];
mouse.x = (touch.clientX - rect.left) * scaleX;
mouse.y = (touch.clientY - rect.top) * scaleY;
};
const onTouchEnd = () => {
isPressed = false;
};

canvas.addEventListener("mousemove", onMouseMove);
window.addEventListener("mousedown", onMouseDown);
window.addEventListener("mouseup", onMouseUp);
canvas.addEventListener("touchmove", onTouchMove, { passive: false });
canvas.addEventListener("touchstart", onTouchStart);
canvas.addEventListener("touchend", onTouchEnd);

return () => {
cancelAnimationFrame(rafId);
clearInterval(glitchInterval);
canvas.removeEventListener("mousemove", onMouseMove);
window.removeEventListener("mousedown", onMouseDown);
window.removeEventListener("mouseup", onMouseUp);
canvas.removeEventListener("touchmove", onTouchMove);
canvas.removeEventListener("touchstart", onTouchStart);
canvas.removeEventListener("touchend", onTouchEnd);
};
}, [contentWidth, contentHeight, padding]);

return (
<div className="invert dark:filter-none flex align-center justify-center overflow-hidden inset-0">
<canvas ref={canvasRef} className="block max-w-full max-h-full" />
Comment on lines +264 to +265
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Occurrences of align-center:"
rg -n --hidden -g '!**/node_modules/**' '\balign-center\b'

echo
echo "Potential custom utility definitions:"
rg -n --hidden -g '!**/node_modules/**' '\.align-center\b|@layer utilities|addUtilities|matchUtilities'

Repository: prisma/web

Length of output: 1157


🏁 Script executed:

cat -n packages/eclipse/src/styles/globals.css | head -n 360 | tail -n 30

Repository: prisma/web

Length of output: 1117


🏁 Script executed:

cat -n apps/docs/src/app/global.css | head -n 50

Repository: prisma/web

Length of output: 1384


🏁 Script executed:

# Also check if there are tailwind config files that might define custom utilities
fd -e 'tailwind.config.*' -e 'tailwind.js' -e 'tailwind.ts'

Repository: prisma/web

Length of output: 36


🏁 Script executed:

# Search for align-center definition in the globals.css file
rg -n 'align-center' packages/eclipse/src/styles/globals.css

# Also check the total size of the file
wc -l packages/eclipse/src/styles/globals.css

Repository: prisma/web

Length of output: 97


🏁 Script executed:

# Search all CSS files for align-center definition
rg -n 'align-center' --type css

# Also search TypeScript/JavaScript files for custom utility definitions
rg -n '\.align-center|addUtilities.*align-center' --type ts --type tsx --type js

Repository: prisma/web

Length of output: 81


🏁 Script executed:

# Search all CSS files for align-center definition
rg -n '\.align-center' --type css

# Also search for align-center in any file that might define utilities
rg -n 'align-center\s*[:{]|addUtilities.*align-center' -g '*.css' -g '*.scss' -g '*.js' -g '*.ts' -g '*.tsx'

Repository: prisma/web

Length of output: 36


🏁 Script executed:

# Verify items-center is a standard Tailwind utility by checking if it's used correctly elsewhere
rg -n 'items-center' apps/site/src/components/ packages/ui/src/components/ | head -20

Repository: prisma/web

Length of output: 4933


Replace align-center with items-center.

align-center is not a Tailwind utility and isn't defined anywhere in this codebase. Line 263 will ignore the undefined class, leaving the canvas uncentered cross-axis. items-center is the correct Tailwind utility for this purpose and is used throughout the rest of the repo.

Fix
-    <div className="invert dark:filter-none flex align-center justify-center overflow-hidden inset-0">
+    <div className="invert dark:filter-none flex items-center justify-center overflow-hidden inset-0">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="invert dark:filter-none flex align-center justify-center overflow-hidden inset-0">
<canvas ref={canvasRef} className="block max-w-full max-h-full" />
<div className="invert dark:filter-none flex items-center justify-center overflow-hidden inset-0">
<canvas ref={canvasRef} className="block max-w-full max-h-full" />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/site/src/components/glitch-particles.tsx` around lines 263 - 264,
Replace the invalid Tailwind class "align-center" with the correct
"items-center" in the component's div to center items on the cross axis; locate
the JSX in the GlitchParticles component (the div containing canvasRef and the
className "invert dark:filter-none flex align-center justify-center
overflow-hidden inset-0") and update the className string to use "items-center"
instead of "align-center".

</div>
);
}
Loading