Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/app/api/goals/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ interface Goal {
created_at: string;
goal_reset_version: number;
is_public: boolean;
category: string | null;
}

interface GoalHistory {
Expand All @@ -34,6 +35,7 @@ interface GoalHistory {
type Recurrence = "none" | "weekly" | "monthly";

const VALID_RECURRENCES = ["none", "weekly", "monthly"] as const;
const VALID_CATEGORIES = ["Side Project", "Work", "DSA", "Open Source"] as const;
const MAX_TITLE_LEN = 100;
const MAX_UNIT_LEN = 30;
const MIN_TARGET = 1;
Expand Down Expand Up @@ -201,7 +203,7 @@ try {
return Response.json({ error: "Invalid request body" }, { status: 400 });
}

const { title, target, unit, recurrence, deadline } = body as Record<string, unknown>;
const { title, target, unit, recurrence, deadline, category } = body as Record<string, unknown>;

if (typeof title !== "string" || title.trim().length === 0) {
return Response.json({ error: "title must be a non-empty string" }, { status: 400 });
Expand Down Expand Up @@ -239,6 +241,11 @@ try {
}
}

const safeCategory =
typeof category === "string" && VALID_CATEGORIES.includes(category as any)
? category
: null;

const user = await resolveAppUser(session.githubId, session.githubLogin);
if (!user) return Response.json({ error: "User not found" }, { status: 404 });

Expand Down Expand Up @@ -287,6 +294,7 @@ try {
deadline: safeDeadline,
current: 0,
goal_reset_version: 0,
category: safeCategory,
})
.select()
.single();
Expand Down
83 changes: 81 additions & 2 deletions src/components/GoalTracker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ interface Goal {
achieved: number;
completed: boolean;
} | null;
category?: string | null;
}

const RECURRENCE_LABELS: Record<Recurrence, string> = {
Expand All @@ -39,6 +40,15 @@ const RECURRENCE_LABELS: Record<Recurrence, string> = {
monthly: "Monthly",
};

export const CATEGORIES = ["Side Project", "Work", "DSA", "Open Source"];

export const CATEGORY_COLORS: Record<string, string> = {
"Side Project": "bg-purple-500/10 text-purple-500 border-purple-500/30",
"Work": "bg-blue-500/10 text-blue-500 border-blue-500/30",
"DSA": "bg-emerald-500/10 text-emerald-500 border-emerald-500/30",
"Open Source": "bg-amber-500/10 text-amber-500 border-amber-500/30",
};

export function useGoalTracker() {
const [goals, setGoals] = useState<Goal[]>([]);
const [loading, setLoading] = useState(true);
Expand All @@ -51,6 +61,7 @@ export function useGoalTracker() {
const [unit, setUnit] = useState("commits");
const [recurrence, setRecurrence] = useState<Recurrence>("none");
const [deadline, setDeadline] = useState("");
const [category, setCategory] = useState("");
const [creating, setCreating] = useState(false);
const [createError, setCreateError] = useState<string | null>(null);
const [confirmingId, setConfirmingId] = useState<string | null>(null);
Expand Down Expand Up @@ -161,7 +172,7 @@ export function useGoalTracker() {

try {
const result = await submitGoalWithRefresh({
payload: { title, target, unit, recurrence, deadline: deadline || null },
payload: { title, target, unit, recurrence, deadline: deadline || null, category: category || null },
handleSync,
loadGoals,
});
Expand All @@ -176,6 +187,7 @@ export function useGoalTracker() {
setUnit("commits");
setRecurrence("none");
setDeadline("");
setCategory("");

if (unit === "commits" || unit === "prs") {
await handleSync();
Expand Down Expand Up @@ -293,6 +305,8 @@ export function useGoalTracker() {
setRecurrence,
deadline,
setDeadline,
category,
setCategory,
creating,
createError,
confirmingId,
Expand Down Expand Up @@ -331,6 +345,8 @@ export default function GoalTracker() {
setRecurrence,
deadline,
setDeadline,
category,
setCategory,
creating,
createError,
confirmingId,
Expand All @@ -347,6 +363,8 @@ export default function GoalTracker() {

const { setSummary, setIsUpdating } = useDashboardWidgetA11y("goal-tracker");

const [filterCategory, setFilterCategory] = useState<string>("All");

useEffect(() => {
setIsUpdating(loading);
}, [loading, setIsUpdating]);
Expand Down Expand Up @@ -520,6 +538,35 @@ export default function GoalTracker() {
</div>
)}

{/* Filter Toggle Pills */}
{goals.length > 0 && (
<div className="mb-4 flex flex-wrap gap-2">
<button
onClick={() => setFilterCategory("All")}
className={`px-3 py-1 text-xs font-medium rounded-full border transition-colors ${
filterCategory === "All"
? "bg-[var(--foreground)] text-[var(--background)] border-[var(--foreground)]"
: "bg-transparent text-[var(--muted-foreground)] border-[var(--border)] hover:border-[var(--muted-foreground)]"
}`}
>
All
</button>
{CATEGORIES.map((cat) => (
<button
key={cat}
onClick={() => setFilterCategory(cat)}
className={`px-3 py-1 text-xs font-medium rounded-full border transition-colors ${
filterCategory === cat
? "bg-[var(--foreground)] text-[var(--background)] border-[var(--foreground)]"
: "bg-transparent text-[var(--muted-foreground)] border-[var(--border)] hover:border-[var(--muted-foreground)]"
}`}
>
{cat}
</button>
))}
</div>
)}

{goals.length === 0 ? (
<div className="mt-6">
<EmptyState
Expand All @@ -534,7 +581,9 @@ export default function GoalTracker() {
) : (
<ul className="space-y-4">

{goals.map((goal) => {
{goals
.filter((goal) => filterCategory === "All" || goal.category === filterCategory)
.map((goal) => {
const pct =
goal.current > 0 && goal.target > 0
? Math.max(
Expand Down Expand Up @@ -580,6 +629,13 @@ export default function GoalTracker() {
{RECURRENCE_LABELS[goal.recurrence]}
</span>
)}
{goal.category && (
<span
className={`text-xs font-medium px-2 py-0.5 rounded-full border ${CATEGORY_COLORS[goal.category] || "bg-[var(--card-muted)] text-[var(--muted-foreground)] border-[var(--border)]"}`}
>
{goal.category}
</span>
)}
{isAutoSynced && (
<span
title={
Expand Down Expand Up @@ -878,6 +934,29 @@ export default function GoalTracker() {
)}
</div>

<div>
<label
htmlFor="goal-category"
className="mb-1 block text-xs font-medium uppercase tracking-wide text-[var(--muted-foreground)]"
>
Category (Optional)
</label>
<select
id="goal-category"
value={category}
onChange={(e) => setCategory(e.target.value)}
disabled={creating}
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)]"
>
<option value="">No Category</option>
{CATEGORIES.map((cat) => (
<option key={cat} value={cat}>
{cat}
</option>
))}
</select>
</div>

{(unit === "commits" || unit === "prs") && (
<p className="text-xs text-[var(--muted-foreground)] rounded-lg bg-[var(--accent)]/10 px-3 py-2">
⚑ This goal will auto-update from your GitHub activity.
Expand Down
Loading
Loading