Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
18 changes: 13 additions & 5 deletions src/app/[locale]/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
:root {
/* Surface Colors */
--background: #F5F5F5;
--nav-background: rgba(245, 245, 245, 0.8); /* Added for glassmorphism */
--nav-background: rgb(245, 245, 245, 0.8); /* Added for glassmorphism */

/* Text Colors */
--foreground: #1C1C1C;
Expand All @@ -36,13 +36,18 @@
--button-primary-hover-bg: var(--color-neutral-50);
--button-primary-hover-border: #121212;
--button-subtle-bg: #9797971e;

/* Background Tile Colors */
--tile-bg: rgb(227, 226, 246);
--tile-stroke: rgb(80, 80, 80, 0.1);
--line-color: rgb(0, 0, 0, 0.05);
}

/* DARK MODE */
.dark {
/* Surface Colors */
--background: #101010;
--nav-background: rgba(16, 16, 16, 0.8); /* Added for glassmorphism */
--nav-background: rgb(16, 16, 16, 0.8); /* Added for glassmorphism */

/* Text Colors */
--foreground: #D3D3D3;
Expand All @@ -64,7 +69,11 @@
--button-primary-hover-bg: var(--color-neutral-100);
--button-primary-hover-border: var(--color-neutral-100);
--button-subtle-bg: #bbbbbb0e;
}

/* Background Tile Colors */
--tile-bg: rgb(30, 29, 28);
--tile-stroke: rgb(80, 80, 80, 0.5);
--line-color: rgb(0, 0, 0, 0.2);}

