@@ -5,7 +5,14 @@ import { api } from "@/lib/api";
55import { useAppStore } from "@/stores/app-store" ;
66import { SegmentRulesEditor } from "@/components/segment-rules-editor" ;
77import { toast } from "@/components/toast" ;
8- import { PageHeader , Card , Button , Input , Label , EmptyState } from "@/components/ui" ;
8+ import {
9+ PageHeader ,
10+ Card ,
11+ Button ,
12+ Input ,
13+ Label ,
14+ EmptyState ,
15+ } from "@/components/ui" ;
916import { Select } from "@/components/ui/select" ;
1017import { Users , Trash2 , ChevronDown } from "lucide-react" ;
1118import { ContextualHint , HINTS } from "@/components/contextual-hint" ;
@@ -23,19 +30,41 @@ export default function SegmentsPage() {
2330 const projectId = useAppStore ( ( s ) => s . currentProjectId ) ;
2431 const [ segments , setSegments ] = useState < Segment [ ] > ( [ ] ) ;
2532 const [ showCreate , setShowCreate ] = useState ( false ) ;
26- const [ form , setForm ] = useState ( { key : "" , name : "" , description : "" , match_type : "all" } ) ;
33+ const [ form , setForm ] = useState ( {
34+ key : "" ,
35+ name : "" ,
36+ description : "" ,
37+ match_type : "all" ,
38+ } ) ;
39+ const [ fieldErrors , setFieldErrors ] = useState < {
40+ key ?: string ;
41+ name ?: string ;
42+ } > ( { } ) ;
2743 const [ deleting , setDeleting ] = useState < string | null > ( null ) ;
2844 const [ expanded , setExpanded ] = useState < string | null > ( null ) ;
2945
3046 function reload ( ) {
3147 if ( ! token || ! projectId ) return ;
32- api . listSegments ( token , projectId ) . then ( ( s ) => setSegments ( s ?? [ ] ) ) . catch ( ( ) => { } ) ;
48+ api
49+ . listSegments ( token , projectId )
50+ . then ( ( s ) => setSegments ( s ?? [ ] ) )
51+ . catch ( ( ) => { } ) ;
3352 }
3453
35- useEffect ( ( ) => { reload ( ) ; } , [ token , projectId ] ) ;
54+ useEffect ( ( ) => {
55+ reload ( ) ;
56+ } , [ token , projectId ] ) ;
3657
3758 async function handleCreate ( e : React . FormEvent ) {
3859 e . preventDefault ( ) ;
60+ const errors : { key ?: string ; name ?: string } = { } ;
61+ if ( ! form . key . trim ( ) ) errors . key = "Key is required" ;
62+ if ( ! form . name . trim ( ) ) errors . name = "Name is required" ;
63+ if ( Object . keys ( errors ) . length > 0 ) {
64+ setFieldErrors ( errors ) ;
65+ return ;
66+ }
67+ setFieldErrors ( { } ) ;
3968 if ( ! token || ! projectId ) {
4069 toast ( "Select a project first" , "error" ) ;
4170 return ;
@@ -47,7 +76,10 @@ export default function SegmentsPage() {
4776 toast ( "Segment created" , "success" ) ;
4877 reload ( ) ;
4978 } catch ( err : unknown ) {
50- toast ( err instanceof Error ? err . message : "Failed to create segment" , "error" ) ;
79+ toast (
80+ err instanceof Error ? err . message : "Failed to create segment" ,
81+ "error" ,
82+ ) ;
5183 }
5284 }
5385
@@ -59,19 +91,32 @@ export default function SegmentsPage() {
5991 toast ( "Segment deleted" , "success" ) ;
6092 reload ( ) ;
6193 } catch ( err : unknown ) {
62- toast ( err instanceof Error ? err . message : "Failed to delete segment" , "error" ) ;
94+ toast (
95+ err instanceof Error ? err . message : "Failed to delete segment" ,
96+ "error" ,
97+ ) ;
6398 setDeleting ( null ) ;
6499 }
65100 }
66101
67- async function handleSaveRules ( segKey : string , rules : Condition [ ] , matchType : string ) {
102+ async function handleSaveRules (
103+ segKey : string ,
104+ rules : Condition [ ] ,
105+ matchType : string ,
106+ ) {
68107 if ( ! token || ! projectId ) return ;
69108 try {
70- await api . updateSegment ( token , projectId , segKey , { rules, match_type : matchType } ) ;
109+ await api . updateSegment ( token , projectId , segKey , {
110+ rules,
111+ match_type : matchType ,
112+ } ) ;
71113 toast ( "Segment rules saved" , "success" ) ;
72114 reload ( ) ;
73115 } catch ( err : unknown ) {
74- toast ( err instanceof Error ? err . message : "Failed to save rules" , "error" ) ;
116+ toast (
117+ err instanceof Error ? err . message : "Failed to save rules" ,
118+ "error" ,
119+ ) ;
75120 }
76121 }
77122
@@ -93,37 +138,98 @@ export default function SegmentsPage() {
93138 description = "Reusable audience definitions for targeting"
94139 docsUrl = { DOCS_LINKS . segments }
95140 actions = {
96- < Button onClick = { ( ) => setShowCreate ( ! showCreate ) } > Create Segment</ Button >
141+ < Button onClick = { ( ) => setShowCreate ( ! showCreate ) } >
142+ Create Segment
143+ </ Button >
97144 }
98145 />
99146
100147 < ContextualHint hint = { HINTS . segmentsIntro } />
101148
102149 { showCreate && (
103- < form onSubmit = { handleCreate } className = "rounded-xl border border-slate-200/80 bg-white p-4 space-y-4 shadow-sm ring-1 ring-indigo-100 sm:p-6" >
150+ < form
151+ onSubmit = { handleCreate }
152+ noValidate
153+ className = "rounded-xl border border-slate-200/80 bg-white p-4 space-y-4 shadow-sm ring-1 ring-indigo-100 sm:p-6"
154+ >
104155 < div className = "grid grid-cols-1 gap-4 sm:grid-cols-2" >
105156 < div >
106157 < Label > Key</ Label >
107- < Input value = { form . key } onChange = { ( e ) => setForm ( { ...form , key : e . target . value } ) } placeholder = "beta-users" required className = "mt-1" />
158+ < Input
159+ value = { form . key }
160+ onChange = { ( e ) => {
161+ setForm ( { ...form , key : e . target . value } ) ;
162+ if ( fieldErrors . key )
163+ setFieldErrors ( { ...fieldErrors , key : undefined } ) ;
164+ } }
165+ placeholder = "beta-users"
166+ required
167+ className = "mt-1"
168+ aria-invalid = { ! ! fieldErrors . key }
169+ aria-describedby = { fieldErrors . key ? "key-error" : undefined }
170+ />
171+ { fieldErrors . key && (
172+ < p className = "text-xs text-red-500" role = "alert" id = "key-error" >
173+ { fieldErrors . key }
174+ </ p >
175+ ) }
108176 </ div >
109177 < div >
110178 < Label > Name</ Label >
111- < Input value = { form . name } onChange = { ( e ) => setForm ( { ...form , name : e . target . value } ) } placeholder = "Beta Users" required className = "mt-1" />
179+ < Input
180+ value = { form . name }
181+ onChange = { ( e ) => {
182+ setForm ( { ...form , name : e . target . value } ) ;
183+ if ( fieldErrors . name )
184+ setFieldErrors ( { ...fieldErrors , name : undefined } ) ;
185+ } }
186+ placeholder = "Beta Users"
187+ required
188+ className = "mt-1"
189+ aria-invalid = { ! ! fieldErrors . name }
190+ aria-describedby = { fieldErrors . name ? "name-error" : undefined }
191+ />
192+ { fieldErrors . name && (
193+ < p
194+ className = "text-xs text-red-500"
195+ role = "alert"
196+ id = "name-error"
197+ >
198+ { fieldErrors . name }
199+ </ p >
200+ ) }
112201 </ div >
113202 </ div >
114203 < div >
115204 < Label > Description</ Label >
116- < Input value = { form . description } onChange = { ( e ) => setForm ( { ...form , description : e . target . value } ) } placeholder = "Users enrolled in beta program" className = "mt-1" />
205+ < Input
206+ value = { form . description }
207+ onChange = { ( e ) =>
208+ setForm ( { ...form , description : e . target . value } )
209+ }
210+ placeholder = "Users enrolled in beta program"
211+ className = "mt-1"
212+ />
117213 </ div >
118214 < div >
119215 < Label > Match Type</ Label >
120216 < div className = "mt-1" >
121- < Select value = { form . match_type } onValueChange = { ( val ) => setForm ( { ...form , match_type : val } ) } options = { MATCH_TYPE_OPTIONS } />
217+ < Select
218+ value = { form . match_type }
219+ onValueChange = { ( val ) => setForm ( { ...form , match_type : val } ) }
220+ options = { MATCH_TYPE_OPTIONS }
221+ />
122222 </ div >
123223 </ div >
124224 < div className = "flex gap-2" >
125225 < Button type = "submit" > Create</ Button >
126- < Button type = "button" variant = "secondary" onClick = { ( ) => setShowCreate ( false ) } > Cancel</ Button >
226+ < Button
227+ type = "button"
228+ variant = "secondary"
229+ onClick = { ( ) => setShowCreate ( false ) }
230+ >
231+ Cancel
232+ </ Button >
127233 </ div >
128234 </ form >
129235 ) }
@@ -151,36 +257,70 @@ export default function SegmentsPage() {
151257 onClick = { ( ) => setExpanded ( isExpanded ? null : seg . key ) }
152258 >
153259 < div className = "min-w-0" >
154- < p className = "font-mono text-sm font-medium text-slate-900" > { seg . key } </ p >
155- < p className = "mt-0.5 text-xs text-slate-500" > { seg . name } · Match { seg . match_type } · { seg . rules ?. length || 0 } rules</ p >
156- { seg . description && < p className = "mt-0.5 text-xs text-slate-400" > { seg . description } </ p > }
260+ < p className = "font-mono text-sm font-medium text-slate-900" >
261+ { seg . key }
262+ </ p >
263+ < p className = "mt-0.5 text-xs text-slate-500" >
264+ { seg . name } · Match { seg . match_type } ·{ " " }
265+ { seg . rules ?. length || 0 } rules
266+ </ p >
267+ { seg . description && (
268+ < p className = "mt-0.5 text-xs text-slate-400" >
269+ { seg . description }
270+ </ p >
271+ ) }
157272 </ div >
158273 < div className = "flex items-center gap-2 shrink-0" >
159274 { deleting === seg . key ? (
160- < div className = "flex items-center gap-1" onClick = { ( e ) => e . stopPropagation ( ) } >
161- < Button size = "sm" variant = "destructive-ghost" onClick = { ( ) => handleDelete ( seg . key ) } > Confirm</ Button >
162- < Button size = "sm" variant = "ghost" onClick = { ( ) => setDeleting ( null ) } > Cancel</ Button >
275+ < div
276+ className = "flex items-center gap-1"
277+ onClick = { ( e ) => e . stopPropagation ( ) }
278+ >
279+ < Button
280+ size = "sm"
281+ variant = "destructive-ghost"
282+ onClick = { ( ) => handleDelete ( seg . key ) }
283+ >
284+ Confirm
285+ </ Button >
286+ < Button
287+ size = "sm"
288+ variant = "ghost"
289+ onClick = { ( ) => setDeleting ( null ) }
290+ >
291+ Cancel
292+ </ Button >
163293 </ div >
164294 ) : (
165295 < Button
166296 size = "icon-sm"
167297 variant = "ghost"
168- onClick = { ( e ) => { e . stopPropagation ( ) ; setDeleting ( seg . key ) ; } }
298+ onClick = { ( e ) => {
299+ e . stopPropagation ( ) ;
300+ setDeleting ( seg . key ) ;
301+ } }
169302 title = "Delete segment"
170303 className = "text-slate-400 hover:text-red-500 hover:bg-red-50"
171304 >
172305 < Trash2 className = "h-4 w-4" />
173306 </ Button >
174307 ) }
175- < ChevronDown className = { cn ( "h-4 w-4 text-slate-400 transition-transform duration-200" , isExpanded && "rotate-180" ) } />
308+ < ChevronDown
309+ className = { cn (
310+ "h-4 w-4 text-slate-400 transition-transform duration-200" ,
311+ isExpanded && "rotate-180" ,
312+ ) }
313+ />
176314 </ div >
177315 </ div >
178316 { isExpanded && (
179317 < div className = "border-t border-slate-100 px-4 py-4 bg-slate-50/50 sm:px-6 animate-fade-in" >
180318 < SegmentRulesEditor
181319 rules = { seg . rules ?? [ ] }
182320 matchType = { seg . match_type }
183- onSave = { ( rules , matchType ) => handleSaveRules ( seg . key , rules , matchType ) }
321+ onSave = { ( rules , matchType ) =>
322+ handleSaveRules ( seg . key , rules , matchType )
323+ }
184324 />
185325 </ div >
186326 ) }
0 commit comments