@burdenoff/microfe-technospam 2026.1101.5 → 2026.1102.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.
Files changed (108) hide show
  1. package/dist/generated/wspace-types.d.ts +7468 -141
  2. package/dist/generated/wspace-types.d.ts.map +1 -1
  3. package/dist/index.js +5 -3
  4. package/dist/node_modules/@codemirror/autocomplete/dist/index.js +527 -0
  5. package/dist/node_modules/@codemirror/autocomplete/dist/index.js.map +1 -0
  6. package/dist/node_modules/@codemirror/commands/dist/index.js +1108 -0
  7. package/dist/node_modules/@codemirror/commands/dist/index.js.map +1 -0
  8. package/dist/node_modules/@codemirror/lang-go/dist/index.js +206 -0
  9. package/dist/node_modules/@codemirror/lang-go/dist/index.js.map +1 -0
  10. package/dist/node_modules/@codemirror/lang-javascript/dist/index.js +321 -0
  11. package/dist/node_modules/@codemirror/lang-javascript/dist/index.js.map +1 -0
  12. package/dist/node_modules/@codemirror/lang-python/dist/index.js +265 -0
  13. package/dist/node_modules/@codemirror/lang-python/dist/index.js.map +1 -0
  14. package/dist/node_modules/@codemirror/language/dist/index.js +957 -0
  15. package/dist/node_modules/@codemirror/language/dist/index.js.map +1 -0
  16. package/dist/node_modules/@codemirror/legacy-modes/mode/ruby.js +164 -0
  17. package/dist/node_modules/@codemirror/legacy-modes/mode/ruby.js.map +1 -0
  18. package/dist/node_modules/@codemirror/legacy-modes/mode/shell.js +109 -0
  19. package/dist/node_modules/@codemirror/legacy-modes/mode/shell.js.map +1 -0
  20. package/dist/node_modules/@codemirror/state/dist/index.js +1739 -0
  21. package/dist/node_modules/@codemirror/state/dist/index.js.map +1 -0
  22. package/dist/node_modules/@codemirror/streamparser/dist/index.js +70 -0
  23. package/dist/node_modules/@codemirror/streamparser/dist/index.js.map +1 -0
  24. package/dist/node_modules/@codemirror/view/dist/index.js +5427 -0
  25. package/dist/node_modules/@codemirror/view/dist/index.js.map +1 -0
  26. package/dist/node_modules/@lezer/common/dist/index.js +896 -0
  27. package/dist/node_modules/@lezer/common/dist/index.js.map +1 -0
  28. package/dist/node_modules/@lezer/go/dist/index.js +215 -0
  29. package/dist/node_modules/@lezer/go/dist/index.js.map +1 -0
  30. package/dist/node_modules/@lezer/highlight/dist/index.js +395 -0
  31. package/dist/node_modules/@lezer/highlight/dist/index.js.map +1 -0
  32. package/dist/node_modules/@lezer/javascript/dist/index.js +437 -0
  33. package/dist/node_modules/@lezer/javascript/dist/index.js.map +1 -0
  34. package/dist/node_modules/@lezer/lr/dist/index.js +842 -0
  35. package/dist/node_modules/@lezer/lr/dist/index.js.map +1 -0
  36. package/dist/node_modules/@lezer/python/dist/index.js +352 -0
  37. package/dist/node_modules/@lezer/python/dist/index.js.map +1 -0
  38. package/dist/node_modules/@marijn/find-cluster-break/src/index.js +66 -0
  39. package/dist/node_modules/@marijn/find-cluster-break/src/index.js.map +1 -0
  40. package/dist/node_modules/style-mod/src/style-mod.js +74 -0
  41. package/dist/node_modules/style-mod/src/style-mod.js.map +1 -0
  42. package/dist/node_modules/w3c-keyname/index.js +90 -0
  43. package/dist/node_modules/w3c-keyname/index.js.map +1 -0
  44. package/dist/stories/fixtures/technospamFixtures.d.ts.map +1 -1
  45. package/dist/stories/journeys/Problems.stories.d.ts.map +1 -1
  46. package/dist/technospam/api/technospamApi.d.ts +39 -0
  47. package/dist/technospam/api/technospamApi.d.ts.map +1 -1
  48. package/dist/technospam/api/technospamApi.js +120 -86
  49. package/dist/technospam/api/technospamApi.js.map +1 -1
  50. package/dist/technospam/components/CertificateVerifyLink.d.ts +11 -0
  51. package/dist/technospam/components/CertificateVerifyLink.d.ts.map +1 -0
  52. package/dist/technospam/components/CertificateVerifyLink.js +63 -0
  53. package/dist/technospam/components/CertificateVerifyLink.js.map +1 -0
  54. package/dist/technospam/components/SubmissionViews.d.ts +6 -2
  55. package/dist/technospam/components/SubmissionViews.d.ts.map +1 -1
  56. package/dist/technospam/components/SubmissionViews.js +6 -5
  57. package/dist/technospam/components/SubmissionViews.js.map +1 -1
  58. package/dist/technospam/components/TechnoSpamErrorBoundary.d.ts +6 -0
  59. package/dist/technospam/components/TechnoSpamErrorBoundary.d.ts.map +1 -1
  60. package/dist/technospam/components/TechnoSpamErrorBoundary.js +2 -2
  61. package/dist/technospam/components/TechnoSpamErrorBoundary.js.map +1 -1
  62. package/dist/technospam/components/editor/CodeEditor.d.ts +26 -0
  63. package/dist/technospam/components/editor/CodeEditor.d.ts.map +1 -0
  64. package/dist/technospam/components/editor/CodeEditor.js +102 -0
  65. package/dist/technospam/components/editor/CodeEditor.js.map +1 -0
  66. package/dist/technospam/components/editor/RunOutputPanel.d.ts +14 -0
  67. package/dist/technospam/components/editor/RunOutputPanel.d.ts.map +1 -0
  68. package/dist/technospam/components/editor/RunOutputPanel.js +127 -0
  69. package/dist/technospam/components/editor/RunOutputPanel.js.map +1 -0
  70. package/dist/technospam/components/editor/editorLanguages.d.ts +11 -0
  71. package/dist/technospam/components/editor/editorLanguages.d.ts.map +1 -0
  72. package/dist/technospam/components/editor/editorLanguages.js +30 -0
  73. package/dist/technospam/components/editor/editorLanguages.js.map +1 -0
  74. package/dist/technospam/components/editor/editorTheme.d.ts +11 -0
  75. package/dist/technospam/components/editor/editorTheme.d.ts.map +1 -0
  76. package/dist/technospam/components/editor/editorTheme.js +120 -0
  77. package/dist/technospam/components/editor/editorTheme.js.map +1 -0
  78. package/dist/technospam/components/index.d.ts +1 -0
  79. package/dist/technospam/components/index.d.ts.map +1 -1
  80. package/dist/technospam/components/index.js +1 -0
  81. package/dist/technospam/hooks/useSolutionRunner.d.ts +35 -0
  82. package/dist/technospam/hooks/useSolutionRunner.d.ts.map +1 -0
  83. package/dist/technospam/hooks/useSolutionRunner.js +66 -0
  84. package/dist/technospam/hooks/useSolutionRunner.js.map +1 -0
  85. package/dist/technospam/hooks/useTechnoSpamAnalytics.d.ts +1 -1
  86. package/dist/technospam/hooks/useTechnoSpamAnalytics.d.ts.map +1 -1
  87. package/dist/technospam/hooks/useTechnoSpamAnalytics.js.map +1 -1
  88. package/dist/technospam/index.d.ts +2 -0
  89. package/dist/technospam/index.d.ts.map +1 -1
  90. package/dist/technospam/index.js +2 -0
  91. package/dist/technospam/lib/certificateVerify.d.ts +71 -0
  92. package/dist/technospam/lib/certificateVerify.d.ts.map +1 -0
  93. package/dist/technospam/lib/certificateVerify.js +55 -0
  94. package/dist/technospam/lib/certificateVerify.js.map +1 -0
  95. package/dist/technospam/pages/ApplicationsProblemDetailsPagePage.d.ts.map +1 -1
  96. package/dist/technospam/pages/ApplicationsProblemDetailsPagePage.js +416 -263
  97. package/dist/technospam/pages/ApplicationsProblemDetailsPagePage.js.map +1 -1
  98. package/dist/technospam/pages/CoursesCourseDetailsPagePage.d.ts.map +1 -1
  99. package/dist/technospam/pages/CoursesCourseDetailsPagePage.js +236 -231
  100. package/dist/technospam/pages/CoursesCourseDetailsPagePage.js.map +1 -1
  101. package/dist/technospam/pages/CoursesMyLearningPagePage.d.ts.map +1 -1
  102. package/dist/technospam/pages/CoursesMyLearningPagePage.js +104 -96
  103. package/dist/technospam/pages/CoursesMyLearningPagePage.js.map +1 -1
  104. package/dist/technospam/pages/public/CertificateVerifyPage.d.ts +4 -0
  105. package/dist/technospam/pages/public/CertificateVerifyPage.d.ts.map +1 -0
  106. package/dist/technospam/pages/public/CertificateVerifyPage.js +257 -0
  107. package/dist/technospam/pages/public/CertificateVerifyPage.js.map +1 -0
  108. package/package.json +14 -4
