feat: setup drizzle (#594)

* chore: migration to SvelteKit 2

* stage 1: migrate to svelte 5 and fix rerendering issues

* stage 2: fix more $: reactivity across dashboard

* fix merge conflicts

* chore: migrate code in main to svelte 5

* chore: update preventDefault

* chore: simplify tsconfig and remove unused mobileMenuRef in MobileMenu.svelte

* chore: enforce .ts across dashboard

* rename from classroomio-com to website

* stage-3: Replace carbon icon svelte with lucide icon

* update lock file

* chore: finish icon replacement

* fix: settings and setup page multiple rerender

* stage 3: More icon and rerendering fixes

* feat: setup drizzle and better auth

* fix: onboarding validation error showing translation key #586 (#593)

* fix: onboarding validation error showing translation key #586

* fix: remove debug log for profile update errors

* chore: add a clean command

* fix: remove message from email verification response
This commit is contained in:
Rotimi Best
2025-11-01 15:49:29 +01:00
committed by GitHub
parent 97fb446df5
commit b96d91cd7c
801 changed files with 22279 additions and 10834 deletions
@@ -1,34 +1,31 @@
<script lang="ts">
import { goto } from '$app/navigation';
import pluralize from 'pluralize';
import TrashCanIcon from 'carbon-icons-svelte/lib/TrashCan.svelte';
import TrashIcon from '@lucide/svelte/icons/trash';
import { Dropdown, SkeletonPlaceholder, SkeletonText } from 'carbon-components-svelte';
import ArrowLeftIcon from 'carbon-icons-svelte/lib/ArrowLeft.svelte';
import ArrowLeftIcon from '@lucide/svelte/icons/arrow-left';
import Vote from '$lib/components/Vote/index.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import Avatar from '$lib/components/Avatar/index.svelte';
import IconButton from '$lib/components/IconButton/index.svelte';
import CheckmarkOutlineIcon from 'carbon-icons-svelte/lib/CheckmarkOutline.svelte';
import { IconButton } from '$lib/components/IconButton';
import CircleCheckIcon from '$lib/components/Icons/CircleCheckIcon.svelte';
import { currentOrg, isOrgAdmin } from '$lib/utils/store/org';
import { profile } from '$lib/utils/store/user';
import { supabase } from '$lib/utils/functions/supabase';
import {
askCommunityValidation,
commentInCommunityValidation
} from '$lib/utils/functions/validator';
import { askCommunityValidation, commentInCommunityValidation } from '$lib/utils/functions/validator';
import { snackbar } from '$lib/components/Snackbar/store';
import TextField from '$lib/components/Form/TextField.svelte';
import DeleteModal from '$lib/components/Org/Community/DeleteModal.svelte';
import TextEditor from '$lib/components/TextEditor/index.svelte';
import { calDateDiff } from '$lib/utils/functions/date';
import { browser } from '$app/environment';
import { untrack } from 'svelte';
import { fetchCourses } from '$lib/utils/services/courses';
import { t } from '$lib/utils/functions/translations';
import { courses } from '$lib/components/Courses/store';
import type { Course } from '$lib/utils/types';
export let data;
let { data } = $props();
const { slug } = data;
interface Comment {
@@ -56,39 +53,38 @@
courseId: string;
}
let question: Question;
let comment = '';
let question: Question | undefined = $state();
let comment = $state('');
let errors: {
title?: string;
courseId?: string;
} = {};
} = $state({});
let isValidAnswer = false; // V2 allow admin mark an answer as accepted
let resetInput = 1;
let voted: {
question: boolean;
comment: {
[key: string]: boolean;
};
} = { question: false, comment: {} };
let isEditMode = false;
let deleteComment = {
} = $state({ question: false, comment: {} });
let isEditMode = $state(false);
let deleteComment = $state({
shouldDelete: false,
commentId: '',
isDeleting: false
};
let deleteQuestion = {
});
let deleteQuestion = $state({
shouldDelete: false,
questionId: '',
isDeleting: false
};
let editContent = {
});
let editContent = $state({
title: '',
body: '',
courseId: ''
};
});
let editorInstance = false;
let fetchedCourses: Course[] = [];
let editorInstance = $state(false);
let fetchedCourses: Course[] = $state([]);
function mapResToQuestion(data): Question {
return {
@@ -161,11 +157,15 @@
return goto(`/lms`);
}
question = mapResToQuestion(data);
question.totalComments = question.comments.length;
untrack(() => {
question = mapResToQuestion(data);
question.totalComments = question.comments.length;
});
}
async function submitComment() {
if (!question) return;
errors = commentInCommunityValidation({ comment });
console.log('submitComment errors', errors);
@@ -209,11 +209,12 @@
// Reset input
comment = '';
resetInput = new Date().getTime();
}
}
async function upvoteQuestion(type: string, commentId?: string) {
if (!question) return;
const isQuestion = type === 'question';
if (isQuestion && voted.question) return;
@@ -249,6 +250,8 @@
}
async function handleQuestionEdit() {
if (!question) return;
if (isEditMode) {
errors = askCommunityValidation(editContent);
console.log('handleQuestionEdit errors', errors);
@@ -296,13 +299,12 @@
}
async function handleDelete(isQuestion: boolean) {
if (!question) return;
if (!isQuestion) {
deleteComment.isDeleting = true;
const { error } = await supabase
.from('community_answer')
.delete()
.match({ id: deleteComment.commentId });
const { error } = await supabase.from('community_answer').delete().match({ id: deleteComment.commentId });
deleteComment.isDeleting = false;
@@ -351,12 +353,14 @@
deleteQuestion.isDeleting = false;
}
$: browser && fetchCommunityQuestion(slug);
$: {
$effect(() => {
fetchCommunityQuestion(slug);
});
$effect(() => {
if ($profile.id && $currentOrg.id) {
getCourses($profile.id, $currentOrg.id);
}
}
});
</script>
<svelte:head>
@@ -365,7 +369,7 @@
<DeleteModal
bind:open={deleteQuestion.shouldDelete}
bind:isDeleting={deleteQuestion.isDeleting}
isDeleting={deleteQuestion.isDeleting}
onCancel={() => {
deleteQuestion.shouldDelete = false;
deleteQuestion.questionId = '';
@@ -383,28 +387,24 @@
}}
onDelete={() => handleDelete(false)}
/>
<section class="max-w-3xl mx-auto md:mx-10 lg:mb-20">
<section class="mx-auto max-w-3xl md:mx-10 lg:mb-20">
{#if !question}
<div class="py-10 px-5 mb-3">
<div class="mb-3 px-5 py-10">
<SkeletonText style="width: 25%;" />
<SkeletonText style="width: 100%; margin-bottom: 2rem" />
<SkeletonPlaceholder style="width: 100%; height: 20rem;" />
</div>
{:else}
<div class="py-10 px-5">
<a class="text-gray-500 dark:text-white text-md flex items-center" href={`/lms/community`}>
<ArrowLeftIcon size={24} class="carbon-icon dark:text-white" />
<div class="px-5 py-10">
<a class="text-md flex items-center text-gray-500 dark:text-white" href="/lms/community">
<ArrowLeftIcon size={16} />
{$t('community.ask.go_back')}
</a>
<div class="my-5 flex justify-between items-center">
<div class="my-5 flex items-center justify-between">
{#if isEditMode}
<TextField
bind:value={editContent.title}
className="w-full mr-2"
errorMessage={errors.title}
/>
<TextField bind:value={editContent.title} className="w-full mr-2" errorMessage={errors.title} />
<Dropdown
class="w-[25%] h-full"
class="h-full w-[25%]"
size="xl"
label="Select Course"
items={fetchedCourses.map((course) => ({ id: course.id, text: course.title }))}
@@ -412,11 +412,7 @@
/>
{:else}
<div class="flex items-center">
<Vote
value={question.votes}
upVote={() => upvoteQuestion('question')}
disabled={voted.question}
/>
<Vote value={question.votes} upVote={() => upvoteQuestion('question')} disabled={voted.question} />
<h2 class="text-3xl">{question.title}</h2>
</div>
{/if}
@@ -439,17 +435,12 @@
{/if}
{/if}
</div>
<div class="my-1 px-1 rounded-lg border border-1 border-gray">
<header class="flex items-center justify-between leading-none p-2">
<div class="flex items-center no-underline hover:underline text-black">
<Avatar
src={question.author.avatar}
name={question.author.name}
width="w-7"
height="h-7"
/>
<p class="dark:text-white ml-2 text-sm">{question.author.name}</p>
<p class="dark:text-white ml-2 text-sm text-gray-500">
<div class="border-1 border-gray my-1 rounded-lg border px-1">
<header class="flex items-center justify-between p-2 leading-none">
<div class="flex items-center text-black no-underline hover:underline">
<Avatar src={question.author.avatar} name={question.author.name} width="w-7" height="h-7" />
<p class="ml-2 text-sm dark:text-white">{question.author.name}</p>
<p class="ml-2 text-sm text-gray-500 dark:text-white">
{question.createdAt}
</p>
</div>
@@ -457,11 +448,13 @@
<IconButton
value="delete-question"
onClick={() => {
if (!question) return;
deleteQuestion.shouldDelete = true;
deleteQuestion.questionId = question.id;
}}
>
<TrashCanIcon size={16} class="carbon-icon dark:text-white" />
<TrashIcon size={16} />
</IconButton>
{/if}
</header>
@@ -485,24 +478,24 @@
</div>
{#each question.comments as comment}
<div class="my-5 px-1 flex items-start">
<div class="my-5 flex items-start px-1">
<Vote
value={comment.votes}
upVote={() => upvoteQuestion('comment', comment.id)}
disabled={voted.comment[comment.id]}
/>
<div class="w-full rounded-lg border border-1 border-gray">
<header class="flex items-center justify-between leading-none p-2">
<div class="border-1 border-gray w-full rounded-lg border">
<header class="flex items-center justify-between p-2 leading-none">
<div class="flex items-center text-black">
<Avatar src={comment.avatar} name={comment.name} width="w-7" height="h-7" />
<p class="dark:text-white ml-2 text-sm">{comment.name}</p>
<p class="dark:text-white ml-2 text-sm text-gray-500">
<p class="ml-2 text-sm dark:text-white">{comment.name}</p>
<p class="ml-2 text-sm text-gray-500 dark:text-white">
{comment.createdAt}
</p>
</div>
{#if isValidAnswer}
<CheckmarkOutlineIcon size={20} />
<CircleCheckIcon size={16} />
{/if}
{#if comment.authorId === $profile.id || $isOrgAdmin}
@@ -513,7 +506,7 @@
deleteComment.commentId = comment.id;
}}
>
<TrashCanIcon size={16} class="carbon-icon dark:text-white" />
<TrashIcon size={16} />
</IconButton>
{/if}
</header>
@@ -528,14 +521,10 @@
<div>
{#if !editorInstance}
<TextEditor
bind:value={comment}
placeholder="Give an answer"
onChange={(html) => (comment = html)}
/>
<TextEditor bind:value={comment} placeholder="Give an answer" onChange={(html) => (comment = html)} />
{/if}
<div class="flex justify-end mr-2">
<div class="mr-2 flex justify-end">
<PrimaryButton label="Comment" onClick={submitComment} />
</div>
</div>