Sync with Server

This commit is contained in:
rotimi-best
2021-08-18 19:47:02 +03:00
parent e8c74de9ce
commit 60f06d6ab1
77 changed files with 2989 additions and 7610 deletions
+38 -15
View File
@@ -10,7 +10,7 @@
import Tailwindcss from '../components/Tailwindcss.svelte';
import Navigation from '../components/Navigation/index.svelte';
// import SideBar from "../components/SideBar/index.svelte";
import Footer from '../components/Footer/index.svelte';
// import Footer from '../components/Footer/index.svelte';
import Apps from '../components/Apps/index.svelte';
import { handleAuthChange } from '../utils/functions/api';
import { user, profile } from '../utils/store/user';
@@ -24,6 +24,7 @@
let path = $page.path.replace('/', '');
async function getProfile() {
console.log('Get user profile');
// Get user profile
const authUser = supabase.auth.user();
@@ -34,39 +35,61 @@
status,
} = await supabase
.from('profile')
.select(`*`)
.eq('user_id', authUser.id)
.select(`*, org:organizationmember(*)`)
.eq('id', authUser.id)
.single();
if (error && !profileData && status === 406) {
// User wasn't found, create profile
console.log(`User wasn't found, create profile`);
const { data, error } = await supabase
.from('profile')
.insert([{ user_id: authUser.id }]);
const [regexUsernameMatch] = [...authUser.email.matchAll(/(.*)@/g)];
const { data, error } = await supabase.from('profile').insert({
id: authUser.id,
username: regexUsernameMatch[1],
fullname: regexUsernameMatch[1],
});
// Profile created, go to profile page
if (!error && data) {
user.set(authUser);
profile.set(data);
return goto('/');
$user.fetchingUser = false;
$user.isLoggedIn = true;
$user.currentSession = authUser;
profile.set(data[0]);
// If user coming to login page, then
if (path.includes('login') || path.includes('signup')) {
goto('/dashboard');
}
}
} else if (profileData) {
// Profile exists, go to profile page
user.set(authUser);
$user.fetchingUser = false;
$user.isLoggedIn = true;
$user.currentSession = authUser;
console.log(`profileData`, profileData);
profile.set(profileData);
return goto('/');
// If user coming to login page, then
if (path.includes('login') || path.includes('signup')) {
goto('/dashboard');
}
}
}
onMount(() => {
console.log('mounting layout');
const { data: authListener } = supabase.auth.onAuthStateChange(
(event, session) => {
handleAuthChange(event, session);
if (event === 'SIGNED_IN') {
$user.fetchingUser = true;
getProfile();
} else {
console.log('not logged in, go to login');
return goto('/');
}
}
@@ -85,21 +108,21 @@
<!-- <Nav {segment} /> -->
<main>
{#if !['login', 'signup', ''].includes(path)}
{#if !['login', 'signup'].includes(path)}
<Navigation {segment} />
{/if}
<div class="flex justify-between">
<slot />
{#if path.includes('courses')}
<!-- {#if path.includes('courses')}
<Apps />
{/if}
{/if} -->
</div>
</main>
{#if !['about', ''].includes(path)}
<Footer {segment} />
<!-- <Footer {segment} /> -->
{/if}
<style>
+2 -2
View File
@@ -378,8 +378,8 @@ export function getLesson(id) {
lesson_id: null,
materials: {
note: '',
slide: '',
video: '',
slide_url: '',
video_url: '',
},
exercises: getExercises(id),
}
+168 -5
View File
@@ -1,12 +1,114 @@
<script context="module">
import { supabase } from '../utils/functions/supabase';
export async function preload(page, params) {
const user = JSON.parse(params.user);
const { data: orgMember } = await supabase
.from('organizationmember')
.select(`organization_id`)
.eq('profile_id', user.id)
.single();
const { data: allCourses } = await supabase
.from('course')
.select(`id, title, description`)
.eq('organization_id', orgMember.organization_id);
return { allCourses, organizationId: orgMember.organization_id };
}
</script>
<script>
import { stores } from '@sapper/app';
import { onMount } from 'svelte';
// import { stores } from '@sapper/app';
import PageNav from '../components/PageNav/index.svelte';
import Courses from '../components/Courses/index.svelte';
import PrimaryButton from '../components/PrimaryButton/index.svelte';
import Modal from '../components/Modal/index.svelte';
import TextField from '../components/Form/TextField.svelte';
import TextArea from '../components/Form/TextArea.svelte';
import { courses, createCourseModal } from '../components/Courses/store';
import { validateForm } from '../components/Courses/functions';
import { profile } from '../utils/store/user';
import { ROLE } from '../utils/constants/roles';
const { page } = stores();
let tab = '';
// const { page } = stores();
$: tab = $page.query.tab || 'mine';
export let allCourses;
export let organizationId;
let errors = {
emails: '',
};
// let tab = '';
async function createCourse(e) {
const { hasError, fieldErrors, students, tutors } =
validateForm($createCourseModal);
errors = fieldErrors;
if (hasError) return;
const { title, description } = $createCourseModal;
// Create group from course_id
const { data: newGroup } = await supabase
.from('group')
.insert({ name: title, description });
console.log(`newGroup`, newGroup);
const { id: group_id } = newGroup[0];
// Create course with group_id
const { data: newCourse } = await supabase.from('course').insert({
title,
description,
group_id,
organization_id: organizationId,
});
// Get profile of author and add as member
const admin = await addGroupMember({
profile_id: $profile.id,
group_id,
role_id: ROLE.ADMIN,
});
let membersStack = [];
// Create groupmemebers from group_id and add teachers and students
for (const student of students) {
membersStack.push({
email: student,
group_id,
role_id: ROLE.STUDENT,
});
}
for (const tutor of tutors) {
membersStack.push({
email: tutor,
group_id,
role_id: ROLE.TUTOR,
});
}
addGroupMember(membersStack).then((membersAdded) => {
console.log(`membersAdded`, membersAdded);
});
}
function addGroupMember(member) {
return supabase.from('groupmember').insert(member, {
returning: 'minimal',
});
}
onMount(() => {
courses.set(allCourses);
});
// $: tab = $page.query.tab || 'mine';
</script>
<svelte:head>
@@ -14,7 +116,68 @@
</svelte:head>
<div class="root">
<PageNav title="Courses" />
<PageNav title="Courses">
<div slot="widget">
<PrimaryButton
label="New"
onClick={() => ($createCourseModal.open = true)}
/>
</div>
</PageNav>
<Modal
onClose={() => ($createCourseModal.open = false)}
bind:open={$createCourseModal.open}
width="w-2/5"
modalHeading="Create a Course"
>
<form on:submit|preventDefault={createCourse}>
<TextField
label="Course name"
bind:value={$createCourseModal.title}
autofocus={true}
placeholder="Your course name"
className="mb-4"
isRequired={true}
errorMessage={errors.title}
autoComplete={false}
/>
<TextArea
label="Invite Tutors"
bind:value={$createCourseModal.tutors}
rows="2"
maxRows="3"
placeholder="tutor@company.com, john@gmail.com"
helperMessage="To invite people, add their emails separated by a comma"
errorMessage={errors.tutors}
className="mb-4"
/>
<TextArea
label="Invite Students"
bind:value={$createCourseModal.students}
rows="2"
maxRows="3"
placeholder="student@gmail.com, john@gmail.com"
helperMessage="To invite people, add their emails separated by a comma"
errorMessage={errors.students}
className="mb-4"
/>
<TextArea
label="Short description"
bind:value={$createCourseModal.description}
rows="4"
maxRows="4"
placeholder="A little description about this course"
className="mb-4"
isRequired={true}
/>
<div class="mt-5 flex items-center">
<PrimaryButton className="px-6 py-2" label="Finish" type="submit" />
</div>
</form>
</Modal>
<Courses />
</div>
-252
View File
@@ -1,252 +0,0 @@
<script context="module">
export async function preload({ params }) {
let [courseId] = params.id;
const res = await this.fetch(`api/course?id=${courseId}`);
const data = await res.json();
if (res.status === 200) {
return { courseId, courseData: data };
} else {
this.error(res.status, data.message);
}
}
</script>
<script>
import { flip } from 'svelte/animate';
import { dndzone } from 'svelte-dnd-action';
import { stores, goto } from '@sapper/app';
import PageNav from '../../../components/PageNav/index.svelte';
import MarkExerciseModal from '../../../components/Course/components/Lesson/Exercise/MarkExerciseModal.svelte';
import Chip from '../../../components/Chip/index.svelte';
import PageBody from '../../../components/PageBody/index.svelte';
import CourseContainer from '../../../components/CourseContainer/index.svelte';
const flipDurationMs = 300;
export let courseData = {};
let sections = [
{
id: 1,
title: 'Submitted',
value: 5,
items: [
{
id: 41,
exerciseTitle: 'Practice props in React.js',
student: {
avatar: 'https://picsum.photos/32/32/?random',
name: 'Brad Traversy',
},
tutor: {
name: 'sergey-semko',
id: 1,
},
lesson: {
number: 1,
},
},
{
id: 42,
exerciseTitle: 'Practice props in React.js',
student: {
avatar: 'https://picsum.photos/32/32/?random',
name: 'James Kola',
},
tutor: {
name: 'sergey-semko',
id: 1,
},
lesson: {
number: 1,
},
},
{
id: 43,
exerciseTitle: 'Practice props in React.js',
student: {
avatar: 'https://picsum.photos/32/32/?random',
name: 'linus-torvalds',
},
tutor: {
name: 'sergey-semko',
id: 1,
},
lesson: {
number: 1,
},
},
{
id: 44,
exerciseTitle: 'Practice props in React.js',
student: {
avatar: 'https://picsum.photos/32/32/?random',
name: 'rotimi-best',
},
tutor: {
name: 'sergey-semko',
id: 1,
},
lesson: {
number: 1,
},
},
{
id: 45,
exerciseTitle: 'Home task: Class and Functional component',
student: {
avatar: 'https://picsum.photos/32/32/?random',
name: 'james-two',
},
tutor: {
name: 'sergey-semko',
id: 1,
},
lesson: {
number: 2,
},
},
{
id: 46,
exerciseTitle: 'Home task: Class and Functional component',
student: {
avatar: 'https://picsum.photos/32/32/?random',
name: 'brarack-obama',
},
tutor: {
name: 'sergey-semko',
id: 1,
},
lesson: {
number: 2,
},
},
{
id: 47,
exerciseTitle: 'Lesson task: Class and Functional component',
student: {
avatar: 'https://picsum.photos/32/32/?random',
name: 'bill-gates',
},
tutor: {
name: 'sergey-semko',
id: 1,
},
lesson: {
number: 3,
},
},
],
},
{
id: 2,
title: 'In Progress',
value: 0,
items: [],
},
{
id: 3,
title: 'Graded',
value: 10,
items: [],
},
];
let openExercise = false;
const { page } = stores();
function handleItemFinalize(columnIdx, newItems) {
sections[columnIdx].items = newItems;
// onFinalUpdate([...sections]);
}
function handleDndConsiderCards(columnIdx) {
return (e) => {
console.warn('got consider');
sections[columnIdx].items = e.detail.items;
};
}
function handleDndFinalizeCards(columnIdx) {
return (e) => handleItemFinalize(columnIdx, e.detail.items);
}
function handleModalClose() {
goto($page.path);
}
$: openExercise = !!$page.query.submissionId;
</script>
<MarkExerciseModal bind:open={openExercise} onClose={handleModalClose} submissionId={$page.query.submissionId}/>
<CourseContainer {courseData}>
<PageNav title="All Exercises" />
<PageBody width="w-11/12 overflow-x-auto overflow-y-hidden">
<div class="flex items-center justify-between w-full">
{#each sections as { id, title, items }, idx (id)}
<div
class="section rounded-md bg-gray-100 border border-gray-50 p-3 h-80 mr-3 overflow-hidden"
animate:flip={{ duration: flipDurationMs }}
>
<div class="flex items-center mb-2">
<Chip value={items.length} />
<p class="ml-2 font-bold">{title}</p>
</div>
<div
class="content pr-2 overflow-y-auto mb-3"
use:dndzone={{
items,
flipDurationMs,
dropTargetStyle: { outline: 'blue' },
}}
on:consider={handleDndConsiderCards(idx)}
on:finalize={handleDndFinalizeCards(idx)}
>
{#each items as item (item.id)}
<div
class="border border-grey-700 w-full my-2 mx-0 rounded-md bg-white py-2 px-3"
animate:flip={{ duration: flipDurationMs }}
>
<a
class="flex items-center no-underline hover:underline text-black mb-2"
href="{$page.path}?submissionId={item.id}"
>
<img
alt="Placeholder"
class="block rounded-full"
width="24"
height="20"
src={item.student.avatar}
/>
<p class="ml-2 text-sm">{item.student.name}</p>
</a>
<a
class="text-blue-700 text-md font-bold"
href="{$page.path}?submissionId={item.id}"
>{item.exerciseTitle}</a
>
<p class="text-grey text-sm">
#{item.lesson.number} created by {item.tutor.name}
</p>
</div>
{/each}
</div>
</div>
{/each}
</div>
</PageBody>
</CourseContainer>
<style>
.section {
max-width: 355px;
min-width: 355px;
height: 80vh;
}
.content {
height: 95%;
}
</style>
+17 -13
View File
@@ -1,25 +1,29 @@
<script context="module">
export async function preload({ params }) {
let [courseId] = params.id;
import { fetchCourse } from '../../../utils/services/courses';
const res = await this.fetch(`api/course?id=${courseId}`);
const data = await res.json();
export async function preload({ params }, session) {
const {
data,
// error
} = await fetchCourse(params.id, session);
if (res.status === 200) {
return { courseId, courseData: data };
} else {
this.error(res.status, data.message);
}
return { courseData: data };
}
</script>
<script>
import Overview from "../../../components/Course/index.svelte";
import CourseContainer from "../../../components/CourseContainer/index.svelte";
import { onMount } from 'svelte';
import Overview from '../../../components/Course/index.svelte';
import CourseContainer from '../../../components/CourseContainer/index.svelte';
import { setCourseData } from '../../../components/Course/store';
export let courseData = {};
export let courseData;
onMount(async () => {
setCourseData(courseData);
});
</script>
<CourseContainer {courseData}>
<CourseContainer>
<Overview />
</CourseContainer>
@@ -1,27 +1,43 @@
<script context="module">
export async function preload({ params }) {
import {
fetchCourse,
fetchLesson,
} from '../../../../../utils/services/courses';
let prevLessonId;
export async function preload({ params }, session) {
const { id: courseId, lessonParams = [] } = params;
const [lessonId, exerciseRouteName, exerciseId] = lessonParams;
let courseData;
let lessonData;
const res = await this.fetch(
`api/course?id=${courseId}&lessonId=${lessonId}&exerciseId=${exerciseId}`
);
const data = await res.json();
if (!process.browser) {
const [course, lesson] = await Promise.all([
fetchCourse(courseId, session),
fetchLesson(lessonId),
]);
if (res.status === 200) {
return {
courseData: data,
lessonId,
isMaterialsTabActive: !exerciseRouteName,
exerciseId,
};
} else {
this.error(res.status, data.message);
courseData = course.data;
lessonData = lesson.data;
} else if (prevLessonId && prevLessonId !== lessonId) {
const lesson = await fetchLesson(lessonId);
lessonData = lesson.data;
}
prevLessonId = lessonId;
return {
courseData,
lessonData,
isMaterialsTabActive: !exerciseRouteName,
lessonId,
exerciseId,
};
}
</script>
<script>
import { onMount, afterUpdate } from 'svelte';
import { stores } from '@sapper/app';
import CourseContainer from '../../../../../components/CourseContainer/index.svelte';
@@ -31,21 +47,42 @@
import Materials from '../../../../../components/Course/components/Lesson/Materials/index.svelte';
import Exercises from '../../../../../components/Course/components/Lesson/Exercises/index.svelte';
import MODES from '../../../../../utils/constants/mode.js';
import { setCourseData } from '../../../../../components/Course/store';
import { lesson } from '../../../../../components/Course/components/Lesson/store/lessons';
export let courseData;
export let lessonData;
export let lessonId;
export let isMaterialsTabActive;
export let exerciseId;
export let isMaterialsTabActive;
let path;
let mode = MODES.view;
const { page } = stores();
$: path = $page.path.replace(/\/exercises[\/ 0-9]*/, '');
function setLesson() {
if (!lessonData) return;
lesson.update((l) => ({
...l,
id: lessonId,
materials: lessonData,
}));
}
onMount(() => {
setCourseData(courseData);
setLesson();
});
afterUpdate(setLesson);
$: path = $page.path.replace(/\/exercises[\/ 0-9 a-z -]*/, '');
</script>
<CourseContainer {courseData} {path}>
<CourseContainer {path} isExercisePage={!isMaterialsTabActive && exerciseId}>
<PageNav
navItems={[
{
@@ -74,11 +111,11 @@
</svelte:fragment>
</PageNav>
<PageBody>
{#if !isMaterialsTabActive}
<Exercises {exerciseId} path={`${path}/exercises`} />
{:else}
{#if !isMaterialsTabActive}
<Exercises {lessonId} {exerciseId} path={`${path}/exercises`} />
{:else}
<PageBody width="w-11/12 m-auto">
<Materials {lessonId} {mode} />
{/if}
</PageBody>
</PageBody>
{/if}
</CourseContainer>
+139 -52
View File
@@ -1,70 +1,113 @@
<script context="module">
export async function preload({ params }) {
let [courseId, courseNavItem, lessonId] = params.id;
const res = await this.fetch(`api/course?id=${courseId}`);
const data = await res.json();
import { fetchCourse } from '../../../../utils/services/courses';
if (res.status === 200) {
return { courseId, courseData: data };
} else {
this.error(res.status, data.message);
export async function preload({ params }, session) {
if (process.browser) {
return {};
}
const {
data,
// error
} = await fetchCourse(params.id, session);
return { courseData: data };
}
</script>
<script>
import Time24 from "carbon-icons-svelte/lib/Time24";
import Edit24 from "carbon-icons-svelte/lib/Edit24";
import Save24 from "carbon-icons-svelte/lib/Save24";
import CheckmarkFilled24 from "carbon-icons-svelte/lib/CheckmarkFilled24";
import PageNav from "../../../../components/PageNav/index.svelte";
import PageBody from "../../../../components/PageBody/index.svelte";
import PrimaryButton from "../../../../components/PrimaryButton/index.svelte";
import TextField from "../../../../components/Form/TextField.svelte";
import IconButton from "../../../../components/IconButton/index.svelte";
import Select from "../../../../components/Form/Select.svelte";
import CourseContainer from "../../../../components/CourseContainer/index.svelte";
import { onMount } from 'svelte';
import Time24 from 'carbon-icons-svelte/lib/Time24';
import Edit24 from 'carbon-icons-svelte/lib/Edit24';
import Save24 from 'carbon-icons-svelte/lib/Save24';
import Phone24 from 'carbon-icons-svelte/lib/Phone24';
import Delete24 from 'carbon-icons-svelte/lib/Delete24';
import CheckmarkFilled24 from 'carbon-icons-svelte/lib/CheckmarkFilled24';
import PageNav from '../../../../components/PageNav/index.svelte';
import Box from '../../../../components/Box/index.svelte';
import PageBody from '../../../../components/PageBody/index.svelte';
import PrimaryButton from '../../../../components/PrimaryButton/index.svelte';
import TextField from '../../../../components/Form/TextField.svelte';
import IconButton from '../../../../components/IconButton/index.svelte';
import Select from '../../../../components/Form/Select.svelte';
import CourseContainer from '../../../../components/CourseContainer/index.svelte';
import {
lessons,
tutors,
handleAddLesson,
} from "../../../../components/Course/components/Lesson/store/lessons";
handleSaveLesson,
handleDelete,
} from '../../../../components/Course/components/Lesson/store/lessons';
import {
course,
setCourseData,
group,
} from '../../../../components/Course/store';
export let courseData;
let lessonEditing;
let ref;
const resources = [
{
label: 'lesson',
value: 1,
},
{
label: 'quiz',
value: 1,
},
{
label: 'home task',
value: 2,
},
];
function formatDate(date) {
const d = new Date(date);
const ye = new Intl.DateTimeFormat("en", { year: "numeric" }).format(d);
const mo = new Intl.DateTimeFormat("en", { month: "2-digit" }).format(d);
const da = new Intl.DateTimeFormat("en", { day: "2-digit" }).format(d);
const ye = new Intl.DateTimeFormat('en', { year: 'numeric' }).format(d);
const mo = new Intl.DateTimeFormat('en', { month: '2-digit' }).format(d);
const da = new Intl.DateTimeFormat('en', { day: '2-digit' }).format(d);
return `${ye}-${mo}-${da}`;
}
// TODO: CRUD of lessons
function addLesson() {
handleAddLesson();
setTimeout(() => {
lessonEditing = $lessons.length - 1;
if (ref) {
ref.scrollIntoView({ block: "end", behavior: "smooth" });
ref.scrollIntoView({
block: 'start',
behavior: 'smooth',
inline: 'nearest',
});
}
}, 100);
}
onMount(() => {
setCourseData(courseData);
});
$: console.log(`$lessons`, $lessons);
</script>
<CourseContainer {courseData}>
<CourseContainer>
<PageNav title="Lessons">
<div slot="widget">
<PrimaryButton label="Add" onClick={addLesson} />
<PrimaryButton
label="Add"
onClick={addLesson}
isDisabled={!!lessonEditing}
/>
</div>
</PageNav>
<PageBody width="w-11/12 m-auto">
<div class="w-4/5 m-auto">
<div class="w-4/5 mt-4 m-auto">
{#each $lessons as lesson, index}
<div
bind:this={ref}
@@ -73,13 +116,16 @@
<!-- Complete or Not complete icon -->
<div class="absolute -left-6 -top-6 success">
<IconButton
onClick={() => (lesson.isComplete = !lesson.isComplete)}
onClick={() => {
lesson.is_complete = !lesson.is_complete;
handleSaveLesson(lesson, $course.id);
}}
toolTipProps={{
title: `Click to ${lesson.isComplete ? "lock" : "unlock"}`,
direction: "right",
title: `Click to ${lesson.is_complete ? 'lock' : 'unlock'}`,
direction: 'right',
}}
>
{#if lesson.isComplete}
{#if lesson.is_complete}
<CheckmarkFilled24 class="carbon-icon" />
{:else}
<span class="text-2xl">🔒</span>
@@ -90,7 +136,12 @@
<!-- Edit/Save -->
<div class="absolute top-2 right-0">
{#if lessonEditing === index}
<IconButton onClick={() => (lessonEditing = null)}>
<IconButton
onClick={() => {
lessonEditing = null;
handleSaveLesson(lesson, $course.id);
}}
>
<Save24 class="carbon-icon" />
</IconButton>
{:else}
@@ -107,62 +158,98 @@
{#if lessonEditing === index}
<TextField bind:value={lesson.title} autofocus={true} />
{:else}
<h3 class="text-3xl font-semibold m-0 flex items-center">
<a href={lesson.to} class="hover:underline text-black">
<h3 class="text-2xl m-0 flex items-center">
<a
href="/courses/{$course.id}/lessons/{lesson.id}"
class="hover:underline text-black"
>
{lesson.title}
</a>
</h3>
{/if}
<div class="flex items-center mb-2">
<!-- <div class="flex items-center mb-2">
<div class="flex items-center mr-2">
{lesson.resources
.map((r) => `${r.value} ${r.label}`)
.join(", ")}
{resources.map((r) => `${r.value} ${r.label}`).join(', ')}
</div>
</div>
</div> -->
<div class="flex items-center justify-between mt-3 w-full">
<div class="flex items-center justify-between mt-6 w-full">
{#if lessonEditing === index}
<Select
bind:value={lesson.tutor.name}
options={$tutors}
valueKey="text"
bind:value={lesson.profile}
options={$group.tutors}
labelKey="fullname"
/>
{:else if !lesson.profile}
<p class="ml-2 text-sm">No tutor added</p>
{:else}
<a
href="courses/1/people"
class="flex items-center hover:underline text-black"
class="flex items-center hover:underline"
>
<img
alt="Placeholder"
class="block rounded-full"
width="24"
height="20"
src={lesson.tutor.avatar}
src={lesson.profile.avatar_url}
/>
<p class="ml-2 text-sm font-bold">{lesson.tutor.name}</p>
<p class="ml-2 text-sm">
{lesson.profile.fullname}
</p>
</a>
{/if}
<div class="flex items-center">
<div class="flex items-cente">
{#if lessonEditing === index}
<input
type="date"
name="lesson-date-picker"
class="p-2 rounded-md w-40"
value={formatDate(lesson.date)}
on:input={(e) => (lesson.date = e.target.value)}
value={formatDate(lesson.lesson_at)}
on:input={(e) => (lesson.lesson_at = e.target.value)}
/>
{:else}
<Time24 class="carbon-icon" />
<p class="text-md font-semibold ml-2">
{new Date(lesson.date).toDateString()}
<p class="text-md ml-2">
{new Date(lesson.lesson_at).toDateString()}
</p>
{/if}
</div>
<div class="flex items-center">
{#if lessonEditing === index}
<TextField
bind:value={lesson.call_url}
autofocus={true}
className="w-40"
placeholder="https://meet.google.com/mga-dsjs-fmb"
/>
{:else}
<Phone24 class="carbon-icon" />
<a
class="text-md ml-2"
href={lesson.call_url || '#'}
target="_blank"
>
{lesson.call_url ? 'Join lesson' : 'No link'}
</a>
{/if}
</div>
</div>
</div>
<div class="absolute bottom-2 right-0">
<IconButton onClick={handleDelete(lesson.id)}>
<Delete24 class="carbon-icon" />
</IconButton>
</div>
</div>
{:else}
<Box>
<h3 class="text-3xl text-gray-500">No lesson added</h3>
<img alt="No lesson added" src="/notfound2.gif" class="w-80" />
</Box>
{/each}
</div>
</PageBody>
+23 -15
View File
@@ -1,33 +1,41 @@
<script context="module">
export async function preload({ params }) {
let [courseId] = params.id;
import { fetchCourse } from '../../../utils/services/courses';
const res = await this.fetch(`api/course?id=${courseId}`);
const data = await res.json();
if (res.status === 200) {
return { courseId, courseData: data };
} else {
this.error(res.status, data.message);
export async function preload({ params }, session) {
if (process.browser) {
return {};
}
const {
data,
// error
} = await fetchCourse(params.id, session);
return { courseData: data };
}
</script>
<script>
import { stores } from "@sapper/app";
import CourseContainer from "../../../components/CourseContainer/index.svelte";
import PageNav from "../../../components/PageNav/index.svelte";
import UserCard from "../../../components/UserCard/index.svelte";
import { onMount } from 'svelte';
import { stores } from '@sapper/app';
import CourseContainer from '../../../components/CourseContainer/index.svelte';
import PageNav from '../../../components/PageNav/index.svelte';
import UserCard from '../../../components/UserCard/index.svelte';
import { setCourseData } from '../../../components/Course/store';
export let courseData = {};
const { page } = stores();
const teachers = courseData.people;
const teachers = [];
const students = [...teachers, ...teachers, ...teachers];
onMount(() => {
setCourseData(courseData);
});
</script>
<CourseContainer {courseData}>
<CourseContainer>
<PageNav title="People" disableSticky={true} />
<div class="m-3">
<h3>
+20 -12
View File
@@ -1,25 +1,33 @@
<script context="module">
export async function preload({ params }) {
let [courseId] = params.id;
import { fetchCourse } from '../../../utils/services/courses';
const res = await this.fetch(`api/course?id=${courseId}`);
const data = await res.json();
if (res.status === 200) {
return { courseId, courseData: data };
} else {
this.error(res.status, data.message);
export async function preload({ params }, session) {
if (process.browser) {
return {};
}
const {
data,
// error
} = await fetchCourse(params.id, session);
return { courseData: data };
}
</script>
<script>
import CourseContainer from "../../../components/CourseContainer/index.svelte";
import PageNav from "../../../components/PageNav/index.svelte";
import { onMount } from 'svelte';
import CourseContainer from '../../../components/CourseContainer/index.svelte';
import PageNav from '../../../components/PageNav/index.svelte';
import { setCourseData } from '../../../components/Course/store';
export let courseData = {};
onMount(() => {
setCourseData(courseData);
});
</script>
<CourseContainer {courseData}>
<CourseContainer>
<PageNav title="Scoreboard" />
</CourseContainer>
+248
View File
@@ -0,0 +1,248 @@
<script context="module">
import { fetchCourse } from '../../../utils/services/courses';
export async function preload({ params }, session) {
if (process.browser) {
return {};
}
const {
data,
// error
} = await fetchCourse(params.id, session);
return { courseData: data };
}
</script>
<script>
import { onMount } from 'svelte';
import { flip } from 'svelte/animate';
import { dndzone } from 'svelte-dnd-action';
import { stores, goto } from '@sapper/app';
import PageNav from '../../../components/PageNav/index.svelte';
import MarkExerciseModal from '../../../components/Course/components/Lesson/Exercise/MarkExerciseModal.svelte';
import Chip from '../../../components/Chip/index.svelte';
import PageBody from '../../../components/PageBody/index.svelte';
import CourseContainer from '../../../components/CourseContainer/index.svelte';
import { setCourseData, course } from '../../../components/Course/store';
import { fetchSubmissions } from '../../../utils/services/submissions';
const flipDurationMs = 300;
export let courseData = {};
let sections = [
{
id: 1,
title: 'Submitted',
value: 0,
items: [],
},
{
id: 2,
title: 'In Progress',
value: 0,
items: [],
},
{
id: 3,
title: 'Graded',
value: 10,
items: [],
},
];
let submissionIdData = {};
let submissionId;
let openExercise = false;
const { page } = stores();
function handleItemFinalize(columnIdx, newItems) {
console.log('handleItemFinalize columnIdx=>', columnIdx, newItems);
sections[columnIdx].items = newItems;
// onFinalUpdate([...sections]);
}
function handleDndConsiderCards(columnIdx) {
return (e) => {
console.warn(
'handleDndConsiderCards columnIdx',
columnIdx,
e.detail.items
);
sections[columnIdx].items = e.detail.items;
};
}
function handleDndFinalizeCards(columnIdx) {
return (e) => handleItemFinalize(columnIdx, e.detail.items);
}
function handleModalClose() {
goto($page.path);
}
function formatAnswers(data) {
const answers = {};
const questionByIdAndName = {};
for (const question of data.questions) {
questionByIdAndName[question.id] = question.name;
}
for (const answer of data.answers) {
const questionName = questionByIdAndName[answer.question_id];
answers[questionName] =
Array.isArray(answer.answers) && answer.answers.length
? answer.answers
: answer.open_answer;
}
return answers;
}
onMount(async () => {
setCourseData(courseData);
console.log(`courseData`, courseData);
console.log(`$course`, $course);
const { data: submissions } = await fetchSubmissions(
courseData.id || $course.id
);
const sectionById = {};
for (const submission of submissions) {
const { id, exercise, course, answers, groupmember, status_id } =
submission;
const submissionItem = {
id,
exercise: {
id: exercise.id,
title: exercise.title,
},
answers,
student: groupmember && groupmember.profile ? groupmember.profile : {},
lesson: {
id: exercise.lesson.id,
title: exercise.lesson.title,
},
};
submissionIdData[id] = {
questions: exercise.questions,
answers: formatAnswers({ questions: exercise.questions, answers }),
};
if (Array.isArray(sectionById[status_id])) {
sectionById[status_id].push(submissionItem);
} else {
sectionById[status_id] = [submissionItem];
}
}
sections = sections.map((section, index) => ({
...section,
value: Array.isArray(sectionById[index + 1])
? sectionById[index + 1].length
: 0,
items: Array.isArray(sectionById[index + 1])
? sectionById[index + 1]
: [],
}));
console.log(`submissionIdData`, submissionIdData);
});
$: {
submissionId = $page.query.submissionId;
openExercise = !!submissionId;
}
</script>
<MarkExerciseModal
bind:open={openExercise}
onClose={handleModalClose}
data={submissionIdData[submissionId] || {}}
{submissionId}
/>
<CourseContainer>
<PageNav title="Submitted Exercises" />
<PageBody width="w-11/12 overflow-x-auto overflow-y-hidden">
<div class="flex items-center justify-between w-full">
{#each sections as { id, title, items }, idx (id)}
<div
class="section rounded-md bg-gray-100 border border-gray-50 p-3 h-80 mr-3 overflow-hidden"
animate:flip={{ duration: flipDurationMs }}
>
<div class="flex items-center mb-2">
<Chip value={items.length} />
<p class="ml-2 font-bold">{title}</p>
</div>
<div
class="content pr-2 overflow-y-auto mb-3"
use:dndzone={{
items,
flipDurationMs,
dropTargetStyle: { outline: 'blue' },
}}
on:consider={handleDndConsiderCards(idx)}
on:finalize={handleDndFinalizeCards(idx)}
>
{#each items as item (item.id)}
<div
class="border border-grey-700 w-full my-2 mx-0 rounded-md bg-white py-2 px-3"
animate:flip={{ duration: flipDurationMs }}
>
<a
class="flex items-center no-underline hover:underline text-black mb-2"
href="{$page.path}?submissionId={item.student.id}"
>
<img
alt="Placeholder"
class="block rounded-full"
width="24"
height="20"
src={item.student.avatar_url}
/>
<p class="ml-2 text-sm">{item.student.username}</p>
</a>
<a
class="text-blue-700 text-md font-bold"
href="{$page.path}?submissionId={item.id}"
>
{item.exercise.title}
</a>
<a
class="flex items-center no-underline hover:underline text-black my-2"
href="{$page.path.replace('submissions', 'lessons')}/{item
.lesson.id}/exercises"
>
<p class="text-grey text-sm">
#{item.lesson.title}
{`${item.tutor ? 'created by' + item.tutor.name : ''}`}
</p>
</a>
</div>
{/each}
</div>
</div>
{/each}
</div>
</PageBody>
</CourseContainer>
<style>
.section {
max-width: 355px;
min-width: 355px;
height: 80vh;
}
.content {
height: 95%;
}
</style>
+20 -12
View File
@@ -1,25 +1,33 @@
<script context="module">
export async function preload({ params }) {
let [courseId] = params.id;
import { fetchCourse } from '../../../utils/services/courses';
const res = await this.fetch(`api/course?id=${courseId}`);
const data = await res.json();
if (res.status === 200) {
return { courseId, courseData: data };
} else {
this.error(res.status, data.message);
export async function preload({ params }, session) {
if (process.browser) {
return {};
}
const {
data,
// error
} = await fetchCourse(params.id, session);
return { courseData: data };
}
</script>
<script>
import CourseContainer from "../../../components/CourseContainer/index.svelte";
import PageNav from "../../../components/PageNav/index.svelte";
import { onMount } from 'svelte';
import CourseContainer from '../../../components/CourseContainer/index.svelte';
import PageNav from '../../../components/PageNav/index.svelte';
import { setCourseData } from '../../../components/Course/store';
export let courseData = {};
onMount(() => {
setCourseData(courseData);
});
</script>
<CourseContainer {courseData}>
<CourseContainer>
<PageNav title="Timetable" />
</CourseContainer>
+5 -1
View File
@@ -80,6 +80,10 @@
];
</script>
<svelte:head>
<title>Dashboard - ClassroomIO</title>
</svelte:head>
<div class="root w-full py-10 bg-gray-100">
<div class="flex items-center justify-evenly max-w-6xl m-auto">
{#each boxes as box}
@@ -138,7 +142,7 @@
<style>
.root {
min-height: 90vh;
min-height: 93vh;
}
.table {
+7 -3
View File
@@ -53,9 +53,7 @@
<title>ClassroomIO - The classroom software</title>
</svelte:head>
<div
class="w-screen flex items-center justify-center flex-col m-2 sm:h-screen sm:m-0"
>
<div class="root w-screen flex items-center justify-center flex-col m-2 sm:m-0">
<img
src="/logo-192.png"
alt="ClassroomIO logo"
@@ -128,4 +126,10 @@
.box {
max-width: 350px;
}
@media (min-width: 640px) {
.root {
height: 93vh;
}
}
</style>
+31 -14
View File
@@ -5,9 +5,11 @@
</script>
<script>
import { Chasing } from 'svelte-loading-spinners';
import TextField from '../components/Form/TextField.svelte';
import PrimaryButton from '../components/PrimaryButton/index.svelte';
import { getSupabase } from '../utils/functions/supabase';
import { user } from '../utils/store/user';
export let config;
@@ -37,7 +39,6 @@
success = true;
email = {};
alert('Check your email for the login link!');
} catch (error) {
errors = {
email: error.error_description || error.message,
@@ -69,33 +70,49 @@
<p class="text-sm">into ClassroomIO</p>
<div class="my-4 w-full">
{#if success}
<p class="text-xl text-center">Please check your email to signin</p>
{#if $user.fetchingUser}
<div class="h-40 text-md flex items-center justify-center">
<Chasing size="60" color="#ff3e00" unit="px" duration="1s" />
</div>
{:else if success}
<div class="h-40 text-md flex items-center justify-center flex-col">
<p class="">
A link has been sent to your email. Please check both your
<strong>spam</strong>and your<strong>inbox</strong>.
</p>
<p class="mt-2 underline">
If our email is in your spam, do make sure you mark our email
address as safe, then move it from your spam to your inbox.
</p>
</div>
{:else}
<TextField
label="Email address"
bind:value={email}
type="email"
autoFocus={true}
placeholder="Enter your email"
placeholder="you@domain.com"
inputClassName="rounded-md w-full px-2 py-4"
isDisabled={loading}
errorMessage={errors.email}
/>
<p class="py-3 text-sm">
<p class="py-3 text-md">
A magic link will be sent to your email, please open the link
</p>
{/if}
</div>
<div class="my-4 w-full flex justify-between items-center">
<a href="/signup" class="text-blue-700 text-sm">Create an account</a>
<PrimaryButton
label={loading ? 'Creating...' : 'Next'}
type="submit"
className="py-2 px-4"
isDisabled={loading}
/>
</div>
{#if !success}
<div class="my-4 w-full flex justify-between items-center">
<a href="/signup" class="text-blue-700 text-sm">Create an account</a>
<PrimaryButton
label={loading ? 'Creating...' : 'Next'}
type="submit"
className="py-2 px-4"
isDisabled={loading}
/>
</div>
{/if}
</form>
</div>
</div>
+15
View File
@@ -0,0 +1,15 @@
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 -1
View File
@@ -87,6 +87,6 @@
<style>
.root {
min-height: 90vh;
min-height: 93vh;
}
</style>
-67
View File
@@ -1,67 +0,0 @@
<script context="module">
export function preload(page, { config }) {
return { config };
}
</script>
<script>
import { getSupabase } from '../utils/functions/supabase';
import { user, profile } from '../utils/store/user';
export let config;
let supabase = getSupabase(config);
let loading = true;
let username = null;
let website = null;
let avatar_url = null;
async function updateProfile() {
try {
loading = true;
const user = supabase.auth.user();
const updates = {
id: user.id,
username,
website,
avatar_url,
updated_at: new Date(),
};
let { error } = await supabase.from('profiles').upsert(updates, {
returning: 'minimal', // Don't return the value after inserting
});
if (error) throw error;
} catch (error) {
alert(error.message);
} finally {
loading = false;
}
}
</script>
<form class="form-widget" on:submit|preventDefault={updateProfile}>
<div>
<label for="email">Email</label>
<input id="email" type="text" value={$user.email} disabled />
</div>
<div>
<label for="username">Name</label>
<input id="username" type="text" bind:value={username} />
</div>
<div>
<label for="website">Website</label>
<input id="website" type="website" bind:value={website} />
</div>
<div>
<input
type="submit"
class="button block primary"
value={loading ? 'Loading ...' : 'Update'}
disabled={loading}
/>
</div>
</form>
+199
View File
@@ -0,0 +1,199 @@
<script context="module">
export function preload({ params }) {
console.log(`params`, params);
return { profileId: params.id };
}
</script>
<script>
// import { goto } from '$app/navigation';
import { onMount } from 'svelte';
import { Chasing } from 'svelte-loading-spinners';
import Search32 from 'carbon-icons-svelte/lib/Search32';
import UserAvatar16 from 'carbon-icons-svelte/lib/UserAvatar16';
import Book16 from 'carbon-icons-svelte/lib/Book16';
import TextField from '../../components/Form/TextField.svelte';
import PrimaryButton from '../../components/PrimaryButton/index.svelte';
import { supabase } from '../../utils/functions/supabase';
import { user, profile } from '../../utils/store/user';
export let profileId;
let loading = false;
let currentProfile = {};
let isOwner = false;
let docs = [];
let initialValueOfUserName;
let initialValueOfFullName;
async function getProfile(profileId) {
loading = true;
// Get user profile
// Check if user has profile
let {
data: profileData,
error,
status,
} = await supabase.from('profile').select(`*`).eq('id', profileId).single();
if (error && !profileData && status === 406 && $user.currentSession) {
// Not found
} else if (profileData) {
// Profile exists, go to profile page
currentProfile = profileData;
initialValueOfUserName = currentProfile.username;
initialValueOfFullName = currentProfile.fullname;
}
loading = false;
}
async function updateProfile() {
try {
loading = true;
const updates = {
fullname: currentProfile.fullname,
username: currentProfile.username,
avatar_url: currentProfile.avatar_url,
};
let { error } = await supabase
.from('profile')
.update(updates, {
returning: 'minimal', // Don't return the value after inserting
})
.match({ id: profileId });
if (error) throw error;
} catch (error) {
alert(error.message);
loading = false;
} finally {
loading = false;
initialValueOfUserName = currentProfile.username;
initialValueOfFullName = currentProfile.fullname;
}
}
onMount(() => {});
$: isOwner = $profile.id === profileId;
$: {
getProfile(profileId);
}
</script>
<svelte:head>
<title>Profile of {currentProfile.username}</title>
</svelte:head>
<section class="root w-11/12 mt-3 m-auto flex items-start justify-between">
{#if currentProfile.id}
<div class="w-1/4 flex items-center flex-col">
<img
alt={currentProfile.username}
src={currentProfile.avatar_url}
height="260"
width="260"
class="rounded-full bg-light-blue-100 mb-4"
/>
{#if isOwner}
<TextField
label="Full name"
bind:value={currentProfile.fullname}
className="w-60 mb-4"
inputClassName="rounded-md"
/>
<TextField
label="Username"
bind:value={currentProfile.username}
className="w-60 mb-4"
inputClassName="rounded-md"
/>
{#if initialValueOfUserName !== currentProfile.username || initialValueOfFullName !== currentProfile.fullname}
<PrimaryButton
label={loading ? 'Updating...' : 'Update profile'}
onClick={updateProfile}
isDisabled={loading}
/>
{/if}
{:else}
<h3 class="text-xl">{currentProfile.username}</h3>
{/if}
</div>
<div class="flex-grow" />
<div class="flex-grow flex flex-wrap mx-3 w-3/5">
<div class="mb-3 w-full">
<h3 class="font-bold text-md m-0">Organizations</h3>
<p class="text-gray-500 text-sm">
This are the organizations you work at
</p>
<div class="my-3 py-2 px-4 border shadow-sm rounded-md">
<h4 class="m-0 font-normal">
<a class="text-blue-700 font-bold" href="/courses"> CitiBim </a>
</h4>
<p class="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.
Sequi doloremque blanditiis earum.
</p>
<div class="mt-2 flex items-center">
<span class="flex mr-2 items-center">
<UserAvatar16 class="mr-2" /> 5
</span>
<span class="flex mr-2 items-center">
<Book16 class="mr-2" /> 5
</span>
</div>
</div>
<div class="my-2 py-2 px-4 border shadow-sm rounded-md">
<h4 class="m-0 font-normal">
<a class="text-blue-700 font-bold" href="/courses">
Climate Change Group
</a>
</h4>
<p class="text-gray-500">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Illo quasi
autem quo possimus illum consequuntur repudiandae nobis error, porro
</p>
<div class="mt-2 flex items-center">
<span class="flex mr-2 items-center">
<UserAvatar16 class="mr-2" /> 30
</span>
<span class="flex mr-2 items-center">
<Book16 class="mr-2" /> 3
</span>
</div>
</div>
</div>
</div>
<!-- {:else if loading}
<Box>
<Chasing size="60" color="#ff3e00" unit="px" duration="1s" />
</Box>
{:else}
<Box>
<div>
<Search32 />
</div>
<h3 class="text-xxl">Profile not found</h3>
</Box> -->
{/if}
</section>
<style>
.root {
max-width: 1204px;
}
</style>