@@ -0,0 +1,257 @@
1
+ import { formatDate as e, formatDateTime as t, humanizeEnum as n } from "../../utils/format.js";
2
+ import { certificateVerifyPath as r, fetchCertificateVerification as i, isPlausibleSerial as a, normalizeSerial as o } from "../../lib/certificateVerify.js";
3
+ import { useState as s } from "react";
4
+ import { Award as c, ShieldAlert as l, ShieldCheck as u, ShieldQuestion as d } from "lucide-react";
5
+ import { useNavigate as f, useParams as p } from "react-router";
6
+ import { QueryClient as m, QueryClientProvider as h, useQuery as g } from "@tanstack/react-query";
7
+ import { Fragment as _, jsx as v, jsxs as y } from "react/jsx-runtime";
8
+ import { Button as b, GlassCard as x, Input as S, Label as C, Skeleton as w } from "@burdenoff/fe-libs/ui";
9
+ //#region src/technospam/pages/public/CertificateVerifyPage.tsx
10
+ function T({ verification: t }) {
11
+ return t.status === "VALID" ? /* @__PURE__ */ y("div", {
12
+ className: "flex items-start gap-3 rounded-xl border border-status-success-border bg-status-success-bg-subtle p-4 text-status-success-text",
13
+ "data-testid": "tec-verify-status",
14
+ "data-status": "VALID",
15
+ children: [/* @__PURE__ */ v(u, {
16
+ size: 22,
17
+ className: "mt-0.5 shrink-0"
18
+ }), /* @__PURE__ */ y("div", { children: [/* @__PURE__ */ v("div", {
19
+ className: "text-base font-semibold",
20
+ children: "Valid certificate"
21
+ }), /* @__PURE__ */ y("p", {
22
+ className: "text-sm",
23
+ children: [
24
+ "This serial belongs to a certificate issued by ",
25
+ t.issuer,
26
+ " that has not been revoked."
27
+ ]
28
+ })] })]
29
+ }) : t.status === "REVOKED" ? /* @__PURE__ */ y("div", {
30
+ className: "flex items-start gap-3 rounded-xl border border-status-error-border bg-status-error-bg-subtle p-4 text-status-error-text",
31
+ "data-testid": "tec-verify-status",
32
+ "data-status": "REVOKED",
33
+ children: [/* @__PURE__ */ v(l, {
34
+ size: 22,
35
+ className: "mt-0.5 shrink-0"
36
+ }), /* @__PURE__ */ y("div", { children: [/* @__PURE__ */ v("div", {
37
+ className: "text-base font-semibold",
38
+ children: "Revoked certificate"
39
+ }), /* @__PURE__ */ y("p", {
40
+ className: "text-sm",
41
+ children: [
42
+ "This certificate was issued by ",
43
+ t.issuer,
44
+ " but has since been revoked",
45
+ t.revokedAt ? ` (on ${e(t.revokedAt)})` : "",
46
+ ". It should not be relied on."
47
+ ]
48
+ })] })]
49
+ }) : /* @__PURE__ */ y("div", {
50
+ className: "flex items-start gap-3 rounded-xl border border-status-warning-border bg-status-warning-bg-subtle p-4 text-status-warning-text",
51
+ "data-testid": "tec-verify-status",
52
+ "data-status": "NOT_FOUND",
53
+ children: [/* @__PURE__ */ v(d, {
54
+ size: 22,
55
+ className: "mt-0.5 shrink-0"
56
+ }), /* @__PURE__ */ y("div", { children: [/* @__PURE__ */ v("div", {
57
+ className: "text-base font-semibold",
58
+ children: "No certificate found"
59
+ }), /* @__PURE__ */ y("p", {
60
+ className: "text-sm",
61
+ children: [
62
+ "No ",
63
+ t.issuer,
64
+ " certificate carries this serial. Check that it was copied exactly as printed on the certificate."
65
+ ]
66
+ })] })]
67
+ });
68
+ }
69
+ function E({ label: e, value: t, testId: n }) {
70
+ return /* @__PURE__ */ y("div", {
71
+ className: "min-w-0",
72
+ children: [/* @__PURE__ */ v("dt", {
73
+ className: "text-[11px] font-medium uppercase tracking-wide text-text-muted",
74
+ children: e
75
+ }), /* @__PURE__ */ v("dd", {
76
+ className: "truncate text-sm text-text-primary",
77
+ "data-testid": n,
78
+ children: t
79
+ })]
80
+ });
81
+ }
82
+ function D({ verification: r }) {
83
+ let i = r.status !== "NOT_FOUND";
84
+ return /* @__PURE__ */ y("div", {
85
+ className: "space-y-4",
86
+ "data-testid": "tec-verify-result",
87
+ children: [
88
+ /* @__PURE__ */ v(T, { verification: r }),
89
+ /* @__PURE__ */ y("dl", {
90
+ className: "grid grid-cols-1 gap-4 sm:grid-cols-2",
91
+ children: [
92
+ /* @__PURE__ */ v(E, {
93
+ label: "Serial number",
94
+ value: r.serial,
95
+ testId: "tec-verify-serial"
96
+ }),
97
+ i ? /* @__PURE__ */ y(_, { children: [
98
+ /* @__PURE__ */ v(E, {
99
+ label: "Course",
100
+ value: r.courseTitle ?? "—",
101
+ testId: "tec-verify-course"
102
+ }),
103
+ /* @__PURE__ */ v(E, {
104
+ label: "Level",
105
+ value: r.courseLevel ? n(r.courseLevel) : "—",
106
+ testId: "tec-verify-level"
107
+ }),
108
+ /* @__PURE__ */ v(E, {
109
+ label: "Final score",
110
+ value: r.score === null ? "—" : `${r.score} / 100`,
111
+ testId: "tec-verify-score"
112
+ }),
113
+ /* @__PURE__ */ v(E, {
114
+ label: "Issued on",
115
+ value: r.issuedAt ? e(r.issuedAt) : "—",
116
+ testId: "tec-verify-issued"
117
+ })
118
+ ] }) : null,
119
+ /* @__PURE__ */ v(E, {
120
+ label: "Checked at",
121
+ value: t(r.verifiedAt),
122
+ testId: "tec-verify-checked"
123
+ })
124
+ ]
125
+ }),
126
+ /* @__PURE__ */ v("p", {
127
+ className: "text-xs text-text-muted",
128
+ children: "For privacy, verification confirms the course, score, dates and status only. It never shows the learner's name or contact details; compare them against the certificate the candidate shared with you."
129
+ })
130
+ ]
131
+ });
132
+ }
133
+ function O({ apiGatewayUrl: e } = {}) {
134
+ let { serial: t } = p(), n = o(t), [r] = s(() => new m({ defaultOptions: { queries: {
135
+ retry: !1,
136
+ refetchOnWindowFocus: !1
137
+ } } }));
138
+ return /* @__PURE__ */ v(h, {
139
+ client: r,
140
+ children: /* @__PURE__ */ v(k, {
141
+ serial: n,
142
+ apiGatewayUrl: e
143
+ }, n)
144
+ });
145
+ }
146
+ function k({ serial: e, apiGatewayUrl: t }) {
147
+ let n = f(), [l, u] = s(e), d = g({
148
+ queryKey: [
149
+ "tecVerifyCertificate",
150
+ t ?? "",
151
+ e
152
+ ],
153
+ queryFn: () => i(t, e),
154
+ enabled: e.length > 0,
155
+ staleTime: 0,
156
+ gcTime: 0
157
+ }), p = d.data, m = e.length > 0 && (d.isPending || d.isFetching), h = d.isError || p?.kind === "error", _ = o(l), T = a(_);
158
+ return /* @__PURE__ */ y("main", {
159
+ className: "mx-auto flex w-full max-w-2xl flex-col gap-6 px-4 py-10 sm:py-16",
160
+ children: [/* @__PURE__ */ y("header", {
161
+ className: "space-y-2 text-center",
162
+ children: [
163
+ /* @__PURE__ */ y("div", {
164
+ className: "inline-flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.2em] text-text-muted",
165
+ children: [/* @__PURE__ */ v(c, {
166
+ size: 14,
167
+ className: "text-status-warning-text"
168
+ }), " TechnoSpam"]
169
+ }),
170
+ /* @__PURE__ */ v("h1", {
171
+ className: "text-2xl font-bold text-text-primary sm:text-3xl",
172
+ children: "Verify a certificate"
173
+ }),
174
+ /* @__PURE__ */ v("p", {
175
+ className: "text-sm text-text-secondary",
176
+ children: "Confirm that a TechnoSpam certificate is genuine and still valid. No account is needed."
177
+ })
178
+ ]
179
+ }), /* @__PURE__ */ y(x, {
180
+ className: "space-y-6 p-5 sm:p-6",
181
+ children: [
182
+ /* @__PURE__ */ y("form", {
183
+ className: "flex flex-col gap-2",
184
+ onSubmit: (t) => {
185
+ if (t.preventDefault(), T) {
186
+ if (_ === e) {
187
+ d.refetch();
188
+ return;
189
+ }
190
+ n(r(_));
191
+ }
192
+ },
193
+ role: "search",
194
+ children: [
195
+ /* @__PURE__ */ v(C, {
196
+ htmlFor: "tec-verify-input",
197
+ children: "Certificate serial number"
198
+ }),
199
+ /* @__PURE__ */ y("div", {
200
+ className: "flex flex-col gap-2 sm:flex-row",
201
+ children: [/* @__PURE__ */ v(S, {
202
+ id: "tec-verify-input",
203
+ "data-testid": "tec-verify-input",
204
+ value: l,
205
+ onChange: (e) => u(e.target.value),
206
+ placeholder: "TEC-XXXXXXXX-XXXXXXXX",
207
+ autoComplete: "off",
208
+ spellCheck: !1,
209
+ className: "font-mono"
210
+ }), /* @__PURE__ */ v(b, {
211
+ type: "submit",
212
+ disabled: !T,
213
+ "data-testid": "tec-verify-submit",
214
+ children: "Verify"
215
+ })]
216
+ }),
217
+ l.trim() && !T ? /* @__PURE__ */ v("p", {
218
+ className: "text-xs text-status-error-text",
219
+ children: "A serial uses only letters, digits, dots, underscores and hyphens."
220
+ }) : null
221
+ ]
222
+ }),
223
+ m ? /* @__PURE__ */ y("div", {
224
+ className: "space-y-3",
225
+ "aria-busy": "true",
226
+ "aria-live": "polite",
227
+ children: [/* @__PURE__ */ v(w, { className: "h-16 w-full" }), /* @__PURE__ */ v(w, { className: "h-24 w-full" })]
228
+ }) : null,
229
+ !m && p?.kind === "result" ? /* @__PURE__ */ v(D, { verification: p.verification }) : null,
230
+ !m && p?.kind === "invalid" ? /* @__PURE__ */ v("p", {
231
+ className: "text-sm text-status-error-text",
232
+ "data-testid": "tec-verify-invalid",
233
+ children: "That does not look like a certificate serial. Check it against the certificate."
234
+ }) : null,
235
+ !m && h ? /* @__PURE__ */ y("div", {
236
+ className: "flex flex-col items-start gap-2",
237
+ "data-testid": "tec-verify-error",
238
+ children: [/* @__PURE__ */ v("p", {
239
+ className: "text-sm text-status-error-text",
240
+ children: "The verification service could not be reached. Please try again."
241
+ }), /* @__PURE__ */ v(b, {
242
+ size: "sm",
243
+ variant: "outline",
244
+ onClick: () => {
245
+ d.refetch();
246
+ },
247
+ children: "Try again"
248
+ })]
249
+ }) : null
250
+ ]
251
+ })]
252
+ });
253
+ }
254
+ //#endregion
255
+ export { O as CertificateVerifyPage };
256
+
257
+ //# sourceMappingURL=CertificateVerifyPage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CertificateVerifyPage.js","names":[],"sources":["../../../../src/technospam/pages/public/CertificateVerifyPage.tsx"],"sourcesContent":["import { useState, type FormEvent } from 'react';\nimport { useNavigate, useParams } from 'react-router';\nimport { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';\nimport { Award, ShieldAlert, ShieldCheck, ShieldQuestion } from 'lucide-react';\nimport { Button, GlassCard, Input, Label, Skeleton } from '@burdenoff/fe-libs/ui';\nimport {\n certificateVerifyPath,\n fetchCertificateVerification,\n isPlausibleSerial,\n normalizeSerial,\n type CertificateVerification,\n} from '../../lib/certificateVerify';\nimport { formatDate, formatDateTime, humanizeEnum } from '../../utils/format';\n\n/**\n * Public, shareable certificate verification page (BOFF-10576).\n *\n * Exported standalone from the package index so the host shell can mount it\n * outside its auth guard on `CERTIFICATE_VERIFY_ROUTE_PATH`\n * (`/public/verify/:serial`), plus the bare `/public/verify` form. It never\n * touches `TechnoSpamProvider` (there is no workspace or actor here) and\n * calls the public `tecVerifyCertificate` field with an anonymous request.\n * Nothing on this page identifies the learner: the service never returns\n * identity, and the page shows only what the certificate itself claims.\n */\n\nfunction StatusPanel({ verification }: { verification: CertificateVerification }) {\n if (verification.status === 'VALID') {\n return (\n <div\n className=\"flex items-start gap-3 rounded-xl border border-status-success-border bg-status-success-bg-subtle p-4 text-status-success-text\"\n data-testid=\"tec-verify-status\"\n data-status=\"VALID\"\n >\n <ShieldCheck size={22} className=\"mt-0.5 shrink-0\" />\n <div>\n <div className=\"text-base font-semibold\">Valid certificate</div>\n <p className=\"text-sm\">\n This serial belongs to a certificate issued by {verification.issuer} that has not been\n revoked.\n </p>\n </div>\n </div>\n );\n }\n if (verification.status === 'REVOKED') {\n return (\n <div\n className=\"flex items-start gap-3 rounded-xl border border-status-error-border bg-status-error-bg-subtle p-4 text-status-error-text\"\n data-testid=\"tec-verify-status\"\n data-status=\"REVOKED\"\n >\n <ShieldAlert size={22} className=\"mt-0.5 shrink-0\" />\n <div>\n <div className=\"text-base font-semibold\">Revoked certificate</div>\n <p className=\"text-sm\">\n This certificate was issued by {verification.issuer} but has since been revoked\n {verification.revokedAt ? ` (on ${formatDate(verification.revokedAt)})` : ''}. It should\n not be relied on.\n </p>\n </div>\n </div>\n );\n }\n return (\n <div\n className=\"flex items-start gap-3 rounded-xl border border-status-warning-border bg-status-warning-bg-subtle p-4 text-status-warning-text\"\n data-testid=\"tec-verify-status\"\n data-status=\"NOT_FOUND\"\n >\n <ShieldQuestion size={22} className=\"mt-0.5 shrink-0\" />\n <div>\n <div className=\"text-base font-semibold\">No certificate found</div>\n <p className=\"text-sm\">\n No {verification.issuer} certificate carries this serial. Check that it was copied exactly\n as printed on the certificate.\n </p>\n </div>\n </div>\n );\n}\n\nfunction Detail({ label, value, testId }: { label: string; value: string; testId: string }) {\n return (\n <div className=\"min-w-0\">\n <dt className=\"text-[11px] font-medium uppercase tracking-wide text-text-muted\">{label}</dt>\n <dd className=\"truncate text-sm text-text-primary\" data-testid={testId}>\n {value}\n </dd>\n </div>\n );\n}\n\nfunction VerificationResult({ verification }: { verification: CertificateVerification }) {\n const found = verification.status !== 'NOT_FOUND';\n return (\n <div className=\"space-y-4\" data-testid=\"tec-verify-result\">\n <StatusPanel verification={verification} />\n <dl className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <Detail label=\"Serial number\" value={verification.serial} testId=\"tec-verify-serial\" />\n {found ? (\n <>\n <Detail\n label=\"Course\"\n value={verification.courseTitle ?? '—'}\n testId=\"tec-verify-course\"\n />\n <Detail\n label=\"Level\"\n value={verification.courseLevel ? humanizeEnum(verification.courseLevel) : '—'}\n testId=\"tec-verify-level\"\n />\n <Detail\n label=\"Final score\"\n value={verification.score === null ? '—' : `${verification.score} / 100`}\n testId=\"tec-verify-score\"\n />\n <Detail\n label=\"Issued on\"\n value={verification.issuedAt ? formatDate(verification.issuedAt) : '—'}\n testId=\"tec-verify-issued\"\n />\n </>\n ) : null}\n <Detail\n label=\"Checked at\"\n value={formatDateTime(verification.verifiedAt)}\n testId=\"tec-verify-checked\"\n />\n </dl>\n <p className=\"text-xs text-text-muted\">\n For privacy, verification confirms the course, score, dates and status only. It never shows\n the learner&apos;s name or contact details; compare them against the certificate the\n candidate shared with you.\n </p>\n </div>\n );\n}\n\nexport function CertificateVerifyPage({ apiGatewayUrl }: { apiGatewayUrl?: string } = {}) {\n const { serial: routeSerial } = useParams<{ serial?: string }>();\n const serial = normalizeSerial(routeSerial);\n // The page is mounted outside TechnoSpamProvider (no workspace, no actor),\n // so it owns a small page-scoped query client for its one public lookup.\n const [queryClient] = useState(\n () =>\n new QueryClient({\n defaultOptions: { queries: { retry: false, refetchOnWindowFocus: false } },\n })\n );\n return (\n <QueryClientProvider client={queryClient}>\n {/* Keyed by serial so a new search starts from a fresh draft. */}\n <CertificateVerifyView key={serial} serial={serial} apiGatewayUrl={apiGatewayUrl} />\n </QueryClientProvider>\n );\n}\n\nfunction CertificateVerifyView({\n serial,\n apiGatewayUrl,\n}: {\n serial: string;\n apiGatewayUrl?: string;\n}) {\n const navigate = useNavigate();\n const [draft, setDraft] = useState(serial);\n\n const query = useQuery({\n queryKey: ['tecVerifyCertificate', apiGatewayUrl ?? '', serial],\n queryFn: () => fetchCertificateVerification(apiGatewayUrl, serial),\n enabled: serial.length > 0,\n // A verification is a point-in-time answer: always ask the service again\n // rather than showing a cached VALID for a certificate revoked since.\n staleTime: 0,\n gcTime: 0,\n });\n const outcome = query.data;\n const loading = serial.length > 0 && (query.isPending || query.isFetching);\n const failed = query.isError || outcome?.kind === 'error';\n\n const draftSerial = normalizeSerial(draft);\n const draftValid = isPlausibleSerial(draftSerial);\n\n const onSubmit = (event: FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n if (!draftValid) return;\n if (draftSerial === serial) {\n // Same URL: navigating would not re-run the lookup, so refetch.\n void query.refetch();\n return;\n }\n navigate(certificateVerifyPath(draftSerial));\n };\n\n const retry = () => {\n void query.refetch();\n };\n\n return (\n <main className=\"mx-auto flex w-full max-w-2xl flex-col gap-6 px-4 py-10 sm:py-16\">\n <header className=\"space-y-2 text-center\">\n <div className=\"inline-flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.2em] text-text-muted\">\n <Award size={14} className=\"text-status-warning-text\" /> TechnoSpam\n </div>\n <h1 className=\"text-2xl font-bold text-text-primary sm:text-3xl\">Verify a certificate</h1>\n <p className=\"text-sm text-text-secondary\">\n Confirm that a TechnoSpam certificate is genuine and still valid. No account is needed.\n </p>\n </header>\n\n <GlassCard className=\"space-y-6 p-5 sm:p-6\">\n <form className=\"flex flex-col gap-2\" onSubmit={onSubmit} role=\"search\">\n <Label htmlFor=\"tec-verify-input\">Certificate serial number</Label>\n <div className=\"flex flex-col gap-2 sm:flex-row\">\n <Input\n id=\"tec-verify-input\"\n data-testid=\"tec-verify-input\"\n value={draft}\n onChange={(event) => setDraft(event.target.value)}\n placeholder=\"TEC-XXXXXXXX-XXXXXXXX\"\n autoComplete=\"off\"\n spellCheck={false}\n className=\"font-mono\"\n />\n <Button type=\"submit\" disabled={!draftValid} data-testid=\"tec-verify-submit\">\n Verify\n </Button>\n </div>\n {draft.trim() && !draftValid ? (\n <p className=\"text-xs text-status-error-text\">\n A serial uses only letters, digits, dots, underscores and hyphens.\n </p>\n ) : null}\n </form>\n\n {loading ? (\n <div className=\"space-y-3\" aria-busy=\"true\" aria-live=\"polite\">\n <Skeleton className=\"h-16 w-full\" />\n <Skeleton className=\"h-24 w-full\" />\n </div>\n ) : null}\n {!loading && outcome?.kind === 'result' ? (\n <VerificationResult verification={outcome.verification} />\n ) : null}\n {!loading && outcome?.kind === 'invalid' ? (\n <p className=\"text-sm text-status-error-text\" data-testid=\"tec-verify-invalid\">\n That does not look like a certificate serial. Check it against the certificate.\n </p>\n ) : null}\n {!loading && failed ? (\n <div className=\"flex flex-col items-start gap-2\" data-testid=\"tec-verify-error\">\n <p className=\"text-sm text-status-error-text\">\n The verification service could not be reached. Please try again.\n </p>\n <Button size=\"sm\" variant=\"outline\" onClick={retry}>\n Try again\n </Button>\n </div>\n ) : null}\n </GlassCard>\n </main>\n );\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-technospam",
3
- "version": "2026.1101.5",
3
+ "version": "2026.1102.2",
4
4
  "description": "Burdenoff TechnoSpam Microfrontend",
