"use client"; import * as React from "react"; import dynamic from "next/dynamic"; import { Box, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography, IconButton, Tooltip, TextField, Button, Stack, } from "@mui/material"; import AddIcon from "@mui/icons-material/Add"; const Canvas = dynamic(() => import("@react-three/fiber").then((m) => m.Canvas), { ssr: false, }); const OrbitControls = dynamic( () => import("@react-three/drei").then((m) => m.OrbitControls), { ssr: false } ); type Product = { record_num: number; product_id: number; name: string; exp_date: string; // ISO yyyy-mm-dd }; function isoTodayPlus(days: number) { const d = new Date(); d.setDate(d.getDate() + days); const yyyy = d.getFullYear(); const mm = String(d.getMonth() + 1).padStart(2, "0"); const dd = String(d.getDate()).padStart(2, "0"); return `${yyyy}-${mm}-${dd}`; } 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) }, { record_num: 5, product_id: 1005, name: "Yogurt", exp_date: isoTodayPlus(20) }, { record_num: 6, product_id: 1006, name: "Butter", exp_date: isoTodayPlus(120) }, ]; const glassPaperSx = { borderRadius: 3, // glossy glass look background: ` linear-gradient(180deg, rgba(255,255,255,0.10) 0%, rgba(255,255,255,0.05) 100%), radial-gradient(1200px 600px at 20% 0%, rgba(130,170,255,0.20) 0%, rgba(0,0,0,0) 55%), radial-gradient(900px 500px at 80% 10%, rgba(255,140,180,0.14) 0%, rgba(0,0,0,0) 60%) `, border: "1px solid rgba(255,255,255,0.14)", backdropFilter: "blur(14px) saturate(130%)", boxShadow: "0 12px 40px rgba(0,0,0,0.35)", color: "rgba(245,245,245,0.92)", }; const PLANE_SIDE = 5; const CUBE_SIZE = 1.0; const GAP = 0.25; const GAP_Z = 0.9; export default function Products() { const [products, setProducts] = React.useState(seed); const [selected, setSelected] = React.useState(null); 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)); }, [products]); const startCreate = () => { setSelected({ record_num: -1, product_id: -1, name: "", exp_date: isoTodayPlus(1), }); setMode("form"); }; const startEdit = (p: Product) => { setSelected(p); setMode("form"); }; const cancelForm = () => { setMode("view3d"); setSelected(null); }; const saveForm = (p: Product) => { const isNew = p.record_num === -1; if (isNew) { const nextRecord = Math.max(0, ...products.map((x) => x.record_num)) + 1; const nextProductId = Math.max(0, ...products.map((x) => x.product_id)) + 1; const inserted: Product = { ...p, record_num: nextRecord, product_id: nextProductId, }; setProducts((prev) => [inserted, ...prev]); } else { setProducts((prev) => prev.map((x) => (x.record_num === p.record_num ? p : x)) ); } setMode("view3d"); setSelected(null); }; return ( Products Exp Date {sorted.map((p) => ( startEdit(p)} sx={{ cursor: "pointer" }} > {p.name} record #{p.record_num} • product_id {p.product_id} {p.exp_date} ))}
{mode === "view3d" ? ( ) : ( )}
); } function rowsUsedForPlane(total: number, planeIndex: number, side: number) { const perPlane = side * side; const start = planeIndex * perPlane; const remaining = Math.max(0, total - start); const countThisPlane = Math.min(perPlane, remaining); return Math.max(1, Math.ceil(countThisPlane / side)); } function computePosition( i: number, total: number, side: number, cubeSize: number, gap: number, gapZ: number ) { const perPlane = side * side; const plane = Math.floor(i / perPlane); const within = i % perPlane; // 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) // How many rows are actually used in this plane? const rowsUsed = rowsUsedForPlane(total, plane, side); // Center the grid in X around 0, but start at right-most column const step = cubeSize + gap; const xHalfSpan = ((side - 1) * step) / 2; // Top-right start: // col=0 should be right-most => x positive const x = (xHalfSpan - col * 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" const z = -plane * (cubeSize + gapZ); return [x, y, z] as const; } function World3D({ products }: { products: Product[] }) { return ( { camera.lookAt(2.8, 2.6, -2); }} > {/* eslint-disable-next-line react/no-unknown-property */} {/* eslint-disable-next-line react/no-unknown-property */} {/* products */} {products.map((p, i) => ( ))} ); } function ProductBox({ product, index, total, }: { product: Product; index: number; total: number; }) { const [x, y, z] = computePosition( index, total, PLANE_SIDE, CUBE_SIZE, GAP, GAP_Z ); return ( // eslint-disable-next-line react/no-unknown-property {/* eslint-disable-next-line react/no-unknown-property */} {/* eslint-disable-next-line react/no-unknown-property */} ); } function ProductForm({ value, onCancel, onSave, }: { value: Product | null; onCancel: () => void; onSave: (p: Product) => void; }) { const [draft, setDraft] = React.useState(value); React.useEffect(() => { setDraft(value); }, [value]); if (!draft) return null; const isNew = draft.record_num === -1; return ( {isNew ? "Create Product" : "Update Product"} setDraft((d) => (d ? { ...d, name: e.target.value } : d)) } inputProps={{ maxLength: 20 }} fullWidth /> setDraft((d) => (d ? { ...d, exp_date: e.target.value } : d)) } InputLabelProps={{ shrink: true }} fullWidth /> ); }