CLASSROOMIO-T-46: Forum CRUD MVP
Co-authored-by: Rotimi Best <rotimiibitoyeemma@gmail.com> Merge-request: CLASSROOMIO-MR-13 Merged-by: Digdippa Team <digdippa@gmail.com>
This commit is contained in:
committed by
Space Cloud
co-authored by
Rotimi Best
parent
779827ba46
commit
c36a49a1ee
@@ -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,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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -16,10 +16,6 @@
|
||||
|
||||
<svelte:head>
|
||||
<title>{status}</title>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://unpkg.com/carbon-components-svelte/css/g90.css"
|
||||
/>
|
||||
</svelte:head>
|
||||
|
||||
<div class="w-3/5 h-3/5 m-auto flex flex-col items-center justify-center">
|
||||
|
||||
@@ -102,6 +102,9 @@
|
||||
console.error('Error adding user to organisation', error);
|
||||
} else {
|
||||
console.log('Success adding user to organisation', data);
|
||||
const memberId = data?.[0]?.id || '';
|
||||
|
||||
$currentOrg.memberId = memberId;
|
||||
}
|
||||
|
||||
if (params.has('redirect')) {
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
<!-- <script context="module">
|
||||
export async function preload(page, { user, config }) {
|
||||
return { user, config };
|
||||
}
|
||||
</script> -->
|
||||
<script>
|
||||
import GroupPresentation32 from 'carbon-icons-svelte/lib/GroupPresentation32';
|
||||
import UserSpeaker32 from 'carbon-icons-svelte/lib/UserSpeaker32';
|
||||
import Book32 from 'carbon-icons-svelte/lib/Book32';
|
||||
|
||||
let borderBottomGrey = 'border-r-0 border-b border-l-0 border-gray-300';
|
||||
let animate = 'transition delay-75 duration-300 ease-in-out';
|
||||
let boxes = [
|
||||
{
|
||||
label: 'Students',
|
||||
value: 50,
|
||||
icon: 'students',
|
||||
},
|
||||
{
|
||||
label: 'Tutors',
|
||||
value: 10,
|
||||
icon: 'tutor',
|
||||
},
|
||||
{
|
||||
label: 'Courses',
|
||||
value: 20,
|
||||
icon: 'courses',
|
||||
},
|
||||
];
|
||||
|
||||
let topStudents = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Brad Traversy',
|
||||
score: 97.8,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Ebuka Jacobs',
|
||||
score: 96,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Ngozi Okonjo Iwuala',
|
||||
score: 95,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Nathaniel Bassey',
|
||||
score: 80,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Barack Obama',
|
||||
score: 79,
|
||||
},
|
||||
];
|
||||
|
||||
let topCourses = [
|
||||
{
|
||||
title: 'CityBIM Basic Module',
|
||||
rating: 5,
|
||||
},
|
||||
{
|
||||
title: 'CityBIM Advanced Module',
|
||||
rating: 4.5,
|
||||
},
|
||||
{
|
||||
title: 'Introduction to ClasroomIO',
|
||||
rating: 4.2,
|
||||
},
|
||||
{
|
||||
title: 'Advanced Builders Module',
|
||||
rating: 4.1,
|
||||
},
|
||||
{
|
||||
title: 'Artificial Intelligence - Basics',
|
||||
rating: 4,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Admin Dashboard - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="root w-full py-10 bg-gray-100 dark:bg-gray-700">
|
||||
<div class="flex items-center justify-evenly max-w-6xl m-auto">
|
||||
{#each boxes as box}
|
||||
<div
|
||||
class="bg-white dark:bg-gray-800 rounded-md py-3 px-16 active shadow-md hover:shadow-2xl border-2 hover:border-blue-700 cursor-pointer {animate}"
|
||||
>
|
||||
{#if box.icon === 'students'}
|
||||
<UserSpeaker32 class="carbon-icon" />
|
||||
{:else if box.icon === 'tutor'}
|
||||
<GroupPresentation32 class="carbon-icon" />
|
||||
{:else if box.icon === 'courses'}
|
||||
<Book32 class="carbon-icon" />
|
||||
{/if}
|
||||
<p class="dark:text-white text-xl">{box.label}</p>
|
||||
<h3 class="dark:text-white text-3xl m-0 my-2">{box.value}</h3>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-evenly mt-16">
|
||||
<div class="table rounded-md bg-white dark:bg-gray-800 shadow-md">
|
||||
<div class="flex border-t-0 {borderBottomGrey} p-3">
|
||||
<h3 class="dark:text-white m-0 py-3 pl-2 text-lg">
|
||||
Outstanding students
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{#each topStudents as topStudent, index}
|
||||
<div
|
||||
class="flex items-center p-3 cursor-pointer {borderBottomGrey} hover:bg-gray-100 dark:bg-gray-700"
|
||||
>
|
||||
<span class="mr-2">{index + 1}</span>
|
||||
<p class="dark:text-white text-lg">{topStudent.name}</p>
|
||||
<span class="flex-grow" />
|
||||
<p class="dark:text-white text-lg">{topStudent.score}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="table rounded-md bg-white dark:bg-gray-800 shadow-md">
|
||||
<div class="flex border-t-0 {borderBottomGrey} p-3">
|
||||
<h3 class="dark:text-white m-0 py-3 pl-2 text-lg">Top rated courses</h3>
|
||||
</div>
|
||||
|
||||
{#each topCourses as topCourse, index}
|
||||
<div
|
||||
class="flex items-center p-3 cursor-pointer {borderBottomGrey} hover:bg-gray-100 dark:bg-gray-700"
|
||||
>
|
||||
<span class="mr-2">{index + 1}</span>
|
||||
<p class="dark:text-white text-lg">{topCourse.title}</p>
|
||||
<span class="flex-grow" />
|
||||
<p class="dark:text-white text-lg">{topCourse.rating}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
min-height: 93vh;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 564px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,399 +0,0 @@
|
||||
import { QUESTION_TYPE } from '../../components/Question/constants';
|
||||
|
||||
const ROLE = {
|
||||
ADMIN: 0,
|
||||
TUTOR: 1,
|
||||
STUDENT: 2,
|
||||
};
|
||||
|
||||
const tutors = [
|
||||
{
|
||||
id: 1,
|
||||
text: 'Sergey Semko',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
text: 'Jonathan Nelson',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
text: 'Sasha Pokidin',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
text: 'Natasha Rudenko',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
text: 'Yulia Marushko',
|
||||
},
|
||||
];
|
||||
|
||||
const people = [
|
||||
{
|
||||
name: 'Alexander Pokidin',
|
||||
title: 'Senior Software Engineer',
|
||||
avatar: 'https://i.pravatar.cc/150?img=1',
|
||||
role: ROLE.TUTOR,
|
||||
telegramLink: 'https://t.me/seed_of_abraham',
|
||||
mailLink: 'mailto:irb.ossystem@gmail.com',
|
||||
},
|
||||
{
|
||||
name: 'Andrey Filatov',
|
||||
title: 'Fullstack Software Engineer',
|
||||
avatar: 'https://i.pravatar.cc/150?img=2',
|
||||
role: ROLE.TUTOR,
|
||||
telegramLink: 'https://t.me/seed_of_abraham',
|
||||
mailLink: 'mailto:irb.ossystem@gmail.com',
|
||||
},
|
||||
{
|
||||
name: 'Sergey Semko',
|
||||
title: 'Senior Software Engineer',
|
||||
avatar: 'https://i.pravatar.cc/150?img=3',
|
||||
role: ROLE.TUTOR,
|
||||
telegramLink: 'https://t.me/seed_of_abraham',
|
||||
mailLink: 'mailto:irb.ossystem@gmail.com',
|
||||
},
|
||||
{
|
||||
name: 'Natalia Fedii',
|
||||
title: 'Project Manager',
|
||||
avatar: 'https://i.pravatar.cc/150?img=4',
|
||||
role: ROLE.TUTOR,
|
||||
telegramLink: 'https://t.me/seed_of_abraham',
|
||||
mailLink: 'mailto:irb.ossystem@gmail.com',
|
||||
},
|
||||
];
|
||||
|
||||
const courses = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'React.js Fundamentals',
|
||||
status: '',
|
||||
},
|
||||
];
|
||||
|
||||
// many to many: id, tutor_id, course_id
|
||||
const course_tutors = [
|
||||
{
|
||||
id: 1,
|
||||
tutor_id: 1,
|
||||
course_id: 1,
|
||||
},
|
||||
];
|
||||
|
||||
// id, course_id
|
||||
const lessons = [
|
||||
{
|
||||
id: 1,
|
||||
course_id: 1,
|
||||
title: 'Soft skills - Team work',
|
||||
to: '/courses/1/lessons/1',
|
||||
tutor: {
|
||||
userId: 1,
|
||||
avatar: 'https://picsum.photos/32/32/?random',
|
||||
name: 'Sergey Semko',
|
||||
},
|
||||
resources: [
|
||||
{
|
||||
label: 'lesson',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: 'quiz',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: 'home task',
|
||||
value: 2,
|
||||
},
|
||||
],
|
||||
date: new Date(),
|
||||
isComplete: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
course_id: 1,
|
||||
title: 'Introduction to ReactJS',
|
||||
to: '/courses/1/lessons/2',
|
||||
tutor: {
|
||||
userId: 1,
|
||||
avatar: 'https://picsum.photos/32/32/?random',
|
||||
name: 'Sergey Semko',
|
||||
},
|
||||
resources: [
|
||||
{
|
||||
label: 'lesson',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: 'quiz',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: 'home task',
|
||||
value: 2,
|
||||
},
|
||||
],
|
||||
date: new Date(),
|
||||
isComplete: false,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
course_id: 1,
|
||||
title: 'Components - Functional and Class',
|
||||
to: '/courses/1/lessons/3',
|
||||
tutor: {
|
||||
userId: 1,
|
||||
avatar: 'https://picsum.photos/32/32/?random',
|
||||
name: 'Sergey Semko',
|
||||
},
|
||||
resources: [
|
||||
{
|
||||
label: 'lesson',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: 'quiz',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: 'home task',
|
||||
value: 2,
|
||||
},
|
||||
],
|
||||
date: new Date(),
|
||||
isComplete: false,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
course_id: 1,
|
||||
title: 'State and Props in components',
|
||||
to: '/courses/1/lessons/4',
|
||||
tutor: {
|
||||
userId: 1,
|
||||
avatar: 'https://picsum.photos/32/32/?random',
|
||||
name: 'Sergey Semko',
|
||||
},
|
||||
resources: [
|
||||
{
|
||||
label: 'lesson',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: 'quiz',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: 'home task',
|
||||
value: 2,
|
||||
},
|
||||
],
|
||||
date: new Date(),
|
||||
isComplete: false,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
course_id: 1,
|
||||
title: 'Life cycle methods',
|
||||
to: '/courses/1/lessons/5',
|
||||
tutor: {
|
||||
userId: 1,
|
||||
avatar: 'https://picsum.photos/32/32/?random',
|
||||
name: 'Sergey Semko',
|
||||
},
|
||||
resources: [
|
||||
{
|
||||
label: 'lesson',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: 'quiz',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: 'home task',
|
||||
value: 2,
|
||||
},
|
||||
],
|
||||
date: new Date(),
|
||||
isComplete: false,
|
||||
},
|
||||
];
|
||||
|
||||
const lessonData = [
|
||||
{
|
||||
lesson_id: 1,
|
||||
materials: {
|
||||
note: `# Introduction\n\nWelcome to this course, we are glad you made it here\n\n## What you'l learn\n\n1. Build powerful, fast, user-friendly and reactive web apps.\n2. Provide amazing user experiences by leveraging the power of JavaScript with ease.\n3. Apply for high-paid jobs or work as a freelancer in one the most-demanded sectors you can find in web dev right now.\n4. Learn all about React Hooks and React Components.`,
|
||||
slide:
|
||||
'https://docs.google.com/presentation/d/e/2PACX-1vS7mulzY5v1X_D0Gz68M2sggq2zjHCuoyY5JI07hcsOA4svDV5rxhLD9V3ckO_Rz7TK1YniQJtu0CXM/embed?start=false&loop=false&delayms=3000',
|
||||
video: 'https://www.youtube.com/embed/luM5uobewhA',
|
||||
},
|
||||
exercises: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Practice Exercise',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Home task',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// id, lesson_id
|
||||
const lesson_exercise = [
|
||||
{
|
||||
id: 1,
|
||||
lesson_id: 1,
|
||||
title: 'Home task 1',
|
||||
description:
|
||||
"You will be to answer 10 questions, it isn't timed so you can take your time to answer. You can also continue from where you left off, you don't need to worry cause everything is automatically syncronized in the cloud.",
|
||||
questions: [
|
||||
{
|
||||
id: 'react-founder',
|
||||
type: QUESTION_TYPE.CHECKBOX,
|
||||
title: 'Who is the creator of React.js',
|
||||
options: [
|
||||
{
|
||||
id: '1',
|
||||
value: 'Dan Abrahmov',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
value: 'Google',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
value: 'Facebook',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
value: 'Traversy Media',
|
||||
},
|
||||
],
|
||||
answers: ['3'],
|
||||
},
|
||||
{
|
||||
id: 'vue-founder',
|
||||
type: QUESTION_TYPE.CHECKBOX,
|
||||
title: 'Who is the creator of Vue.js',
|
||||
code: `const name = 'Josh Perez';\nconst element = <h1>Hello, {name}</h1>;\n\nReactDOM.render(\n
|
||||
element,\,
|
||||
document.getElementById('root')
|
||||
)
|
||||
`,
|
||||
options: [
|
||||
{
|
||||
id: '1',
|
||||
value: 'Evan Vue',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
value: 'Mark Zukerberg',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
value: 'Prince Charles',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
value: 'Bill Gates',
|
||||
},
|
||||
],
|
||||
answers: ['3'],
|
||||
},
|
||||
{
|
||||
id: 'svelte-founder',
|
||||
type: QUESTION_TYPE.RADIO,
|
||||
title: 'Who is the creator of Svelte.js',
|
||||
options: [
|
||||
{
|
||||
id: '1',
|
||||
value: 'Hillary Svelte',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
value: 'Mircosoft',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
value: 'Elevate',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
value: 'Coding Train',
|
||||
},
|
||||
],
|
||||
answers: ['2'],
|
||||
},
|
||||
{
|
||||
id: 'angular-founder',
|
||||
type: QUESTION_TYPE.TEXTAREA,
|
||||
title: 'Who is the creator of Angular.js',
|
||||
value: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const exercise_answers = [
|
||||
{
|
||||
lesson_id: 1,
|
||||
exercise_id: 1,
|
||||
taken_by: {},
|
||||
answers: {},
|
||||
scores: {},
|
||||
currentQuestionIndex: 0,
|
||||
isFinished: false,
|
||||
progressValue: 100,
|
||||
status: 'STATUS.PENDING',
|
||||
},
|
||||
];
|
||||
|
||||
// const lessonLookup = new Map();
|
||||
// lessons.forEach((lesson) => {
|
||||
// lessonLookup.set(`${lesson.id}`, JSON.stringify(lesson));
|
||||
// });
|
||||
|
||||
export function getCourse(id) {
|
||||
return {
|
||||
id,
|
||||
name: 'React.js',
|
||||
people,
|
||||
roles: ROLE,
|
||||
lessons: getLessons(id),
|
||||
};
|
||||
}
|
||||
|
||||
export function getLessons(courseId) {
|
||||
return lessons.filter((lesson) => lesson.course_id === courseId);
|
||||
}
|
||||
|
||||
export function getLesson(id) {
|
||||
return (
|
||||
lessonData.find((lesson) => lesson.lesson_id === id) || {
|
||||
lesson_id: null,
|
||||
materials: {
|
||||
note: '',
|
||||
slide_url: '',
|
||||
video_url: '',
|
||||
},
|
||||
exercises: getExercises(id),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function getExercises(lessonId) {
|
||||
return lesson_exercise.filter((exercise) => exercise.lesson_id === lessonId);
|
||||
}
|
||||
|
||||
export function getExercise(exerciseId) {
|
||||
return {
|
||||
data: lesson_exercise.find((exercise) => exercise.id === exerciseId),
|
||||
answers: exercise_answers.find(
|
||||
(answer) => answer.exercise_id === exerciseId
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { getSupabase } from '../../utils/functions/supabase';
|
||||
|
||||
export function post(req, res) {
|
||||
const supabase = getSupabase();
|
||||
|
||||
supabase.auth.api.setAuthCookie(req, res);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { getCourse, getExercise, getLesson } from './_db';
|
||||
|
||||
function writeJsonHead(res, statusCode) {
|
||||
res.writeHead(statusCode, {
|
||||
'Content-Type': 'application/json',
|
||||
});
|
||||
}
|
||||
|
||||
export function get(req, res, next) {
|
||||
const { id, lessonId, exerciseId } = req.query;
|
||||
|
||||
const result = {
|
||||
...getCourse(parseInt(id, 2)),
|
||||
};
|
||||
|
||||
if (lessonId) {
|
||||
result.lesson = getLesson(parseInt(lessonId, 2));
|
||||
}
|
||||
|
||||
if (exerciseId) {
|
||||
result.exercise = getExercise(parseInt(exerciseId, 2));
|
||||
}
|
||||
|
||||
writeJsonHead(res, 200);
|
||||
|
||||
return res.end(JSON.stringify(result));
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { getLessons } from './_db';
|
||||
|
||||
function writeJsonHead(res, statusCode) {
|
||||
res.writeHead(statusCode, {
|
||||
'Content-Type': 'application/json',
|
||||
});
|
||||
}
|
||||
|
||||
export function get(req, res, next) {
|
||||
const { id } = req.query;
|
||||
|
||||
writeJsonHead(res, 200);
|
||||
|
||||
if (id) {
|
||||
return res.end(JSON.stringify(getLesson(id)));
|
||||
}
|
||||
|
||||
return res.end(JSON.stringify(getLessons()));
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
function writeJsonHead(res, statusCode) {
|
||||
res.writeHead(statusCode, {
|
||||
'Content-Type': 'application/json',
|
||||
});
|
||||
}
|
||||
|
||||
export function post(req, res) {
|
||||
const { email } = req.body;
|
||||
|
||||
console.log(`req.session`, req.session);
|
||||
|
||||
writeJsonHead(res, 200);
|
||||
|
||||
return res.end(JSON.stringify({ success: true }));
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
<script>
|
||||
import ChevronDown24 from 'carbon-icons-svelte/lib/ChevronDown24';
|
||||
import TextEditor from '../components/TextEditor/index.svelte';
|
||||
|
||||
let questionTitle = '';
|
||||
let questionDescription = '';
|
||||
let facultyFilterOpened = false;
|
||||
|
||||
const faculties = [
|
||||
'ИЭКСУ',
|
||||
'ИМБТ',
|
||||
'ИМИ',
|
||||
'ИПТДМ',
|
||||
'ИИИР',
|
||||
'ГФ',
|
||||
'ХТФ',
|
||||
'ИДЗО',
|
||||
'УИИ',
|
||||
'УНИ',
|
||||
];
|
||||
let checkbox = faculties.reduce((acc, cur) => {
|
||||
acc[cur] = false;
|
||||
return acc;
|
||||
}, {});
|
||||
$: clickedTotal = Object.values(checkbox).filter((box) => !!box).length;
|
||||
|
||||
function handleFacultyFilters() {
|
||||
facultyFilterOpened = facultyFilterOpened ? false : true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Спросить собшество вопрос</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="root lg:w-3/5 mx-auto md:mx-10 lg:mb-20">
|
||||
<h2 class="pb-2">Спросить собшество вопрос</h2>
|
||||
<div class="main-title my-1 flex">
|
||||
<div class="faculty-filter mr-1">
|
||||
<button
|
||||
class="flex border rounded-lg border-grey py-3 px-3 focus:outline-none focus:border-gray-400 focus:bg-gray-200 relative"
|
||||
on:click={handleFacultyFilters}
|
||||
>
|
||||
<span class="flex flex-row"
|
||||
>Факултеты <span> ({clickedTotal})</span></span
|
||||
>
|
||||
<ChevronDown24 class="ml-2" />
|
||||
</button>
|
||||
|
||||
{#if facultyFilterOpened}
|
||||
<div
|
||||
class="w-48 bg-white dark:bg-gray-800 border border-grey rounded-lg mt-2 py-3 absolute"
|
||||
>
|
||||
{#each faculties as faculty}
|
||||
<div class="block px-4 py-3 border-b">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="border border-gray checked:bg-blue-600 checked:border-transparent mr-1 cursor-pointer"
|
||||
bind:checked={checkbox[faculty]}
|
||||
/>
|
||||
<span>{faculty}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<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"
|
||||
value={questionTitle}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TextEditor
|
||||
value={questionDescription}
|
||||
placeholder="Задайте вопрос или начните разговор"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
max-width: 600px;
|
||||
margin: 10px auto 0;
|
||||
}
|
||||
@media only screen and (max-width: 1002px) {
|
||||
.faculty-filter {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.main-title {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,96 +0,0 @@
|
||||
<script context="module">
|
||||
import { profile } from '../utils/store/user';
|
||||
import { fetchCourses } from '../components/Courses/api';
|
||||
|
||||
export async function preload(page, params) {
|
||||
const parsedUser = JSON.parse(params.user);
|
||||
|
||||
// if (parsedUser && parsedUser.id) {
|
||||
// return fetchCourses(parsedUser.id);
|
||||
// }
|
||||
|
||||
return {
|
||||
allCourses: [],
|
||||
organizationId: null,
|
||||
cantFetch: true,
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import PageNav from '../components/PageNav/index.svelte';
|
||||
import Courses from '../components/Courses/index.svelte';
|
||||
import NewCourseModal from '../components/Courses/components/NewCourseModal/index.svelte';
|
||||
import PrimaryButton from '../components/PrimaryButton/index.svelte';
|
||||
import {
|
||||
courses,
|
||||
createCourseModal,
|
||||
courseMetaDeta,
|
||||
} from '../components/Courses/store';
|
||||
import { setProfileIdOfGroupMember } from '../utils/services/courses';
|
||||
|
||||
export let allCourses = [];
|
||||
export let cantFetch;
|
||||
|
||||
async function getCourses(userId) {
|
||||
if (process.browser) {
|
||||
if (cantFetch && typeof cantFetch === 'boolean' && !allCourses.length) {
|
||||
$courseMetaDeta.isLoading = true;
|
||||
|
||||
const updatedProfile = await setProfileIdOfGroupMember(
|
||||
$profile.email,
|
||||
userId
|
||||
);
|
||||
console.log(`updatedProfile`, updatedProfile);
|
||||
const coursesResult = await fetchCourses(userId);
|
||||
console.log(`coursesResult`, coursesResult);
|
||||
|
||||
$courseMetaDeta.isLoading = false;
|
||||
if (!coursesResult) return;
|
||||
|
||||
// organizationId = coursesResult.organizationId;
|
||||
courses.set(coursesResult.allCourses);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
courses.set(allCourses);
|
||||
});
|
||||
|
||||
$: getCourses($profile.id);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Courses - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="root">
|
||||
<PageNav title="All courses">
|
||||
<div slot="widget">
|
||||
{#if $profile.role === 'educator'}
|
||||
<PrimaryButton
|
||||
label="New"
|
||||
onClick={() => ($createCourseModal.open = true)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</PageNav>
|
||||
|
||||
<NewCourseModal />
|
||||
|
||||
<Courses />
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
width: 100%;
|
||||
padding: 0 10px;
|
||||
min-height: 90vh;
|
||||
/* min-width: 600px;
|
||||
margin: 0 auto;
|
||||
border-left: 1px solid var(--border-color);
|
||||
border-right: 1px solid var(--border-color); */
|
||||
}
|
||||
</style>
|
||||
@@ -1,229 +0,0 @@
|
||||
<script>
|
||||
import { stores } from '@sapper/app';
|
||||
import AddComment20 from 'carbon-icons-svelte/lib/AddComment20';
|
||||
import Vote from '../components/Vote/index.svelte';
|
||||
import Space from '../components/Space/index.svelte';
|
||||
import HashTags from '../components/HashTags/index.svelte';
|
||||
import PageNav from '../components/PageNav/index.svelte';
|
||||
|
||||
function getDiscussion(filterBy, allDiscussions) {
|
||||
switch (filterBy) {
|
||||
// case 'popular':
|
||||
// return allDiscussions.filter((a, i) => i >= 0 && i < 2);
|
||||
case 'recent':
|
||||
return allDiscussions.filter((a, i) => i >= 2 && i < 5);
|
||||
case 'upvoted':
|
||||
return allDiscussions.filter((a, i) => i > 4 && i < 8);
|
||||
default:
|
||||
return allDiscussions;
|
||||
}
|
||||
}
|
||||
|
||||
const { page } = stores();
|
||||
let filterBy = '';
|
||||
let discussions = [];
|
||||
|
||||
$: filterBy = $page.query.filter || 'popular';
|
||||
|
||||
const _allDiscussions = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'How do I pass Anicimov?',
|
||||
author: {
|
||||
name: 'rotimibest',
|
||||
},
|
||||
tags: ['ибэит', 'икс'],
|
||||
answered: false,
|
||||
comments: 10,
|
||||
votes: 10,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title:
|
||||
'Where do I find the nearest restaurant. Where do I find the nearest restaurant?. Where do I find the nearest restaurant? Where do I find the nearest restaurant?',
|
||||
author: {
|
||||
name: 'attronaldo',
|
||||
},
|
||||
tags: ['иибрт', 'иее'],
|
||||
answered: false,
|
||||
comments: 2,
|
||||
votes: 2,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Who is Nikolenko?',
|
||||
author: {
|
||||
name: 'donald',
|
||||
},
|
||||
tags: ['ибэит', 'иее', 'иибрт', 'ИПИГ'],
|
||||
answered: false,
|
||||
comments: 10,
|
||||
votes: 10,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: 'Archive discussion button',
|
||||
author: {
|
||||
name: 'acharlesvv',
|
||||
},
|
||||
tags: [
|
||||
'ИЭКСУ',
|
||||
'ИМБТ',
|
||||
'ИМИ',
|
||||
'ИПТДМ',
|
||||
'ИИИР',
|
||||
'ГФ',
|
||||
'ХТФ',
|
||||
'ИДЗО',
|
||||
'УИИ',
|
||||
'УНИ',
|
||||
],
|
||||
answered: false,
|
||||
comments: 15,
|
||||
votes: 15,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: 'How do I pass Anicimov?',
|
||||
author: {
|
||||
name: 'rotimibest',
|
||||
},
|
||||
tags: ['ибэит', 'икс'],
|
||||
answered: false,
|
||||
comments: 7,
|
||||
votes: 7,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Where do I find the nearest restaurant?',
|
||||
author: {
|
||||
name: 'attronaldo',
|
||||
},
|
||||
tags: ['иибрт', 'иее'],
|
||||
answered: false,
|
||||
comments: 4,
|
||||
votes: 4,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Who is Nikolenko?',
|
||||
author: {
|
||||
name: 'donald',
|
||||
},
|
||||
tags: ['ибэит', 'иее', 'иибрт', 'ИПИГ'],
|
||||
answered: false,
|
||||
comments: 100,
|
||||
votes: 100,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: 'Archive discussion button',
|
||||
author: {
|
||||
name: 'acharlesvv',
|
||||
},
|
||||
tags: [
|
||||
'ИЭКСУ',
|
||||
'ИМБТ',
|
||||
'ИМИ',
|
||||
'ИПТДМ',
|
||||
'ИИИР',
|
||||
'ГФ',
|
||||
'ХТФ',
|
||||
'ИДЗО',
|
||||
'УИИ',
|
||||
'УНИ',
|
||||
],
|
||||
answered: false,
|
||||
comments: 5,
|
||||
votes: 5,
|
||||
},
|
||||
];
|
||||
const allDiscussions = [];
|
||||
|
||||
_allDiscussions.forEach((d, i) => {
|
||||
allDiscussions.push(d, _allDiscussions[_allDiscussions.length - (i + 1)]);
|
||||
});
|
||||
|
||||
$: discussions = getDiscussion(filterBy, allDiscussions);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>ClassroomIO - Help students in your university</title>
|
||||
</svelte:head>
|
||||
<div class="root md:w-3/4 mx-auto md:mb-20">
|
||||
<PageNav
|
||||
title="Talks"
|
||||
addButtonHref="/ask"
|
||||
addButtonLabel="Ask"
|
||||
navItems={[
|
||||
{
|
||||
label: 'Popular',
|
||||
isActive: filterBy === 'popular',
|
||||
href: '/discussions?filter=popular',
|
||||
},
|
||||
{
|
||||
label: 'Recent',
|
||||
isActive: filterBy === 'recent',
|
||||
href: '/discussions?filter=recent',
|
||||
},
|
||||
{
|
||||
label: 'Upvoted',
|
||||
isActive: filterBy === 'upvoted',
|
||||
href: '/discussions?filter=upvoted',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{#each discussions as discussion}
|
||||
<div class="discussion-box">
|
||||
<Vote value={discussion.votes} />
|
||||
<div class="discussion-topic-author">
|
||||
<h4 class="dark:text-white">
|
||||
<a rel="prefetch" href="discussions/{discussion.id}"
|
||||
>{discussion.title}</a
|
||||
>
|
||||
</h4>
|
||||
<span>{discussion.author.name} asked 1 day ago</span>
|
||||
<HashTags tags={discussion.tags} />
|
||||
</div>
|
||||
<Space />
|
||||
<div class="flex items-center">
|
||||
<AddComment20 />
|
||||
<span>{discussion.comments}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
border-left: 1px solid var(--border-color);
|
||||
border-right: 1px solid var(--border-color);
|
||||
}
|
||||
.discussion-box {
|
||||
/* border: 1px solid var(--border-color); */
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 10px 15px;
|
||||
/* border-radius: 10px; */
|
||||
margin: 0px auto;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
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 {
|
||||
color: #586069;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,30 +0,0 @@
|
||||
import _discussions from './_discussions.js';
|
||||
|
||||
const lookup = new Map();
|
||||
_discussions.forEach((discussion) => {
|
||||
lookup.set(`${discussion.id}`, JSON.stringify(discussion));
|
||||
});
|
||||
|
||||
export function get(req, res, next) {
|
||||
// the `id` parameter is available because
|
||||
// this file is called [id].json.js
|
||||
const { id } = req.params;
|
||||
|
||||
if (lookup.has(id)) {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/json',
|
||||
});
|
||||
|
||||
res.end(lookup.get(id));
|
||||
} else {
|
||||
res.writeHead(404, {
|
||||
'Content-Type': 'application/json',
|
||||
});
|
||||
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
message: `Not found`,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
<script context="module">
|
||||
export async function preload({ params }) {
|
||||
// the `id` parameter is available because
|
||||
// this file is called [id].svelte
|
||||
const res = await this.fetch(`discussions/${params.id}.json`);
|
||||
const data = await res.json();
|
||||
|
||||
if (res.status === 200) {
|
||||
return { discussion: data };
|
||||
} else {
|
||||
this.error(res.status, data.message);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import marked from 'marked';
|
||||
import Vote from '../../components/Vote/index.svelte';
|
||||
import HashTags from '../../components/HashTags/index.svelte';
|
||||
import TextEditor from '../../components/TextEditor/index.svelte';
|
||||
import CheckmarkOutline20 from 'carbon-icons-svelte/lib/CheckmarkOutline20';
|
||||
|
||||
export let discussion;
|
||||
|
||||
let userComment = '';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{discussion.title}</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="root lg:w-3/5 mx-auto md:mx-10 lg:mb-20">
|
||||
<div class="flex items-start">
|
||||
<Vote value={discussion.votes} />
|
||||
<h2 class="text-3xl mb-8">{discussion.title}</h2>
|
||||
</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">
|
||||
<a
|
||||
class="flex items-center no-underline hover:underline text-black"
|
||||
href="/"
|
||||
>
|
||||
<img
|
||||
alt="Placeholder"
|
||||
class="block rounded-full"
|
||||
src="https://picsum.photos/32/32/?random"
|
||||
/>
|
||||
<p class="dark:text-white ml-2 text-sm">{discussion.author.name}</p>
|
||||
<p
|
||||
class="dark:text-white ml-2 text-sm text-gray-500 dark:text-gray-200"
|
||||
>
|
||||
12h ago
|
||||
</p>
|
||||
</a>
|
||||
</header>
|
||||
<section class="prose prose-sm sm:prose p-2">
|
||||
{@html marked(discussion.question)}
|
||||
</section>
|
||||
<section class="p-2">
|
||||
<HashTags tags={discussion.tags} />
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="my-8"><strong>{discussion.comments.length} ответы</strong></div>
|
||||
|
||||
{#each discussion.comments as comment}
|
||||
<div class="my-5 px-1 flex items-start">
|
||||
<Vote value={comment.votes} />
|
||||
<div class="w-full rounded-lg border border-1 border-gray">
|
||||
<header class="flex items-center justify-between leading-none p-2">
|
||||
<a
|
||||
class="flex items-center no-underline hover:underline text-black"
|
||||
href="/"
|
||||
>
|
||||
<img
|
||||
alt="Placeholder"
|
||||
class="block rounded-full"
|
||||
width="24"
|
||||
height="20"
|
||||
src={comment.avatar}
|
||||
/>
|
||||
<p class="dark:text-white ml-2 text-sm">{comment.name}</p>
|
||||
<p
|
||||
class="dark:text-white ml-2 text-sm text-gray-500 dark:text-gray-200"
|
||||
>
|
||||
12h ago
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<CheckmarkOutline20 />
|
||||
</header>
|
||||
<article class="prose prose-sm sm:prose p-2">
|
||||
{@html marked(comment.comment)}
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<hr />
|
||||
<TextEditor value={userComment} placeholder="Предлогайте ваш ответ" />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
margin: 15px auto 5px;
|
||||
max-width: 800px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,181 +0,0 @@
|
||||
// Ordinarily, you'd generate this data from markdown files in your
|
||||
// repo, or fetch them from a database of some kind. But in order to
|
||||
// avoid unnecessary dependencies in the starter template, and in the
|
||||
// service of obviousness, we're just going to leave it here.
|
||||
|
||||
// This file is called `_discussions.js` rather than `posts.js`, because
|
||||
// we don't want to create an `/blog/posts` route — the leading
|
||||
// underscore tells Sapper not to do that.
|
||||
const question = "\ Hey!\n>\n> I'm working on a dashboard app that's where most things are login only and I like using ### Outlined a draft of the flows\n>\n> ### API Proposals\n> #### Through `getServerSideProps`\n> * Add new return property to `getServerSideProps` called i.e. `fetchPolicy` that can default to `always` being current behaviour and add new option `cache-and-network`/`stale-while-revalidate` as outlined above\n> * Add new router API event, called i.e. `routePropsChanged`, that can be triggered when page props change\n> * Maybe it could also send the `stale-while-revalidate` http header to the browser for instant page reloads?\n> * Problem: needs a way to clear cache when mutating to prevent a flash of unchanged content\n>\n> #### Through router\n> * Add new prop to Link/Router called `fetchPolicy: 'cache-and-network'` which could use a cache of the page's data if it exists and warm it up on load (similar to [`shallow`](https://nextjs.org/docs/routing/shallow-routing).)\n> * If `fetchPolicy: 'cache-and-network` is combined with `prefetch` it could actually preload the data on each page as well and make every route navigation change snappy, even when using getServerSideProps`\n> * This removes the \"flash of unchanged content\" on mutations as you can trigger a route change after a mutation w/o using the cache.\n>\n> Later on, this could be extended to allow for polling as well maybe, which could enable some real-time views without the need of any actual client-side query wiring.\n"
|
||||
export default [
|
||||
{
|
||||
title: "What is Sapper?",
|
||||
id: 1,
|
||||
votes: 100,
|
||||
author: {
|
||||
name: "babdraa",
|
||||
avatar: "https://avatars3.githubusercontent.com/u/24832873?s=60&v=4"
|
||||
},
|
||||
tags: [
|
||||
'ИЭКСУ', 'ИМБТ', 'ИМИ', 'ИПТДМ', 'ИИИР', 'ГФ', 'ХТФ', 'ИДЗО', 'УИИ', 'УНИ'
|
||||
],
|
||||
question: '# Problem\n\nThere is a problem with my code please help me check it out.\n\n```js\nconst name = "dfdf";\n```',
|
||||
createdAt: Date.now(),
|
||||
comments: [
|
||||
{
|
||||
name: "babdraa",
|
||||
avatar: "https://avatars3.githubusercontent.com/u/24832873?s=60&v=4",
|
||||
votes: 5,
|
||||
comment:
|
||||
"getStaticProps must always be called from a page located in pages/*"
|
||||
},
|
||||
{
|
||||
name: "crana",
|
||||
avatar: "https://avatars1.githubusercontent.com/u/31749594?s=60&v=4",
|
||||
votes: 0,
|
||||
comment:
|
||||
" Hello, thank you for reply. Of course I know that :-) I was just thinking if there is some way to do same as getStaticProps for other components too. I don't think such feature is completely necessary, though. Hello, thank you for reply. Of course I know that :-) I was just thinking if there is some way to do same as getStaticProps for other components too. I don't think such feature is completely necessary, though. Hello, thank you for reply. Of course I know that :-) I was just thinking if there is some way to do same as getStaticProps for other components too. I don't think such feature is completely necessary, though. Hello, thank you for reply. Of course I know that :-) I was just thinking if there is some way to do same as getStaticProps for other components too. I don't think such feature is completely necessary, though. Hello, thank you for reply. Of course I know that :-) I was just thinking if there is some way to do same as getStaticProps for other components too. I don't think such feature is completely necessary, though. Hello, thank you for reply. Of course I know that :-) I was just thinking if there is some way to do same as getStaticProps for other components too. I don't think such feature is completely necessary, though. Hello, thank you for reply. Of course I know that :-) I was just thinking if there is some way to do same as getStaticProps for other components too. I don't think such feature is completely necessary, though."
|
||||
},
|
||||
{
|
||||
name: "loris",
|
||||
avatar: "https://avatars3.githubusercontent.com/u/11896398?s=60&v=4",
|
||||
votes: 100,
|
||||
comment:
|
||||
"getStaticProps must always be called from a page located in pages/*"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
title: "How to use Sapper",
|
||||
id: 2,
|
||||
votes: 100,
|
||||
|
||||
author: {
|
||||
name: "babdraa",
|
||||
avatar: "https://avatars3.githubusercontent.com/u/24832873?s=60&v=4"
|
||||
},
|
||||
tags: [
|
||||
'ИЭКСУ', 'ИМБТ', 'ИМИ', 'ИПТДМ', 'ИИИР', 'ГФ', 'ХТФ', 'ИДЗО', 'УИИ', 'УНИ'
|
||||
],
|
||||
question:
|
||||
question,
|
||||
createdAt: Date.now(),
|
||||
comments: [
|
||||
{
|
||||
name: "rawe",
|
||||
avatar: "https://avatars1.githubusercontent.com/u/31749594?s=60&v=4",
|
||||
votes: 10,
|
||||
comment:
|
||||
"Hello, thank you for reply. Of course I know that :-) I was just thinking if there is some way to do same as getStaticProps for other components too. I don't think such feature is completely necessary, though."
|
||||
},
|
||||
{
|
||||
name: "hlaow",
|
||||
avatar: "https://avatars3.githubusercontent.com/u/11896398?s=60&v=4",
|
||||
votes: 5,
|
||||
comment:
|
||||
"getStaticProps must always be called from a page located in pages/*"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
title: "Why the name?",
|
||||
votes: 100,
|
||||
id: 3,
|
||||
author: {
|
||||
name: "babdraa",
|
||||
avatar: "https://avatars3.githubusercontent.com/u/24832873?s=60&v=4"
|
||||
},
|
||||
tags: [
|
||||
'ИЭКСУ', 'ИМБТ', 'ИМИ', 'ИПТДМ', 'ИИИР', 'ГФ', 'ХТФ', 'ИДЗО', 'УИИ', 'УНИ'
|
||||
],
|
||||
question:
|
||||
question,
|
||||
createdAt: Date.now(),
|
||||
comments: [
|
||||
{
|
||||
name: "babdraa",
|
||||
avatar: "https://avatars3.githubusercontent.com/u/24832873?s=60&v=4",
|
||||
votes: 50,
|
||||
comment:
|
||||
"getStaticProps must always be called from a page located in pages/*"
|
||||
},
|
||||
{
|
||||
name: "crana",
|
||||
avatar: "https://avatars1.githubusercontent.com/u/31749594?s=60&v=4",
|
||||
votes: 9,
|
||||
comment:
|
||||
"Hello, thank you for reply. Of course I know that :-) I was just thinking if there is some way to do same as getStaticProps for other components too. I don't think such feature is completely necessary, though."
|
||||
},
|
||||
{
|
||||
name: "loris",
|
||||
avatar: "https://avatars3.githubusercontent.com/u/11896398?s=60&v=4",
|
||||
votes: 5,
|
||||
comment:
|
||||
"getStaticProps must always be called from a page located in pages/*"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
title: "How is Sapper different from Next.js?",
|
||||
id: 4,
|
||||
votes: 10,
|
||||
author: {
|
||||
name: "babdraa",
|
||||
avatar: "https://avatars3.githubusercontent.com/u/24832873?s=60&v=4"
|
||||
},
|
||||
tags: [
|
||||
'ИЭКСУ', 'ИМБТ', 'ИМИ', 'ИПТДМ', 'ИИИР', 'ГФ', 'ХТФ', 'ИДЗО', 'УИИ', 'УНИ'
|
||||
],
|
||||
question: question,
|
||||
createdAt: Date.now(),
|
||||
comments: [
|
||||
{
|
||||
name: "babdraa",
|
||||
avatar: "https://avatars3.githubusercontent.com/u/24832873?s=60&v=4",
|
||||
votes: 0,
|
||||
comment:
|
||||
"getStaticProps must always be called from a page located in pages/*"
|
||||
},
|
||||
{
|
||||
name: "crana",
|
||||
avatar: "https://avatars1.githubusercontent.com/u/31749594?s=60&v=4",
|
||||
votes: 1,
|
||||
comment:
|
||||
"Hello, thank you for reply. Of course I know that :-) I was just thinking if there is some way to do same as getStaticProps for other components too. I don't think such feature is completely necessary, though."
|
||||
},
|
||||
{
|
||||
name: "loris",
|
||||
avatar: "https://avatars3.githubusercontent.com/u/11896398?s=60&v=4",
|
||||
comment:
|
||||
"getStaticProps must always be called from a page located in pages/*"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
title: "How can I get involved?",
|
||||
id: 5,
|
||||
votes: 7,
|
||||
tags: [
|
||||
'ИЭКСУ', 'ИМБТ', 'ИМИ', 'ИПТДМ', 'ИИИР', 'ГФ', 'ХТФ', 'ИДЗО', 'УИИ', 'УНИ'
|
||||
],
|
||||
author: {
|
||||
name: "living",
|
||||
avatar: "https://avatars3.githubusercontent.com/u/24832873?s=60&v=4"
|
||||
},
|
||||
question: question,
|
||||
createdAt: Date.now(),
|
||||
comments: [
|
||||
{
|
||||
name: "gbenga",
|
||||
avatar: "https://avatars3.githubusercontent.com/u/11896398?s=60&v=4",
|
||||
votes: 1000,
|
||||
comment:
|
||||
"getStaticProps must always be called from a page located in pages/*"
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
@@ -1,15 +0,0 @@
|
||||
function writeJsonHead(res, statusCode) {
|
||||
res.writeHead(statusCode, {
|
||||
'Content-Type': 'application/json',
|
||||
});
|
||||
}
|
||||
|
||||
export function post(req, res) {
|
||||
const { email } = req.body;
|
||||
|
||||
console.log(`req.session`, req.session);
|
||||
|
||||
writeJsonHead(res, 200);
|
||||
|
||||
return res.end(JSON.stringify({ success: true }));
|
||||
}
|
||||
@@ -141,8 +141,13 @@
|
||||
});
|
||||
console.log('Create organisation member', data);
|
||||
|
||||
const memberId = Array.isArray(data) && data.length ? data[0].id : '';
|
||||
|
||||
orgs.set([orgData as never]);
|
||||
currentOrg.set(orgData);
|
||||
currentOrg.set({
|
||||
...orgData,
|
||||
memberId: memberId,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.log('Error: create organisation member', error);
|
||||
|
||||
@@ -0,0 +1,431 @@
|
||||
<script context="module">
|
||||
export async function preload({ params }) {
|
||||
return { slug: params.slug };
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import marked from 'marked';
|
||||
import dayjs from 'dayjs';
|
||||
import pluralize from 'pluralize';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import Delete16 from 'carbon-icons-svelte/lib/Delete16';
|
||||
import { SkeletonPlaceholder, SkeletonText } from 'carbon-components-svelte';
|
||||
import ArrowLeft24 from 'carbon-icons-svelte/lib/ArrowLeft24';
|
||||
import Vote from '../../../../components/Vote/index.svelte';
|
||||
import PrimaryButton from '../../../../components/PrimaryButton/index.svelte';
|
||||
import { VARIANTS } from '../../../../components/PrimaryButton/constants';
|
||||
import TextEditor from '../../../../components/TextEditor/index.svelte';
|
||||
import Avatar from '../../../../components/Avatar/index.svelte';
|
||||
import IconButton from '../../../../components/IconButton/index.svelte';
|
||||
import CheckmarkOutline20 from 'carbon-icons-svelte/lib/CheckmarkOutline20';
|
||||
import { currentOrgPath, currentOrg } from '../../../../utils/store/org';
|
||||
import { profile } from '../../../../utils/store/user';
|
||||
import { supabase } from '../../../../utils/functions/supabase';
|
||||
import {
|
||||
askCommunityValidation,
|
||||
commentInCommunityValidation,
|
||||
} from '../../../../utils/functions/validator';
|
||||
import { SNACKBAR_SEVERITY } from '../../../../components/Snackbar/constants';
|
||||
import { snackbarStore } from '../../../../components/Snackbar/store';
|
||||
import TextField from '../../../../components/Form/TextField.svelte';
|
||||
import DeleteCommentModal from '../../../../components/Org/Community/DeleteCommentModal.svelte';
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
export let slug;
|
||||
|
||||
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(`${currentOrgPath}`);
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
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-gray-200 text-md flex items-center"
|
||||
href={`${$currentOrgPath}/community`}
|
||||
>
|
||||
<ArrowLeft24 class="carbon-icon" /> 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.CONTAINED_INFO}
|
||||
onClick={handleQuestionEdit}
|
||||
className="py-3 px-6 rounded-sm h-fit"
|
||||
disablePadding={true}
|
||||
/>
|
||||
{#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}
|
||||
<CheckmarkOutline20 />
|
||||
{/if}
|
||||
|
||||
{#if comment.authorId === $profile.id}
|
||||
<IconButton
|
||||
value="delete-comment"
|
||||
onClick={() => {
|
||||
deleteComment.shouldDelete = true;
|
||||
deleteComment.commentId = comment.id;
|
||||
}}
|
||||
>
|
||||
<Delete16 class="carbon-icon" />
|
||||
</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,96 @@
|
||||
<script>
|
||||
import { goto } from '@sapper/app';
|
||||
import ArrowLeft24 from 'carbon-icons-svelte/lib/ArrowLeft24';
|
||||
import TextEditor from '../../../../components/TextEditor/index.svelte';
|
||||
import { currentOrgPath, currentOrg } from '../../../../utils/store/org';
|
||||
import PrimaryButton from '../../../../components/PrimaryButton/index.svelte';
|
||||
import { askCommunityValidation } from '../../../../utils/functions/validator';
|
||||
import { supabase } from '../../../../utils/functions/supabase';
|
||||
import { SNACKBAR_SEVERITY } from '../../../../components/Snackbar/constants';
|
||||
import { snackbarStore } from '../../../../components/Snackbar/store';
|
||||
import generateSlug from '../../../../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),
|
||||
});
|
||||
|
||||
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(`${$currentOrgPath}/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-gray-200 text-md flex items-center"
|
||||
href={`${$currentOrgPath}/community`}
|
||||
>
|
||||
<ArrowLeft24 class="carbon-icon" /> 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>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script>
|
||||
import { goto } from '@sapper/app';
|
||||
import Community from '../../../../components/Org/Community/index.svelte';
|
||||
import PrimaryButton from '../../../../components/PrimaryButton/index.svelte';
|
||||
import { currentOrgPath } from '../../../../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 />
|
||||
</div>
|
||||
</section>
|
||||
@@ -65,7 +65,7 @@
|
||||
<title>Courses - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="w-full max-w-7xl mx-auto">
|
||||
<section class="w-full max-w-6xl 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">Courses</h1>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import Domains from '../../../../components/Org/Settings/Domains.svelte';
|
||||
import { currentOrg } from '../../../../utils/store/org';
|
||||
import { currentOrgPath } from '../../../../utils/store/org';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -11,7 +11,7 @@
|
||||
<div class="py-10 px-5">
|
||||
<a
|
||||
class="text-gray-500 dark:text-gray-200 text-md"
|
||||
href={`/org/${$currentOrg.siteName}/settings`}>Back to Settings</a
|
||||
href={`${$currentOrgPath}/settings`}>Back to Settings</a
|
||||
>
|
||||
<div class="flex items-center justify-between mb-10">
|
||||
<h1 class="dark:text-white text-3xl font-bold">Custom domain</h1>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import TeamMembers from '../../../../components/Org/Settings/TeamMembers.svelte';
|
||||
import { currentOrg } from '../../../../utils/store/org';
|
||||
import { currentOrgPath } from '../../../../utils/store/org';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -11,7 +11,7 @@
|
||||
<div class="py-10 px-5">
|
||||
<a
|
||||
class="text-gray-500 dark:text-gray-200 text-md"
|
||||
href={`/org/${$currentOrg.siteName}/settings`}>Back to Settings</a
|
||||
href={`${$currentOrgPath}/settings`}>Back to Settings</a
|
||||
>
|
||||
<div class="flex items-center justify-between mb-10">
|
||||
<h1 class="dark:text-white text-3xl font-bold">Team members</h1>
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
<script>
|
||||
import AddAlt32 from 'carbon-icons-svelte/lib/AddAlt32';
|
||||
import IconButton from '../components/IconButton/index.svelte';
|
||||
import Avatar from '../components/Avatar/index.svelte';
|
||||
let teachers = [
|
||||
{
|
||||
avatar: 'https://avatars.githubusercontent.com/u/43971542?s=96&v=4',
|
||||
name: 'Imed Jaberi',
|
||||
},
|
||||
{
|
||||
avatar: 'https://avatars.githubusercontent.com/u/3624098?s=96&v=4',
|
||||
name: 'Andrew Clark',
|
||||
},
|
||||
{
|
||||
avatar: 'https://avatars.githubusercontent.com/u/857709?s=96&v=4',
|
||||
name: 'Daniel Lytkin',
|
||||
},
|
||||
];
|
||||
|
||||
let students = [
|
||||
{
|
||||
avatar: 'https://avatars.githubusercontent.com/u/20658625?s=96&v=4',
|
||||
name: 'Aissaoui Ahmed',
|
||||
},
|
||||
{
|
||||
avatar: 'https://avatars.githubusercontent.com/u/26669835?s=96&v=4',
|
||||
name: 'Mahfoudh Arous',
|
||||
},
|
||||
];
|
||||
|
||||
let borderBottom = 'border-r-0 border-b border-l-0';
|
||||
function addTeacher() {}
|
||||
function addStudent() {}
|
||||
</script>
|
||||
|
||||
<div class="root w-full py-5">
|
||||
<div class="max-w-2xl mx-auto my-5">
|
||||
<div
|
||||
class="flex items-center justify-between {borderBottom} border-blue-700"
|
||||
>
|
||||
<h3 class="dark:text-white text-3xl text-blue-700">Teachers</h3>
|
||||
<IconButton
|
||||
toolTipProps={{ title: 'Add', direction: 'right' }}
|
||||
onClick={addTeacher}
|
||||
selected={true}
|
||||
>
|
||||
<AddAlt32 class="carbon-icon" />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
{#each teachers as teacher, index}
|
||||
<div
|
||||
class="flex items-center px-2 py-3 {teachers[index + 1] &&
|
||||
borderBottom} border-gray-300"
|
||||
>
|
||||
<Avatar src={teacher.avatar} name={teacher.name} />
|
||||
<p class="dark:text-white ml-2">{teacher.name}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="max-w-2xl mx-auto my-5">
|
||||
<div
|
||||
class="flex items-center justify-between {borderBottom} border-blue-700"
|
||||
>
|
||||
<h3 class="dark:text-white text-3xl text-blue-700">Students</h3>
|
||||
<IconButton
|
||||
toolTipProps={{ title: 'Add', direction: 'right' }}
|
||||
onClick={addStudent}
|
||||
selected={true}
|
||||
>
|
||||
<AddAlt32 class="carbon-icon" />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
{#each students as student, index}
|
||||
<div
|
||||
class="flex items-center px-2 py-3 {students[index + 1] &&
|
||||
borderBottom} border-gray-300"
|
||||
>
|
||||
<Avatar src={student.avatar} name={student.name} />
|
||||
<p class="dark:text-white ml-2">{student.name}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
min-height: 93vh;
|
||||
}
|
||||
</style>
|
||||
@@ -179,7 +179,7 @@
|
||||
<div class="flex-grow flex flex-wrap mx-3 w-3/5">
|
||||
<div class="mb-3 w-full">
|
||||
<h3 class="dark:text-white font-bold text-md m-0">Organizations</h3>
|
||||
<p class="dark:text-white text-gray-500 dark:text-gray-200 text-sm">
|
||||
<p class="dark:text-white text-gray-500 text-sm">
|
||||
This are the organizations you work at
|
||||
</p>
|
||||
|
||||
@@ -187,7 +187,7 @@
|
||||
<h4 class="dark:text-white m-0 font-normal">
|
||||
<a class="text-blue-700 font-bold" href="/courses"> CitiBim </a>
|
||||
</h4>
|
||||
<p class="dark:text-white text-gray-500 dark:text-gray-200">
|
||||
<p class="dark:text-white text-gray-500">
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Non aliquam
|
||||
tenetur magnam. Alias cumque quaerat expedita dolores placeat
|
||||
laboriosam culpa, suscipit veritatis adipisci eius magni obcaecati.
|
||||
@@ -210,7 +210,7 @@
|
||||
Climate Change Group
|
||||
</a>
|
||||
</h4>
|
||||
<p class="dark:text-white text-gray-500 dark:text-gray-200">
|
||||
<p class="dark:text-white text-gray-500">
|
||||
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Illo quasi
|
||||
autem quo possimus illum consequuntur repudiandae nobis error, porro
|
||||
</p>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
function generateSlug(title) {
|
||||
return title
|
||||
.match(/[^\W\s\d]*/g, '')
|
||||
.join('')
|
||||
.toLowerCase()
|
||||
.concat(`-${new Date().getTime()}`);
|
||||
}
|
||||
|
||||
export default generateSlug;
|
||||
@@ -1,6 +1,23 @@
|
||||
import z from 'zod';
|
||||
import { validateEmail } from './validateEmail';
|
||||
|
||||
const askCommunityValidationSchema = z.object({
|
||||
title: z.string().min(6, {
|
||||
message: 'Must be 6 or more characters long',
|
||||
invalid_type_error: 'Must not be empty',
|
||||
}),
|
||||
body: z.string().min(20, {
|
||||
message: 'Must be 10 or more characters long',
|
||||
invalid_type_error: 'Must not be empty',
|
||||
}),
|
||||
});
|
||||
const commentInCommunityValidationSchema = z.object({
|
||||
comment: z.string().min(6, {
|
||||
message: 'Must be 6 or more characters long',
|
||||
invalid_type_error: 'Must not be empty',
|
||||
}),
|
||||
});
|
||||
|
||||
const forgotValidationSchema = z.object({
|
||||
email: z.string().email({
|
||||
message: 'Invalid email address',
|
||||
@@ -98,6 +115,18 @@ export const onboardingValidation = (fields = {}, step) => {
|
||||
return processErrors(error);
|
||||
};
|
||||
|
||||
export const askCommunityValidation = (fields = {}) => {
|
||||
const { error } = askCommunityValidationSchema.safeParse(fields);
|
||||
|
||||
return processErrors(error);
|
||||
};
|
||||
|
||||
export const commentInCommunityValidation = (fields = {}) => {
|
||||
const { error } = commentInCommunityValidationSchema.safeParse(fields);
|
||||
|
||||
return processErrors(error);
|
||||
};
|
||||
|
||||
export const getDisableSubmit = ({ password, confirmPassword }) => {
|
||||
return !!(password && confirmPassword && password !== confirmPassword);
|
||||
};
|
||||
|
||||
@@ -52,8 +52,9 @@ export async function getOrganizations(userId) {
|
||||
.from('organizationmember')
|
||||
.select(
|
||||
`
|
||||
id,
|
||||
profile_id,
|
||||
organization (
|
||||
organization!organizationmember_organization_id_fkey (
|
||||
id,
|
||||
name,
|
||||
siteName,
|
||||
@@ -64,7 +65,7 @@ export async function getOrganizations(userId) {
|
||||
.eq('profile_id', userId);
|
||||
|
||||
const orgsArray = [];
|
||||
if (data.length) {
|
||||
if (Array.isArray(data) && data.length) {
|
||||
data.forEach((orgMember) => {
|
||||
orgsArray.push({
|
||||
id: orgMember?.organization?.id,
|
||||
@@ -73,6 +74,7 @@ export async function getOrganizations(userId) {
|
||||
orgMember?.organization?.name?.substring(0, 2)?.toUpperCase() || '',
|
||||
siteName: orgMember?.organization?.siteName,
|
||||
avatar_url: orgMember?.organization?.avatar_url,
|
||||
memberId: orgMember?.id,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -98,16 +100,14 @@ export async function getOrgAudience(orgId) {
|
||||
email,
|
||||
avatar_url,
|
||||
created_at,
|
||||
groupmember!inner(
|
||||
organizationmember!inner(
|
||||
role_id,
|
||||
group!inner(
|
||||
organization!inner(id)
|
||||
)
|
||||
organization_id
|
||||
)
|
||||
`
|
||||
)
|
||||
.eq('groupmember.group.organization.id', orgId)
|
||||
.eq('groupmember.role_id', 3); // is a student, tutor is 2 and admin is 1
|
||||
.eq('organizationmember.organization_id', orgId)
|
||||
.eq('organizationmember.role_id', 3); // is a student, tutor is 2 and admin is 1
|
||||
|
||||
console.log('data', data);
|
||||
|
||||
@@ -173,5 +173,5 @@ export async function getCurrentOrg(siteName) {
|
||||
return goto('/404');
|
||||
}
|
||||
|
||||
currentOrg.set(data);
|
||||
currentOrg.set(data[0]);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import { writable, derived } from 'svelte/store';
|
||||
|
||||
export const orgs = writable([]);
|
||||
export const currentOrg = writable({
|
||||
@@ -10,3 +10,7 @@ export const currentOrg = writable({
|
||||
});
|
||||
export const orgAudience = writable([]);
|
||||
export const orgTeam = writable([]);
|
||||
export const currentOrgPath = derived(
|
||||
currentOrg,
|
||||
($currentOrg) => `/org/${$currentOrg.siteName}`
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user