feat: mock exercise template api (#613)

* feat: move mock exercise template to api

* feat: add template migration file

* chore:
- remove exercise templates form api
- rework api to pull exercise templates from db
- add exercise template seed to seed file
- declutter seed file

* chore:
- refactor template fetch to be dependent on the selected tag
- add tag to schema, remote db and template's seed file
- clear remote db's template table and replace data and their respective tags

* chore: add number of questions and center loading screen on the frontend

* chore: pr fixes

* chore: type fixes

* chore:
- move seed functionality to /seed folder
- add typing for individual seed functions
- pr fixes

* chore: pr fixes v2

* update output css

* refactor: update seeding script and schema definitions

- Renamed `seedTypes` to `seedQuestionTypes` for clarity in the seeding process.
- Enhanced the seeding script to include a command-line interface for better usability, allowing specific seed functions to be executed based on flags.
- Removed redundant unique constraints in the schema definitions for `courseNewsfeedComment` and `exerciseTemplate` to streamline database structure.

* feat: add email verification error handling page and update session management

- Introduced a new page for handling email verification errors, providing user-friendly messages based on error types.
- Updated session management logic to improve handling of public routes and session states.
- Enhanced security policies in the Svelte configuration to include additional sources for styles and scripts.
- Removed the old email verification error page to streamline the codebase.

* move the exercise template to cloudflare cdn

---------

Co-authored-by: Rotimi Best <best@classroomio.com>
This commit is contained in:
Adeyemi Adetunji
2025-12-25 05:09:27 +01:00
committed by GitHub
co-authored by Rotimi Best
parent ac3f35a97f
commit 50d6ee3111
263 changed files with 1568 additions and 37195 deletions
@@ -0,0 +1,96 @@
<script lang="ts">
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { t } from '$lib/utils/functions/translations';
import { Button } from '@cio/ui/base/button';
import { Empty } from '@cio/ui/custom/empty';
import OctagonAlertIcon from '@lucide/svelte/icons/octagon-alert';
import TriangleAlertIcon from '@lucide/svelte/icons/triangle-alert';
let type = $derived($page.url.searchParams.get('error') || 'unknown');
let { message, title } = $derived.by(() => {
const fields = {
title: '',
message: '',
};
if (!type) return fields;
switch (type) {
case 'token_expired':
fields.title = $t('email_verification.errors.token_expired.title');
fields.message = $t('email_verification.errors.token_expired.message');
break;
case 'email_mismatch':
fields.title = $t('email_verification.errors.email_mismatch.title');
fields.message = $t('email_verification.errors.email_mismatch.message');
break;
case 'legacy_method_blocked':
fields.title = $t('email_verification.errors.legacy_blocked.title');
fields.message = $t('email_verification.errors.legacy_blocked.message');
break;
case 'database_error':
fields.title = $t('email_verification.errors.database.title');
fields.message = $t('email_verification.errors.database.message');
break;
default:
fields.title = $t('email_verification.errors.unknown.title');
fields.message = $t('email_verification.errors.unknown.message');
}
return fields;
});
function handleSupport() {
window.open(
'mailto:help@classroomio.com?subject=Email Verification Issue&body=Error Type: ' + type,
'_blank'
);
}
</script>
<svelte:head>
<title>Email Verification Error - ClassroomIO</title>
<meta name="description" content="There was an issue verifying your email address." />
</svelte:head>
<Empty
title={title}
description={message}
icon={OctagonAlertIcon}
variant="page"
layout="full-page"
showLogo={true}
class="h-fit! flex-none! max-h-none!"
>
{#if type === 'legacy_method_blocked'}
<div
class="rounded-md border border-yellow-200 bg-yellow-50 p-4 dark:border-yellow-800 dark:bg-yellow-900/20"
>
<div class="flex items-center">
<TriangleAlertIcon class="h-5 w-5 flex-shrink-0 text-yellow-400" />
<div class="ml-3">
<h3 class="text-sm font-medium text-yellow-800 dark:text-yellow-200">
{$t('email_verification.security_notice.title')}
</h3>
<p class="mt-1 text-xs text-yellow-700 dark:text-yellow-300">
{$t('email_verification.security_notice.message')}
</p>
</div>
</div>
</div>
{/if}
<div class="lex w-full flex-col justify-center gap-4 md:flex-row">
<Button
onclick={handleSupport}
variant="ghost"
>
{$t('email_verification.actions.contact_support')}
</Button>
<Button
onclick={() => goto('/')}
>
{$t('email_verification.actions.back_to_dashboard')}
</Button>
</div>
</Empty>
+4 -2
View File
@@ -71,15 +71,17 @@
// The goal of this effect is to make sure that we do a redirect to /login if the session data is expired
// This should be happening on the hooks.server.ts but we've made the home page public so we can show a loading spinner while the app is initializing OR an error message if the backend is down.
$effect(() => {
if ($session.isPending || $session.isRefetching) {
if ($session.isPending || $session.isRefetching || !!$session.data) {
console.log('session is pending or refetching');
return;
}
console.log('path', path);
console.log('isPublicRoute', isPublicRoute(path));
console.log('data', $session.data)
// No need to require login for public routes
if (isPublicRoute(path)) return;
if (isPublicRoute(path) && path !== '/') return;
if (!$session.data && path !== '/login') {
console.log('session data is not available, go to login');
@@ -15,11 +15,11 @@
<Sidebar.Provider>
<CourseSidebar {path} />
<Sidebar.Inset>
<Sidebar.Inset class="w-[calc(100vw-var(--sidebar-width))] overflow-x-hidden group-data-[collapsible=icon]:w-[calc(100vw-var(--sidebar-width-icon))]">
<CourseHeader />
<Page.Root
class="mx-auto flex w-[calc(100vw-var(--sidebar-width))] overflow-x-hidden px-4 group-data-[collapsible=icon]:w-[calc(100vw-var(--sidebar-width-icon))] lg:max-w-6xl"
class="mx-auto flex px-4 lg:max-w-6xl"
>
{@render children?.()}
</Page.Root>
@@ -1,147 +0,0 @@
<script>
import { page } from '$app/stores';
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { t } from '$lib/utils/functions/translations';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
let errorType = '';
let errorMessage = '';
let errorTitle = '';
let showRetry = false;
onMount(() => {
errorType = $page.url.searchParams.get('error') || 'unknown';
switch (errorType) {
case 'invalid_token':
errorTitle = $t('email_verification.errors.invalid_token.title');
errorMessage = $t('email_verification.errors.invalid_token.message');
showRetry = true;
break;
case 'token_already_used':
errorTitle = $t('email_verification.errors.token_used.title');
errorMessage = $t('email_verification.errors.token_used.message');
showRetry = true;
break;
case 'token_expired':
errorTitle = $t('email_verification.errors.token_expired.title');
errorMessage = $t('email_verification.errors.token_expired.message');
showRetry = true;
break;
case 'email_mismatch':
errorTitle = $t('email_verification.errors.email_mismatch.title');
errorMessage = $t('email_verification.errors.email_mismatch.message');
showRetry = true;
break;
case 'legacy_method_blocked':
errorTitle = $t('email_verification.errors.legacy_blocked.title');
errorMessage = $t('email_verification.errors.legacy_blocked.message');
showRetry = true;
break;
case 'database_error':
errorTitle = $t('email_verification.errors.database.title');
errorMessage = $t('email_verification.errors.database.message');
showRetry = false;
break;
default:
errorTitle = $t('email_verification.errors.unknown.title');
errorMessage = $t('email_verification.errors.unknown.message');
showRetry = true;
}
});
function handleRetry() {
goto('/dashboard');
}
function handleSupport() {
window.open(
'mailto:help@classroomio.com?subject=Email Verification Issue&body=Error Type: ' + errorType,
'_blank'
);
}
</script>
<svelte:head>
<title>Email Verification Error - ClassroomIO</title>
<meta name="description" content="There was an issue verifying your email address." />
</svelte:head>
<div
class="flex min-h-screen w-screen items-center justify-center bg-gray-50 px-4 py-12 sm:px-6 lg:px-8 dark:bg-gray-900"
>
<div class="w-full max-w-md space-y-8">
<div class="text-center">
<!-- Error Icon -->
<svg
class="mx-auto h-12 w-12 text-red-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<h2 class="mt-6 text-3xl font-bold tracking-tight text-gray-900 dark:text-white">
{errorTitle}
</h2>
<p class="mt-4 text-sm text-gray-600 dark:text-gray-400">
{errorMessage}
</p>
{#if errorType === 'legacy_method_blocked'}
<div
class="mt-4 rounded-md border border-yellow-200 bg-yellow-50 p-4 dark:border-yellow-800 dark:bg-yellow-900/20"
>
<div class="flex">
<svg
class="h-5 w-5 flex-shrink-0 text-yellow-400"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
clip-rule="evenodd"
/>
</svg>
<div class="ml-3">
<h3 class="text-sm font-medium text-yellow-800 dark:text-yellow-200">
{$t('email_verification.security_notice.title')}
</h3>
<p class="mt-1 text-sm text-yellow-700 dark:text-yellow-300">
{$t('email_verification.security_notice.message')}
</p>
</div>
</div>
</div>
{/if}
</div>
<div class="mt-8 flex w-full flex-col justify-center gap-4 md:flex-row">
<PrimaryButton
onClick={handleSupport}
variant={VARIANTS.OUTLINED}
label={$t('email_verification.actions.contact_support')}
/>
<PrimaryButton
onClick={() => goto('/')}
variant={VARIANTS.OUTLINED}
label={$t('email_verification.actions.back_to_dashboard')}
/>
</div>
<!-- Security Information -->
<div class="mt-8 text-center text-xs text-gray-500 dark:text-gray-400">
<p>{$t('email_verification.security_footer')}</p>
</div>
</div>
</div>