@@ -12,6 +12,7 @@ import WidgetSkeleton, { SkeletonBlock } from "./WidgetSkeleton";
1212
1313
1414type Recurrence = "none" | "weekly" | "monthly" ;
15+ type GoalCategory = "side-project" | "work" | "dsa" | "open-source" ;
1516
1617interface Goal {
1718 id : string ;
@@ -25,6 +26,7 @@ interface Goal {
2526 period_start : string ;
2627 last_synced_at : string | null ;
2728 week_start : string | null ;
29+ category ?: GoalCategory | null ;
2830 last_period : {
2931 period_start : string ;
3032 period_end : string ;
@@ -40,6 +42,22 @@ const RECURRENCE_LABELS: Record<Recurrence, string> = {
4042 monthly : "Monthly" ,
4143} ;
4244
45+ const CATEGORY_LABELS : Record < GoalCategory , string > = {
46+ "side-project" : "Side Project" ,
47+ work : "Work" ,
48+ dsa : "DSA" ,
49+ "open-source" : "Open Source" ,
50+ } ;
51+
52+ const CATEGORY_BADGE_CLASSES : Record < GoalCategory , string > = {
53+ "side-project" : "bg-purple-500/10 text-purple-400 border-purple-500/30" ,
54+ work : "bg-blue-500/10 text-blue-400 border-blue-500/30" ,
55+ dsa : "bg-amber-500/10 text-amber-400 border-amber-500/30" ,
56+ "open-source" : "bg-emerald-500/10 text-emerald-400 border-emerald-500/30" ,
57+ } ;
58+
59+ const CATEGORY_OPTIONS : GoalCategory [ ] = [ "side-project" , "work" , "dsa" , "open-source" ] ;
60+
4361export function useGoalTracker ( ) {
4462 const [ goals , setGoals ] = useState < Goal [ ] > ( [ ] ) ;
4563 const [ loading , setLoading ] = useState ( true ) ;
@@ -52,6 +70,8 @@ export function useGoalTracker() {
5270 const [ unit , setUnit ] = useState ( "commits" ) ;
5371 const [ recurrence , setRecurrence ] = useState < Recurrence > ( "none" ) ;
5472 const [ deadline , setDeadline ] = useState ( "" ) ;
73+ const [ category , setCategory ] = useState < GoalCategory | "" > ( "" ) ;
74+ const [ activeCategoryFilter , setActiveCategoryFilter ] = useState < GoalCategory | null > ( null ) ;
5575 const [ creating , setCreating ] = useState ( false ) ;
5676 const [ createError , setCreateError ] = useState < string | null > ( null ) ;
5777 const [ confirmingId , setConfirmingId ] = useState < string | null > ( null ) ;
@@ -162,7 +182,14 @@ export function useGoalTracker() {
162182
163183 try {
164184 const result = await submitGoalWithRefresh ( {
165- payload : { title, target, unit, recurrence, deadline : deadline || null } ,
185+ payload : {
186+ title,
187+ target,
188+ unit,
189+ recurrence,
190+ deadline : deadline || null ,
191+ category : category || null ,
192+ } ,
166193 handleSync,
167194 loadGoals,
168195 } ) ;
@@ -177,6 +204,7 @@ export function useGoalTracker() {
177204 setUnit ( "commits" ) ;
178205 setRecurrence ( "none" ) ;
179206 setDeadline ( "" ) ;
207+ setCategory ( "" ) ;
180208
181209 if ( unit === "commits" || unit === "prs" ) {
182210 await handleSync ( ) ;
@@ -294,6 +322,10 @@ export function useGoalTracker() {
294322 setRecurrence,
295323 deadline,
296324 setDeadline,
325+ category,
326+ setCategory,
327+ activeCategoryFilter,
328+ setActiveCategoryFilter,
297329 creating,
298330 createError,
299331 confirmingId,
@@ -332,6 +364,10 @@ export default function GoalTracker() {
332364 setRecurrence,
333365 deadline,
334366 setDeadline,
367+ category,
368+ setCategory,
369+ activeCategoryFilter,
370+ setActiveCategoryFilter,
335371 creating,
336372 createError,
337373 confirmingId,
@@ -530,9 +566,50 @@ export default function GoalTracker() {
530566
531567 </ div >
532568 ) : (
569+ < >
570+ { goals . some ( ( g ) => g . category ) && (
571+ < div
572+ role = "group"
573+ aria-label = "Filter goals by category"
574+ className = "mb-3 flex flex-wrap gap-1.5"
575+ >
576+ < button
577+ type = "button"
578+ onClick = { ( ) => setActiveCategoryFilter ( null ) }
579+ aria-pressed = { activeCategoryFilter === null }
580+ className = { `rounded-full border px-2.5 py-1 text-xs font-medium transition ${
581+ activeCategoryFilter === null
582+ ? "border-[var(--accent)] bg-[var(--accent)] text-[var(--accent-foreground)]"
583+ : "border-[var(--border)] text-[var(--muted-foreground)] hover:border-[var(--accent)]"
584+ } `}
585+ >
586+ All
587+ </ button >
588+ { CATEGORY_OPTIONS . map ( ( c ) => (
589+ < button
590+ key = { c }
591+ type = "button"
592+ onClick = { ( ) =>
593+ setActiveCategoryFilter ( ( curr ) => ( curr === c ? null : c ) )
594+ }
595+ aria-pressed = { activeCategoryFilter === c }
596+ className = { `rounded-full border px-2.5 py-1 text-xs font-medium transition ${
597+ activeCategoryFilter === c
598+ ? CATEGORY_BADGE_CLASSES [ c ] . replace ( "/10" , "/20" )
599+ : "border-[var(--border)] text-[var(--muted-foreground)] hover:border-[var(--accent)]"
600+ } `}
601+ >
602+ { CATEGORY_LABELS [ c ] }
603+ </ button >
604+ ) ) }
605+ </ div >
606+ ) }
607+
533608 < ul className = "space-y-4" >
534609
535- { goals . map ( ( goal ) => {
610+ { goals
611+ . filter ( ( goal ) => ! activeCategoryFilter || goal . category === activeCategoryFilter )
612+ . map ( ( goal ) => {
536613 const pct =
537614 goal . current > 0 && goal . target > 0
538615 ? Math . max (
@@ -567,6 +644,13 @@ export default function GoalTracker() {
567644 < div className = "flex flex-col gap-0.5" >
568645 < div className = "flex items-center gap-2 flex-wrap" >
569646 < span className = "text-[var(--card-foreground)]" > { goal . title } </ span >
647+ { goal . category && (
648+ < span
649+ className = { `text-xs font-medium px-2 py-0.5 rounded-full border ${ CATEGORY_BADGE_CLASSES [ goal . category ] } ` }
650+ >
651+ { CATEGORY_LABELS [ goal . category ] }
652+ </ span >
653+ ) }
570654 { goal . recurrence !== "none" && (
571655 < span
572656 className = { `text-xs font-medium px-2 py-0.5 rounded-full border ${
@@ -749,6 +833,7 @@ export default function GoalTracker() {
749833 ) ;
750834 } ) }
751835 </ ul >
836+ </ >
752837 ) }
753838
754839 { lastUpdated && (
@@ -825,6 +910,29 @@ export default function GoalTracker() {
825910 </ div >
826911 </ div >
827912
913+ < div >
914+ < label
915+ htmlFor = "goal-category"
916+ className = "mb-1 block text-xs font-medium uppercase tracking-wide text-[var(--muted-foreground)]"
917+ >
918+ Category (Optional)
919+ </ label >
920+ < select
921+ id = "goal-category"
922+ value = { category }
923+ onChange = { ( e ) => setCategory ( e . target . value as GoalCategory | "" ) }
924+ disabled = { creating }
925+ className = "w-full rounded-lg border border-[var(--border)] bg-[var(--background)] px-3 py-2 text-sm text-[var(--foreground)] transition focus-visible:border-[var(--accent)]"
926+ >
927+ < option value = "" > No category</ option >
928+ { CATEGORY_OPTIONS . map ( ( c ) => (
929+ < option key = { c } value = { c } >
930+ { CATEGORY_LABELS [ c ] }
931+ </ option >
932+ ) ) }
933+ </ select >
934+ </ div >
935+
828936 { recurrence === "none" && (
829937 < div >
830938 < label
0 commit comments