/* --- GLOBAL STYLES --- */
body {
Expand Down Expand Up @@ -96,5 +105,4 @@ body {
/* Dark Mode: Icon flips to White */
:is(.dark .theme-icon) {
filter: invert(1) brightness(100);
}

}
3 changes: 2 additions & 1 deletion src/app/[locale]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client';

import BackgroundTile from '@/components/ui/BackgroundTile';
import { useTranslations } from 'next-intl';

import dynamic from 'next/dynamic';
Expand All @@ -12,7 +13,7 @@ import dynamic from 'next/dynamic';
export default function Home() {
const t = useTranslations('Home');
return (
<div className="flex min-h-screen flex-col items-center justify-center p-24">
<div className="flex min-h-screen flex-col items-center justify-center mx-4 sm:mx-8 lg:mx-16 xl:mx-48">
<h1 className="text-4xl font-bold">{t('title')}</h1>
<p className="mt-4 text-xl">{t('subtitle')}</p>
</div>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Expand Down
206 changes: 206 additions & 0 deletions src/components/ui/BackgroundTile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
"use client";

import { useEffect, useRef } from "react";
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Stable random values generated once at module level — fixes "impure function in render" warning
const PHASES = Array.from({ length: 6 }, () => Math.random() * Math.PI * 2);
const FREQS = Array.from({ length: 6 }, (_, i) => 0.3 + i * 0.13);

interface BackgroundTileProps {
/** Height of the tile in viewport height units. Defaults to 50. */
heightVh?: number;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

The heightVh prop is defined but never applied.

The interface documents heightVh as controlling tile height in viewport units (default 50), but the component never uses this value. The container relies on h-full from parent sizing instead.

🐛 Proposed fix to apply the height prop
   return (
     <div
       style={{
         background:   "var(--tile-bg, `#1a1a1a`)",
         border:       "0.8px solid var(--tile-stroke, rgba(255,255,255,0.08))",
         borderRadius: "24px",
-        width:        "100%",
+        height:       `${heightVh}vh`,
         overflow:     "hidden",
         position:     "relative",
       }}
-      className="w-full h-full"
+      className="w-full"
     >

Also applies to: 195-206

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

In `@src/components/ui/BackgroundTile.tsx` around lines 9 - 12,
BackgroundTileProps declares heightVh but the BackgroundTile component never
uses it; update the BackgroundTile component to accept heightVh (default 50) and
apply it to the tile container (e.g., via an inline style or computed class) so
the tile height is set to `${heightVh}vh` instead of relying on h-full;
reference the prop name heightVh and the interface BackgroundTileProps when
making the change and ensure the same fix is applied to the other instance noted
(lines ~195-206) so both components honor the heightVh prop.


const BackgroundTile = ({ heightVh = 50 }: BackgroundTileProps) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const rafRef = useRef<number | null>(null);
const lineColorRef = useRef<string>("");
const isRunning = useRef(false);
const timeRef = useRef(0);
const ampRef = useRef(0); // target amplitude
const ampSmoothed = useRef(0); // smoothed amplitude (what's actually rendered)
const stopTimer = useRef<ReturnType<typeof setTimeout> | null>(null);

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

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

// ── cache line color, refresh on theme changes ────────────────────
const updateLineColor = () => {
lineColorRef.current = getComputedStyle(document.documentElement)
.getPropertyValue("--line-color")
.trim();
};
updateLineColor();

const themeObserver = new MutationObserver((mutations) => {
for (const m of mutations) {
if (m.attributeName === "class" || m.attributeName === "data-theme") {
updateLineColor();
}
}
});
themeObserver.observe(document.documentElement, { attributes: true });

// ── resize ────────────────────────────────────────────────────────
const setSize = () => {
const dpr = window.devicePixelRatio || 1;
canvas.width = canvas.offsetWidth * dpr;
canvas.height = canvas.offsetHeight * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
};
setSize();
const ro = new ResizeObserver(setSize);
ro.observe(canvas);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

// ── render one frame ──────────────────────────────────────────────
const render = () => {
const w = canvas.offsetWidth;
const h = canvas.offsetHeight;
const amp = ampSmoothed.current;
const t = timeRef.current;

ctx.clearRect(0, 0, w, h);

const N = 6;
const pts = Array.from({ length: N }, (_, i) => {
const w1 = Math.sin(t * FREQS[i] + PHASES[i]) * 0.50;
const w2 = Math.sin(t * FREQS[i] * 0.43 + PHASES[i] * 1.77) * 0.30;
const w3 = Math.cos(t * FREQS[i] * 0.27 + PHASES[i] * 0.85) * 0.20;
return {
x: (i / (N - 1)) * w,
y: h * 0.5 + (w1 + w2 + w3) * h * amp * 0.95,
};
});

ctx.beginPath();
ctx.moveTo(pts[0].x, pts[0].y);

for (let i = 0; i < N - 1; i++) {
const p0 = pts[Math.max(0, i - 1)];
const p1 = pts[i];
const p2 = pts[i + 1];
const p3 = pts[Math.min(N - 1, i + 2)];
const k = 0.5;
ctx.bezierCurveTo(
p1.x + (p2.x - p0.x) * k / 3,
p1.y + (p2.y - p0.y) * k / 3,
p2.x - (p3.x - p1.x) * k / 3,
p2.y - (p3.y - p1.y) * k / 3,
p2.x, p2.y,
);
}

ctx.strokeStyle = lineColorRef.current; // ← cached, not computed per-frame
ctx.lineWidth = 14;
ctx.lineJoin = "round";
ctx.lineCap = "round";
ctx.stroke();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
};

render(); // initial flat line

// ── loop — only alive while scrolling ─────────────────────────────
const tick = () => {
if (!isRunning.current) {
// Smoothly finish lerping to the frozen target, then stop
const target = ampRef.current;
const current = ampSmoothed.current;
ampSmoothed.current += (target - current) * 0.18;

if (Math.abs(ampSmoothed.current - target) > 0.001) {
render();
rafRef.current = requestAnimationFrame(tick);
} else {
ampSmoothed.current = target; // snap to exact target
render();
rafRef.current = null;
}
return;
}

timeRef.current += 0.055;

// Lerp smoothed amp toward target — fast rise
const target = ampRef.current;
const current = ampSmoothed.current;
ampSmoothed.current += (target - current) * 0.18;

render();
rafRef.current = requestAnimationFrame(tick);
};

const startLoop = () => {
if (!isRunning.current) {
isRunning.current = true;
if (rafRef.current === null) {
rafRef.current = requestAnimationFrame(tick);
}
}
};

const stopLoop = () => {
// Stop animating time — amplitude stays frozen at current ampRef value
isRunning.current = false;
};

// ── scroll / wheel ─────────────────────────────────────────────────
const onWheel = (e: WheelEvent) => {
const vel = Math.min(Math.abs(e.deltaY) / 60, 1);
ampRef.current = 0.18 + vel * 0.74;
startLoop();
if (stopTimer.current) clearTimeout(stopTimer.current);
stopTimer.current = setTimeout(stopLoop, 80);
};

const onScroll = () => {
ampRef.current = 0.55;
startLoop();
if (stopTimer.current) clearTimeout(stopTimer.current);
stopTimer.current = setTimeout(stopLoop, 80);
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

window.addEventListener("wheel", onWheel, { passive: true });
window.addEventListener("scroll", onScroll, { passive: true });

return () => {
stopLoop();
ro.disconnect();
themeObserver.disconnect();
if (rafRef.current) cancelAnimationFrame(rafRef.current);
if (stopTimer.current) clearTimeout(stopTimer.current);
window.removeEventListener("wheel", onWheel);
window.removeEventListener("scroll", onScroll);
};
}, []);

return (
<div
style={{
background: "var(--tile-bg, #1a1a1a)",
border: "0.8px solid var(--tile-stroke, rgba(255,255,255,0.08))",
borderRadius: "24px",
width: "100%",
height: `${heightVh}vh`,
overflow: "hidden",
position: "relative",
}}
>
<canvas
ref={canvasRef}
style={{
position: "absolute",
inset: 0,
width: "100%",
height: "100%",
display: "block",
}}
/>
</div>
);
};

export default BackgroundTile;