@fayz-ai/portal 0.2.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Faya Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/auth.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ import type { AuthAdapter } from '@fayz-ai/core';
2
+ /** Deterministic learner id from an email, so the same login always maps to the
3
+ * same enrollments/progress across reloads. */
4
+ export declare function customerIdForEmail(email: string): string;
5
+ export declare function resolveAuthAdapter(configured: unknown, supabase?: {
6
+ url?: string;
7
+ anonKey?: string;
8
+ }): AuthAdapter;
9
+ export declare function initMemberAuth(adapter: AuthAdapter, opts?: {
10
+ autoEnroll?: boolean;
11
+ }): void;
12
+ export declare function establishMemberSession(email: string, opts?: {
13
+ password?: string;
14
+ name?: string;
15
+ }): Promise<{
16
+ customerId: string;
17
+ }>;
18
+ export declare function signUpMember(email: string, password: string, name: string): Promise<{
19
+ customerId: string;
20
+ }>;
21
+ export declare function signOutMember(): Promise<void>;
22
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAY,MAAM,eAAe,CAAA;AAgB1D;gDACgD;AAChD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAExD;AAKD,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,OAAO,EACnB,QAAQ,CAAC,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAC5C,WAAW,CAMb;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,WAAW,EAAE,IAAI,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAY1F;AAsBD,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAC1C,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,CAQjC;AAED,wBAAsB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,CAMjH;AAED,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAMnD"}
@@ -0,0 +1,3 @@
1
+ import React from 'react';
2
+ export declare function MemberHeader(): React.JSX.Element;
3
+ //# sourceMappingURL=MemberHeader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MemberHeader.d.ts","sourceRoot":"","sources":["../../src/components/MemberHeader.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAMzB,wBAAgB,YAAY,sBAkD3B"}
@@ -0,0 +1,25 @@
1
+ import React from 'react';
2
+ export interface MemberConfig {
3
+ name: string;
4
+ /** Logo image URL shown in the header (falls back to the name). */
5
+ logoUrl?: string;
6
+ locale?: string;
7
+ /** Accent color (CSS color) for the player progress + active states. */
8
+ accent?: string;
9
+ /** Auth adapter: 'mock' (default) | 'supabase' | a bring-your-own adapter. */
10
+ auth?: {
11
+ adapter?: 'mock' | 'supabase' | unknown;
12
+ };
13
+ supabaseUrl?: string;
14
+ supabaseAnonKey?: string;
15
+ /** Grant the logged-in learner every published course (demo behaviour). When
16
+ * false, the learner only sees the courses they were explicitly enrolled in. */
17
+ autoEnroll?: boolean;
18
+ }
19
+ export interface ResolvedMemberConfig extends MemberConfig {
20
+ locale: string;
21
+ }
22
+ export declare function resolveConfig(config: MemberConfig): ResolvedMemberConfig;
23
+ export declare const MemberConfigProvider: React.Provider<ResolvedMemberConfig | null>;
24
+ export declare function useMemberConfig(): ResolvedMemberConfig;
25
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAOzB,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,mEAAmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,8EAA8E;IAC9E,IAAI,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAAA;KAAE,CAAA;IAClD,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB;qFACiF;IACjF,UAAU,CAAC,EAAE,OAAO,CAAA;CACrB;AAED,MAAM,WAAW,oBAAqB,SAAQ,YAAY;IACxD,MAAM,EAAE,MAAM,CAAA;CACf;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,YAAY,GAAG,oBAAoB,CAExE;AAGD,eAAO,MAAM,oBAAoB,6CAA+B,CAAA;AAEhE,wBAAgB,eAAe,IAAI,oBAAoB,CAItD"}
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ import type { MemberConfig } from './config';
3
+ /** Side-effect runtime init shared by the factory and the manifest scaffold:
4
+ * wires learner auth (+ optional Supabase client). Idempotent. */
5
+ export declare function initMemberRuntime(config: MemberConfig): void;
6
+ /** Inner portal UI — reads everything from config context, so it is shared by
7
+ * createMemberApp and the manifest-driven MemberScaffold. */
8
+ export declare function MemberShell(): React.JSX.Element;
9
+ /** Declarative member-portal factory — sugar over the manifest path (equivalent
10
+ * to renderApp(defineMember(config))). */
11
+ export declare function createMemberApp(config: MemberConfig): React.ComponentType;
12
+ //# sourceMappingURL=createMemberApp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createMemberApp.d.ts","sourceRoot":"","sources":["../src/createMemberApp.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAsB5C;mEACmE;AACnE,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,CAQ5D;AAED;8DAC8D;AAC9D,wBAAgB,WAAW,sBAW1B;AAED;2CAC2C;AAC3C,wBAAgB,eAAe,CAAC,MAAM,EAAE,YAAY,GAAG,KAAK,CAAC,aAAa,CAYzE"}
package/dist/index.cjs ADDED
@@ -0,0 +1,542 @@
1
+ 'use strict';
2
+
3
+ var React6 = require('react');
4
+ var auth = require('@fayz-ai/auth');
5
+ var courses = require('@fayz-ai/courses');
6
+ var zustand = require('zustand');
7
+ var middleware = require('zustand/middleware');
8
+ var core = require('@fayz-ai/core');
9
+ var jsxRuntime = require('react/jsx-runtime');
10
+
11
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
12
+
13
+ var React6__default = /*#__PURE__*/_interopDefault(React6);
14
+
15
+ // src/config.ts
16
+ function resolveConfig(config) {
17
+ return { ...config, locale: config.locale ?? "pt-BR" };
18
+ }
19
+ var MemberConfigContext = React6__default.default.createContext(null);
20
+ var MemberConfigProvider = MemberConfigContext.Provider;
21
+ function useMemberConfig() {
22
+ const ctx = React6__default.default.useContext(MemberConfigContext);
23
+ if (!ctx) throw new Error("useMemberConfig must be used inside a MemberConfigProvider");
24
+ return ctx;
25
+ }
26
+ var useMemberSession = zustand.create()(
27
+ middleware.persist(
28
+ (set) => ({
29
+ customerId: null,
30
+ email: null,
31
+ name: null,
32
+ setSession: ({ customerId, email, name }) => set({ customerId, email, name: name ?? null }),
33
+ signOut: () => set({ customerId: null, email: null, name: null })
34
+ }),
35
+ { name: "fayz.portal.session.v1" }
36
+ )
37
+ );
38
+
39
+ // src/auth.ts
40
+ function slug(s) {
41
+ return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
42
+ }
43
+ function customerIdForEmail(email) {
44
+ return `cust-${slug(email.trim().toLowerCase())}`;
45
+ }
46
+ var _adapter = null;
47
+ var _autoEnroll = true;
48
+ function resolveAuthAdapter(configured, supabase) {
49
+ if (configured && configured !== "mock" && configured !== "supabase") return configured;
50
+ if (configured === "supabase" && supabase?.url && supabase?.anonKey) {
51
+ return auth.createSupabaseAuthAdapter({ supabaseUrl: supabase.url, supabaseAnonKey: supabase.anonKey });
52
+ }
53
+ return auth.createMockAuthAdapter();
54
+ }
55
+ function initMemberAuth(adapter2, opts) {
56
+ _adapter = adapter2;
57
+ _autoEnroll = opts?.autoEnroll ?? true;
58
+ adapter2.getSession().then((existing) => {
59
+ if (existing) void link(existing.user);
60
+ }).catch(() => {
61
+ });
62
+ adapter2.onAuthStateChange((user) => {
63
+ if (!user) useMemberSession.getState().signOut();
64
+ });
65
+ }
66
+ function getAdapter() {
67
+ if (!_adapter) _adapter = auth.createMockAuthAdapter();
68
+ return _adapter;
69
+ }
70
+ async function grantAllPublished(customerId) {
71
+ const provider = courses.getCoursesProvider();
72
+ const courses$1 = await provider.listCourses({ status: "published" });
73
+ for (const c of courses$1) await provider.enroll(c.id, customerId);
74
+ }
75
+ async function link(user, fullName) {
76
+ const email = (user.email ?? "").trim().toLowerCase();
77
+ const customerId = customerIdForEmail(email);
78
+ if (_autoEnroll) await grantAllPublished(customerId);
79
+ useMemberSession.getState().setSession({ customerId, email, name: fullName ?? user.fullName ?? null });
80
+ return { customerId };
81
+ }
82
+ async function establishMemberSession(email, opts) {
83
+ const adapter2 = getAdapter();
84
+ const normalized = email.trim().toLowerCase();
85
+ const session = opts?.password ? await adapter2.signIn(normalized, opts.password) : await adapter2.signUp(normalized, `member-${normalized}`, opts?.name ?? "");
86
+ const user = session.user.email ? session.user : { ...session.user, email: normalized };
87
+ return link(user, opts?.name);
88
+ }
89
+ async function signUpMember(email, password, name) {
90
+ const adapter2 = getAdapter();
91
+ const normalized = email.trim().toLowerCase();
92
+ const session = await adapter2.signUp(normalized, password, name);
93
+ const user = session.user.email ? session.user : { ...session.user, email: normalized };
94
+ return link(user, name);
95
+ }
96
+ async function signOutMember() {
97
+ try {
98
+ await getAdapter().signOut();
99
+ } finally {
100
+ useMemberSession.getState().signOut();
101
+ }
102
+ }
103
+ var adapter = core.hashRouterAdapter();
104
+ function navigateTo(to) {
105
+ adapter.navigate(to);
106
+ }
107
+ function useHashPath() {
108
+ const [path, setPath] = React6.useState(() => adapter.getCurrentPath() || "/");
109
+ React6.useEffect(() => {
110
+ return adapter.onPathChange((p) => {
111
+ setPath(p || "/");
112
+ if (typeof window !== "undefined") window.scrollTo(0, 0);
113
+ });
114
+ }, []);
115
+ return path;
116
+ }
117
+ function matchPath(pattern, path) {
118
+ const cleanPath = (path.split("?")[0] ?? "").replace(/\/+$/, "") || "/";
119
+ const patternParts = pattern.split("/").filter(Boolean);
120
+ const pathParts = cleanPath.split("/").filter(Boolean);
121
+ if (patternParts.length !== pathParts.length) return null;
122
+ const params = {};
123
+ for (let i = 0; i < patternParts.length; i++) {
124
+ const pat = patternParts[i];
125
+ const part = pathParts[i];
126
+ if (pat.startsWith(":")) params[pat.slice(1)] = decodeURIComponent(part);
127
+ else if (pat !== part) return null;
128
+ }
129
+ return params;
130
+ }
131
+ function Link({ to, children, ...rest }) {
132
+ return React6__default.default.createElement("a", { href: `#${to}`, ...rest }, children);
133
+ }
134
+ function MemberHeader() {
135
+ const config = useMemberConfig();
136
+ const session = useMemberSession();
137
+ const [menuOpen, setMenuOpen] = React6__default.default.useState(false);
138
+ return /* @__PURE__ */ jsxRuntime.jsx("header", { className: "sticky top-0 z-30 border-b border-border bg-card/95 backdrop-blur", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto flex h-14 max-w-6xl items-center justify-between px-4 sm:px-6", children: [
139
+ /* @__PURE__ */ jsxRuntime.jsx(Link, { to: "/", className: "flex items-center gap-2 font-bold text-foreground", "data-testid": "member-logo", children: config.logoUrl ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: config.logoUrl, alt: config.name, className: "h-7" }) : /* @__PURE__ */ jsxRuntime.jsx("span", { children: config.name }) }),
140
+ session.customerId ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
141
+ /* @__PURE__ */ jsxRuntime.jsxs(
142
+ "button",
143
+ {
144
+ "data-testid": "member-usermenu",
145
+ onClick: () => setMenuOpen((v) => !v),
146
+ className: "flex items-center gap-2 rounded-full border border-border bg-card px-3 py-1.5 text-sm font-medium text-foreground hover:bg-muted",
147
+ children: [
148
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex h-6 w-6 items-center justify-center rounded-full bg-primary text-xs text-primary-foreground", children: (session.email ?? "?").charAt(0).toUpperCase() }),
149
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden sm:inline", children: session.name || session.email })
150
+ ]
151
+ }
152
+ ),
153
+ menuOpen && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute right-0 mt-2 w-44 overflow-hidden rounded-lg border border-border bg-card shadow-lg", children: [
154
+ /* @__PURE__ */ jsxRuntime.jsx(
155
+ "button",
156
+ {
157
+ onClick: () => {
158
+ setMenuOpen(false);
159
+ navigateTo("/account");
160
+ },
161
+ className: "block w-full px-4 py-2.5 text-left text-sm text-foreground hover:bg-muted",
162
+ children: "Minha conta"
163
+ }
164
+ ),
165
+ /* @__PURE__ */ jsxRuntime.jsx(
166
+ "button",
167
+ {
168
+ "data-testid": "member-header-signout",
169
+ onClick: () => {
170
+ setMenuOpen(false);
171
+ void signOutMember();
172
+ navigateTo("/");
173
+ },
174
+ className: "block w-full px-4 py-2.5 text-left text-sm text-foreground hover:bg-muted",
175
+ children: "Sair"
176
+ }
177
+ )
178
+ ] })
179
+ ] }) : null
180
+ ] }) });
181
+ }
182
+ function AuthForm() {
183
+ const config = useMemberConfig();
184
+ const [email, setEmail] = React6.useState("");
185
+ const [password, setPassword] = React6.useState("");
186
+ const [busy, setBusy] = React6.useState(false);
187
+ const [error, setError] = React6.useState(null);
188
+ async function submit(e) {
189
+ e.preventDefault();
190
+ setError(null);
191
+ if (!email.trim() || !password) return;
192
+ setBusy(true);
193
+ try {
194
+ await establishMemberSession(email, { password });
195
+ } catch (err) {
196
+ setError(err instanceof Error ? err.message : "N\xE3o foi poss\xEDvel entrar.");
197
+ } finally {
198
+ setBusy(false);
199
+ }
200
+ }
201
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto max-w-sm rounded-xl border border-border bg-card p-7 shadow-sm", children: [
202
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-xl font-semibold text-foreground", children: config.name }),
203
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: "Entre para acessar seus cursos." }),
204
+ /* @__PURE__ */ jsxRuntime.jsxs("form", { className: "mt-5 space-y-3", onSubmit: submit, children: [
205
+ /* @__PURE__ */ jsxRuntime.jsx(
206
+ "input",
207
+ {
208
+ "data-testid": "member-email",
209
+ type: "email",
210
+ required: true,
211
+ placeholder: "voce@exemplo.com",
212
+ value: email,
213
+ onChange: (e) => setEmail(e.target.value),
214
+ className: "w-full rounded-md border border-input bg-background px-3 py-2.5 text-sm"
215
+ }
216
+ ),
217
+ /* @__PURE__ */ jsxRuntime.jsx(
218
+ "input",
219
+ {
220
+ "data-testid": "member-password",
221
+ type: "password",
222
+ required: true,
223
+ placeholder: "Senha",
224
+ value: password,
225
+ onChange: (e) => setPassword(e.target.value),
226
+ className: "w-full rounded-md border border-input bg-background px-3 py-2.5 text-sm"
227
+ }
228
+ ),
229
+ error && /* @__PURE__ */ jsxRuntime.jsx("p", { "data-testid": "member-auth-error", className: "text-sm text-destructive", children: error }),
230
+ /* @__PURE__ */ jsxRuntime.jsx(
231
+ "button",
232
+ {
233
+ type: "submit",
234
+ "data-testid": "member-signin",
235
+ disabled: busy,
236
+ className: "w-full rounded-md bg-primary py-2.5 font-semibold text-primary-foreground disabled:opacity-60",
237
+ children: busy ? "Entrando\u2026" : "Entrar"
238
+ }
239
+ )
240
+ ] })
241
+ ] });
242
+ }
243
+ function MyCoursesPage() {
244
+ const session = useMemberSession();
245
+ const { courses: courses$1, loading } = courses.useMyCourses(session.customerId);
246
+ if (!session.customerId) {
247
+ return /* @__PURE__ */ jsxRuntime.jsx("main", { className: "mx-auto max-w-3xl px-4 py-12 sm:px-6", children: /* @__PURE__ */ jsxRuntime.jsx(AuthForm, {}) });
248
+ }
249
+ return /* @__PURE__ */ jsxRuntime.jsxs("main", { className: "mx-auto max-w-5xl px-4 py-8 sm:px-6", children: [
250
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "mb-1 text-2xl font-bold tracking-tight text-foreground", children: "Meus cursos" }),
251
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mb-6 text-sm text-muted-foreground", children: session.email }),
252
+ loading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "py-16 text-center text-muted-foreground", children: "Carregando\u2026" }) : courses$1.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { "data-testid": "member-empty", className: "rounded-xl border border-border bg-card py-16 text-center text-muted-foreground", children: "Voc\xEA ainda n\xE3o tem cursos." }) : /* @__PURE__ */ jsxRuntime.jsx("div", { "data-testid": "member-courses", className: "grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3", children: courses$1.map(({ course, progressPercent, completedLessons, totalLessons }) => /* @__PURE__ */ jsxRuntime.jsxs(
253
+ "button",
254
+ {
255
+ "data-testid": "member-course-card",
256
+ "data-slug": course.slug,
257
+ onClick: () => navigateTo(`/course/${course.slug}`),
258
+ className: "group overflow-hidden rounded-xl border border-border bg-card text-left transition hover:border-primary hover:shadow-md",
259
+ children: [
260
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "aspect-video w-full overflow-hidden bg-muted", children: course.thumbnailUrl && /* @__PURE__ */ jsxRuntime.jsx("img", { src: course.thumbnailUrl, alt: "", className: "h-full w-full object-cover" }) }),
261
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2 p-4", children: [
262
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "font-semibold text-foreground line-clamp-2", children: course.title }),
263
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-1.5 w-full overflow-hidden rounded-full bg-muted", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-full rounded-full bg-primary transition-all", style: { width: `${progressPercent}%` } }) }),
264
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-muted-foreground", children: [
265
+ completedLessons,
266
+ "/",
267
+ totalLessons,
268
+ " aulas \xB7 ",
269
+ progressPercent,
270
+ "%"
271
+ ] })
272
+ ] })
273
+ ]
274
+ },
275
+ course.id
276
+ )) })
277
+ ] });
278
+ }
279
+ function CoursePlayerPage({ slug: slug3, lessonId }) {
280
+ const session = useMemberSession();
281
+ const { course, modules, lessons, loading } = courses.useCourse(slug3);
282
+ const [enrollmentId, setEnrollmentId] = React6__default.default.useState(null);
283
+ React6__default.default.useEffect(() => {
284
+ let active = true;
285
+ if (!course || !session.customerId) return;
286
+ (async () => {
287
+ const provider = courses.getCoursesProvider();
288
+ const enrollments = await provider.listEnrollments(session.customerId);
289
+ let enrollment = enrollments.find((e) => e.courseId === course.id);
290
+ if (!enrollment) enrollment = await provider.enroll(course.id, session.customerId);
291
+ if (active) setEnrollmentId(enrollment.id);
292
+ })();
293
+ return () => {
294
+ active = false;
295
+ };
296
+ }, [course, session.customerId]);
297
+ const { isCompleted, markLessonComplete } = courses.useCourseProgress(enrollmentId);
298
+ if (loading || !course) {
299
+ return /* @__PURE__ */ jsxRuntime.jsx("main", { className: "px-4 py-12 text-center text-muted-foreground", children: "Carregando\u2026" });
300
+ }
301
+ const orderedLessons = modules.flatMap((m) => lessons.filter((l) => l.moduleId === m.id));
302
+ const activeLesson = lessonId && orderedLessons.find((l) => l.id === lessonId) || orderedLessons[0];
303
+ if (!activeLesson) {
304
+ return /* @__PURE__ */ jsxRuntime.jsx("main", { className: "px-4 py-12 text-center text-muted-foreground", children: "Curso sem aulas ainda." });
305
+ }
306
+ const idx = orderedLessons.findIndex((l) => l.id === activeLesson.id);
307
+ const nextLesson = idx >= 0 && idx < orderedLessons.length - 1 ? orderedLessons[idx + 1] : null;
308
+ function open(lid) {
309
+ navigateTo(`/course/${course.slug}/lesson/${lid}`);
310
+ }
311
+ async function completeAndAdvance() {
312
+ await markLessonComplete(activeLesson.id, true);
313
+ if (nextLesson) open(nextLesson.id);
314
+ }
315
+ return /* @__PURE__ */ jsxRuntime.jsxs("main", { className: "mx-auto grid max-w-6xl grid-cols-1 gap-6 px-4 py-6 sm:px-6 lg:grid-cols-[1fr_320px]", children: [
316
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
317
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => navigateTo("/"), className: "mb-3 text-sm text-muted-foreground hover:text-foreground", children: "\u2190 Meus cursos" }),
318
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "aspect-video w-full overflow-hidden rounded-xl bg-black", children: /* @__PURE__ */ jsxRuntime.jsx(
319
+ "iframe",
320
+ {
321
+ "data-testid": "lesson-video",
322
+ src: activeLesson.videoUrl,
323
+ title: activeLesson.title,
324
+ className: "h-full w-full",
325
+ allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture",
326
+ allowFullScreen: true
327
+ },
328
+ activeLesson.id
329
+ ) }),
330
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-4 flex items-start justify-between gap-4", children: [
331
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
332
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: course.title }),
333
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { "data-testid": "lesson-title", className: "text-xl font-bold text-foreground", children: activeLesson.title })
334
+ ] }),
335
+ /* @__PURE__ */ jsxRuntime.jsx(
336
+ "button",
337
+ {
338
+ "data-testid": "mark-complete",
339
+ onClick: completeAndAdvance,
340
+ className: `shrink-0 rounded-md px-4 py-2 text-sm font-semibold ${isCompleted(activeLesson.id) ? "border border-border bg-card text-muted-foreground" : "bg-primary text-primary-foreground"}`,
341
+ children: isCompleted(activeLesson.id) ? "\u2713 Conclu\xEDda" : "Marcar como conclu\xEDda"
342
+ }
343
+ )
344
+ ] }),
345
+ activeLesson.description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-3 text-sm text-muted-foreground", children: activeLesson.description })
346
+ ] }),
347
+ /* @__PURE__ */ jsxRuntime.jsx("aside", { "data-testid": "lesson-sidebar", className: "rounded-xl border border-border bg-card p-3", children: modules.map((mod) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-3", children: [
348
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "px-2 py-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: mod.title }),
349
+ /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "space-y-0.5", children: lessons.filter((l) => l.moduleId === mod.id).map((lesson) => {
350
+ const done = isCompleted(lesson.id);
351
+ const current = lesson.id === activeLesson.id;
352
+ return /* @__PURE__ */ jsxRuntime.jsx("li", { children: /* @__PURE__ */ jsxRuntime.jsxs(
353
+ "button",
354
+ {
355
+ "data-testid": "sidebar-lesson",
356
+ "data-completed": done ? "true" : "false",
357
+ onClick: () => open(lesson.id),
358
+ className: `flex w-full items-center gap-2 rounded-md px-2 py-2 text-left text-sm ${current ? "bg-muted font-medium text-foreground" : "text-muted-foreground hover:bg-muted/60"}`,
359
+ children: [
360
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: `flex h-4 w-4 shrink-0 items-center justify-center rounded-full border text-[10px] ${done ? "border-primary bg-primary text-primary-foreground" : "border-border"}`, children: done ? "\u2713" : "" }),
361
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "line-clamp-2", children: lesson.title })
362
+ ]
363
+ }
364
+ ) }, lesson.id);
365
+ }) })
366
+ ] }, mod.id)) })
367
+ ] });
368
+ }
369
+ function AccountPage() {
370
+ const session = useMemberSession();
371
+ if (!session.customerId) {
372
+ navigateTo("/");
373
+ return null;
374
+ }
375
+ return /* @__PURE__ */ jsxRuntime.jsxs("main", { className: "mx-auto max-w-xl px-4 py-10 sm:px-6", children: [
376
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "mb-6 text-2xl font-bold text-foreground", children: "Minha conta" }),
377
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4 rounded-xl border border-border bg-card p-6", children: [
378
+ /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Nome", value: session.name || "\u2014" }),
379
+ /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "E-mail", value: session.email || "\u2014" }),
380
+ /* @__PURE__ */ jsxRuntime.jsx(
381
+ "button",
382
+ {
383
+ "data-testid": "account-signout",
384
+ onClick: () => {
385
+ void signOutMember();
386
+ navigateTo("/");
387
+ },
388
+ className: "mt-2 rounded-md border border-border bg-card px-4 py-2 text-sm font-medium text-foreground hover:bg-muted",
389
+ children: "Sair"
390
+ }
391
+ )
392
+ ] })
393
+ ] });
394
+ }
395
+ function Row({ label, value }) {
396
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-between border-b border-border pb-3 last:border-0 last:pb-0", children: [
397
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-muted-foreground", children: label }),
398
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium text-foreground", children: value })
399
+ ] });
400
+ }
401
+ function RouteSwitch() {
402
+ const path = useHashPath();
403
+ const lesson = matchPath("/course/:slug/lesson/:id", path);
404
+ if (lesson?.slug) return /* @__PURE__ */ jsxRuntime.jsx(CoursePlayerPage, { slug: lesson.slug, lessonId: lesson.id });
405
+ const course = matchPath("/course/:slug", path);
406
+ if (course?.slug) return /* @__PURE__ */ jsxRuntime.jsx(CoursePlayerPage, { slug: course.slug });
407
+ if (matchPath("/account", path)) return /* @__PURE__ */ jsxRuntime.jsx(AccountPage, {});
408
+ return /* @__PURE__ */ jsxRuntime.jsx(MyCoursesPage, {});
409
+ }
410
+ function initMemberRuntime(config) {
411
+ initMemberAuth(
412
+ resolveAuthAdapter(config.auth?.adapter, { url: config.supabaseUrl, anonKey: config.supabaseAnonKey }),
413
+ { autoEnroll: config.autoEnroll ?? true }
414
+ );
415
+ }
416
+ function MemberShell() {
417
+ const config = useMemberConfig();
418
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-h-screen bg-background text-foreground", "data-portal": config.name, children: [
419
+ config.accent && /* @__PURE__ */ jsxRuntime.jsx("style", { children: `:root{--primary:${config.accent};}` }),
420
+ /* @__PURE__ */ jsxRuntime.jsx(MemberHeader, {}),
421
+ /* @__PURE__ */ jsxRuntime.jsx(RouteSwitch, {})
422
+ ] });
423
+ }
424
+ function createMemberApp(config) {
425
+ const resolved = resolveConfig(config);
426
+ initMemberRuntime(config);
427
+ function MemberApp() {
428
+ return /* @__PURE__ */ jsxRuntime.jsx(MemberConfigProvider, { value: resolved, children: /* @__PURE__ */ jsxRuntime.jsx(MemberShell, {}) });
429
+ }
430
+ MemberApp.displayName = "MemberApp";
431
+ return MemberApp;
432
+ }
433
+ function slug2(name) {
434
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "app";
435
+ }
436
+ function defineMember(config) {
437
+ return core.defineApp({
438
+ id: slug2(config.name),
439
+ name: config.name,
440
+ backend: config.supabaseUrl ? { provider: "supabase", url: config.supabaseUrl } : { provider: "mock" },
441
+ locale: { default: config.locale ?? "pt-BR", supported: [config.locale ?? "pt-BR"] },
442
+ surfaces: {
443
+ member: {
444
+ scaffold: "member",
445
+ options: {
446
+ logoUrl: config.logoUrl,
447
+ accent: config.accent,
448
+ autoEnroll: config.autoEnroll ?? true
449
+ }
450
+ }
451
+ }
452
+ });
453
+ }
454
+ function manifestToMemberConfig(manifest, surfaceName) {
455
+ const o = manifest.surfaces[surfaceName]?.options ?? {};
456
+ return {
457
+ name: manifest.name,
458
+ locale: manifest.locale?.default,
459
+ logoUrl: o.logoUrl,
460
+ accent: o.accent,
461
+ autoEnroll: o.autoEnroll ?? true,
462
+ supabaseUrl: manifest.backend?.provider === "supabase" ? manifest.backend.url : void 0
463
+ };
464
+ }
465
+ function MemberScaffold({ manifest, surface }) {
466
+ const config = React6__default.default.useMemo(() => manifestToMemberConfig(manifest, surface), [manifest, surface]);
467
+ const resolved = React6__default.default.useMemo(() => resolveConfig(config), [config]);
468
+ const inited = React6__default.default.useRef(false);
469
+ if (!inited.current) {
470
+ initMemberRuntime(config);
471
+ inited.current = true;
472
+ }
473
+ return /* @__PURE__ */ jsxRuntime.jsx(MemberConfigProvider, { value: resolved, children: /* @__PURE__ */ jsxRuntime.jsx(MemberShell, {}) });
474
+ }
475
+ MemberScaffold.displayName = "MemberScaffold";
476
+ core.registerScaffold("member", MemberScaffold, { source: "sdk", label: "Member" });
477
+
478
+ Object.defineProperty(exports, "MockCoursesProvider", {
479
+ enumerable: true,
480
+ get: function () { return courses.MockCoursesProvider; }
481
+ });
482
+ Object.defineProperty(exports, "createMockCoursesProvider", {
483
+ enumerable: true,
484
+ get: function () { return courses.createMockCoursesProvider; }
485
+ });
486
+ Object.defineProperty(exports, "getCoursesProvider", {
487
+ enumerable: true,
488
+ get: function () { return courses.getCoursesProvider; }
489
+ });
490
+ Object.defineProperty(exports, "getCoursesProviderOptional", {
491
+ enumerable: true,
492
+ get: function () { return courses.getCoursesProviderOptional; }
493
+ });
494
+ Object.defineProperty(exports, "setCoursesProvider", {
495
+ enumerable: true,
496
+ get: function () { return courses.setCoursesProvider; }
497
+ });
498
+ Object.defineProperty(exports, "useCourse", {
499
+ enumerable: true,
500
+ get: function () { return courses.useCourse; }
501
+ });
502
+ Object.defineProperty(exports, "useCourseProgress", {
503
+ enumerable: true,
504
+ get: function () { return courses.useCourseProgress; }
505
+ });
506
+ Object.defineProperty(exports, "useMyCourses", {
507
+ enumerable: true,
508
+ get: function () { return courses.useMyCourses; }
509
+ });
510
+ Object.defineProperty(exports, "defineApp", {
511
+ enumerable: true,
512
+ get: function () { return core.defineApp; }
513
+ });
514
+ Object.defineProperty(exports, "renderApp", {
515
+ enumerable: true,
516
+ get: function () { return core.renderApp; }
517
+ });
518
+ exports.AccountPage = AccountPage;
519
+ exports.CoursePlayerPage = CoursePlayerPage;
520
+ exports.Link = Link;
521
+ exports.MemberConfigProvider = MemberConfigProvider;
522
+ exports.MemberHeader = MemberHeader;
523
+ exports.MemberScaffold = MemberScaffold;
524
+ exports.MemberShell = MemberShell;
525
+ exports.MyCoursesPage = MyCoursesPage;
526
+ exports.createMemberApp = createMemberApp;
527
+ exports.customerIdForEmail = customerIdForEmail;
528
+ exports.defineMember = defineMember;
529
+ exports.establishMemberSession = establishMemberSession;
530
+ exports.initMemberAuth = initMemberAuth;
531
+ exports.initMemberRuntime = initMemberRuntime;
532
+ exports.matchPath = matchPath;
533
+ exports.navigateTo = navigateTo;
534
+ exports.resolveAuthAdapter = resolveAuthAdapter;
535
+ exports.resolveConfig = resolveConfig;
536
+ exports.signOutMember = signOutMember;
537
+ exports.signUpMember = signUpMember;
538
+ exports.useHashPath = useHashPath;
539
+ exports.useMemberConfig = useMemberConfig;
540
+ exports.useMemberSession = useMemberSession;
541
+ //# sourceMappingURL=index.cjs.map
542
+ //# sourceMappingURL=index.cjs.map