Sync with Server
This commit is contained in:
+38
-15
@@ -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>
|
||||
|
||||
@@ -378,8 +378,8 @@ export function getLesson(id) {
|
||||
lesson_id: null,
|
||||
materials: {
|
||||
note: '',
|
||||
slide: '',
|
||||
video: '',
|
||||
slide_url: '',
|
||||
video_url: '',
|
||||
},
|
||||
exercises: getExercises(id),
|
||||
}
|
||||
|
||||
+168
-5
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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 }));
|
||||
}
|
||||
@@ -87,6 +87,6 @@
|
||||
|
||||
<style>
|
||||
.root {
|
||||
min-height: 90vh;
|
||||
min-height: 93vh;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user