CLASSROOMIO-T-46: Forum CRUD MVP

Co-authored-by: Rotimi Best <[email protected]>

Merge-request: CLASSROOMIO-MR-13
Merged-by: Digdippa Team <[email protected]>
This commit is contained in:
Rotimi Best
2023-05-20 23:53:28 +00:00
committed by Space Cloud
co-authored by Rotimi Best
parent 779827ba46
commit c36a49a1ee
42 changed files with 871 additions and 1573 deletions
-1
View File
@@ -22,7 +22,6 @@
}
.bg-set {
color: #586069;
background-color: #d1d5da80;
}
</style>
@@ -24,7 +24,7 @@
import { course } from '../../store';
import { updateCourse } from '../../../../utils/services/courses';
import { profile } from '../../../../utils/store/user';
import { currentOrg } from '../../../../utils/store/org';
import { currentOrg, currentOrgPath } from '../../../../utils/store/org';
import { isMobile } from '../../../../utils/store/useMobile';
// export let lessonId;
@@ -209,7 +209,7 @@
>
<IconButton
value="toggle"
onClick={() => goto(`/org/${$currentOrg.siteName}/courses`)}
onClick={() => goto(`${$currentOrgPath}/courses`)}
buttonClassName="mx-3"
>
<Home32 class="carbon-icon" />
@@ -16,6 +16,7 @@
import PrimaryButton from '../../../PrimaryButton/index.svelte';
import { VARIANTS } from '../../../PrimaryButton/constants';
import { updateCourse } from '../../../../utils/services/courses';
import generateSlug from '../../../../utils/functions/generateSlug';
export let course;
export let courseId;
@@ -66,13 +67,6 @@
};
}
function generateSlug(title) {
return title
.replaceAll(' ', '-')
.toLowerCase()
.concat(`-${new Date().getTime()}`);
}
async function handleSave() {
loading = true;
course.slug = course.slug || generateSlug(course.title);
+4 -1
View File
@@ -4,6 +4,9 @@
<div class="break-all">
{#each tags as tag}
<a class="text-blue-700 mr-5 text-sm" href="?filter={tag}">#{tag.toLowerCase()}</a>
<a
class="text-blue-700 dark:text-blue-500 mr-5 text-sm"
href="?filter={tag}">#{tag.toLowerCase()}</a
>
{/each}
</div>
+8 -4
View File
@@ -1,5 +1,4 @@
<script>
import { onMount } from 'svelte';
import Box from '../../Box/index.svelte';
import AudienceList from './AudienceList.svelte';
import AudienceEmptyIcon from '../../Icons/AudienceEmptyIcon.svelte';
@@ -7,11 +6,16 @@
import { orgAudience, currentOrg } from '../../../utils/store/org';
let isLoading = false;
onMount(async () => {
async function fetchInitData(orgId) {
if (!orgId) return;
isLoading = true;
await getOrgAudience($currentOrg.id);
await getOrgAudience(orgId);
isLoading = false;
});
}
$: fetchInitData($currentOrg.id);
</script>
<div
@@ -0,0 +1,37 @@
<script>
import Modal from '../../Modal/index.svelte';
import PrimaryButton from '../../PrimaryButton/index.svelte';
import { VARIANTS } from '../../PrimaryButton/constants';
export let onDelete = () => {};
export let onCancel = () => {};
export let open = false;
</script>
<Modal
onClose={() => (open = false)}
bind:open
width="w-96"
modalHeading="Delete comment"
>
<div>
<h1 class="dark:text-white text-lg">
Are you sure you want to delete this comment?
</h1>
<div class="mt-5 flex items-center justify-between">
<PrimaryButton
className="px-6 py-3"
variant={VARIANTS.OUTLINED}
label="No, keep it"
onClick={onCancel}
/>
<PrimaryButton
className="px-6 py-3"
variant={VARIANTS.CONTAINED_DANGER}
label="Yes, delete"
onClick={onDelete}
/>
</div>
</div>
</Modal>
+110
View File
@@ -0,0 +1,110 @@
<script>
import { goto } from '@sapper/app';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import AddComment20 from 'carbon-icons-svelte/lib/AddComment20';
import Vote from '../../Vote/index.svelte';
import Box from '../../Box/index.svelte';
import CoursesEmptyIcon from '../../Icons/CoursesEmptyIcon.svelte';
import Space from '../../Space/index.svelte';
import { currentOrgPath, currentOrg } from '../../../utils/store/org';
import { supabase } from '../../../utils/functions/supabase';
let discussions = [];
dayjs.extend(relativeTime);
async function fetchCommunityQuestions(orgId) {
if (!orgId) return;
const { data, error } = await supabase
.from('community_question')
.select(
`
organization_id,
title,
votes,
created_at,
slug,
comments:community_answer(count),
author:organizationmember!community_question_author_id_fkey!inner(
profile!inner(fullname)
)
`
)
.eq('organization_id', orgId);
console.log('data', data);
console.log('error', error);
if (error) {
console.error('Error loading community', error);
return goto(`${currentOrgPath}`);
}
discussions =
data?.map((discussion) => ({
title: discussion.title,
slug: discussion.slug,
author: discussion?.author?.profile?.fullname,
comments: discussion.comments?.[0]?.count || 0,
votes: discussion.votes,
createdAt: dayjs(discussion.created_at).fromNow(true),
})) || [];
}
$: fetchCommunityQuestions($currentOrg.id);
</script>
<div
class="flex items-center justify-center lg:justify-start flex-wrap my-4 m-auto border-c rounded bg-gray-100 dark:bg-gray-700"
>
{#each discussions as discussion}
<div class="w-full flex border-bottom-c p-5">
<Vote value={discussion.votes} />
<div class="discussion-topic-author">
<h4>
<a
class="text-black dark:text-white"
rel="prefetch"
href="{$currentOrgPath}/community/{discussion.slug}"
>
{discussion.title}
</a>
</h4>
<span class="text-gray-600 dark:text-gray-100">
{discussion.author} asked {discussion.createdAt}
</span>
</div>
<Space />
<div class="flex items-center">
<AddComment20 />
<span class="ml-1">{discussion.comments}</span>
</div>
</div>
{:else}
<Box>
<CoursesEmptyIcon />
<h3 class="dark:text-white text-2xl my-5">No Questions asked</h3>
<p class="dark:text-white w-1/3 text-center">
Ask a question to the community
</p>
</Box>
{/each}
</div>
<style>
h4 {
font-size: 16px;
font-weight: 900;
word-break: break-word;
overflow-wrap: break-word;
margin: 0;
}
.discussion-topic-author {
margin-right: 20px;
}
.discussion-topic-author span {
font-size: 12px;
}
</style>
@@ -8,7 +8,7 @@
import { supabase } from '../../../utils/functions/supabase';
import { snackbarStore } from '../../Snackbar/store';
import { SNACKBAR_SEVERITY } from '../../Snackbar/constants';
import { currentOrg } from '../../../utils/store/org';
import { currentOrg, currentOrgPath } from '../../../utils/store/org';
import SectionTitle from '../SectionTitle.svelte';
let avatar;
@@ -74,7 +74,7 @@
}
function gotoSetting(pathname) {
goto(`/org/${$currentOrg.siteName}/settings${pathname}`);
goto(`${$currentOrgPath}/settings${pathname}`);
}
</script>
+51 -51
View File
@@ -1,6 +1,7 @@
<script>
import { stores } from '@sapper/app';
import Help from 'carbon-icons-svelte/lib/Help20';
import UserMultipleIcon from 'carbon-icons-svelte/lib/UserMultiple20';
import TextChip from '../../components/Chip/Text.svelte';
import OrgSelector from '../../components/OrgSelector/OrgSelector.svelte';
@@ -9,13 +10,35 @@
import SiteSettingsIcon from '../../components/Icons/SiteSettingsIcon.svelte';
import AudienceIcon from '../../components/Icons/AudienceIcon.svelte';
import Avatar from '../../components/Avatar/index.svelte';
import { currentOrg } from '../../utils/store/org';
import { currentOrg, currentOrgPath } from '../../utils/store/org';
import { profile } from '../../utils/store/user';
let orgSiteName = '';
let activeClass = 'bg-gray-200 dark:bg-gray-500';
let { page } = stores();
const menuItems = [
{
path: '',
label: 'Home',
},
{
path: '/courses',
label: 'Courses',
},
{
path: '/community',
label: 'Community',
},
{
path: '/site',
label: 'Site settings',
},
{
path: '/audience',
label: 'Audience',
},
];
function isActive(pagePath, itemPath) {
const pageLinkItems = pagePath.split('/');
const itemLinkItems = itemPath.split('/');
@@ -26,8 +49,6 @@
return pagePath.includes(itemPath);
}
$: orgSiteName = $currentOrg.siteName;
</script>
<aside class="root bg-gray-100 dark:bg-gray-700 h-full">
@@ -51,55 +72,34 @@
<OrgSelector />
<ul class="my-5">
<a href="/org/{orgSiteName}" class="text-black">
<li
class="flex items-center py-3 px-4 mb-2 rounded hover:bg-gray-200 dark:hover:bg-gray-500 {isActive(
$page.path,
`/org/${orgSiteName}`
) && activeClass}"
>
<HomeIcon />
<p class="dark:text-white ml-2">Home</p>
</li>
</a>
<a href="/org/{orgSiteName}/courses" class="text-black">
<li
class="flex items-center py-3 px-4 mb-2 rounded hover:bg-gray-200 dark:hover:bg-gray-500 {isActive(
$page.path,
`/org/${orgSiteName}/courses`
) && activeClass}"
>
<CourseIcon />
<p class="dark:text-white ml-2">Courses</p>
</li>
</a>
<a href="/org/{orgSiteName}/site" class="text-black">
<li
class="flex items-center py-3 px-4 mb-2 rounded hover:bg-gray-200 dark:hover:bg-gray-500 {isActive(
$page.path,
`/org/${orgSiteName}/site`
) && activeClass}"
>
<SiteSettingsIcon />
<p class="dark:text-white ml-2">Site settings</p>
</li>
</a>
<a href="/org/{orgSiteName}/audience" class="text-black">
<li
class="flex items-center py-3 px-4 mb-2 rounded hover:bg-gray-200 dark:hover:bg-gray-500 {isActive(
$page.path,
`/org/${orgSiteName}/audience`
) && activeClass}"
>
<AudienceIcon />
<p class="dark:text-white ml-2">Audience</p>
</li>
</a>
{#each menuItems as menuItem}
<a href="{$currentOrgPath}{menuItem.path}" class="text-black">
<li
class="flex items-center py-3 px-4 mb-2 rounded hover:bg-gray-200 dark:hover:bg-gray-500 {isActive(
$page.path,
`${$currentOrgPath}${menuItem.path}`
) && activeClass}"
>
{#if menuItem.path === ''}
<HomeIcon />
{:else if menuItem.path === '/courses'}
<CourseIcon />
{:else if menuItem.path === '/site'}
<SiteSettingsIcon />
{:else if menuItem.path === '/community'}
<UserMultipleIcon class="carbon-icon" />
{:else if menuItem.path === '/audience'}
<AudienceIcon />
{/if}
<p class="dark:text-white ml-2">{menuItem.label}</p>
</li>
</a>
{/each}
</ul>
</div>
<span class="flex-grow" />
<ul class="my-5 pb-5 px-4">
<a href="/org" class="text-black">
<a href={$currentOrgPath} class="text-black">
<li
class="flex items-center py-3 px-4 mb-2 rounded hover:bg-gray-200 dark:hover:bg-gray-500"
>
@@ -107,11 +107,11 @@
<p class="dark:text-white ml-2">Help</p>
</li>
</a>
<a href="/org/{orgSiteName}/settings" class="text-black">
<a href="{$currentOrgPath}/settings" class="text-black">
<li
class="flex items-center py-3 px-4 mb-2 rounded hover:bg-gray-200 dark:hover:bg-gray-500 {isActive(
$page.path,
`/org/${orgSiteName}/settings`
`${$currentOrgPath}/settings`
) && activeClass}"
>
<Avatar
+1 -1
View File
@@ -20,7 +20,7 @@
</script>
<div
class="header dark:bg-gray-700 flex items-center justify-between {!disableSticky &&
class="header dark:bg-gray-700 bg-white flex items-center justify-between {!disableSticky &&
'sticky'} {paddingClass} {dynamicRootClass}"
style={overidableStyle}
>
+4 -2
View File
@@ -1,5 +1,6 @@
export const VARIANTS = {
CONTAINED: 'CONTAINED',
CONTAINED_INFO: 'CONTAINED_INFO',
CONTAINED_DANGER: 'CONTAINED_DANGER',
CONTAINED_SUCCESS: 'CONTAINED_SUCCESS',
OUTLINED: 'OUTLINED',
@@ -10,14 +11,15 @@ export const VARIANTS = {
export const VARIANTS_CLASS = {
[VARIANTS.CONTAINED]: 'border-none bg-blue-700 hover:bg-blue-900 text-white',
[VARIANTS.CONTAINED_INFO]:
'border-none bg-gray-400 hover:bg-gray-600 text-white',
[VARIANTS.CONTAINED_SUCCESS]: 'bg-green-700 hover:bg-green-900 text-white',
[VARIANTS.CONTAINED_DANGER]: 'bg-red-700 hover:bg-red-900 text-white',
[VARIANTS.OUTLINED]:
'border border-blue-700 hover:border-black-300 hover:bg-blue-200 text-black',
[VARIANTS.NONE]:
'border-none hover:border-black-300 hover:bg-gray-200 dark:hover:bg-gray-500 text-black',
[VARIANTS.TEXT]:
'text-black hover:border border-t-0 border-l-0 border-r-0 border-black-300',
[VARIANTS.TEXT]: 'text-black hover:underline',
[VARIANTS.TEXT_DANGER]:
'text-red-500 hover:border border-t-0 border-l-0 border-r-0 border-red-300',
};
+4 -3
View File
@@ -16,11 +16,12 @@
</script>
<button
class="{isDisabled &&
'bg-opacity-25 hover:bg-opacity-25 cursor-not-allowed'} flex items-center h-auto {VARIANTS_CLASS[
class="{isDisabled
? 'bg-opacity-25 hover:bg-opacity-25 cursor-not-allowed'
: 'cursor-pointer'} flex items-center h-auto {VARIANTS_CLASS[
isLoading ? VARIANTS.OUTLINED : variant
]} {!disablePadding &&
'py-1 px-5'} rounded-none {className} py-3 w-fit justify-center sm:w-auto {isLoading &&
'py-3 px-5'} rounded-none {className} w-fit justify-center sm:w-auto {isLoading &&
'py-3 border-blue-700'} hover:shadow-xl transition delay-150 duration-300 ease-in-out"
on:click={onClick}
{name}
+20
View File
@@ -6,6 +6,7 @@
export let disable;
export let docId;
export let placeholder = 'Type something...';
export let errorMessage;
const settings = {
modules: {
@@ -74,4 +75,23 @@
<div class="p-2 w-full h-full">
<div bind:this={container} />
{#if errorMessage}
<p class="dark:text-white text-red-500 text-sm">
{errorMessage}
</p>
{/if}
</div>
<style>
:global(.dark .ql-stroke) {
stroke: #fff;
}
:global(.dark .ql-snow .ql-picker) {
color: #fff;
}
:global(.dark .ql-editor.ql-blank::before) {
color: #fff;
}
</style>
+6 -5
View File
@@ -3,13 +3,14 @@
import Chip from '../Chip/index.svelte';
export let value = 0;
function upVote() {
value = value + 1;
}
export let upVote = () => {};
export let disabled = false;
</script>
<div class="vote border-none cursor-pointer" on:click={upVote}>
<div
class="vote border-none {disabled ? 'cursor-not-allowed' : 'cursor-pointer'}"
on:click={disabled ? undefined : upVote}
>
<ArrowUp16 />
<Chip {value} />
</div>