5
5
  "type": "module",
6
6
  "files": [
@@ -52,17 +52,27 @@
52
52
  "dependencies": {
53
53
  "@apollo/client": "^4.0.11",
54
54
  "@burdenoff/fe-libs": "2026.801.1",
55
+ "@codemirror/autocomplete": "6.20.3",
56
+ "@codemirror/commands": "6.11.1",
57
+ "@codemirror/lang-go": "6.0.1",
58
+ "@codemirror/lang-javascript": "6.2.5",
59
+ "@codemirror/lang-python": "6.2.1",
60
+ "@codemirror/language": "6.13.1",
61
+ "@codemirror/legacy-modes": "6.5.5",
62
+ "@codemirror/state": "6.7.6",
63
+ "@codemirror/view": "6.43.14",
64
+ "@lezer/highlight": "1.2.5",
55
65
  "@tanstack/react-query": "^5.90.16",
56
66
  "clsx": "^2.1.1",
67
+ "framer-motion": "^12.0.0",
57
68
  "graphql": "^16.10.0",
58
69
  "lucide-react": "^0.468.0",
59
70
  "react": "^19.0.0",
60
71
  "react-dom": "^19.0.0",
61
72
  "react-router": "^8.3.0",
62
- "tailwind-merge": "^3.4.0",
63
- "zustand": "^5.0.3",
64
73
  "recharts": "^2.15.0",
65
- "framer-motion": "^12.0.0"
74
+ "tailwind-merge": "^3.4.0",
75
+ "zustand": "^5.0.3"
66
76
  },
67
77
  "devDependencies": {
68
78
  "@axe-core/playwright": "4.11.0",