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 })
|
||||
}
|
||||
}
|
||||
|
||||
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 }
|
||||
);
|
||||
|
||||
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 col = Math.floor(within / side); // 0..side-1 (right->left)
|
||||
const within = i - planeStart; // 0..planeCount-1
|
||||
|
||||
// How many rows are actually used in this plane?
|
||||
const rowsUsed = rowsUsedForPlane(total, plane, side);
|
||||
// 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)
|
||||
|
||||
// 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;
|
||||
|
||||
// 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);
|
||||
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]}>
|
||||
{/* 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>
|
||||
<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 */}
|
||||
<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,
|
||||
|
||||
@@ -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,
|
||||
name VARCHAR(20),
|
||||
exp_date DATE NOT NULL,
|
||||
CREATE TABLE products (
|
||||
record_num INTEGER PRIMARY KEY DEFAULT nextval('product_record_num_sequence'),
|
||||
product_id INTEGER NOT NULL DEFAULT nextval('product_product_id_sequence'),
|
||||
name VARCHAR(20),
|
||||
exp_date DATE NOT NULL
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user