From 13ab070178dbaa301516ef3a1be60143afd31c9c Mon Sep 17 00:00:00 2001 From: Adeyemi Adetunji Date: Tue, 13 Feb 2024 22:17:15 +0100 Subject: [PATCH] feat: filter community questions by course (#157) * checkpoint save * checkpoint * feat: course dropdown * added comments * feat: generate db migration * fixes * PR Fixes * PR Fixes * refactoring * show empty state * improve styling * fix styling issue * PR Fixes * PR Fixes * PR Fixes * dropdown for edit mode * test db schema change on staging * fix: change course_id to courseId * fix theming issue --------- Co-authored-by: Rotimi Best --- README.md | 1 - apps/dashboard/src/app.postcss | 9 +++ .../lib/components/Buttons/Close/index.svelte | 4 +- .../src/lib/components/Navigation/lms.svelte | 2 +- .../components/Org/Community/Loader.svelte | 2 +- .../lib/components/Org/Community/index.svelte | 68 +++++++++++++++---- .../Org/Settings/OrgSettings.svelte | 2 + .../src/lib/utils/functions/theme.ts | 8 +-- .../src/lib/utils/functions/validator.js | 4 ++ .../src/lib/utils/services/org/index.ts | 4 ++ apps/dashboard/src/routes/+layout.server.ts | 1 + apps/dashboard/src/routes/+layout.svelte | 7 +- .../routes/lms/community/[slug]/+page.svelte | 55 +++++++++++---- .../src/routes/lms/community/ask/+page.svelte | 43 ++++++++++-- .../org/[slug]/community/[slug]/+page.svelte | 54 +++++++++++---- .../org/[slug]/community/ask/+page.svelte | 45 ++++++++++-- ...855_add_courseid_to_community_question.sql | 20 ++++++ supabase/seed.sql | 2 +- 18 files changed, 268 insertions(+), 63 deletions(-) create mode 100644 supabase/migrations/20240212110855_add_courseid_to_community_question.sql diff --git a/README.md b/README.md index 94533fed..d941397e 100644 --- a/README.md +++ b/README.md @@ -154,7 +154,6 @@ This repo is a mono repo that consists of 3 projects: ```env PUBLIC_SUPABASE_URL= PUBLIC_SUPABASE_ANON_KEY= - PUBLIC_SUPABASE_BUCKET_URL= ``` - To view the Supabase studio, open the Studio URL from the result of `pnpm supabase start` diff --git a/apps/dashboard/src/app.postcss b/apps/dashboard/src/app.postcss index 8c831e6a..910bae5c 100644 --- a/apps/dashboard/src/app.postcss +++ b/apps/dashboard/src/app.postcss @@ -127,6 +127,15 @@ code { overflow-y: auto; } +.bx--list-box__menu-item__option { + white-space: normal; +} +.bx--list-box--xl .bx--list-box__menu-item, +.bx--list-box--lg .bx--list-box__menu-item, +.bx--list-box__menu-item__option { + height: fit-content !important; +} + @media screen and (max-width: 457px) { .org-slot { min-width: 100%; diff --git a/apps/dashboard/src/lib/components/Buttons/Close/index.svelte b/apps/dashboard/src/lib/components/Buttons/Close/index.svelte index a159b957..4e57b3a8 100644 --- a/apps/dashboard/src/lib/components/Buttons/Close/index.svelte +++ b/apps/dashboard/src/lib/components/Buttons/Close/index.svelte @@ -10,9 +10,9 @@ {#if size === 'large'} - + {:else} - + {/if} diff --git a/apps/dashboard/src/lib/components/Navigation/lms.svelte b/apps/dashboard/src/lib/components/Navigation/lms.svelte index 2362e5a1..90f8f1c7 100644 --- a/apps/dashboard/src/lib/components/Navigation/lms.svelte +++ b/apps/dashboard/src/lib/components/Navigation/lms.svelte @@ -64,7 +64,7 @@ {:else} {/if} diff --git a/apps/dashboard/src/lib/components/Org/Community/Loader.svelte b/apps/dashboard/src/lib/components/Org/Community/Loader.svelte index 1ffd356f..1c4092b3 100644 --- a/apps/dashboard/src/lib/components/Org/Community/Loader.svelte +++ b/apps/dashboard/src/lib/components/Org/Community/Loader.svelte @@ -3,7 +3,7 @@
diff --git a/apps/dashboard/src/lib/components/Org/Community/index.svelte b/apps/dashboard/src/lib/components/Org/Community/index.svelte index 3e49ae05..6e2ee905 100644 --- a/apps/dashboard/src/lib/components/Org/Community/index.svelte +++ b/apps/dashboard/src/lib/components/Org/Community/index.svelte @@ -9,20 +9,34 @@ import { currentOrgPath, currentOrg } from '$lib/utils/store/org'; import { supabase } from '$lib/utils/functions/supabase'; import { calDateDiff } from '$lib/utils/functions/date'; + import { fetchCourses } from '$lib/components/Courses/api'; + import { profile } from '$lib/utils/store/user'; + import { Search, Dropdown } from 'carbon-components-svelte'; export let isLMS = false; let isLoading = false; let discussions = []; + let searchValue = ''; + let allCourses: any[] = []; + let selectedId = ''; - async function fetchCommunityQuestions(orgId?: string) { - if (!orgId) return; + async function fetchCommunityQuestions(orgId?: string, profileId?: string) { + if (!orgId || !profileId) return; isLoading = true; + + const courseResult = (await fetchCourses(profileId, orgId)) || { allCourses: [] }; + allCourses = courseResult.allCourses; + + const courseIds = allCourses.map((course) => course.id); + const courseIdsFilter = `(${courseIds.join(',')})`; + const { data, error } = await supabase .from('community_question') .select( ` organization_id, + course_id, title, votes, created_at, @@ -30,13 +44,17 @@ comments:community_answer(count), author:profile( fullname + ), + course!inner ( + title ) ` ) - .eq('organization_id', orgId) + .filter('course_id', 'in', courseIdsFilter) .order('created_at', { ascending: false }); console.log('data', data); console.log('error', error); + isLoading = false; if (error) { @@ -47,6 +65,8 @@ discussions = data?.map((discussion) => ({ title: discussion.title, + courseId: discussion.course_id, + courseTitle: discussion.course?.title, slug: discussion.slug, author: discussion?.author?.fullname, comments: discussion.comments?.[0]?.count || 0, @@ -55,9 +75,29 @@ })) || []; } - $: fetchCommunityQuestions($currentOrg.id); + $: fetchCommunityQuestions($currentOrg.id, $profile.id); + $: filteredDiscussions = discussions.filter( + (discussion) => + discussion.title.toLowerCase().includes(searchValue.toLowerCase()) && + (!selectedId || discussion.courseId === selectedId) + ); +
+ + ({ id: course.id, text: course.title }))} + bind:selectedId + /> +
@@ -67,11 +107,11 @@ {:else} - {#each discussions as discussion} -
+ {#each filteredDiscussions as discussion} +
-
-

+
@@ -107,11 +152,4 @@ overflow-wrap: break-word; margin: 0; } - - .discussion-topic-author { - margin-right: 20px; - } - .discussion-topic-author span { - font-size: 12px; - } diff --git a/apps/dashboard/src/lib/components/Org/Settings/OrgSettings.svelte b/apps/dashboard/src/lib/components/Org/Settings/OrgSettings.svelte index fe77c09e..f95c7c07 100644 --- a/apps/dashboard/src/lib/components/Org/Settings/OrgSettings.svelte +++ b/apps/dashboard/src/lib/components/Org/Settings/OrgSettings.svelte @@ -92,6 +92,8 @@ function gotoSetting(pathname) { goto(`${$currentOrgPath}/settings${pathname}`); } + + $: console.log('theme', $currentOrg.theme); diff --git a/apps/dashboard/src/lib/utils/functions/theme.ts b/apps/dashboard/src/lib/utils/functions/theme.ts index 4b67e669..832f77ad 100644 --- a/apps/dashboard/src/lib/utils/functions/theme.ts +++ b/apps/dashboard/src/lib/utils/functions/theme.ts @@ -1,4 +1,4 @@ -export function setTheme(theme: string) { +export function setTheme(theme?: string) { // In case the default theme is added but another theme exists if (!theme && document.body.className.includes('theme-')) { const regex = /theme-[\w]+/gi; @@ -7,8 +7,8 @@ export function setTheme(theme: string) { } // In case theme already exists in dom, don't add - if (document.body.className.includes(theme)) return; + if (document.body.className.includes(theme || '')) return; - localStorage.setItem('theme', theme); - document.body.className = document.body.className.concat(' ', theme); + localStorage.setItem('theme', theme || ''); + document.body.className = document.body.className.concat(' ', theme || ''); } diff --git a/apps/dashboard/src/lib/utils/functions/validator.js b/apps/dashboard/src/lib/utils/functions/validator.js index 25dd9bc2..c6fed32c 100644 --- a/apps/dashboard/src/lib/utils/functions/validator.js +++ b/apps/dashboard/src/lib/utils/functions/validator.js @@ -17,6 +17,10 @@ const askCommunityValidationSchema = z.object({ body: z.string().min(20, { message: 'Must be 10 or more characters long', invalid_type_error: 'Must not be empty' + }), + courseId: z.string().min(36, { + message: 'Select a course', + invalid_type_error: 'Select a course' }) }); const commentInCommunityValidationSchema = z.object({ diff --git a/apps/dashboard/src/lib/utils/services/org/index.ts b/apps/dashboard/src/lib/utils/services/org/index.ts index 16ccbf77..1ede2739 100644 --- a/apps/dashboard/src/lib/utils/services/org/index.ts +++ b/apps/dashboard/src/lib/utils/services/org/index.ts @@ -200,6 +200,7 @@ export async function getCourseBySiteName(siteName: string) { } export async function getCurrentOrg(siteName: string, justGet = false) { + const { data, error } = await supabase .from('organization') .select( @@ -214,6 +215,9 @@ export async function getCurrentOrg(siteName: string, justGet = false) { ) .eq('siteName', siteName) .returns(); + console.log('data =', data); + console.log('error =', error); + const isDataEmpty = !data?.[0]; diff --git a/apps/dashboard/src/routes/+layout.server.ts b/apps/dashboard/src/routes/+layout.server.ts index 31acc1a7..e85176be 100644 --- a/apps/dashboard/src/routes/+layout.server.ts +++ b/apps/dashboard/src/routes/+layout.server.ts @@ -42,6 +42,7 @@ export const load = async ({ url, cookies }): Promise => { response.isOrgSite = debugMode || answer; response.orgSiteName = debugMode ? _orgSiteName : subdomain; response.org = (await getCurrentOrg(response.orgSiteName, true)) || null; + if (!response.org && !dev) { throw redirect(307, 'https://app.classroomio.com/404?type=org'); diff --git a/apps/dashboard/src/routes/+layout.svelte b/apps/dashboard/src/routes/+layout.svelte index e13f58b3..14bbe3b5 100644 --- a/apps/dashboard/src/routes/+layout.svelte +++ b/apps/dashboard/src/routes/+layout.svelte @@ -184,9 +184,8 @@ goto(`/org/${orgRes.currentOrg.siteName}`); } } - if (typeof orgRes?.currentOrg?.theme === 'string') { - setTheme(orgRes.currentOrg.theme); - } + + setTheme(orgRes?.currentOrg?.theme); } if (!profileData && !isPublicRoute($page.url?.pathname)) { @@ -286,7 +285,7 @@ {#if data.skipAuth} {:else if data.isOrgSite && !path} - + {:else}
{#if $navigating && $delayedPreloading} diff --git a/apps/dashboard/src/routes/lms/community/[slug]/+page.svelte b/apps/dashboard/src/routes/lms/community/[slug]/+page.svelte index cbe6b3e7..24f06f34 100644 --- a/apps/dashboard/src/routes/lms/community/[slug]/+page.svelte +++ b/apps/dashboard/src/routes/lms/community/[slug]/+page.svelte @@ -2,7 +2,7 @@ import { goto } from '$app/navigation'; import pluralize from 'pluralize'; import TrashCanIcon from 'carbon-icons-svelte/lib/TrashCan.svelte'; - import { SkeletonPlaceholder, SkeletonText } from 'carbon-components-svelte'; + import { Dropdown, 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'; @@ -10,7 +10,7 @@ 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 { isOrgAdmin } from '$lib/utils/store/org'; + import { currentOrg, isOrgAdmin } from '$lib/utils/store/org'; import { profile } from '$lib/utils/store/user'; import { supabase } from '$lib/utils/functions/supabase'; import { @@ -23,6 +23,7 @@ import TextEditor from '$lib/components/TextEditor/index.svelte'; import { calDateDiff } from '$lib/utils/functions/date'; import { browser } from '$app/environment'; + import { fetchCourses } from '$lib/components/Courses/api.js'; export let data; const { slug } = data; @@ -49,12 +50,14 @@ createdAt: string; comments: Comment[]; totalComments: number; + courseId: string; } let question: Question; let comment = ''; let errors: { title?: string; + courseId?: string; } = {}; let isValidAnswer = false; // V2 allow admin mark an answer as accepted let resetInput = 1; @@ -77,10 +80,12 @@ }; let editContent = { title: '', - body: '' + body: '', + courseId: '' }; let editorInstance = false; + let fetchedCourses = []; function mapResToQuestion(data): Question { return { @@ -103,10 +108,16 @@ comment: c.body, createdAt: calDateDiff(c.created_at) })), - totalComments: 0 + totalComments: 0, + courseId: data.course_id }; } + async function getCourses(userId: string | null, orgId: string) { + const coursesResults = await fetchCourses(userId, orgId); + fetchedCourses = coursesResults.allCourses; + } + async function fetchCommunityQuestion(slug: string) { if (!slug) return; @@ -119,6 +130,7 @@ body, votes, created_at, + course_id, slug, comments:community_answer( id, @@ -127,7 +139,10 @@ created_at, author:profile(id, fullname, avatar_url) ), - author:profile(id, fullname, avatar_url) + author:profile(id, fullname, avatar_url), + course!inner ( + title + ) ` ) .eq('slug', slug) @@ -227,8 +242,7 @@ async function handleQuestionEdit() { if (isEditMode) { - const fields = { title: editContent.title, body: editContent.body }; - errors = askCommunityValidation(fields); + errors = askCommunityValidation(editContent); console.log('handleQuestionEdit errors', errors); if (Object.keys(errors).length) { @@ -240,8 +254,7 @@ editorInstance = !editorInstance; if (!isEditMode) { - const fields = { title: editContent.title, body: editContent.body }; - errors = askCommunityValidation(fields); + errors = askCommunityValidation(editContent); console.log('handleQuestionEdit errors', errors); if (Object.keys(errors).length) { @@ -249,21 +262,27 @@ } const { error } = await supabase .from('community_question') - .update(fields) + .update({ + ...editContent, + course_id: editContent.courseId + }) .match({ id: question.id }); if (error) { console.error('Error: handleQuestionEdit', error); snackbar.error('Error - Please try again later'); } else { - question.title = fields.title; - question.body = fields.body; + question.title = editContent.title; + question.body = editContent.body; + question.courseId = editContent.courseId; editContent.title = ''; editContent.body = ''; + editContent.courseId = ''; } } else { editContent.title = question.title; editContent.body = question.body; + editContent.courseId = question.courseId; } } @@ -324,6 +343,11 @@ } $: browser && fetchCommunityQuestion(slug); + $: { + if ($profile.id && $currentOrg.id) { + getCourses($profile.id, $currentOrg.id); + } + } @@ -369,6 +393,13 @@ className="w-full mr-2" errorMessage={errors.title} /> + ({ id: course.id, text: course.title }))} + bind:selectedId={editContent.courseId} + /> {:else}
import { goto } from '$app/navigation'; import ArrowLeftIcon from 'carbon-icons-svelte/lib/ArrowLeft.svelte'; + import { Dropdown } from 'carbon-components-svelte'; import { currentOrg } from '$lib/utils/store/org'; import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; import { askCommunityValidation } from '$lib/utils/functions/validator'; @@ -10,15 +11,25 @@ import TextEditor from '$lib/components/TextEditor/index.svelte'; import TextField from '$lib/components/Form/TextField.svelte'; import { profile } from '$lib/utils/store/user'; + import { fetchCourses } from '$lib/components/Courses/api'; let errors: { title?: string; + courseId?: string; } = {}; let fields = { title: '', - body: '' + body: '', + courseId: '' }; + let fetchedCourses = []; + + async function getCourses(userId: string | null, orgId: string) { + const coursesResults = await fetchCourses(userId, orgId); + fetchedCourses = coursesResults.allCourses; + } + async function handleSave() { errors = askCommunityValidation(fields); console.log('handleSave errors', errors); @@ -30,7 +41,9 @@ const { data: question, error } = await supabase .from('community_question') .insert({ - ...fields, + title: fields.title, + body: fields.body, + course_id: fields.courseId, organization_id: $currentOrg.id, author_profile_id: $profile.id, votes: 0, @@ -44,11 +57,17 @@ } else { const slug = question[0]?.slug; console.log('Success: asking question in lms', question, slug); - snackbar.success(); + snackbar.success('Question Submitted!'); goto(`/lms/community/${slug}`); } } + + $: { + if ($profile.id && $currentOrg.id) { + getCourses($profile.id, $currentOrg.id); + } + } @@ -66,8 +85,22 @@
-
- +
+ + ({ id: course.id, text: course.title }))} + bind:selectedId={fields.courseId} + />
@@ -372,6 +395,13 @@ className="w-full mr-2" errorMessage={errors.title} /> + ({ id: course.id, text: course.title }))} + bind:selectedId={editContent.courseId} + /> {:else}
import { goto } from '$app/navigation'; + import { Dropdown } from 'carbon-components-svelte'; import ArrowLeftIcon from 'carbon-icons-svelte/lib/ArrowLeft.svelte'; import { currentOrgPath, currentOrg } from '$lib/utils/store/org'; import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; @@ -10,15 +11,27 @@ import TextEditor from '$lib/components/TextEditor/index.svelte'; import TextField from '$lib/components/Form/TextField.svelte'; import { profile } from '$lib/utils/store/user'; + import { fetchCourses } from '$lib/components/Courses/api'; + + export let data; let errors: { title?: string; + courseId?: string; } = {}; let fields = { title: '', - body: '' + body: '', + courseId: '' }; + let fetchedCourses = []; + + async function getCourses(userId: string | null, orgId: string) { + const coursesResults = await fetchCourses(userId, orgId); + fetchedCourses = coursesResults.allCourses; + } + async function handleSave() { errors = askCommunityValidation(fields); console.log('handleSave errors', errors); @@ -30,11 +43,13 @@ const { data: question, error } = await supabase .from('community_question') .insert({ - ...fields, + title: fields.title, + body: fields.body, organization_id: $currentOrg.id, author_profile_id: $profile.id, votes: 0, - slug: generateSlug(fields.title) + slug: generateSlug(fields.title), + course_id: fields.courseId }) .select(); @@ -49,6 +64,12 @@ goto(`${$currentOrgPath}/community/${slug}`); } } + + $: { + if ($profile.id && $currentOrg.id) { + getCourses($profile.id, $currentOrg.id); + } + } @@ -69,8 +90,22 @@
-
- +
+ + ({ id: course.id, text: course.title }))} + bind:selectedId={fields.courseId} + />