A working state for the product expiry page
This commit is contained in:
@@ -16,3 +16,15 @@ export async function GET(req: NextRequest){
|
|||||||
return NextResponse.json({ message: "Serverside eception occurred" }, { status: 500 })
|
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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,6 +28,21 @@ const OrbitControls = dynamic(
|
|||||||
{ ssr: false }
|
{ 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 = {
|
type Product = {
|
||||||
record_num: number;
|
record_num: number;
|
||||||
product_id: number;
|
product_id: number;
|
||||||
@@ -45,7 +60,6 @@ function isoTodayPlus(days: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const seed: Product[] = [
|
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: 2, product_id: 1002, name: "Eggs", exp_date: isoTodayPlus(12) },
|
||||||
{ record_num: 3, product_id: 1003, name: "Bread", exp_date: isoTodayPlus(6) },
|
{ record_num: 3, product_id: 1003, name: "Bread", exp_date: isoTodayPlus(6) },
|
||||||
{ record_num: 4, product_id: 1004, name: "Cheese", exp_date: isoTodayPlus(90) },
|
{ 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)",
|
color: "rgba(245,245,245,0.92)",
|
||||||
};
|
};
|
||||||
|
|
||||||
const PLANE_SIDE = 5;
|
const PLANE_SIDE = 4;
|
||||||
const CUBE_SIZE = 1.0;
|
const CUBE_SIZE = 1.0;
|
||||||
const GAP = 0.25;
|
const GAP = 0.25;
|
||||||
const GAP_Z = 0.9;
|
const GAP_Z = 0.9;
|
||||||
@@ -78,7 +92,7 @@ export default function Products() {
|
|||||||
const [mode, setMode] = React.useState<"view3d" | "form">("view3d");
|
const [mode, setMode] = React.useState<"view3d" | "form">("view3d");
|
||||||
|
|
||||||
const sorted = React.useMemo(() => {
|
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]);
|
}, [products]);
|
||||||
|
|
||||||
const startCreate = () => {
|
const startCreate = () => {
|
||||||
@@ -281,37 +295,35 @@ function computePosition(
|
|||||||
gapZ: number
|
gapZ: number
|
||||||
) {
|
) {
|
||||||
const perPlane = side * side;
|
const perPlane = side * side;
|
||||||
|
|
||||||
const plane = Math.floor(i / perPlane);
|
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:
|
const within = i - planeStart; // 0..planeCount-1
|
||||||
// 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 col = Math.floor(within / side); // 0..side-1 (right->left)
|
|
||||||
|
|
||||||
// How many rows are actually used in this plane?
|
// Fill DOWN first, then LEFT (next column)
|
||||||
const rowsUsed = rowsUsedForPlane(total, plane, side);
|
const col = Math.floor(within / side); // 0..side-1 (right -> left)
|
||||||
|
const row = within % side; // 0..side-1 (top -> down)
|
||||||
|
|
||||||
// Center the grid in X around 0, but start at right-most column
|
// How many cubes are actually in *this column* on this plane?
|
||||||
|
// (last column may be partial)
|
||||||
|
const countInThisCol = Math.min(side, planeCount - col * side);
|
||||||
|
|
||||||
|
// spacing
|
||||||
const step = cubeSize + gap;
|
const step = cubeSize + gap;
|
||||||
|
|
||||||
|
// Right-most column is col=0 => x positive
|
||||||
const xHalfSpan = ((side - 1) * step) / 2;
|
const xHalfSpan = ((side - 1) * step) / 2;
|
||||||
|
const x = xHalfSpan - col * step;
|
||||||
|
|
||||||
// Top-right start:
|
// FLOOR-LOCK PER COLUMN:
|
||||||
// col=0 should be right-most => x positive
|
// bottom cube of this column sits at y = cubeSize/2
|
||||||
const x = (xHalfSpan - col * step);
|
// row=0 is top => higher y
|
||||||
|
const y = cubeSize / 2 + (countInThisCol - 1 - row) * step;
|
||||||
|
|
||||||
// FLOOR-LOCK:
|
// Next plane goes "forward/back". You said you want front first,
|
||||||
// Bottom row sits on floor (grid) at y = cubeSize/2
|
// so plane 0 should be the front. Put further planes behind it:
|
||||||
// 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"
|
|
||||||
const z = -plane * (cubeSize + gapZ);
|
const z = -plane * (cubeSize + gapZ);
|
||||||
|
|
||||||
return [x, y, z] as const;
|
return [x, y, z] as const;
|
||||||
@@ -320,23 +332,32 @@ function computePosition(
|
|||||||
|
|
||||||
function World3D({ products }: { products: Product[] }) {
|
function World3D({ products }: { products: Product[] }) {
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<Canvas
|
<Canvas
|
||||||
camera={{ position: [7, 6, 13], fov: 45, near: 0.1, far: 200 }}
|
camera={{ position: [7, 6, 13], fov: 45, near: 0.1, far: 200 }}
|
||||||
onCreated={({ camera }) => {
|
onCreated={({ camera }) => {
|
||||||
camera.lookAt(2.8, 2.6, -2);
|
camera.lookAt(2.8, 2.6, -2);
|
||||||
}}
|
}}
|
||||||
|
gl={{ antialias: true }}
|
||||||
>
|
>
|
||||||
|
{/* Nice reflections / lighting */}
|
||||||
|
<Environment preset="city" />
|
||||||
|
|
||||||
{/* eslint-disable-next-line react/no-unknown-property */}
|
{/* eslint-disable-next-line react/no-unknown-property */}
|
||||||
<ambientLight intensity={0.8} />
|
<ambientLight intensity={0.35} />
|
||||||
{/* eslint-disable-next-line react/no-unknown-property */}
|
{/* 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]} />
|
<gridHelper args={[60, 60]} />
|
||||||
|
|
||||||
{/* products */}
|
|
||||||
{products.map((p, i) => (
|
{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
|
<OrbitControls
|
||||||
@@ -359,27 +380,115 @@ function ProductBox({
|
|||||||
index: number;
|
index: number;
|
||||||
total: number;
|
total: number;
|
||||||
}) {
|
}) {
|
||||||
const [x, y, z] = computePosition(
|
const [x, y, z] = computePosition(index, total, PLANE_SIDE, CUBE_SIZE, GAP, GAP_Z);
|
||||||
index,
|
|
||||||
total,
|
const label = String(product.product_id);
|
||||||
PLANE_SIDE,
|
|
||||||
CUBE_SIZE,
|
const s = CUBE_SIZE;
|
||||||
GAP,
|
const half = s / 2;
|
||||||
GAP_Z
|
const pad = 0.01; // push text slightly off the face to avoid z-fighting
|
||||||
);
|
|
||||||
|
const fontSize = Math.max(0.14, s * 0.18);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// eslint-disable-next-line react/no-unknown-property
|
// eslint-disable-next-line react/no-unknown-property
|
||||||
<mesh position={[x, y, z]}>
|
<group position={[x, y, z]}>
|
||||||
{/* eslint-disable-next-line react/no-unknown-property */}
|
{/* Rounded cube */}
|
||||||
<boxGeometry args={[CUBE_SIZE, CUBE_SIZE, CUBE_SIZE]} />
|
<RoundedBox args={[s, s, s]} radius={0.12} smoothness={6}>
|
||||||
{/* eslint-disable-next-line react/no-unknown-property */}
|
{/* eslint-disable-next-line react/no-unknown-property */}
|
||||||
<meshStandardMaterial />
|
<meshPhysicalMaterial
|
||||||
</mesh>
|
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({
|
function ProductForm({
|
||||||
value,
|
value,
|
||||||
onCancel,
|
onCancel,
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
DROP TABLE products IF EXISTS;
|
DROP TABLE IF EXISTS products;
|
||||||
DROP product_record_num_sequence IF EXISTS;
|
DROP SEQUENCE IF EXISTS product_record_num_sequence;
|
||||||
DROP product_product_id_sequence IF EXISTS;
|
DROP SEQUENCE IF EXISTS product_product_id_sequence;
|
||||||
|
|
||||||
CREATE product_record_num_sequence CASCADE;
|
CREATE SEQUENCE product_record_num_sequence;
|
||||||
CREATE product_product_id_sequence CASCADE;
|
CREATE SEQUENCE product_product_id_sequence;
|
||||||
|
|
||||||
CREATE TABLE products(
|
CREATE TABLE products (
|
||||||
record_num INTEGER PRIMARY KEY DEFAULT nextval('product_record_num_sequence'),
|
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),
|
name VARCHAR(20),
|
||||||
exp_date DATE NOT NULL,
|
exp_date DATE NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user