Polish quiz completion UX, room PNG backgrounds, and habits list order

Polish quiz completion UX, rooms, and app layout.
This commit is contained in:
pricewowen
2026-05-24 07:09:40 -06:00
parent 33ba52def1
commit 5ebcf33bca
25 changed files with 219 additions and 476 deletions

View File

@@ -1,7 +1,5 @@
"use client";
import { useEffect, useRef, useState } from "react";
import {
getRoomBackground,
type RoomBackgroundId,
@@ -11,83 +9,25 @@ import { cn } from "@/lib/utils";
type ParallaxRoomBackgroundProps = {
roomId: RoomBackgroundId;
className?: string;
interactive?: boolean;
};
export function ParallaxRoomBackground({
roomId,
className,
interactive = true,
}: ParallaxRoomBackgroundProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [offset, setOffset] = useState({ x: 0, y: 0 });
const room = getRoomBackground(roomId);
const usesSceneImage = Boolean(room.sceneImage);
useEffect(() => {
if (!interactive || usesSceneImage) {
return;
}
const container = containerRef.current;
if (!container) {
return;
}
const handlePointerMove = (event: PointerEvent) => {
const rect = container.getBoundingClientRect();
const x = ((event.clientX - rect.left) / rect.width - 0.5) * 2;
const y = ((event.clientY - rect.top) / rect.height - 0.5) * 2;
setOffset({ x, y });
};
container.addEventListener("pointermove", handlePointerMove);
return () => {
container.removeEventListener("pointermove", handlePointerMove);
};
}, [interactive, usesSceneImage]);
if (room.sceneImage) {
return (
<div
ref={containerRef}
className={cn(
"absolute inset-0 z-0 overflow-hidden",
room.previewClassName,
className,
)}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={room.sceneImage}
alt=""
aria-hidden
className="absolute inset-0 size-full object-cover image-pixelated"
/>
</div>
);
}
return (
<div
ref={containerRef}
className={cn("absolute inset-0 z-0 overflow-hidden", className)}
className={cn("absolute inset-0 z-0 overflow-hidden bg-lcd-bg", className)}
>
{room.layers.map((layer, index) => (
<div
key={layer.id}
aria-hidden
className={cn(
"absolute inset-[-12%]",
layer.className,
index % 2 === 0 ? "animate-parallax-drift" : "animate-star-twinkle",
)}
style={{
transform: `translate3d(${offset.x * layer.speed * 10}px, ${offset.y * layer.speed * 8}px, 0)`,
transition: "transform 0.15s steps(4)",
}}
/>
))}
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={room.sceneImage}
alt=""
aria-hidden
className="absolute inset-0 size-full object-cover image-pixelated"
/>
</div>
);
}

View File

@@ -71,16 +71,12 @@ export function RoomBackgroundPicker({
"hover:-translate-y-0.5",
)}
>
{room.sceneImage ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={room.sceneImage}
alt=""
className="absolute inset-0 size-full object-cover image-pixelated"
/>
) : (
<div className={cn("absolute inset-0", room.previewClassName)} />
)}
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={room.sceneImage}
alt=""
className="absolute inset-0 size-full object-cover image-pixelated"
/>
<p className="absolute inset-x-0 bottom-0 bg-black/55 px-1.5 py-1 text-left text-[8px] uppercase tracking-wider text-foreground">
{room.name}
</p>

View File

@@ -70,16 +70,12 @@ export function RoomStyleSelector({
: "border-border/60 hover:border-border",
)}
>
{room.sceneImage ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={room.sceneImage}
alt=""
className="absolute inset-0 size-full object-cover image-pixelated"
/>
) : (
<div className={cn("absolute inset-0", room.previewClassName)} />
)}
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={room.sceneImage}
alt=""
className="absolute inset-0 size-full object-cover image-pixelated"
/>
{locked ? (
<span
aria-hidden