Dashboard improvements + LMS community
This commit is contained in:
@@ -2,6 +2,8 @@
|
|||||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||||
import GoogleIconColored from '../Icons/GoogleIconColored.svelte';
|
import GoogleIconColored from '../Icons/GoogleIconColored.svelte';
|
||||||
|
import Avatar from '$lib/components/Avatar/index.svelte';
|
||||||
|
import { currentOrg } from '$lib/utils/store/org';
|
||||||
|
|
||||||
export let supabase;
|
export let supabase;
|
||||||
export let handleSubmit;
|
export let handleSubmit;
|
||||||
@@ -34,9 +36,17 @@
|
|||||||
<div class="container border border-gray bg-white dark:bg-gray-800">
|
<div class="container border border-gray bg-white dark:bg-gray-800">
|
||||||
<div class="flex items-center flex-col p-2 lg:px-8 lg:py-3">
|
<div class="flex items-center flex-col p-2 lg:px-8 lg:py-3">
|
||||||
{#if !showOnlyContent || showLogo}
|
{#if !showOnlyContent || showLogo}
|
||||||
<div class="flex items-center w-full justify-center pt-4">
|
<div class="flex flex-col items-center w-full justify-center pt-2">
|
||||||
<img src="/logo-192.png" alt="ClassroomIO logo" height="50" width="50" data-atf="1" />
|
<Avatar
|
||||||
<h4 class="dark:text-white text-xl">ClassroomIO</h4>
|
src={$currentOrg.avatar_url ? $currentOrg.avatar_url : '/logo-192.png'}
|
||||||
|
name={$currentOrg.name ? $currentOrg.name : 'ClassroomIO'}
|
||||||
|
shape="rounded-md"
|
||||||
|
width="w-10"
|
||||||
|
height="h-10"
|
||||||
|
/>
|
||||||
|
<h4 class="dark:text-white text-xl mt-1">
|
||||||
|
{$currentOrg.name ? $currentOrg.name : 'ClassroomIO'}
|
||||||
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<form
|
<form
|
||||||
|
|||||||
@@ -31,7 +31,6 @@
|
|||||||
import { course } from '$lib/components/Course/store';
|
import { course } from '$lib/components/Course/store';
|
||||||
import * as CONSTANTS from './constants';
|
import * as CONSTANTS from './constants';
|
||||||
import { fetchLesson } from '$lib/utils/services/courses';
|
import { fetchLesson } from '$lib/utils/services/courses';
|
||||||
import type { LessonPage } from '$lib/utils/types';
|
|
||||||
|
|
||||||
export let mode = MODES.view;
|
export let mode = MODES.view;
|
||||||
export let prevMode = '';
|
export let prevMode = '';
|
||||||
|
|||||||
@@ -81,7 +81,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Courses - ClassroomIO</title>
|
<title>Create a new course</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<aside class=" root bg-gray-100 dark:bg-gray-700 h-full">
|
<aside class="w-[250px] min-w-[250px] bg-gray-100 dark:bg-gray-700 h-full">
|
||||||
<div class="h-full flex flex-col">
|
<div class="h-full flex flex-col">
|
||||||
<div class="border-b border-gray-200 pt-5 px-4">
|
<div class="border-b border-gray-200 pt-5 px-4">
|
||||||
{#if $currentOrg.avatar_url}
|
{#if $currentOrg.avatar_url}
|
||||||
@@ -109,9 +109,3 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<style>
|
|
||||||
.root {
|
|
||||||
width: 250px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
import { currentOrgPath, currentOrg } from '$lib/utils/store/org';
|
import { currentOrgPath, currentOrg } from '$lib/utils/store/org';
|
||||||
import { supabase } from '$lib/utils/functions/supabase';
|
import { supabase } from '$lib/utils/functions/supabase';
|
||||||
|
|
||||||
|
export let isLMS = false;
|
||||||
|
|
||||||
let discussions = [];
|
let discussions = [];
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
@@ -38,7 +40,7 @@
|
|||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error('Error loading community', error);
|
console.error('Error loading community', error);
|
||||||
return goto(`${currentOrgPath}`);
|
return goto(isLMS ? '/lms' : $currentOrgPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
discussions =
|
discussions =
|
||||||
@@ -48,7 +50,7 @@
|
|||||||
author: discussion?.author?.profile?.fullname,
|
author: discussion?.author?.profile?.fullname,
|
||||||
comments: discussion.comments?.[0]?.count || 0,
|
comments: discussion.comments?.[0]?.count || 0,
|
||||||
votes: discussion.votes,
|
votes: discussion.votes,
|
||||||
createdAt: dayjs(discussion.created_at).fromNow(true),
|
createdAt: dayjs(discussion.created_at).fromNow(true)
|
||||||
})) || [];
|
})) || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +67,7 @@
|
|||||||
<h4>
|
<h4>
|
||||||
<a
|
<a
|
||||||
class="text-black dark:text-white"
|
class="text-black dark:text-white"
|
||||||
href="{$currentOrgPath}/community/{discussion.slug}"
|
href="{isLMS ? '/lms' : $currentOrgPath}/community/{discussion.slug}"
|
||||||
>
|
>
|
||||||
{discussion.title}
|
{discussion.title}
|
||||||
</a>
|
</a>
|
||||||
@@ -84,9 +86,7 @@
|
|||||||
<Box>
|
<Box>
|
||||||
<CoursesEmptyIcon />
|
<CoursesEmptyIcon />
|
||||||
<h3 class="dark:text-white text-2xl my-5">No Questions asked</h3>
|
<h3 class="dark:text-white text-2xl my-5">No Questions asked</h3>
|
||||||
<p class="dark:text-white w-1/3 text-center">
|
<p class="dark:text-white w-1/3 text-center">Ask a question to the community</p>
|
||||||
Ask a question to the community
|
|
||||||
</p>
|
|
||||||
</Box>
|
</Box>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -133,7 +133,9 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
{$currentOrg.name}
|
<title>
|
||||||
|
{$currentOrg.name}'s Landing Page
|
||||||
|
</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
@@ -148,7 +150,7 @@
|
|||||||
class="md:w-11/12 lg:w-5/6 w-full flex items-center justify-between flex-col-reverse md:flex-row z-20 relative"
|
class="md:w-11/12 lg:w-5/6 w-full flex items-center justify-between flex-col-reverse md:flex-row z-20 relative"
|
||||||
>
|
>
|
||||||
<!-- Course Description -->
|
<!-- Course Description -->
|
||||||
<div class="md:w-2/5 w-11/12 py-10 flex flex-col items-start">
|
<div class="md:w-2/5 w-11/12 py-10 flex flex-col items-start mr-3">
|
||||||
<p class=" text-blue-600 text-2xl font-semibold">Coding Dojo</p>
|
<p class=" text-blue-600 text-2xl font-semibold">Coding Dojo</p>
|
||||||
<h1 class="text-4xl md:text-5xl lg:text-6xl my-4 font-bold">
|
<h1 class="text-4xl md:text-5xl lg:text-6xl my-4 font-bold">
|
||||||
Web Developement Courses for <br /><span class="text-blue-600">Everyone</span>
|
Web Developement Courses for <br /><span class="text-blue-600">Everyone</span>
|
||||||
@@ -164,7 +166,7 @@
|
|||||||
onClick={() => goto('#contact')}
|
onClick={() => goto('#contact')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-4/6 md:w-2/3 md:max-w-[650px] flex">
|
<div class="w-4/6 md:w-1/2 md:max-w-[650px] flex">
|
||||||
<video class="w-full rounded-xl" controls loop autoplay>
|
<video class="w-full rounded-xl" controls loop autoplay>
|
||||||
<source src={video} type="video/mp4" />
|
<source src={video} type="video/mp4" />
|
||||||
<!-- <source src="/path/to/video.webm" type="video/webm" /> -->
|
<!-- <source src="/path/to/video.webm" type="video/webm" /> -->
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<aside
|
<aside
|
||||||
class="root bg-gray-100 dark:bg-gray-800 h-full border border-l-0 border-t-0 border-b-0 border-r-1"
|
class="w-[250px] min-w-[250px] bg-gray-100 dark:bg-gray-800 h-full border border-l-0 border-t-0 border-b-0 border-r-1"
|
||||||
>
|
>
|
||||||
<div class="h-full flex flex-col">
|
<div class="h-full flex flex-col">
|
||||||
<div class="border-b border-gray-200 pt-5 px-4">
|
<div class="border-b border-gray-200 pt-5 px-4">
|
||||||
@@ -128,9 +128,3 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<style>
|
|
||||||
.root {
|
|
||||||
width: 250px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1 +1,25 @@
|
|||||||
community page
|
<script>
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import Community from '$lib/components/Org/Community/index.svelte';
|
||||||
|
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||||
|
import { currentOrgPath } from '$lib/utils/store/org';
|
||||||
|
|
||||||
|
function askCommunity() {
|
||||||
|
goto(`${$currentOrgPath}/community/ask`);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Community - ClassroomIO</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<section class="w-full max-w-4xl mx-auto">
|
||||||
|
<div class="py-10 px-5">
|
||||||
|
<div class="flex items-center justify-between mb-10">
|
||||||
|
<h1 class="dark:text-white text-3xl font-bold">Community</h1>
|
||||||
|
<PrimaryButton label="Ask Community" onClick={askCommunity} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Community isLMS={true} />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export const load = ({ params = { slug: '' } }) => {
|
||||||
|
return {
|
||||||
|
slug: params.slug,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,414 @@
|
|||||||
|
<script>
|
||||||
|
import { marked } from 'marked';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import pluralize from 'pluralize';
|
||||||
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
|
import TrashCanIcon from 'carbon-icons-svelte/lib/TrashCan.svelte';
|
||||||
|
import { SkeletonPlaceholder, SkeletonText } from 'carbon-components-svelte';
|
||||||
|
import ArrowLeftIcon from 'carbon-icons-svelte/lib/ArrowLeft.svelte';
|
||||||
|
import Vote from '$lib/components/Vote/index.svelte';
|
||||||
|
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||||
|
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||||
|
import TextEditor from '$lib/components/TextEditor/index.svelte';
|
||||||
|
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 { currentOrg } 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 { SNACKBAR_SEVERITY } from '$lib/components/Snackbar/constants';
|
||||||
|
import { snackbarStore } from '$lib/components/Snackbar/store';
|
||||||
|
import TextField from '$lib/components/Form/TextField.svelte';
|
||||||
|
import DeleteCommentModal from '$lib/components/Org/Community/DeleteCommentModal.svelte';
|
||||||
|
|
||||||
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
|
export let data;
|
||||||
|
const { slug } = data;
|
||||||
|
|
||||||
|
let question;
|
||||||
|
let comment = '';
|
||||||
|
let errors = {};
|
||||||
|
let isValidAnswer = false; // V2 allow admin mark an answer as accepted
|
||||||
|
let resetInput = 1;
|
||||||
|
let voted = { question: false, comment: {} };
|
||||||
|
let isEditMode = false;
|
||||||
|
let deleteComment = {
|
||||||
|
shouldDelete: false,
|
||||||
|
commentId: null
|
||||||
|
};
|
||||||
|
let editContent = {
|
||||||
|
title: '',
|
||||||
|
body: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
function mapResToQuestion(data) {
|
||||||
|
return {
|
||||||
|
id: data.id,
|
||||||
|
title: data.title,
|
||||||
|
votes: data.votes,
|
||||||
|
author: {
|
||||||
|
id: data?.author?.profile?.id || '',
|
||||||
|
name: data?.author?.profile?.fullname || '',
|
||||||
|
avatar: data?.author?.profile?.avatar_url || ''
|
||||||
|
},
|
||||||
|
body: data.body,
|
||||||
|
createdAt: dayjs(data.created_at).fromNow(true),
|
||||||
|
comments: data.comments.map((c) => ({
|
||||||
|
id: c.id,
|
||||||
|
authorId: c.author?.profile?.id || '',
|
||||||
|
name: c.author?.profile?.fullname || '',
|
||||||
|
avatar: c.author?.profile?.avatar_url || '',
|
||||||
|
votes: c.votes,
|
||||||
|
comment: c.body,
|
||||||
|
createdAt: dayjs(c.created_at).fromNow(true)
|
||||||
|
})),
|
||||||
|
totalComments: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchCommunityQuestion(slug) {
|
||||||
|
if (!slug) return;
|
||||||
|
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('community_question')
|
||||||
|
.select(
|
||||||
|
`
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
votes,
|
||||||
|
created_at,
|
||||||
|
comments:community_answer(
|
||||||
|
id,
|
||||||
|
body,
|
||||||
|
votes,
|
||||||
|
created_at,
|
||||||
|
author:organizationmember!community_answer_author_id_fkey!inner(
|
||||||
|
profile!inner(id, fullname, avatar_url)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
author:organizationmember!community_question_author_id_fkey!inner(
|
||||||
|
profile!inner(id, fullname, avatar_url)
|
||||||
|
)
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.eq('slug', slug)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('Error loading community', error);
|
||||||
|
return goto('/lms/community');
|
||||||
|
}
|
||||||
|
|
||||||
|
question = mapResToQuestion(data);
|
||||||
|
question.totalComments = question.comments.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitComment() {
|
||||||
|
errors = commentInCommunityValidation({ comment });
|
||||||
|
console.log('submitComment errors', errors);
|
||||||
|
|
||||||
|
if (Object.keys(errors).length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('community_answer')
|
||||||
|
.insert({
|
||||||
|
id: undefined,
|
||||||
|
body: comment,
|
||||||
|
question_id: question.id,
|
||||||
|
author_id: $currentOrg.memberId,
|
||||||
|
votes: 0
|
||||||
|
})
|
||||||
|
.select();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('Error: commenting', error);
|
||||||
|
$snackbarStore.open = true;
|
||||||
|
$snackbarStore.message = 'Error - Please try again later';
|
||||||
|
$snackbarStore.severity = SNACKBAR_SEVERITY.ERROR;
|
||||||
|
} else {
|
||||||
|
console.log('Success: commenting', data);
|
||||||
|
|
||||||
|
// Notification
|
||||||
|
$snackbarStore.open = true;
|
||||||
|
$snackbarStore.message = 'Success';
|
||||||
|
$snackbarStore.severity = SNACKBAR_SEVERITY.SUCCESS;
|
||||||
|
|
||||||
|
// Add to comment
|
||||||
|
const _c = data?.[0];
|
||||||
|
question.comments = [
|
||||||
|
...question.comments,
|
||||||
|
{
|
||||||
|
id: _c.id,
|
||||||
|
name: $profile?.fullname || '',
|
||||||
|
avatar: $profile?.avatar_url || '',
|
||||||
|
votes: 0,
|
||||||
|
comment: _c.body,
|
||||||
|
createdAt: dayjs(_c.created_at).fromNow(true)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Reset input
|
||||||
|
comment = '';
|
||||||
|
resetInput = new Date().getTime();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function upvoteQuestion(type, commentId) {
|
||||||
|
const isQuestion = type === 'question';
|
||||||
|
|
||||||
|
if (isQuestion && voted.question) return;
|
||||||
|
if (!isQuestion && voted.comment[commentId]) return;
|
||||||
|
|
||||||
|
const table = isQuestion ? 'community_question' : 'community_answer';
|
||||||
|
const matchId = isQuestion ? question.id : commentId;
|
||||||
|
let votes = 0;
|
||||||
|
|
||||||
|
if (isQuestion) {
|
||||||
|
question.votes = question.votes + 1;
|
||||||
|
votes = question.votes;
|
||||||
|
} else {
|
||||||
|
question.comments = question.comments.map((c) => {
|
||||||
|
if (c.id === commentId) {
|
||||||
|
c.votes = c.votes + 1;
|
||||||
|
votes = c.votes;
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const { error } = await supabase.from(table).update({ votes }).match({ id: matchId });
|
||||||
|
if (error) {
|
||||||
|
console.error('Error: upvoteQuestion', error);
|
||||||
|
$snackbarStore.open = true;
|
||||||
|
$snackbarStore.message = 'Error - Please try again later';
|
||||||
|
$snackbarStore.severity = SNACKBAR_SEVERITY.ERROR;
|
||||||
|
} else {
|
||||||
|
if (isQuestion) {
|
||||||
|
voted.question = true;
|
||||||
|
} else {
|
||||||
|
voted.comment[commentId] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleQuestionEdit() {
|
||||||
|
if (isEditMode) {
|
||||||
|
const fields = { title: editContent.title, body: editContent.body };
|
||||||
|
errors = askCommunityValidation(fields);
|
||||||
|
console.log('handleQuestionEdit errors', errors);
|
||||||
|
|
||||||
|
if (Object.keys(errors).length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isEditMode = !isEditMode;
|
||||||
|
|
||||||
|
if (!isEditMode) {
|
||||||
|
const fields = { title: editContent.title, body: editContent.body };
|
||||||
|
errors = askCommunityValidation(fields);
|
||||||
|
console.log('handleQuestionEdit errors', errors);
|
||||||
|
|
||||||
|
if (Object.keys(errors).length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { error } = await supabase
|
||||||
|
.from('community_question')
|
||||||
|
.update(fields)
|
||||||
|
.match({ id: question.id });
|
||||||
|
if (error) {
|
||||||
|
console.error('Error: handleQuestionEdit', error);
|
||||||
|
$snackbarStore.open = true;
|
||||||
|
$snackbarStore.message = 'Error - Please try again later';
|
||||||
|
$snackbarStore.severity = SNACKBAR_SEVERITY.ERROR;
|
||||||
|
} else {
|
||||||
|
question.title = fields.title;
|
||||||
|
question.body = fields.body;
|
||||||
|
|
||||||
|
editContent.title = '';
|
||||||
|
editContent.body = '';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
editContent.title = question.title;
|
||||||
|
editContent.body = question.body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCommentDelete() {
|
||||||
|
const { error } = await supabase
|
||||||
|
.from('community_answer')
|
||||||
|
.delete()
|
||||||
|
.match({ id: deleteComment.commentId });
|
||||||
|
|
||||||
|
if (!error) {
|
||||||
|
question.comments = question.comments.filter((c) => c.id !== deleteComment.commentId);
|
||||||
|
|
||||||
|
deleteComment.shouldDelete = false;
|
||||||
|
deleteComment.commentId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: fetchCommunityQuestion(slug);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{question?.title || 'Question'}</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<DeleteCommentModal
|
||||||
|
bind:open={deleteComment.shouldDelete}
|
||||||
|
onCancel={() => {
|
||||||
|
deleteComment.shouldDelete = false;
|
||||||
|
deleteComment.commentId = null;
|
||||||
|
}}
|
||||||
|
onDelete={handleCommentDelete}
|
||||||
|
/>
|
||||||
|
<section class="max-w-3xl mx-auto md:mx-10 lg:mb-20">
|
||||||
|
{#if !question}
|
||||||
|
<div class="py-10 px-5 mb-3">
|
||||||
|
<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" /> Go Back
|
||||||
|
</a>
|
||||||
|
<div class="my-5 flex justify-between items-center">
|
||||||
|
{#if isEditMode}
|
||||||
|
<TextField
|
||||||
|
bind:value={editContent.title}
|
||||||
|
className="w-full mr-2"
|
||||||
|
errorMessage={errors.title}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Vote
|
||||||
|
value={question.votes}
|
||||||
|
upVote={() => upvoteQuestion('question')}
|
||||||
|
disabled={voted.question}
|
||||||
|
/>
|
||||||
|
<h2 class="text-3xl">{question.title}</h2>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if question.author.id === $profile.id}
|
||||||
|
<PrimaryButton
|
||||||
|
label={isEditMode ? 'Save' : 'Edit'}
|
||||||
|
variant={VARIANTS.OUTLINED}
|
||||||
|
onClick={handleQuestionEdit}
|
||||||
|
className="h-fit"
|
||||||
|
/>
|
||||||
|
{#if isEditMode}
|
||||||
|
<PrimaryButton
|
||||||
|
label="Cancel"
|
||||||
|
variant={VARIANTS.TEXT}
|
||||||
|
onClick={() => (isEditMode = !isEditMode)}
|
||||||
|
className="py-3 px-6 rounded-sm h-fit"
|
||||||
|
disablePadding={true}
|
||||||
|
/>
|
||||||
|
{/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">
|
||||||
|
{question.createdAt}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
{#if isEditMode}
|
||||||
|
<div class="my-2">
|
||||||
|
<TextEditor
|
||||||
|
content={question.body}
|
||||||
|
onChange={(c) => (editContent.body = c)}
|
||||||
|
placeholder="Update question"
|
||||||
|
errorMessage={errors.body}
|
||||||
|
docId={resetInput}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<section class="prose prose-sm sm:prose p-2">
|
||||||
|
{@html marked(question.body)}
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-8 font-bold">
|
||||||
|
{pluralize('answers', question.totalComments, true)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#each question.comments as comment}
|
||||||
|
<div class="my-5 px-1 flex items-start">
|
||||||
|
<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="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">
|
||||||
|
{comment.createdAt}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if isValidAnswer}
|
||||||
|
<CheckmarkOutlineIcon size={20} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if comment.authorId === $profile.id}
|
||||||
|
<IconButton
|
||||||
|
value="delete-comment"
|
||||||
|
onClick={() => {
|
||||||
|
deleteComment.shouldDelete = true;
|
||||||
|
deleteComment.commentId = comment.id;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TrashCanIcon size={16} class="carbon-icon dark:text-white" />
|
||||||
|
</IconButton>
|
||||||
|
{/if}
|
||||||
|
</header>
|
||||||
|
<article class="prose prose-sm sm:prose p-2">
|
||||||
|
{@html marked(comment.comment)}
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<TextEditor
|
||||||
|
onChange={(c) => (comment = c)}
|
||||||
|
placeholder="Give an answer"
|
||||||
|
errorMessage={errors.comment}
|
||||||
|
docId={resetInput}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex justify-end mr-2">
|
||||||
|
<PrimaryButton label="Comment" onClick={submitComment} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
<script>
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import ArrowLeftIcon from 'carbon-icons-svelte/lib/ArrowLeft.svelte';
|
||||||
|
import TextEditor from '$lib/components/TextEditor/index.svelte';
|
||||||
|
import { currentOrg } from '$lib/utils/store/org';
|
||||||
|
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||||
|
import { askCommunityValidation } from '$lib/utils/functions/validator';
|
||||||
|
import { supabase } from '$lib/utils/functions/supabase';
|
||||||
|
import { SNACKBAR_SEVERITY } from '$lib/components/Snackbar/constants';
|
||||||
|
import { snackbarStore } from '$lib/components/Snackbar/store';
|
||||||
|
import generateSlug from '$lib/utils/functions/generateSlug';
|
||||||
|
|
||||||
|
let errors = {};
|
||||||
|
let fields = {
|
||||||
|
title: '',
|
||||||
|
body: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
errors = askCommunityValidation(fields);
|
||||||
|
console.log('handleSave errors', errors);
|
||||||
|
|
||||||
|
if (Object.keys(errors).length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: question, error } = await supabase
|
||||||
|
.from('community_question')
|
||||||
|
.insert({
|
||||||
|
...fields,
|
||||||
|
organization_id: $currentOrg.id,
|
||||||
|
author_id: $currentOrg.memberId,
|
||||||
|
votes: 0,
|
||||||
|
slug: generateSlug(fields.title)
|
||||||
|
})
|
||||||
|
.select();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('Error: asking question', error);
|
||||||
|
$snackbarStore.open = true;
|
||||||
|
$snackbarStore.message = 'Error - Please try again later';
|
||||||
|
$snackbarStore.severity = SNACKBAR_SEVERITY.ERROR;
|
||||||
|
} else {
|
||||||
|
console.log('Success: asking question', question);
|
||||||
|
$snackbarStore.open = true;
|
||||||
|
$snackbarStore.message = 'Success';
|
||||||
|
$snackbarStore.severity = SNACKBAR_SEVERITY.SUCCESS;
|
||||||
|
|
||||||
|
goto(`/lms/community/${question[0].slug}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Ask the Community - ClassroomIO</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<section class="w-full max-w-3xl mx-auto">
|
||||||
|
<div class="p-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" /> Go Back
|
||||||
|
</a>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h1 class="dark:text-white text-3xl font-bold">Ask the community</h1>
|
||||||
|
<PrimaryButton label="Publish" onClick={handleSave} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3 p-2">
|
||||||
|
<input
|
||||||
|
class="title rounded-lg border border-1 border-gray p-2 w-full focus:outline-none"
|
||||||
|
error="false"
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="Title"
|
||||||
|
aria-label="Title"
|
||||||
|
aria-describedby="title-input-validation"
|
||||||
|
type="text"
|
||||||
|
name="discussion_title"
|
||||||
|
id="discussion_title"
|
||||||
|
bind:value={fields.title}
|
||||||
|
/>
|
||||||
|
{#if errors.title}
|
||||||
|
<p class="dark:text-white text-red-500 text-sm">
|
||||||
|
{errors.title}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TextEditor
|
||||||
|
onChange={(v) => (fields.body = v)}
|
||||||
|
placeholder="Ask the community any question you might have"
|
||||||
|
errorMessage={errors.body}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
import { authValidation } from '$lib/utils/functions/validator';
|
import { authValidation } from '$lib/utils/functions/validator';
|
||||||
import { LOGIN_FIELDS } from '$lib/utils/constants/authentication';
|
import { LOGIN_FIELDS } from '$lib/utils/constants/authentication';
|
||||||
import AuthUI from '$lib/components/AuthUI/index.svelte';
|
import AuthUI from '$lib/components/AuthUI/index.svelte';
|
||||||
|
import { currentOrg } from '$lib/utils/store/org';
|
||||||
|
|
||||||
let formRef;
|
let formRef;
|
||||||
let supabase = getSupabase();
|
let supabase = getSupabase();
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Welcome back to ClassroomIO</title>
|
<title>Welcome back to {$currentOrg.name ?? 'ClassroomIO'}</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<AuthUI {supabase} isLogin={true} {handleSubmit} isLoading={loading} bind:formRef>
|
<AuthUI {supabase} isLogin={true} {handleSubmit} isLoading={loading} bind:formRef>
|
||||||
|
|||||||
Reference in New Issue
Block a user