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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user