Skip to content

Commit 1e64936

Browse files
rmzlbclaude
andcommitted
feat: Add configuration history and validation system
- Implement localStorage-based history system (max 10 items) - Add HistoryPanel component with numbered items and visual capacity indicator - Enable 0 quantity for pieces with +/- increment buttons - Add configuration validation to prevent impossible cuts - Show error messages when pieces exceed board dimensions - Auto-save configurations after each optimization - Sync all input fields properly when restoring from history - Add export functionality for saved configurations 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 856e056 commit 1e64936

5 files changed

Lines changed: 771 additions & 17 deletions

File tree

components/CuttingOptimizer.tsx

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@ import { motion } from 'motion/react'
55
import { useState } from 'react'
66

77
import { optimizeCutting } from '@/lib/optimizer'
8+
import { saveToHistory } from '@/lib/utils/history'
9+
import { validateConfiguration } from '@/lib/utils/validation'
810

911
import BoardInput from './BoardInput'
1012
import BoardVisualizer from './BoardVisualizer'
1113
import ExportPanel from './ExportPanel'
14+
import HistoryPanel from './HistoryPanel'
1215
import HowItWorks from './HowItWorks'
1316
import PieceInput from './PieceInput'
1417
import PositioningReport from './PositioningReport'
@@ -43,9 +46,14 @@ export default function CuttingOptimizer() {
4346
{ id: 'D', w: 200, h: 300, qty: 6 },
4447
])
4548

49+
// Validate configuration
50+
const validation = validateConfiguration(config, pieces)
51+
const hasErrors = validation.errors.length > 0
52+
const hasWarnings = validation.warnings.length > 0
53+
4654
// Calculate optimization when button is clicked
4755
const handleOptimize = () => {
48-
if (pieces.length === 0) return
56+
if (pieces.length === 0 || hasErrors) return
4957
setIsCalculating(true)
5058
setShowResults(false)
5159

@@ -55,6 +63,9 @@ export default function CuttingOptimizer() {
5563
setIsCalculating(false)
5664
setShowResults(true)
5765

66+
// Save to history
67+
saveToHistory(config, pieces, res)
68+
5869
// Scroll to visualization on mobile devices
5970
if (window.innerWidth < 1024) {
6071
// lg breakpoint
@@ -82,6 +93,13 @@ export default function CuttingOptimizer() {
8293
setConfig((prev) => ({ ...prev, ...updates }))
8394
}
8495

96+
const handleRestore = (restoredConfig: OptimizationConfig, restoredPieces: PieceSpec[]) => {
97+
setConfig(restoredConfig)
98+
setPieces(restoredPieces)
99+
setShowResults(false)
100+
setResult(null)
101+
}
102+
85103
const resetExample = () => {
86104
setConfig({
87105
boardWidth: 1500,
@@ -179,11 +197,42 @@ export default function CuttingOptimizer() {
179197
<PieceInput pieces={pieces} onChange={setPieces} />
180198
</div>
181199

200+
{/* History panel */}
201+
<HistoryPanel onRestore={handleRestore} />
202+
182203
{/* Optimize button in sidebar */}
183204
<div className="rounded-xl border border-neutral-200 bg-white p-5">
205+
{/* Validation messages */}
206+
{(hasErrors || hasWarnings) && (
207+
<div className="mb-3 space-y-2">
208+
{validation.errors.map((error, i) => (
209+
<motion.div
210+
key={`error-${i}`}
211+
initial={{ opacity: 0, x: -10 }}
212+
animate={{ opacity: 1, x: 0 }}
213+
className="flex items-start gap-2 rounded-lg bg-red-50 p-2 text-xs"
214+
>
215+
<span className="text-red-600">⚠️</span>
216+
<span className="text-red-700">{error}</span>
217+
</motion.div>
218+
))}
219+
{validation.warnings.map((warning, i) => (
220+
<motion.div
221+
key={`warning-${i}`}
222+
initial={{ opacity: 0, x: -10 }}
223+
animate={{ opacity: 1, x: 0 }}
224+
className="flex items-start gap-2 rounded-lg bg-amber-50 p-2 text-xs"
225+
>
226+
<span className="text-amber-600"></span>
227+
<span className="text-amber-700">{warning}</span>
228+
</motion.div>
229+
))}
230+
</div>
231+
)}
232+
184233
<button
185234
onClick={handleOptimize}
186-
disabled={pieces.length === 0 || isCalculating}
235+
disabled={pieces.length === 0 || isCalculating || hasErrors}
187236
className="w-full transform rounded-lg bg-gradient-to-r from-amber-500 to-orange-500 px-4 py-3 font-medium text-white transition-all hover:scale-[1.02] hover:shadow-lg disabled:cursor-not-allowed disabled:opacity-50"
188237
>
189238
{isCalculating ? (
@@ -368,12 +417,31 @@ export default function CuttingOptimizer() {
368417
<p className="mx-auto mb-8 max-w-md text-neutral-600">
369418
{pieces.length === 0
370419
? 'Ajoutez des pièces à découper dans le panneau de gauche pour commencer'
371-
: "Cliquez sur le bouton pour lancer l'optimisation"}
420+
: hasErrors
421+
? 'Corrigez les erreurs de configuration pour continuer'
422+
: "Cliquez sur le bouton pour lancer l'optimisation"}
372423
</p>
373424

425+
{/* Show validation errors in center view */}
426+
{hasErrors && (
427+
<div className="mx-auto mb-6 max-w-md space-y-2">
428+
{validation.errors.map((error, i) => (
429+
<motion.div
430+
key={`center-error-${i}`}
431+
initial={{ opacity: 0, y: -10 }}
432+
animate={{ opacity: 1, y: 0 }}
433+
className="flex items-start gap-2 rounded-lg bg-red-50 p-3 text-sm"
434+
>
435+
<span className="text-red-600">⚠️</span>
436+
<span className="text-red-700">{error}</span>
437+
</motion.div>
438+
))}
439+
</div>
440+
)}
441+
374442
<button
375443
onClick={handleOptimize}
376-
disabled={pieces.length === 0 || isCalculating}
444+
disabled={pieces.length === 0 || isCalculating || hasErrors}
377445
className="transform rounded-xl bg-gradient-to-r from-amber-500 to-orange-500 px-8 py-4 text-lg font-medium text-white transition-all hover:scale-105 hover:shadow-2xl disabled:cursor-not-allowed disabled:opacity-50"
378446
>
379447
{isCalculating ? (

0 commit comments

Comments
 (0)