@burdenoff/microfe-technospam 2026.728.1 → 2026.730.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/technospam/TechnoSpamRoot.d.ts.map +1 -1
- package/dist/technospam/TechnoSpamRoot.js +174 -212
- package/dist/technospam/TechnoSpamRoot.js.map +1 -1
- package/dist/technospam/TechnoSpamRoutes.d.ts.map +1 -1
- package/dist/technospam/TechnoSpamRoutes.js +31 -30
- package/dist/technospam/TechnoSpamRoutes.js.map +1 -1
- package/dist/technospam/api/technospamApi.d.ts +141 -0
- package/dist/technospam/api/technospamApi.d.ts.map +1 -1
- package/dist/technospam/api/technospamApi.js +316 -120
- package/dist/technospam/api/technospamApi.js.map +1 -1
- package/dist/technospam/pages/ApplicationsPortfolioPagePage.d.ts +2 -0
- package/dist/technospam/pages/ApplicationsPortfolioPagePage.d.ts.map +1 -0
- package/dist/technospam/pages/ApplicationsPortfolioPagePage.js +275 -0
- package/dist/technospam/pages/ApplicationsPortfolioPagePage.js.map +1 -0
- package/dist/technospam/pages/MentorshipMentorProfilePagePage.d.ts.map +1 -1
- package/dist/technospam/pages/MentorshipMentorProfilePagePage.js +151 -110
- package/dist/technospam/pages/MentorshipMentorProfilePagePage.js.map +1 -1
- package/dist/technospam/pages/StudyPlannerStudyPlannerPagePage.d.ts +2 -0
- package/dist/technospam/pages/StudyPlannerStudyPlannerPagePage.d.ts.map +1 -0
- package/dist/technospam/pages/StudyPlannerStudyPlannerPagePage.js +266 -0
- package/dist/technospam/pages/StudyPlannerStudyPlannerPagePage.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { useTechnoSpamNavigate as e } from "../providers/TechnoSpamProvider.js";
|
|
2
|
+
import { PageLayout as t } from "../components/PageLayout.js";
|
|
3
|
+
import { StatCard as n } from "../components/StatCard.js";
|
|
4
|
+
import { formatDate as r } from "../utils/format.js";
|
|
5
|
+
import { StatusBadge as i } from "../components/StatusBadge.js";
|
|
6
|
+
import { EmptyView as a, ErrorView as o, LoadingRows as s } from "../components/StateViews.js";
|
|
7
|
+
import "../components/index.js";
|
|
8
|
+
import { notify as c } from "../lib/toast.js";
|
|
9
|
+
import { useArchiveStudyPlan as l, useGenerateStudyPlan as u, useMyStudyPlansQuery as d, useUpdateStudyPlanStepStatus as f } from "../api/technospamApi.js";
|
|
10
|
+
import { useState as p } from "react";
|
|
11
|
+
import { Archive as m, Calendar as h, CheckCircle2 as g, Circle as _, Clock as v, ListChecks as y, Sparkles as b, Target as x } from "lucide-react";
|
|
12
|
+
import { jsx as S, jsxs as C } from "react/jsx-runtime";
|
|
13
|
+
import { Button as w, GlassCard as T, Input as E } from "@burdenoff/fe-libs/ui";
|
|
14
|
+
import { nativeConfirm as D } from "@burdenoff/fe-libs/shared/native";
|
|
15
|
+
//#region src/technospam/pages/StudyPlannerStudyPlannerPagePage.tsx
|
|
16
|
+
var O = 1, k = 52;
|
|
17
|
+
function A({ step: e, onMark: t }) {
|
|
18
|
+
let n = e.status === "COMPLETED", r = e.status === "SKIPPED";
|
|
19
|
+
return /* @__PURE__ */ C("div", {
|
|
20
|
+
className: "flex flex-col gap-2 rounded-lg border border-border-seam bg-bg-surface p-3 sm:flex-row sm:items-start sm:justify-between",
|
|
21
|
+
children: [/* @__PURE__ */ C("div", {
|
|
22
|
+
className: "flex min-w-0 gap-3",
|
|
23
|
+
children: [/* @__PURE__ */ S("button", {
|
|
24
|
+
type: "button",
|
|
25
|
+
onClick: () => t("COMPLETED"),
|
|
26
|
+
disabled: n,
|
|
27
|
+
className: "mt-0.5 shrink-0 text-text-muted transition-colors hover:text-status-success-text disabled:cursor-default",
|
|
28
|
+
"aria-label": n ? "Completed" : "Mark complete",
|
|
29
|
+
children: n ? /* @__PURE__ */ S(g, {
|
|
30
|
+
size: 18,
|
|
31
|
+
className: "text-status-success-text"
|
|
32
|
+
}) : /* @__PURE__ */ S(_, { size: 18 })
|
|
33
|
+
}), /* @__PURE__ */ C("div", {
|
|
34
|
+
className: "min-w-0",
|
|
35
|
+
children: [/* @__PURE__ */ C("div", {
|
|
36
|
+
className: "flex flex-wrap items-center gap-2",
|
|
37
|
+
children: [
|
|
38
|
+
/* @__PURE__ */ S("span", {
|
|
39
|
+
className: `text-sm font-medium ${n || r ? "text-text-muted line-through" : "text-text-primary"}`,
|
|
40
|
+
children: e.title
|
|
41
|
+
}),
|
|
42
|
+
/* @__PURE__ */ S("span", {
|
|
43
|
+
className: "rounded-full border border-border-seam px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-text-muted",
|
|
44
|
+
children: e.kind
|
|
45
|
+
}),
|
|
46
|
+
/* @__PURE__ */ C("span", {
|
|
47
|
+
className: "text-xs text-text-muted",
|
|
48
|
+
children: ["Week ", e.scheduledWeek]
|
|
49
|
+
})
|
|
50
|
+
]
|
|
51
|
+
}), /* @__PURE__ */ S("p", {
|
|
52
|
+
className: "mt-1 text-xs text-text-secondary",
|
|
53
|
+
children: e.rationale
|
|
54
|
+
})]
|
|
55
|
+
})]
|
|
56
|
+
}), !n && !r ? /* @__PURE__ */ S(w, {
|
|
57
|
+
variant: "ghost",
|
|
58
|
+
size: "sm",
|
|
59
|
+
onClick: () => t("SKIPPED"),
|
|
60
|
+
className: "shrink-0",
|
|
61
|
+
children: "Skip"
|
|
62
|
+
}) : /* @__PURE__ */ S(i, { status: e.status })]
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
function j({ plan: e, onArchived: t }) {
|
|
66
|
+
let [n] = f(), [a, { loading: o }] = l(), [s, u] = p(e.status === "ACTIVE"), d = async (e, t) => {
|
|
67
|
+
await n({ variables: { input: {
|
|
68
|
+
stepId: e,
|
|
69
|
+
status: t
|
|
70
|
+
} } });
|
|
71
|
+
};
|
|
72
|
+
return /* @__PURE__ */ C(T, {
|
|
73
|
+
className: "space-y-4 p-4 sm:p-5",
|
|
74
|
+
children: [
|
|
75
|
+
/* @__PURE__ */ C("div", {
|
|
76
|
+
className: "flex flex-wrap items-start justify-between gap-3",
|
|
77
|
+
children: [/* @__PURE__ */ C("div", {
|
|
78
|
+
className: "min-w-0",
|
|
79
|
+
children: [/* @__PURE__ */ C("div", {
|
|
80
|
+
className: "flex flex-wrap items-center gap-2",
|
|
81
|
+
children: [
|
|
82
|
+
/* @__PURE__ */ S("h3", {
|
|
83
|
+
className: "text-sm font-semibold text-text-primary",
|
|
84
|
+
children: e.goal
|
|
85
|
+
}),
|
|
86
|
+
/* @__PURE__ */ S(i, { status: e.status }),
|
|
87
|
+
e.topic ? /* @__PURE__ */ S("span", {
|
|
88
|
+
className: "rounded-full bg-accent-teal/15 px-2 py-0.5 text-[11px] font-medium text-accent-teal",
|
|
89
|
+
children: e.topic
|
|
90
|
+
}) : null
|
|
91
|
+
]
|
|
92
|
+
}), /* @__PURE__ */ C("div", {
|
|
93
|
+
className: "mt-1 flex flex-wrap items-center gap-3 text-xs text-text-muted",
|
|
94
|
+
children: [
|
|
95
|
+
/* @__PURE__ */ C("span", {
|
|
96
|
+
className: "inline-flex items-center gap-1",
|
|
97
|
+
children: [
|
|
98
|
+
/* @__PURE__ */ S(h, { size: 12 }),
|
|
99
|
+
" ",
|
|
100
|
+
e.timeframeWeeks,
|
|
101
|
+
"-week plan"
|
|
102
|
+
]
|
|
103
|
+
}),
|
|
104
|
+
e.targetCompletionDate ? /* @__PURE__ */ C("span", {
|
|
105
|
+
className: "inline-flex items-center gap-1",
|
|
106
|
+
children: [
|
|
107
|
+
/* @__PURE__ */ S(v, { size: 12 }),
|
|
108
|
+
" target ",
|
|
109
|
+
r(e.targetCompletionDate)
|
|
110
|
+
]
|
|
111
|
+
}) : null,
|
|
112
|
+
/* @__PURE__ */ C("span", {
|
|
113
|
+
className: "inline-flex items-center gap-1",
|
|
114
|
+
children: [
|
|
115
|
+
/* @__PURE__ */ S(y, { size: 12 }),
|
|
116
|
+
" ",
|
|
117
|
+
e.completedSteps,
|
|
118
|
+
"/",
|
|
119
|
+
e.totalSteps,
|
|
120
|
+
" steps done"
|
|
121
|
+
]
|
|
122
|
+
})
|
|
123
|
+
]
|
|
124
|
+
})]
|
|
125
|
+
}), /* @__PURE__ */ C("div", {
|
|
126
|
+
className: "flex shrink-0 items-center gap-2",
|
|
127
|
+
children: [/* @__PURE__ */ S(w, {
|
|
128
|
+
variant: "ghost",
|
|
129
|
+
size: "sm",
|
|
130
|
+
onClick: () => u((e) => !e),
|
|
131
|
+
children: s ? "Hide steps" : "Show steps"
|
|
132
|
+
}), e.status === "ARCHIVED" ? null : /* @__PURE__ */ C(w, {
|
|
133
|
+
variant: "outline",
|
|
134
|
+
size: "sm",
|
|
135
|
+
onClick: async () => {
|
|
136
|
+
await D({
|
|
137
|
+
title: "Archive plan",
|
|
138
|
+
message: `Archive the plan for "${e.goal}"? You can always generate a new one.`,
|
|
139
|
+
okButtonTitle: "Archive",
|
|
140
|
+
cancelButtonTitle: "Cancel"
|
|
141
|
+
}) && (await a({ variables: { id: e.id } })).data?.tecArchiveStudyPlan && (c.success("Plan archived"), t());
|
|
142
|
+
},
|
|
143
|
+
disabled: o,
|
|
144
|
+
children: [/* @__PURE__ */ S(m, { size: 14 }), " Archive"]
|
|
145
|
+
})]
|
|
146
|
+
})]
|
|
147
|
+
}),
|
|
148
|
+
/* @__PURE__ */ S("div", {
|
|
149
|
+
className: "h-1.5 w-full overflow-hidden rounded-full bg-bg-sunken",
|
|
150
|
+
children: /* @__PURE__ */ S("div", {
|
|
151
|
+
className: "h-full rounded-full bg-gradient-to-r from-accent-primary via-accent-indigo to-accent-purple transition-all duration-500",
|
|
152
|
+
style: { width: `${e.progressPct}%` }
|
|
153
|
+
})
|
|
154
|
+
}),
|
|
155
|
+
s ? /* @__PURE__ */ S("div", {
|
|
156
|
+
className: "space-y-2",
|
|
157
|
+
children: e.steps.map((e) => /* @__PURE__ */ S(A, {
|
|
158
|
+
step: e,
|
|
159
|
+
onMark: (t) => void d(e.id, t)
|
|
160
|
+
}, e.id))
|
|
161
|
+
}) : null
|
|
162
|
+
]
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function M() {
|
|
166
|
+
let r = e(), { data: i, loading: l, error: f, refetch: m } = d(), [h, { loading: g }] = u(), [_, v] = p(""), [D, A] = p(""), [M, N] = p("4"), P = i?.tecMyStudyPlans.items ?? [], F = P.filter((e) => e.status !== "ARCHIVED");
|
|
167
|
+
return /* @__PURE__ */ C(t, {
|
|
168
|
+
hero: !0,
|
|
169
|
+
title: "Study Planner",
|
|
170
|
+
description: "Tell us your goal and timeframe — we'll sequence real courses and practice problems for you, skipping what you've already done.",
|
|
171
|
+
actions: /* @__PURE__ */ C(w, {
|
|
172
|
+
variant: "outline",
|
|
173
|
+
onClick: () => r("/courses/courses-page"),
|
|
174
|
+
children: [/* @__PURE__ */ S(x, { size: 16 }), " Browse catalog"]
|
|
175
|
+
}),
|
|
176
|
+
children: [
|
|
177
|
+
/* @__PURE__ */ C(T, {
|
|
178
|
+
className: "mb-6 space-y-4 p-4 sm:p-5",
|
|
179
|
+
children: [/* @__PURE__ */ C("div", {
|
|
180
|
+
className: "flex items-center gap-2",
|
|
181
|
+
children: [/* @__PURE__ */ S("span", {
|
|
182
|
+
className: "flex h-8 w-8 items-center justify-center rounded-lg bg-accent-primary/15 text-accent-primary",
|
|
183
|
+
children: /* @__PURE__ */ S(b, { size: 16 })
|
|
184
|
+
}), /* @__PURE__ */ C("div", { children: [/* @__PURE__ */ S("h2", {
|
|
185
|
+
className: "text-sm font-semibold text-text-primary",
|
|
186
|
+
children: "Generate a plan"
|
|
187
|
+
}), /* @__PURE__ */ S("p", {
|
|
188
|
+
className: "text-xs text-text-muted",
|
|
189
|
+
children: "Rule-based sequencing over the real catalog — not an AI/LLM feature."
|
|
190
|
+
})] })]
|
|
191
|
+
}), /* @__PURE__ */ C("div", {
|
|
192
|
+
className: "grid grid-cols-1 gap-3 sm:grid-cols-[2fr_1fr_120px_auto]",
|
|
193
|
+
children: [
|
|
194
|
+
/* @__PURE__ */ S(E, {
|
|
195
|
+
value: _,
|
|
196
|
+
onChange: (e) => v(e.target.value),
|
|
197
|
+
placeholder: "Your goal, e.g. \"become a full-stack engineer\""
|
|
198
|
+
}),
|
|
199
|
+
/* @__PURE__ */ S(E, {
|
|
200
|
+
value: D,
|
|
201
|
+
onChange: (e) => A(e.target.value),
|
|
202
|
+
placeholder: "Topic (optional)"
|
|
203
|
+
}),
|
|
204
|
+
/* @__PURE__ */ S(E, {
|
|
205
|
+
type: "number",
|
|
206
|
+
min: O,
|
|
207
|
+
max: k,
|
|
208
|
+
value: M,
|
|
209
|
+
onChange: (e) => N(e.target.value),
|
|
210
|
+
placeholder: "Weeks"
|
|
211
|
+
}),
|
|
212
|
+
/* @__PURE__ */ S(w, {
|
|
213
|
+
onClick: async () => {
|
|
214
|
+
let e = _.trim();
|
|
215
|
+
if (!e) return c.error("Describe your goal first", "e.g. \"become a full-stack engineer\"");
|
|
216
|
+
let t = Math.min(k, Math.max(O, Number.parseInt(M, 10) || 4)), n = await h({ variables: { input: {
|
|
217
|
+
goal: e,
|
|
218
|
+
topic: D.trim() || void 0,
|
|
219
|
+
timeframeWeeks: t
|
|
220
|
+
} } });
|
|
221
|
+
n.data?.tecGenerateStudyPlan && (c.success("Plan generated", `${n.data.tecGenerateStudyPlan.totalSteps} steps sequenced for you.`), v(""), A(""));
|
|
222
|
+
},
|
|
223
|
+
disabled: g,
|
|
224
|
+
children: g ? "Generating…" : "Generate plan"
|
|
225
|
+
})
|
|
226
|
+
]
|
|
227
|
+
})]
|
|
228
|
+
}),
|
|
229
|
+
/* @__PURE__ */ C("div", {
|
|
230
|
+
className: "mb-3 flex items-center justify-between",
|
|
231
|
+
children: [/* @__PURE__ */ S("h2", {
|
|
232
|
+
className: "text-sm font-semibold text-text-primary",
|
|
233
|
+
children: "Your plans"
|
|
234
|
+
}), /* @__PURE__ */ S(n, {
|
|
235
|
+
label: "Active plans",
|
|
236
|
+
value: F.length,
|
|
237
|
+
hint: "Plans currently in progress",
|
|
238
|
+
icon: y,
|
|
239
|
+
tone: "indigo",
|
|
240
|
+
className: "hidden sm:block"
|
|
241
|
+
})]
|
|
242
|
+
}),
|
|
243
|
+
l && !P.length ? /* @__PURE__ */ S(s, {
|
|
244
|
+
count: 2,
|
|
245
|
+
height: "h-32"
|
|
246
|
+
}) : f ? /* @__PURE__ */ S(o, {
|
|
247
|
+
title: "Couldn't load your study plans",
|
|
248
|
+
error: f
|
|
249
|
+
}) : P.length === 0 ? /* @__PURE__ */ S(a, {
|
|
250
|
+
icon: /* @__PURE__ */ S(b, { size: 26 }),
|
|
251
|
+
title: "No study plans yet",
|
|
252
|
+
description: "Describe a goal above and we'll sequence a plan from the real course and practice catalog."
|
|
253
|
+
}) : /* @__PURE__ */ S("div", {
|
|
254
|
+
className: "space-y-4",
|
|
255
|
+
children: P.map((e) => /* @__PURE__ */ S(j, {
|
|
256
|
+
plan: e,
|
|
257
|
+
onArchived: () => void m()
|
|
258
|
+
}, e.id))
|
|
259
|
+
})
|
|
260
|
+
]
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
//#endregion
|
|
264
|
+
export { M as StudyPlannerStudyPlannerPagePage };
|
|
265
|
+
|
|
266
|
+
//# sourceMappingURL=StudyPlannerStudyPlannerPagePage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StudyPlannerStudyPlannerPagePage.js","names":[],"sources":["../../../src/technospam/pages/StudyPlannerStudyPlannerPagePage.tsx"],"sourcesContent":["/**\n * Study Planner (BOFF-4415).\n *\n * HONESTY NOTE: plan generation is a deterministic, rule-based scheduler —\n * it sequences real published courses/problems by level/difficulty and\n * excludes content you've already completed/solved. It is NOT an AI/LLM\n * feature. True adaptive re-planning from your submission history and\n * skill-gap analysis (the \"TechnoSage\" epic, BOFF-3874/3895) is deferred —\n * it depends on the shared platform AI gateway (BOFF-3601), which has not\n * been built yet.\n *\n * This single page intentionally consolidates what the nav scaffolding\n * originally anticipated as several separate pages (a plans list, a plan\n * details view, an edit view, learning paths, goals) into one focused\n * surface: generate a plan, see your active plans, and track progress. The\n * richer standalone views are deferred as follow-up scope.\n */\nimport { useState } from 'react';\nimport {\n Archive,\n Calendar,\n CheckCircle2,\n Circle,\n Clock,\n ListChecks,\n Sparkles,\n Target,\n} from 'lucide-react';\nimport { Button, GlassCard, Input } from '@burdenoff/fe-libs/ui';\nimport {\n EmptyView,\n ErrorView,\n LoadingRows,\n PageLayout,\n StatCard,\n StatusBadge,\n} from '../components';\nimport { useTechnoSpamNavigate } from '../providers/TechnoSpamProvider';\nimport {\n type TecStudyPlan,\n type TecStudyPlanStep,\n useArchiveStudyPlan,\n useGenerateStudyPlan,\n useMyStudyPlansQuery,\n useUpdateStudyPlanStepStatus,\n} from '../api/technospamApi';\nimport { formatDate } from '../utils/format';\nimport { notify } from '../lib/toast';\nimport { nativeConfirm } from '@burdenoff/fe-libs/shared/native';\n\nconst MIN_WEEKS = 1;\nconst MAX_WEEKS = 52;\n\nfunction StepRow({\n step,\n onMark,\n}: {\n step: TecStudyPlanStep;\n onMark: (status: 'COMPLETED' | 'SKIPPED') => void;\n}) {\n const done = step.status === 'COMPLETED';\n const skipped = step.status === 'SKIPPED';\n return (\n <div className=\"flex flex-col gap-2 rounded-lg border border-border-seam bg-bg-surface p-3 sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"flex min-w-0 gap-3\">\n <button\n type=\"button\"\n onClick={() => onMark('COMPLETED')}\n disabled={done}\n className=\"mt-0.5 shrink-0 text-text-muted transition-colors hover:text-status-success-text disabled:cursor-default\"\n aria-label={done ? 'Completed' : 'Mark complete'}\n >\n {done ? (\n <CheckCircle2 size={18} className=\"text-status-success-text\" />\n ) : (\n <Circle size={18} />\n )}\n </button>\n <div className=\"min-w-0\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span\n className={`text-sm font-medium ${done || skipped ? 'text-text-muted line-through' : 'text-text-primary'}`}\n >\n {step.title}\n </span>\n <span className=\"rounded-full border border-border-seam px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-text-muted\">\n {step.kind}\n </span>\n <span className=\"text-xs text-text-muted\">Week {step.scheduledWeek}</span>\n </div>\n <p className=\"mt-1 text-xs text-text-secondary\">{step.rationale}</p>\n </div>\n </div>\n {!done && !skipped ? (\n <Button variant=\"ghost\" size=\"sm\" onClick={() => onMark('SKIPPED')} className=\"shrink-0\">\n Skip\n </Button>\n ) : (\n <StatusBadge status={step.status} />\n )}\n </div>\n );\n}\n\nfunction PlanCard({ plan, onArchived }: { plan: TecStudyPlan; onArchived: () => void }) {\n const [updateStep] = useUpdateStudyPlanStepStatus();\n const [archivePlan, { loading: archiving }] = useArchiveStudyPlan();\n const [expanded, setExpanded] = useState(plan.status === 'ACTIVE');\n\n const handleMark = async (stepId: string, status: 'COMPLETED' | 'SKIPPED') => {\n await updateStep({ variables: { input: { stepId, status } } });\n };\n\n const handleArchive = async () => {\n const confirmed = await nativeConfirm({\n title: 'Archive plan',\n message: `Archive the plan for \"${plan.goal}\"? You can always generate a new one.`,\n okButtonTitle: 'Archive',\n cancelButtonTitle: 'Cancel',\n });\n if (!confirmed) return;\n const res = await archivePlan({ variables: { id: plan.id } });\n if (res.data?.tecArchiveStudyPlan) {\n notify.success('Plan archived');\n onArchived();\n }\n };\n\n return (\n <GlassCard className=\"space-y-4 p-4 sm:p-5\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <h3 className=\"text-sm font-semibold text-text-primary\">{plan.goal}</h3>\n <StatusBadge status={plan.status} />\n {plan.topic ? (\n <span className=\"rounded-full bg-accent-teal/15 px-2 py-0.5 text-[11px] font-medium text-accent-teal\">\n {plan.topic}\n </span>\n ) : null}\n </div>\n <div className=\"mt-1 flex flex-wrap items-center gap-3 text-xs text-text-muted\">\n <span className=\"inline-flex items-center gap-1\">\n <Calendar size={12} /> {plan.timeframeWeeks}-week plan\n </span>\n {plan.targetCompletionDate ? (\n <span className=\"inline-flex items-center gap-1\">\n <Clock size={12} /> target {formatDate(plan.targetCompletionDate)}\n </span>\n ) : null}\n <span className=\"inline-flex items-center gap-1\">\n <ListChecks size={12} /> {plan.completedSteps}/{plan.totalSteps} steps done\n </span>\n </div>\n </div>\n <div className=\"flex shrink-0 items-center gap-2\">\n <Button variant=\"ghost\" size=\"sm\" onClick={() => setExpanded((v) => !v)}>\n {expanded ? 'Hide steps' : 'Show steps'}\n </Button>\n {plan.status !== 'ARCHIVED' ? (\n <Button variant=\"outline\" size=\"sm\" onClick={handleArchive} disabled={archiving}>\n <Archive size={14} /> Archive\n </Button>\n ) : null}\n </div>\n </div>\n\n <div className=\"h-1.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-gradient-to-r from-accent-primary via-accent-indigo to-accent-purple transition-all duration-500\"\n style={{ width: `${plan.progressPct}%` }}\n />\n </div>\n\n {expanded ? (\n <div className=\"space-y-2\">\n {plan.steps.map((step) => (\n <StepRow\n key={step.id}\n step={step}\n onMark={(status) => void handleMark(step.id, status)}\n />\n ))}\n </div>\n ) : null}\n </GlassCard>\n );\n}\n\nexport function StudyPlannerStudyPlannerPagePage() {\n const navigate = useTechnoSpamNavigate();\n const { data, loading, error, refetch } = useMyStudyPlansQuery();\n const [generatePlan, { loading: generating }] = useGenerateStudyPlan();\n\n const [goal, setGoal] = useState('');\n const [topic, setTopic] = useState('');\n const [weeks, setWeeks] = useState('4');\n\n const plans = data?.tecMyStudyPlans.items ?? [];\n const activePlans = plans.filter((p) => p.status !== 'ARCHIVED');\n\n const handleGenerate = async () => {\n const trimmedGoal = goal.trim();\n if (!trimmedGoal)\n return notify.error('Describe your goal first', 'e.g. \"become a full-stack engineer\"');\n const parsedWeeks = Math.min(MAX_WEEKS, Math.max(MIN_WEEKS, Number.parseInt(weeks, 10) || 4));\n const res = await generatePlan({\n variables: {\n input: { goal: trimmedGoal, topic: topic.trim() || undefined, timeframeWeeks: parsedWeeks },\n },\n });\n if (res.data?.tecGenerateStudyPlan) {\n notify.success(\n 'Plan generated',\n `${res.data.tecGenerateStudyPlan.totalSteps} steps sequenced for you.`\n );\n setGoal('');\n setTopic('');\n }\n };\n\n return (\n <PageLayout\n hero\n title=\"Study Planner\"\n description=\"Tell us your goal and timeframe — we'll sequence real courses and practice problems for you, skipping what you've already done.\"\n actions={\n <Button variant=\"outline\" onClick={() => navigate('/courses/courses-page')}>\n <Target size={16} /> Browse catalog\n </Button>\n }\n >\n <GlassCard className=\"mb-6 space-y-4 p-4 sm:p-5\">\n <div className=\"flex items-center gap-2\">\n <span className=\"flex h-8 w-8 items-center justify-center rounded-lg bg-accent-primary/15 text-accent-primary\">\n <Sparkles size={16} />\n </span>\n <div>\n <h2 className=\"text-sm font-semibold text-text-primary\">Generate a plan</h2>\n <p className=\"text-xs text-text-muted\">\n Rule-based sequencing over the real catalog — not an AI/LLM feature.\n </p>\n </div>\n </div>\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-[2fr_1fr_120px_auto]\">\n <Input\n value={goal}\n onChange={(e) => setGoal(e.target.value)}\n placeholder='Your goal, e.g. \"become a full-stack engineer\"'\n />\n <Input\n value={topic}\n onChange={(e) => setTopic(e.target.value)}\n placeholder=\"Topic (optional)\"\n />\n <Input\n type=\"number\"\n min={MIN_WEEKS}\n max={MAX_WEEKS}\n value={weeks}\n onChange={(e) => setWeeks(e.target.value)}\n placeholder=\"Weeks\"\n />\n <Button onClick={handleGenerate} disabled={generating}>\n {generating ? 'Generating…' : 'Generate plan'}\n </Button>\n </div>\n </GlassCard>\n\n <div className=\"mb-3 flex items-center justify-between\">\n <h2 className=\"text-sm font-semibold text-text-primary\">Your plans</h2>\n <StatCard\n label=\"Active plans\"\n value={activePlans.length}\n hint=\"Plans currently in progress\"\n icon={ListChecks}\n tone=\"indigo\"\n className=\"hidden sm:block\"\n />\n </div>\n\n {loading && !plans.length ? (\n <LoadingRows count={2} height=\"h-32\" />\n ) : error ? (\n <ErrorView title=\"Couldn't load your study plans\" error={error} />\n ) : plans.length === 0 ? (\n <EmptyView\n icon={<Sparkles size={26} />}\n title=\"No study plans yet\"\n description=\"Describe a goal above and we'll sequence a plan from the real course and practice catalog.\"\n />\n ) : (\n <div className=\"space-y-4\">\n {plans.map((plan) => (\n <PlanCard key={plan.id} plan={plan} onArchived={() => void refetch()} />\n ))}\n </div>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|