"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 } ); 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; 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: 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 = 4; 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 planeStart = plane * perPlane; const planeCount = Math.min(perPlane, Math.max(0, total - planeStart)); 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 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; // 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; // 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; } function World3D({ products }: { products: Product[] }) { return ( { camera.lookAt(2.8, 2.6, -2); }} gl={{ antialias: true }} > {/* Nice reflections / lighting */} {/* eslint-disable-next-line react/no-unknown-property */} {/* eslint-disable-next-line react/no-unknown-property */} {/* eslint-disable-next-line react/no-unknown-property */} {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); 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 {/* Rounded cube */} {/* eslint-disable-next-line react/no-unknown-property */} {/* +Z (front) */} {label} {/* -Z (back) */} {label} {/* +X (right) */} {label} {/* -X (left) */} {label} {/* +Y (top) */} {label} {/* -Y (bottom) */} {label} ); } 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 /> ); }