fix(dashboard): save validation feedback, public site light mode, global verify modal (#716)

* fix(dashboard): surface save errors, force public site light mode, global verify modal

- Show snackbar with first validation error when course update fails client-side validation
- Force light mode on public org-site pages; lesson player and dashboard keep theme selector
- Mount VerifyEmailModal in root layout for all authenticated routes; keep trigger=app email guard

Co-authored-by: Rotimi Best <[email protected]>

* fix: instructor editor sync, auto-enroll after auth, email verification API

- Initialize instructor form from org fallbacks and batch metadata updates
- Auto-complete course enrollment after login/signup return from enroll page
- Block new org auto-enroll and course enroll until email is verified

Co-authored-by: Rotimi Best <[email protected]>

* fix: address PR review on instructor setter, color scheme, auto-enroll

- Use existing setter with batched metadata.instructor updates in InstructorForm
- Allowlist public org-site paths so /lms and /courses/:id keep theme selector
- Auto-enroll logged-in users via Better Auth session; remove sessionStorage flow
- Retry enrollment after email verification without duplicate Join Course clicks

Co-authored-by: Rotimi Best <[email protected]>

* fix: allow enrollment without verification; gate dashboard only

- Remove API email verification checks from enroll and auto-enroll flows
- Move VerifyEmailModal to (app) layout so public org-site pages stay usable
- Unverified users can enroll but hit the verification modal in LMS/dashboard

Co-authored-by: Rotimi Best <[email protected]>

* fix: address PR feedback on verify modal, routes, instructor form

- Restore currentOrg.id guard on VerifyEmailModal open condition
- InstructorForm imports currentOrg directly instead of prop drilling
- Move public org-site light-mode route patterns to routes constants
- Reset enrollment attempt key on enroll failure to allow retry

Co-authored-by: Rotimi Best <[email protected]>

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
This commit is contained in:
Rotimi Best
2026-07-10 01:55:13 +01:00
committed by GitHub
co-authored by Rotimi Best Cursor Agent
parent 35f5216839
commit be0085f090
10 changed files with 196 additions and 68 deletions
@@ -11,10 +11,18 @@
import { snackbar } from '$features/ui/snackbar/store';
import { capturePosthogEvent } from '$lib/utils/services/posthog';
import { resolve } from '$app/paths';
import { authClient } from '$lib/utils/services/auth/client';
let { data } = $props();
let loading = $state(false);
let enrollmentInFlight = $state(false);
let enrollmentAttemptKey = $state('');
const session = authClient.useSession();
const sessionReady = $derived(!$session.isPending && !$session.isRefetching);
const sessionUser = $derived($session.data?.user ?? null);
const isLoggedIn = $derived(sessionReady && Boolean(sessionUser));
const inviteStatus = $derived(data.invite?.status ?? 'INVALID');
const hasActiveInvite = $derived(Boolean(data.invite) && inviteStatus === 'ACTIVE');
@@ -51,12 +59,59 @@
return '';
}
async function completeEnrollment() {
if (!data.course?.id || enrollmentInFlight) {
return;
}
enrollmentInFlight = true;
loading = true;
let navigatingAway = false;
try {
const body = data.token ? { inviteToken: data.token } : {};
const result = await courseApi.enroll(data.course.id, body);
if (!result?.data) {
return;
}
const studentId = $profile.id || sessionUser?.id || '';
const studentEmail = $profile.email || sessionUser?.email || '';
capturePosthogEvent('student_joined_course', {
course_name: data.course?.title,
student_id: studentId,
student_email: studentEmail,
already_joined: result.data.alreadyJoined
});
navigatingAway = true;
window.location.href = result.data.redirectTo || '/lms';
} finally {
enrollmentInFlight = false;
if (!navigatingAway) {
loading = false;
enrollmentAttemptKey = '';
}
}
}
async function handleSubmit() {
if (!canJoinCourse) {
snackbar.error(getBlockedMessage());
return;
}
console.log('profile', $profile);
if (!sessionReady) {
return;
}
if (isLoggedIn) {
await completeEnrollment();
return;
}
loading = true;
@@ -64,41 +119,28 @@
const redirectSearch = data.token ? `?invite_token=${encodeURIComponent(data.token)}` : '';
const redirectUrl = `${redirectPath}${redirectSearch}`;
if (!$profile.id || !$profile.email) {
const inviteEmail = data.inviteEmail ?? '';
const target = data.inviteEmailExists ? '/login' : '/signup';
const params = new URLSearchParams({ redirect: redirectUrl });
if (inviteEmail) params.set('email', inviteEmail);
const inviteEmail = data.inviteEmail ?? '';
const target = data.inviteEmailExists ? '/login' : '/signup';
const params = new URLSearchParams({ redirect: redirectUrl });
if (inviteEmail) params.set('email', inviteEmail);
goto(resolve(`${target}?${params.toString()}`, {}));
loading = false;
goto(resolve(`${target}?${params.toString()}`, {}));
loading = false;
}
$effect(() => {
if (!sessionReady || !isLoggedIn || !canJoinCourse || enrollmentInFlight || loading) {
return;
}
let navigatingAway = false;
try {
const body = data.token ? { inviteToken: data.token } : {};
const result = await courseApi.enroll(data.course!.id, body);
if (!result?.data) {
return;
}
capturePosthogEvent('student_joined_course', {
course_name: data.course?.title,
student_id: $profile.id,
student_email: $profile.email,
already_joined: result.data.alreadyJoined
});
navigatingAway = true;
// need to force page reload to avoid cache issues
window.location.href = result.data.redirectTo || '/lms';
} finally {
if (!navigatingAway) loading = false;
const attemptKey = `${sessionUser?.id ?? ''}:${data.course?.id ?? ''}`;
if (enrollmentAttemptKey === attemptKey) {
return;
}
}
enrollmentAttemptKey = attemptKey;
void completeEnrollment();
});
$effect(() => {
if (!data.currentOrg) return;
@@ -107,12 +149,12 @@
const themeString = typeof rawTheme === 'string' ? rawTheme : '';
setTheme(themeString);
currentOrg.update((o) => ({
...o,
id: data.currentOrg?.id ?? o.id,
name: data.currentOrg?.name ?? o.name,
siteName: data.currentOrg?.siteName ?? o.siteName,
theme: themeString || o.theme
currentOrg.update((organization) => ({
...organization,
id: data.currentOrg?.id ?? organization.id,
name: data.currentOrg?.name ?? organization.name,
siteName: data.currentOrg?.siteName ?? organization.siteName,
theme: themeString || organization.theme
}));
});
</script>
@@ -122,7 +164,13 @@
<meta name="robots" content="noindex, nofollow" />
</svelte:head>
<AuthUI isLogin={false} {handleSubmit} isLoading={loading || !$profile.id} showOnlyContent={true} showLogo={true}>
<AuthUI
isLogin={false}
{handleSubmit}
isLoading={loading || !sessionReady || (isLoggedIn && canJoinCourse)}
showOnlyContent={true}
showLogo={true}
>
<div class="mt-0 w-full">
<h3 class="mt-0 mb-4 text-center text-lg font-medium dark:text-white">{data.course?.title}</h3>
<p class="text-center text-sm font-light dark:text-white">{data.course?.description}</p>
@@ -141,9 +189,11 @@
{/if}
</div>
<div class="my-4 flex w-full items-center justify-center">
<Button type="submit" disabled={!canJoinCourse || loading} {loading}>
{$t('course.navItem.landing_page.enroll_page.join_course')}
</Button>
</div>
{#if !isLoggedIn}
<div class="my-4 flex w-full items-center justify-center">
<Button type="submit" disabled={!canJoinCourse || loading || !sessionReady} {loading}>
{$t('course.navItem.landing_page.enroll_page.join_course')}
</Button>
</div>
{/if}
</AuthUI>