A working state for the product expiry page

This commit is contained in:
HP
2026-01-14 15:49:04 -05:00
parent 0eabb9eb9e
commit e3141030ab
3 changed files with 178 additions and 56 deletions

View File

@@ -16,3 +16,15 @@ export async function GET(req: NextRequest){
return NextResponse.json({ message: "Serverside eception occurred" }, { status: 500 })
}
}
export async function POST(req: NextRequest){
try{
const body = (await req.json()) as { name: string, expiration_date: string }
console.log("GOT BODY:", body)
const result = await db.query("INSERT INTO products (name, exp_date) VALUES ($1, $2) RETURNING record_num;", [name, expiration_date]);
return NextResponse.json(result.rows[0].record_num ?? []);
} catch(error){
return NextResponse.json({ message: "Serverside eception occurred" }, { status: 500 })
}
}

View File

@@ -28,6 +28,21 @@ const OrbitControls = dynamic(
{ ssr: false }
);
const Text = dynamic(() => import("@react-three/drei").then((m) => m.Text), {
ssr: false,
});
const RoundedBox = dynamic(
() => import("@react-three/drei").then((m) => m.RoundedBox),
{ ssr: false }
);
const Environment = dynamic(
() => import("@react-three/drei").then((m) => m.Environment),
{ ssr: false }
);
type Product = {
record_num: number;
product_id: number;
@@ -45,7 +60,6 @@ function isoTodayPlus(days: number) {
}
const seed: Product[] = [
{ record_num: 1, product_id: 1001, name: "Milk", exp_date: isoTodayPlus(30) },
{ record_num: 2, product_id: 1002, name: "Eggs", exp_date: isoTodayPlus(12) },
{ record_num: 3, product_id: 1003, name: "Bread", exp_date: isoTodayPlus(6) },
{ record_num: 4, product_id: 1004, name: "Cheese", exp_date: isoTodayPlus(90) },
@@ -67,7 +81,7 @@ const glassPaperSx = {
color: "rgba(245,245,245,0.92)",
};
const PLANE_SIDE = 5;
const PLANE_SIDE = 4;
const CUBE_SIZE = 1.0;
const GAP = 0.25;
const GAP_Z = 0.9;
@@ -78,7 +92,7 @@ export default function Products() {
const [mode, setMode] = React.useState<"view3d" | "form">("view3d");
const sorted = React.useMemo(() => {
return [...products].sort((a, b) => (a.exp_date < b.exp_date ? 1 : -1));
return [...products].sort((a, b) => (a.exp_date < b.exp_date ? -1 : 1));
}, [products]);
const startCreate = () => {
@@ -281,37 +295,35 @@ function computePosition(
gapZ: number
) {
const perPlane = side * side;
const plane = Math.floor(i / perPlane);
const within = i % perPlane;
const planeStart = plane * perPlane;
const planeCount = Math.min(perPlane, Math.max(0, total - planeStart));
// We fill "down" first, then left:
// row changes slowest? Actually: for down-first, row = within % rowsUsed, col = floor(within / rowsUsed)
// But we still want a bounded side x side layout. Easiest is:
// row = within % side (down)
// col = floor(within / side) (left)
const row = within % side; // 0..side-1 (top->down)
const within = i - planeStart; // 0..planeCount-1
// Fill DOWN first, then LEFT (next column)
const col = Math.floor(within / side); // 0..side-1 (right -> left)
const row = within % side; // 0..side-1 (top -> down)
// How many rows are actually used in this plane?
const rowsUsed = rowsUsedForPlane(total, plane, side);
// How many cubes are actually in *this column* on this plane?
// (last column may be partial)
const countInThisCol = Math.min(side, planeCount - col * side);
// Center the grid in X around 0, but start at right-most column
// spacing
const step = cubeSize + gap;
// Right-most column is col=0 => x positive
const xHalfSpan = ((side - 1) * step) / 2;
const x = xHalfSpan - col * step;
// Top-right start:
// col=0 should be right-most => x positive
const x = (xHalfSpan - col * step);
// FLOOR-LOCK PER COLUMN:
// bottom cube of this column sits at y = cubeSize/2
// row=0 is top => higher y
const y = cubeSize / 2 + (countInThisCol - 1 - row) * step;
// FLOOR-LOCK:
// Bottom row sits on floor (grid) at y = cubeSize/2
// If rowsUsed < side, we compress downward so the lowest row hits the floor.
// row=0 is the top row; we want top row higher.
const rowInUsedRange = row; // row already 0..side-1
const clampedRow = Math.min(rowInUsedRange, rowsUsed - 1);
const y = cubeSize / 2 + (rowsUsed - 1 - clampedRow) * step;
// Next plane goes "forward" (you said z is forward/back); using negative z to go "forward"
// Next plane goes "forward/back". You said you want front first,
// so plane 0 should be the front. Put further planes behind it:
const z = -plane * (cubeSize + gapZ);
return [x, y, z] as const;
@@ -320,23 +332,32 @@ function computePosition(
function World3D({ products }: { products: Product[] }) {
return (
<Canvas
camera={{ position: [7, 6, 13], fov: 45, near: 0.1, far: 200 }}
onCreated={({ camera }) => {
camera.lookAt(2.8, 2.6, -2);
}}
gl={{ antialias: true }}
>
{/* Nice reflections / lighting */}
<Environment preset="city" />
{/* eslint-disable-next-line react/no-unknown-property */}
<ambientLight intensity={0.8} />
<ambientLight intensity={0.35} />
{/* eslint-disable-next-line react/no-unknown-property */}
<directionalLight position={[8, 10, 6]} intensity={1.2} />
<directionalLight position={[8, 10, 6]} intensity={1.4} />
{/* eslint-disable-next-line react/no-unknown-property */}
<directionalLight position={[-8, 6, -6]} intensity={0.6} />
<gridHelper args={[60, 60]} />
{/* products */}
{products.map((p, i) => (
<ProductBox key={p.record_num} product={p} index={i} total={products.length} />
<ProductBox
key={p.record_num}
product={p}
index={i}
total={products.length}
/>
))}
<OrbitControls
@@ -359,27 +380,115 @@ function ProductBox({
index: number;
total: number;
}) {
const [x, y, z] = computePosition(
index,
total,
PLANE_SIDE,
CUBE_SIZE,
GAP,
GAP_Z
);
const [x, y, z] = computePosition(index, total, PLANE_SIDE, CUBE_SIZE, GAP, GAP_Z);
const label = String(product.product_id);
const s = CUBE_SIZE;
const half = s / 2;
const pad = 0.01; // push text slightly off the face to avoid z-fighting
const fontSize = Math.max(0.14, s * 0.18);
return (
// eslint-disable-next-line react/no-unknown-property
<mesh position={[x, y, z]}>
<group position={[x, y, z]}>
{/* Rounded cube */}
<RoundedBox args={[s, s, s]} radius={0.12} smoothness={6}>
{/* eslint-disable-next-line react/no-unknown-property */}
<boxGeometry args={[CUBE_SIZE, CUBE_SIZE, CUBE_SIZE]} />
{/* eslint-disable-next-line react/no-unknown-property */}
<meshStandardMaterial />
</mesh>
<meshPhysicalMaterial
transmission={0.65} // glassiness
roughness={0.18}
thickness={1.2}
clearcoat={1}
clearcoatRoughness={0.12}
metalness={0.05}
envMapIntensity={1.25}
/>
</RoundedBox>
{/* +Z (front) */}
<Text
position={[0, 0, half + pad]}
fontSize={fontSize}
anchorX="center"
anchorY="middle"
outlineWidth={0.01}
outlineColor="black"
>
{label}
</Text>
{/* -Z (back) */}
<Text
position={[0, 0, -half - pad]}
rotation={[0, Math.PI, 0]}
fontSize={fontSize}
anchorX="center"
anchorY="middle"
outlineWidth={0.01}
outlineColor="black"
>
{label}
</Text>
{/* +X (right) */}
<Text
position={[half + pad, 0, 0]}
rotation={[0, -Math.PI / 2, 0]}
fontSize={fontSize}
anchorX="center"
anchorY="middle"
outlineWidth={0.01}
outlineColor="black"
>
{label}
</Text>
{/* -X (left) */}
<Text
position={[-half - pad, 0, 0]}
rotation={[0, Math.PI / 2, 0]}
fontSize={fontSize}
anchorX="center"
anchorY="middle"
outlineWidth={0.01}
outlineColor="black"
>
{label}
</Text>
{/* +Y (top) */}
<Text
position={[0, half + pad, 0]}
rotation={[-Math.PI / 2, 0, 0]}
fontSize={fontSize}
anchorX="center"
anchorY="middle"
outlineWidth={0.01}
outlineColor="black"
>
{label}
</Text>
{/* -Y (bottom) */}
<Text
position={[0, -half - pad, 0]}
rotation={[Math.PI / 2, 0, 0]}
fontSize={fontSize}
anchorX="center"
anchorY="middle"
outlineWidth={0.01}
outlineColor="black"
>
{label}
</Text>
</group>
);
}
function ProductForm({
value,
onCancel,

View File

@@ -1,13 +1,14 @@
DROP TABLE products IF EXISTS;
DROP product_record_num_sequence IF EXISTS;
DROP product_product_id_sequence IF EXISTS;
DROP TABLE IF EXISTS products;
DROP SEQUENCE IF EXISTS product_record_num_sequence;
DROP SEQUENCE IF EXISTS product_product_id_sequence;
CREATE product_record_num_sequence CASCADE;
CREATE product_product_id_sequence CASCADE;
CREATE SEQUENCE product_record_num_sequence;
CREATE SEQUENCE product_product_id_sequence;
CREATE TABLE products (
record_num INTEGER PRIMARY KEY DEFAULT nextval('product_record_num_sequence'),
product_id INTEGER PRIMARY KEY DEFAULT,
product_id INTEGER NOT NULL DEFAULT nextval('product_product_id_sequence'),
name VARCHAR(20),
exp_date DATE NOT NULL,
exp_date DATE NOT NULL
);