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 <rotimi-best@users.noreply.github.com> * 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 <rotimi-best@users.noreply.github.com> * 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 <rotimi-best@users.noreply.github.com> * 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 <rotimi-best@users.noreply.github.com> * 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 <rotimi-best@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
This commit is contained in:
co-authored by
Rotimi Best
Cursor Agent
parent
35f5216839
commit
be0085f090
@@ -440,6 +440,10 @@ export class CourseApi extends BaseApiWithErrors {
|
|||||||
const result = ZCourseUpdate.safeParse(fields);
|
const result = ZCourseUpdate.safeParse(fields);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
this.errors = mapZodErrorsToTranslations(result.error, 'course');
|
this.errors = mapZodErrorsToTranslations(result.error, 'course');
|
||||||
|
const firstError = this.errors.general ?? Object.values(this.errors).find(Boolean);
|
||||||
|
if (firstError) {
|
||||||
|
snackbar.error(firstError);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+32
-20
@@ -7,19 +7,31 @@
|
|||||||
import { uploadImage } from '$lib/utils/services/upload';
|
import { uploadImage } from '$lib/utils/services/upload';
|
||||||
import type { Course } from '$features/course/utils/types';
|
import type { Course } from '$features/course/utils/types';
|
||||||
import { t } from '$lib/utils/functions/translations';
|
import { t } from '$lib/utils/functions/translations';
|
||||||
|
import { currentOrg } from '$lib/utils/store/org';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
course: Course;
|
course: Course;
|
||||||
setter: (value: any, key: string) => void;
|
setter: (value: unknown, key: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { course = $bindable(), setter }: Props = $props();
|
let { course = $bindable(), setter }: Props = $props();
|
||||||
|
|
||||||
let name = $state(get(course, 'metadata.instructor.name'));
|
function readInstructorField(path: string, fallback = '') {
|
||||||
let role = $state(get(course, 'metadata.instructor.role'));
|
const value = get(course, path);
|
||||||
let imgUrl = $state(get(course, 'metadata.instructor.imgUrl'));
|
if (typeof value === 'string' && value.trim()) {
|
||||||
let description = $state(get(course, 'metadata.instructor.description'));
|
return value.trim();
|
||||||
let courseNo = $state(get(course, 'metadata.instructor.coursesNo') ?? get(course, 'metadata.instructor.courseNo'));
|
}
|
||||||
|
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = $state(readInstructorField('metadata.instructor.name', $currentOrg.name ?? ''));
|
||||||
|
let role = $state(readInstructorField('metadata.instructor.role'));
|
||||||
|
let imgUrl = $state(readInstructorField('metadata.instructor.imgUrl', $currentOrg.avatarUrl ?? ''));
|
||||||
|
let description = $state(readInstructorField('metadata.instructor.description'));
|
||||||
|
let courseNo = $state(
|
||||||
|
get(course, 'metadata.instructor.coursesNo') ?? get(course, 'metadata.instructor.courseNo') ?? ''
|
||||||
|
);
|
||||||
let avatar: File | undefined = $state();
|
let avatar: File | undefined = $state();
|
||||||
let isUploading = $state(false);
|
let isUploading = $state(false);
|
||||||
|
|
||||||
@@ -29,26 +41,26 @@
|
|||||||
isUploading = true;
|
isUploading = true;
|
||||||
const logo = await uploadImage(_avatar);
|
const logo = await uploadImage(_avatar);
|
||||||
|
|
||||||
if (!logo) return;
|
if (!logo) {
|
||||||
|
isUploading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
imgUrl = logo;
|
imgUrl = logo;
|
||||||
isUploading = false;
|
isUploading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
setter(name, 'metadata.instructor.name');
|
setter(
|
||||||
});
|
{
|
||||||
$effect(() => {
|
name,
|
||||||
setter(role, 'metadata.instructor.role');
|
role,
|
||||||
});
|
imgUrl,
|
||||||
$effect(() => {
|
description,
|
||||||
setter(imgUrl, 'metadata.instructor.imgUrl');
|
coursesNo: courseNo
|
||||||
});
|
},
|
||||||
$effect(() => {
|
'metadata.instructor'
|
||||||
setter(description, 'metadata.instructor.description');
|
);
|
||||||
});
|
|
||||||
$effect(() => {
|
|
||||||
setter(courseNo, 'metadata.instructor.coursesNo');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
|
|||||||
@@ -20,6 +20,13 @@ export const ROUTE = {
|
|||||||
VERIFY_EMAIL_ERROR: '/verify-email-error'
|
VERIFY_EMAIL_ERROR: '/verify-email-error'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const PUBLIC_ORG_SITE_LIGHT_MODE_ROUTES = [
|
||||||
|
`^${ROUTE.HOME}$`,
|
||||||
|
`^${ROUTE.COURSES}/?$`,
|
||||||
|
`^${ROUTE.COURSE}/[^/]+/?$`,
|
||||||
|
`^${ROUTE.COURSE}/[^/]+/enroll/?$`
|
||||||
|
] as const;
|
||||||
|
|
||||||
export const PUBLIC_ROUTES = [
|
export const PUBLIC_ROUTES = [
|
||||||
`^${ROUTE.HOME}$`,
|
`^${ROUTE.HOME}$`,
|
||||||
ROUTE.LOGIN,
|
ROUTE.LOGIN,
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { isPublicOrgSitePage } from './color-scheme';
|
||||||
|
|
||||||
|
describe('isPublicOrgSitePage', () => {
|
||||||
|
it('returns false for non-org-site routes', () => {
|
||||||
|
expect(isPublicOrgSitePage(false, '/lms')).toBe(false);
|
||||||
|
expect(isPublicOrgSitePage(false, '/courses/abc')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true for public org-site pages', () => {
|
||||||
|
expect(isPublicOrgSitePage(true, '/')).toBe(true);
|
||||||
|
expect(isPublicOrgSitePage(true, '/courses')).toBe(true);
|
||||||
|
expect(isPublicOrgSitePage(true, '/courses/')).toBe(true);
|
||||||
|
expect(isPublicOrgSitePage(true, '/course/my-course')).toBe(true);
|
||||||
|
expect(isPublicOrgSitePage(true, '/course/my-course/enroll')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false for authenticated org-site app routes', () => {
|
||||||
|
expect(isPublicOrgSitePage(true, '/lms')).toBe(false);
|
||||||
|
expect(isPublicOrgSitePage(true, '/lms/settings')).toBe(false);
|
||||||
|
expect(isPublicOrgSitePage(true, '/courses/abc')).toBe(false);
|
||||||
|
expect(isPublicOrgSitePage(true, '/courses/abc/newsfeed')).toBe(false);
|
||||||
|
expect(isPublicOrgSitePage(true, '/course/my-course/lesson/intro')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { PUBLIC_ORG_SITE_LIGHT_MODE_ROUTES } from '$lib/utils/constants/routes';
|
||||||
|
|
||||||
|
function normalizePathname(pathname: string): string {
|
||||||
|
const pathOnly = pathname.split('?')[0] ?? '/';
|
||||||
|
if (pathOnly === '/') {
|
||||||
|
return '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
return pathOnly.replace(/\/+$/, '') || '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public org-site pages (landing, catalog, course landing, enroll) always use light mode.
|
||||||
|
* Authenticated areas on the same tenant host (e.g. /lms, /courses/:id) keep the theme selector.
|
||||||
|
*/
|
||||||
|
export function isPublicOrgSitePage(isOrgSite: boolean, pathname: string): boolean {
|
||||||
|
if (!isOrgSite) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedPath = normalizePathname(pathname);
|
||||||
|
return PUBLIC_ORG_SITE_LIGHT_MODE_ROUTES.some((pattern) => new RegExp(pattern).test(normalizedPath));
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
|
|
||||||
import { UpgradeModal, PageLoadProgress, PageRestricted } from '$features/ui';
|
import { UpgradeModal, PageLoadProgress, PageRestricted } from '$features/ui';
|
||||||
|
import { VerifyEmailModal } from '$features/onboarding/components';
|
||||||
import { CommandPalette, KeyboardShortcutListener } from '$features/search';
|
import { CommandPalette, KeyboardShortcutListener } from '$features/search';
|
||||||
import { isPublicRoute } from '$lib/utils/functions/routes/isPublicRoute';
|
import { isPublicRoute } from '$lib/utils/functions/routes/isPublicRoute';
|
||||||
import { currentOrg } from '$lib/utils/store/org';
|
import { currentOrg } from '$lib/utils/store/org';
|
||||||
@@ -42,6 +43,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<UpgradeModal />
|
<UpgradeModal />
|
||||||
|
<VerifyEmailModal />
|
||||||
<CommandPalette />
|
<CommandPalette />
|
||||||
<KeyboardShortcutListener />
|
<KeyboardShortcutListener />
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,6 @@
|
|||||||
import { AppHeader } from '$features/ui';
|
import { AppHeader } from '$features/ui';
|
||||||
import { PUBLIC_IS_SELFHOSTED } from '$env/static/public';
|
import { PUBLIC_IS_SELFHOSTED } from '$env/static/public';
|
||||||
|
|
||||||
import { VerifyEmailModal } from '$features/onboarding/components';
|
|
||||||
|
|
||||||
import { OrgSidebar } from '$features/ui/sidebar/org-sidebar';
|
import { OrgSidebar } from '$features/ui/sidebar/org-sidebar';
|
||||||
import { AddOrgModal } from '$features/org';
|
import { AddOrgModal } from '$features/org';
|
||||||
|
|
||||||
@@ -41,8 +39,6 @@
|
|||||||
<AddOrgModal />
|
<AddOrgModal />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<VerifyEmailModal />
|
|
||||||
|
|
||||||
<Sidebar.Provider>
|
<Sidebar.Provider>
|
||||||
<OrgSidebar />
|
<OrgSidebar />
|
||||||
|
|
||||||
|
|||||||
@@ -11,10 +11,18 @@
|
|||||||
import { snackbar } from '$features/ui/snackbar/store';
|
import { snackbar } from '$features/ui/snackbar/store';
|
||||||
import { capturePosthogEvent } from '$lib/utils/services/posthog';
|
import { capturePosthogEvent } from '$lib/utils/services/posthog';
|
||||||
import { resolve } from '$app/paths';
|
import { resolve } from '$app/paths';
|
||||||
|
import { authClient } from '$lib/utils/services/auth/client';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
let loading = $state(false);
|
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 inviteStatus = $derived(data.invite?.status ?? 'INVALID');
|
||||||
const hasActiveInvite = $derived(Boolean(data.invite) && inviteStatus === 'ACTIVE');
|
const hasActiveInvite = $derived(Boolean(data.invite) && inviteStatus === 'ACTIVE');
|
||||||
@@ -51,12 +59,59 @@
|
|||||||
return '';
|
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() {
|
async function handleSubmit() {
|
||||||
if (!canJoinCourse) {
|
if (!canJoinCourse) {
|
||||||
snackbar.error(getBlockedMessage());
|
snackbar.error(getBlockedMessage());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log('profile', $profile);
|
|
||||||
|
if (!sessionReady) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoggedIn) {
|
||||||
|
await completeEnrollment();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
loading = true;
|
loading = true;
|
||||||
|
|
||||||
@@ -64,41 +119,28 @@
|
|||||||
const redirectSearch = data.token ? `?invite_token=${encodeURIComponent(data.token)}` : '';
|
const redirectSearch = data.token ? `?invite_token=${encodeURIComponent(data.token)}` : '';
|
||||||
const redirectUrl = `${redirectPath}${redirectSearch}`;
|
const redirectUrl = `${redirectPath}${redirectSearch}`;
|
||||||
|
|
||||||
if (!$profile.id || !$profile.email) {
|
const inviteEmail = data.inviteEmail ?? '';
|
||||||
const inviteEmail = data.inviteEmail ?? '';
|
const target = data.inviteEmailExists ? '/login' : '/signup';
|
||||||
const target = data.inviteEmailExists ? '/login' : '/signup';
|
const params = new URLSearchParams({ redirect: redirectUrl });
|
||||||
const params = new URLSearchParams({ redirect: redirectUrl });
|
if (inviteEmail) params.set('email', inviteEmail);
|
||||||
if (inviteEmail) params.set('email', inviteEmail);
|
|
||||||
|
|
||||||
goto(resolve(`${target}?${params.toString()}`, {}));
|
goto(resolve(`${target}?${params.toString()}`, {}));
|
||||||
loading = false;
|
loading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (!sessionReady || !isLoggedIn || !canJoinCourse || enrollmentInFlight || loading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let navigatingAway = false;
|
const attemptKey = `${sessionUser?.id ?? ''}:${data.course?.id ?? ''}`;
|
||||||
|
if (enrollmentAttemptKey === attemptKey) {
|
||||||
try {
|
return;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
enrollmentAttemptKey = attemptKey;
|
||||||
|
void completeEnrollment();
|
||||||
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!data.currentOrg) return;
|
if (!data.currentOrg) return;
|
||||||
@@ -107,12 +149,12 @@
|
|||||||
const themeString = typeof rawTheme === 'string' ? rawTheme : '';
|
const themeString = typeof rawTheme === 'string' ? rawTheme : '';
|
||||||
|
|
||||||
setTheme(themeString);
|
setTheme(themeString);
|
||||||
currentOrg.update((o) => ({
|
currentOrg.update((organization) => ({
|
||||||
...o,
|
...organization,
|
||||||
id: data.currentOrg?.id ?? o.id,
|
id: data.currentOrg?.id ?? organization.id,
|
||||||
name: data.currentOrg?.name ?? o.name,
|
name: data.currentOrg?.name ?? organization.name,
|
||||||
siteName: data.currentOrg?.siteName ?? o.siteName,
|
siteName: data.currentOrg?.siteName ?? organization.siteName,
|
||||||
theme: themeString || o.theme
|
theme: themeString || organization.theme
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -122,7 +164,13 @@
|
|||||||
<meta name="robots" content="noindex, nofollow" />
|
<meta name="robots" content="noindex, nofollow" />
|
||||||
</svelte:head>
|
</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">
|
<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>
|
<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>
|
<p class="text-center text-sm font-light dark:text-white">{data.course?.description}</p>
|
||||||
@@ -141,9 +189,11 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-4 flex w-full items-center justify-center">
|
{#if !isLoggedIn}
|
||||||
<Button type="submit" disabled={!canJoinCourse || loading} {loading}>
|
<div class="my-4 flex w-full items-center justify-center">
|
||||||
{$t('course.navItem.landing_page.enroll_page.join_course')}
|
<Button type="submit" disabled={!canJoinCourse || loading || !sessionReady} {loading}>
|
||||||
</Button>
|
{$t('course.navItem.landing_page.enroll_page.join_course')}
|
||||||
</div>
|
</Button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</AuthUI>
|
</AuthUI>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
import { MetaTags } from 'svelte-meta-tags';
|
import { MetaTags } from 'svelte-meta-tags';
|
||||||
import { ModeWatcher } from '@cio/ui/base/dark-mode';
|
import { ModeWatcher } from '@cio/ui/base/dark-mode';
|
||||||
import OrgSiteFavicon from '$features/app/org-site-favicon.svelte';
|
import OrgSiteFavicon from '$features/app/org-site-favicon.svelte';
|
||||||
|
import { isPublicOrgSitePage } from '$lib/utils/functions/color-scheme';
|
||||||
|
|
||||||
import '../app.css';
|
import '../app.css';
|
||||||
|
|
||||||
@@ -24,6 +25,7 @@
|
|||||||
setUploadLimitsContext(data.uploadLimits);
|
setUploadLimitsContext(data.uploadLimits);
|
||||||
|
|
||||||
const metaTags = $derived(merge(data.baseMetaTags, page.data.pageMetaTags));
|
const metaTags = $derived(merge(data.baseMetaTags, page.data.pageMetaTags));
|
||||||
|
const forceLightMode = $derived(isPublicOrgSitePage(data.isOrgSite, page.url.pathname));
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
console.log('Layout', data);
|
console.log('Layout', data);
|
||||||
@@ -75,7 +77,13 @@
|
|||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<ModeWatcher />
|
{#key forceLightMode}
|
||||||
|
{#if forceLightMode}
|
||||||
|
<ModeWatcher defaultMode="light" track={false} />
|
||||||
|
{:else}
|
||||||
|
<ModeWatcher />
|
||||||
|
{/if}
|
||||||
|
{/key}
|
||||||
|
|
||||||
<MetaTags {...metaTags} />
|
<MetaTags {...metaTags} />
|
||||||
|
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ export const ErrorCodes = {
|
|||||||
|
|
||||||
// Auth errors
|
// Auth errors
|
||||||
FORBIDDEN: 'FORBIDDEN',
|
FORBIDDEN: 'FORBIDDEN',
|
||||||
|
EMAIL_NOT_VERIFIED: 'EMAIL_NOT_VERIFIED',
|
||||||
|
|
||||||
// Course errors
|
// Course errors
|
||||||
COURSE_NOT_FOUND: 'COURSE_NOT_FOUND',
|
COURSE_NOT_FOUND: 'COURSE_NOT_FOUND',
|
||||||
|
|||||||
Reference in New Issue
Block a user