Skip to content

Commit 765d2c0

Browse files
authored
Merge pull request #61 from AnasSarkiz/main
Add persistent custom laser profiles (saved in localStorage) with validated “Add Profile” dialog
2 parents ea321d1 + 5706565 commit 765d2c0

File tree

3 files changed

+398
-77
lines changed

3 files changed

+398
-77
lines changed
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
import React, { useState } from "react"
2+
3+
import { Button } from "@/components/ui/button"
4+
import {
5+
Dialog,
6+
DialogContent,
7+
DialogDescription,
8+
DialogFooter,
9+
DialogHeader,
10+
DialogTitle,
11+
} from "@/components/ui/dialog"
12+
import { Input } from "@/components/ui/input"
13+
import { Separator } from "@/components/ui/separator"
14+
import { NumericControl } from "./numeric-control"
15+
16+
export type LaserProfile = {
17+
copper: {
18+
speed: number
19+
numPasses: number
20+
frequency: number
21+
pulseWidth: number
22+
}
23+
board: {
24+
speed: number
25+
numPasses: number
26+
frequency: number
27+
pulseWidth: number
28+
}
29+
}
30+
31+
type LaserProfileDialogProps = {
32+
open: boolean
33+
onOpenChange: (open: boolean) => void
34+
initialProfile: LaserProfile
35+
existingProfileNames: string[]
36+
onSave: (name: string, profile: LaserProfile) => void
37+
}
38+
39+
export function LaserProfileDialog({
40+
open,
41+
onOpenChange,
42+
initialProfile,
43+
existingProfileNames,
44+
onSave,
45+
}: LaserProfileDialogProps) {
46+
const [profileName, setProfileName] = useState("")
47+
const [profileError, setProfileError] = useState<string | null>(null)
48+
const [profileForm, setProfileForm] = useState<LaserProfile>(initialProfile)
49+
50+
React.useEffect(() => {
51+
if (!open) return
52+
setProfileName("")
53+
setProfileError(null)
54+
setProfileForm(initialProfile)
55+
}, [open, initialProfile])
56+
57+
const handleSave = () => {
58+
const trimmedName = profileName.trim()
59+
if (!trimmedName) {
60+
setProfileError("Profile name is required.")
61+
return
62+
}
63+
if (existingProfileNames.includes(trimmedName)) {
64+
setProfileError("A profile with this name already exists.")
65+
return
66+
}
67+
68+
onSave(trimmedName, profileForm)
69+
onOpenChange(false)
70+
}
71+
72+
return (
73+
<Dialog open={open} onOpenChange={onOpenChange}>
74+
<DialogContent>
75+
<DialogHeader>
76+
<DialogTitle>Add Laser Profile</DialogTitle>
77+
<DialogDescription>
78+
Save the current laser settings as a reusable profile.
79+
</DialogDescription>
80+
</DialogHeader>
81+
<div className="grid gap-4 py-2">
82+
<div className="grid grid-cols-4 items-center gap-4">
83+
<label htmlFor="profile-name" className="text-right">
84+
Name
85+
</label>
86+
<div className="col-span-3 space-y-1">
87+
<Input
88+
id="profile-name"
89+
value={profileName}
90+
onChange={(e) => {
91+
setProfileName(e.target.value)
92+
setProfileError(null)
93+
}}
94+
placeholder="e.g. 20W Copper 2-pass"
95+
/>
96+
{profileError && (
97+
<div className="text-xs text-destructive">{profileError}</div>
98+
)}
99+
</div>
100+
</div>
101+
<Separator />
102+
<div className="space-y-3">
103+
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
104+
Copper Cutting
105+
</div>
106+
<NumericControl
107+
value={profileForm.copper.speed}
108+
onChange={(value) =>
109+
setProfileForm((prev) => ({
110+
...prev,
111+
copper: { ...prev.copper, speed: value },
112+
}))
113+
}
114+
label="Speed"
115+
min={1}
116+
unit="mm/s"
117+
/>
118+
<NumericControl
119+
value={profileForm.copper.numPasses}
120+
onChange={(value) =>
121+
setProfileForm((prev) => ({
122+
...prev,
123+
copper: { ...prev.copper, numPasses: value },
124+
}))
125+
}
126+
label="Passes"
127+
min={1}
128+
unit=" "
129+
/>
130+
<NumericControl
131+
value={profileForm.copper.frequency}
132+
onChange={(value) =>
133+
setProfileForm((prev) => ({
134+
...prev,
135+
copper: { ...prev.copper, frequency: value },
136+
}))
137+
}
138+
label="Frequency"
139+
min={1000}
140+
unit="kHz"
141+
/>
142+
<NumericControl
143+
value={profileForm.copper.pulseWidth}
144+
onChange={(value) =>
145+
setProfileForm((prev) => ({
146+
...prev,
147+
copper: { ...prev.copper, pulseWidth: value },
148+
}))
149+
}
150+
label="Pulse Width"
151+
min={1}
152+
unit="ns"
153+
/>
154+
</div>
155+
<Separator />
156+
<div className="space-y-3">
157+
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
158+
Board Cutting
159+
</div>
160+
<NumericControl
161+
value={profileForm.board.speed}
162+
onChange={(value) =>
163+
setProfileForm((prev) => ({
164+
...prev,
165+
board: { ...prev.board, speed: value },
166+
}))
167+
}
168+
label="Speed"
169+
min={1}
170+
unit="mm/s"
171+
/>
172+
<NumericControl
173+
value={profileForm.board.numPasses}
174+
onChange={(value) =>
175+
setProfileForm((prev) => ({
176+
...prev,
177+
board: { ...prev.board, numPasses: value },
178+
}))
179+
}
180+
label="Passes"
181+
min={1}
182+
unit=" "
183+
/>
184+
<NumericControl
185+
value={profileForm.board.frequency}
186+
onChange={(value) =>
187+
setProfileForm((prev) => ({
188+
...prev,
189+
board: { ...prev.board, frequency: value },
190+
}))
191+
}
192+
label="Frequency"
193+
min={1000}
194+
unit="kHz"
195+
/>
196+
<NumericControl
197+
value={profileForm.board.pulseWidth}
198+
onChange={(value) =>
199+
setProfileForm((prev) => ({
200+
...prev,
201+
board: { ...prev.board, pulseWidth: value },
202+
}))
203+
}
204+
label="Pulse Width"
205+
min={1}
206+
unit="ns"
207+
/>
208+
</div>
209+
</div>
210+
<DialogFooter>
211+
<Button
212+
type="button"
213+
variant="outline"
214+
onClick={() => onOpenChange(false)}
215+
>
216+
Cancel
217+
</Button>
218+
<Button type="button" onClick={handleSave}>
219+
Save Profile
220+
</Button>
221+
</DialogFooter>
222+
</DialogContent>
223+
</Dialog>
224+
)
225+
}

