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 <[email protected]>
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
@@ -1,16 +1,17 @@
<script lang="ts">
import { onMount } from 'svelte';
import { Badge } from '@cio/ui/base/badge';
import { Skeleton } from '@cio/ui/base/skeleton';
import * as Dialog from '@cio/ui/base/dialog';
import { Confetti, ComingSoon } from '$features/ui';
import { InputField } from '@cio/ui/custom/input-field';
import { Button } from '@cio/ui/base/button';
import { CircleCheckIcon } from '$features/ui/icons';
import { t } from '$lib/utils/functions/translations';
import { exerciseTemplateApi } from '$features/exercise-template/api';
import { snackbar } from '$features/ui/snackbar/store';
import { Confetti, ComingSoon } from '$features/ui';
import { CircleCheckIcon } from '$features/ui/icons';
import { TAGS } from '$features/exercise-template/utils/constants';
import type { ExerciseTemplate } from '$lib/utils/types';
import { type GeneratedTemplates, getAllTemplates, TAGS } from '$lib/mocks';
interface Props {
open: boolean;
@@ -64,7 +65,6 @@
// let optionNumber = $state(5);
let isLoading = $state(false);
let isAIStarted = $state(false);
let allTemplates: GeneratedTemplates | undefined = $state();
let selectedTag = $state(tags[0]);
let selectedTemplateId = $state('');
@@ -110,9 +110,32 @@
// }, 500);
// }
onMount(async () => {
allTemplates = await getAllTemplates();
});
async function handleTemplateSelection() {
isTemplateFinishedLoading = true;
const template = exerciseTemplateApi.templates?.find((t) => t.id === Number(selectedTemplateId));
if (!template) return;
let fetchedTemplate: ExerciseTemplate;
try {
await exerciseTemplateApi.fetchTemplateById(template.id);
fetchedTemplate = exerciseTemplateApi.template[0];
console.log('Fetched template', fetchedTemplate);
await handleTemplateCreate(fetchedTemplate);
} catch (error) {
console.log('Error fetching template', error);
snackbar.error($t('course.navItem.lessons.exercises.new_exercise_modal.errors.template_fetch'));
} finally {
isTemplateFinishedLoading = false;
}
}
const handleTagSelection = async (tag: string) => {
selectedTag = tag;
selectedTemplateId = '';
await exerciseTemplateApi.fetchTemplatesByTag(selectedTag);
};
// const content = $derived(
// lessonFallbackNote(
@@ -146,7 +169,7 @@
<div class="my-8 flex justify-between gap-2">
{#each options as option}
<button
class="h-[240px] w-[261px] rounded-md border-2 p-5 dark:bg-neutral-700 {option.type === type
class="h-60 w-[261px] rounded-md border-2 p-5 dark:bg-neutral-700 {option.type === type
? 'border-primary-400'
: `border-gray-200 dark:border-neutral-600 ${
!option.isDisabled && 'hover:scale-95'
@@ -210,21 +233,33 @@
<div>
<div class="mb-5 flex items-center gap-2">
{#each tags as tag}
<Badge
class={selectedTag === tag ? 'bg-primary-400' : ''}
onclick={() => {
selectedTag = tag;
selectedTemplateId = '';
}}>{tag}</Badge
<Badge class={selectedTag === tag ? 'bg-primary-400' : ''} onclick={() => handleTagSelection(tag)}
>{tag}</Badge
>
{/each}
</div>
<div class="flex flex-wrap items-start gap-2">
{#if allTemplates}
{#each allTemplates[selectedTag] as template}
{#if exerciseTemplateApi.isLoading}
<div class="grid grid-cols-2 items-start gap-4 lg:grid-cols-3 xl:grid-cols-4">
{#each Array(16) as _}
<div
class="h-[140px] w-full rounded-md border-2 border-gray-200 p-5 dark:border-neutral-600 dark:bg-neutral-700"
>
<div class="flex h-full flex-col justify-evenly">
<Skeleton class="h-4 w-3/4" />
<div class="flex flex-col items-start justify-between gap-1">
<Skeleton class="h-3 w-20" />
<Skeleton class="h-3 w-16" />
</div>
</div>
</div>
{/each}
</div>
{:else if exerciseTemplateApi.templates.length > 0}
<div class="grid grid-cols-2 items-start gap-4 lg:grid-cols-3 xl:grid-cols-4">
{#each exerciseTemplateApi.templates as template}
<button
class="h-[140px] w-[161px] rounded-md border-2 p-5 hover:scale-95 dark:bg-neutral-700 {template.id ===
class="h-[140px] w-full rounded-md border-2 p-5 hover:scale-95 dark:bg-neutral-700 {template.id ===
selectedTemplateId
? 'border-primary-400'
: `border-gray-200 dark:border-neutral-600 `} flex flex-col transition-all ease-in-out"
@@ -248,28 +283,17 @@
</div>
</button>
{/each}
{/if}
</div>
</div>
{/if}
<div class="mt-5 flex items-center justify-between">
<Button variant="outline" onclick={handleBack}>
{$t('course.navItem.lessons.exercises.new_exercise_modal.back')}
</Button>
<Button
disabled={!selectedTemplateId || !allTemplates}
disabled={!selectedTemplateId || exerciseTemplateApi.templates.length === 0}
loading={isTemplateFinishedLoading}
onclick={async () => {
isTemplateFinishedLoading = true;
const template = allTemplates?.[selectedTag]?.find((t) => t.id === selectedTemplateId);
if (!template) return;
console.log('Selected template', template);
await handleTemplateCreate(template.data);
isTemplateFinishedLoading = true;
}}
onclick={handleTemplateSelection}
>
{$t('course.navItem.lessons.exercises.new_exercise_modal.finish')}
</Button>
@@ -7,8 +7,8 @@
import { t } from '$lib/utils/functions/translations';
import * as Breadcrumb from '@cio/ui/base/breadcrumb';
import { supabase } from '$lib/utils/functions/supabase';
import type { ExerciseTemplate } from '$lib/utils/types';
import type { Exercise as ExerciseType } from '$lib/utils/types';
import type { ExerciseTemplate, Exercise as ExerciseType } from '$lib/utils/types';
import { formatDate } from '$lib/utils/functions/routes/dashboard';
import { isQuestionnaireFetching, questionnaire } from '../store/exercise';
import { createExercise, createExerciseFromTemplate } from '$lib/utils/services/courses';