lib/components/numeric-control.tsx

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import React, { useState } from "react"
2+
3+
type NumericControlProps = {
4+
value: number
5+
onChange: (value: number) => void
6+
label: string
7+
min?: number
8+
unit?: string
9+
}
10+
11+
export function NumericControl({
12+
value,
13+
onChange,
14+
label,
15+
min = 0,
16+
unit = "",
17+
}: NumericControlProps) {
18+
const [inputValue, setInputValue] = useState(value.toString())
19+
20+
React.useEffect(() => {
21+
setInputValue(value.toString())
22+
}, [value])
23+
24+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
25+
const newValue = e.target.value
26+
setInputValue(newValue)
27+
}
28+
29+
const handleInputBlur = () => {
30+
const numericValue = parseFloat(inputValue)
31+
if (Number.isNaN(numericValue) || numericValue < min) {
32+
setInputValue(value.toString())
33+
} else {
34+
onChange(Math.max(min, numericValue))
35+
}
36+
}
37+
38+
return (
39+
<div className="flex items-center justify-between">
40+
<span className="text-sm">{label}</span>
41+
<div className="flex items-center gap-1">
42+
<input
43+
type="text"
44+
value={inputValue}
45+
onChange={handleInputChange}
46+
onBlur={handleInputBlur}
47+
className="text-xs w-24 text-center border border-input bg-background rounded px-1 py-0.5 focus:outline-none focus:ring-1 focus:ring-ring"
48+
/>
49+
{unit && (
50+
<span className="text-xs text-muted-foreground w-6">{unit}</span>
51+
)}
52+
</div>
53+
</div>
54+
)
55+
}

0 commit comments

Comments
 (0)