CLASSROOMIO-T-21: Course landing page + editor

This commit is contained in:
Rotimi Best
2022-12-30 13:08:54 +01:00
parent 38eae9dafb
commit 963d899adf
80 changed files with 2543 additions and 653 deletions
@@ -1,6 +1,6 @@
<script>
import Label from "./Label.svelte";
import Avatar from "./Avatar.svelte";
import Label from './Label.svelte';
import Avatar from './Avatar.svelte';
export let poll = {};
export let onSelect = () => {};
@@ -19,7 +19,7 @@
}
function getSelectedUsers(users) {
return users.map((o) => o.label).join(", ");
return users.map((o) => o.label).join(', ');
}
function getTotalVotes(options) {
@@ -103,7 +103,7 @@
<div class="mr-5">
<Label>Expiration</Label>
<p class="text-sm text-gray-600 mb-4">
{poll.expiration || "Today"}
{poll.expiration || 'Today'}
</p>
</div>
<div class="flex flex-col items-center">
@@ -170,7 +170,7 @@
{/each}
<div class="flex items-center">
<p class="text-sm text-gray-600">
Expires: {poll.expiration || "Today"}
Expires: {poll.expiration || 'Today'}
</p>
<span class="mx-2">|</span>
<p class="text-sm text-gray-600">
@@ -182,7 +182,7 @@
{#if isAuthor}
<div class="w-full text-center mt-3">
<button
class="px-5 py-2 bg-blue-700 text-white"
class="px-5 py-3 bg-blue-700 text-white"
on:click={() => (viewResult = !viewResult)}
>
{#if viewResult}
@@ -192,7 +192,7 @@
{/if}
</button>
<button
class="px-5 py-2 bg-red-500 text-white"
class="px-5 py-3 bg-red-500 text-white"
on:click={handlePollDelete}
>
Delete
+2 -2
View File
@@ -57,9 +57,9 @@
</button>
{#if facultyFilterOpened}
<div class="w-48 bg-white border border-grey rounded-lg mt-2 py-2">
<div class="w-48 bg-white border border-grey rounded-lg mt-2 py-3">
{#each faculties as faculty}
<div class="block px-4 py-2 border-b">
<div class="block px-4 py-3 border-b">
<input
type="checkbox"
class="border border-gray checked:bg-blue-600 checked:border-transparent mr-1 cursor-pointer"
+1 -1
View File
@@ -28,7 +28,7 @@
<div class="w-full min-h-screen flex items-center justify-center mt-4">
<div class="container border border-gray bg-white">
<div class="flex items-center flex-col p-2 lg:px-8 lg:py-4">
<div class="flex items-center flex-col p-2 lg:px-8 lg:py-3">
{#if !showOnlyContent || showLogo}
<div class="flex items-center w-full justify-center pt-4">
<img
+4 -1
View File
@@ -9,13 +9,16 @@
p {
display: inline-block;
min-width: 20px;
padding: 0 6px;
font-size: 12px;
font-weight: 500;
line-height: 18px;
text-align: center;
border: 1px solid transparent;
border-radius: 2em;
width: 24px;
height: 24px;
padding-top: 2px;
}
.bg-set {
@@ -25,7 +25,7 @@
<div class="mt-5 flex items-center justify-between">
<PrimaryButton
className="px-6 py-2"
className="px-6 py-3"
variant={VARIANTS.OUTLINED}
label="No, please don't"
onClick={() => {
@@ -34,7 +34,7 @@
}}
/>
<PrimaryButton
className="px-6 py-2"
className="px-6 py-3"
variant={VARIANTS.CONTAINED_DANGER}
label="Yes, delete"
onClick={() => {
@@ -132,13 +132,13 @@
<div class="mt-5 flex items-center justify-between">
<PrimaryButton
className="px-6 py-2"
className="px-6 py-3"
variant={VARIANTS.OUTLINED}
label="No, cancel"
type="submit"
/>
<PrimaryButton
className="px-6 py-2"
className="px-6 py-3"
variant={VARIANTS.CONTAINED_DANGER}
label={isDeleting ? 'Deleting...' : 'Yes, delete'}
isDisabled={isDeleting}
@@ -174,7 +174,7 @@
</QuestionContainer>
<div class="float-right">
<PrimaryButton
className="px-6 py-2"
className="px-6 py-3"
variant={VARIANTS.CONTAINED_DANGER}
label="Delete this exercise"
onClick={() => (shouldDelete = true)}
@@ -111,7 +111,7 @@
}}
label="Save"
variant={VARIANTS.CONTAINED_SUCCESS}
className="py-2 px-8"
className="py-3 px-8"
/>
</div>
<div class="border border-gray-300 rounded-md mt-2">
@@ -228,14 +228,14 @@
<h2 class="text-xl mb-2 mt-0">{$questionnaire.title}</h2>
{#if STATUS.GRADED === $questionnaireMetaData.status}
<span
class="status-text bg-green-700 text-white rounded-full py-2 px-6 text-center"
class="status-text bg-green-700 text-white rounded-full py-3 px-6 text-center"
title="Status: Pending Review"
>
Graded
</span>
{:else}
<span
class="status-text bg-yellow-600 text-white rounded-full py-2 px-6 text-center"
class="status-text bg-yellow-600 text-white rounded-full py-3 px-6 text-center"
title="Status: Pending Review"
>
Pending
@@ -3,7 +3,7 @@
import { onMount } from 'svelte';
import AudioConsole32 from 'carbon-icons-svelte/lib/AudioConsole32';
import Box from '../../../../Box/index.svelte';
import { Firework } from 'svelte-loading-spinners';
import { Moon } from 'svelte-loading-spinners';
import Backdrop from '../../../../Backdrop/index.svelte';
import PrimaryButton from '../../../../PrimaryButton/index.svelte';
import RoleBasedSecurity from '../../../../RoleBasedSecurity/index.svelte';
@@ -122,7 +122,7 @@
{#if isFetching}
<Backdrop>
<Firework size="60" color="#1d4ed8" unit="px" duration="1s" />
<Moon size="60" color="#1d4ed8" unit="px" duration="1s" />
</Backdrop>
{/if}
@@ -147,7 +147,7 @@
<div class="mt-5 flex items-center">
<PrimaryButton
className="px-6 py-2"
className="px-6 py-3"
label="Submit"
onClick={handleAddExercise}
/>
@@ -1,5 +1,6 @@
<script>
import MODES from '../../../../../utils/constants/mode.js';
import { formatYoutubeVideo } from '../../../../../utils/functions/formatYoutubeVideo';
import Tabs from '../../../../Tabs/index.svelte';
import TabContent from '../../../../TabContent/index.svelte';
@@ -24,34 +25,6 @@
const onChange = (tab) => () => (currentTab = tab);
function formatYoutubeVideo(url) {
console.log(`url`, url);
const prefix = 'https://www.youtube.com/embed';
// https://www.youtube.com/embed/qajK1J1neAM
if (url.includes('embed')) {
return url;
}
// https://youtu.be/qajK1J1neAM
if (url.includes('.be/')) {
const splittedUrlWithBe = url.split('.be/');
return `${prefix}/${splittedUrlWithBe[1]}`;
}
// https://www.youtube.com/watch?v=qajK1J1neAM
const splitedUrl = url.split('watch');
if (splitedUrl.length !== 2) {
errors.video = 'Wrong url format';
return;
}
const query = new URLSearchParams(splitedUrl[1]);
return `${prefix}/${query.get('v')}`;
}
function handleSave(prevMode) {
if (prevMode === MODES.edit) {
handleUpdateLessonMaterials($lesson, lessonId);
@@ -146,7 +119,7 @@
width="100%"
height="569"
class="iframe"
src={formatYoutubeVideo(url)}
src={formatYoutubeVideo(url, errors)}
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
@@ -1,11 +1,13 @@
import { writable } from 'svelte/store';
import { writable, Writable, Updater } from 'svelte/store';
import {
createLesson,
updateLesson,
deleteLesson,
} from '../../../../../utils/services/courses';
export const lessons = writable([]);
import type { Lesson, Course } from '../../../../../utils/types';
export const lessons: Writable<Lesson[]> = writable([]);
export const lesson = writable({
id: null,
totalExercises: 0,
@@ -19,7 +21,7 @@ export const lesson = writable({
export const isLessonDirty = writable(false);
export function handleAddLesson() {
lessons.update((_lessons) => {
lessons.update(((_lessons) => {
return [
..._lessons,
{
@@ -31,10 +33,10 @@ export function handleAddLesson() {
is_complete: false,
},
];
});
}) as Updater<any>);
}
export function handleDelete(lessonId) {
export function handleDelete(lessonId: Lesson['id']) {
return async () => {
// Need to implement soft delete
if (lessonId) {
@@ -52,18 +54,21 @@ export function handleDelete(lessonId) {
};
}
export async function handleSaveLesson(lesson, course_id) {
export async function handleSaveLesson(
lesson: Lesson,
course_id: Course['id']
) {
console.log(`handleSaveLesson lesson`, lesson);
const newLesson = {
title: lesson.title,
lesson_at: lesson.lesson_at,
call_url: lesson.call_url,
teacher_id: lesson.profile ? lesson.profile.id : null,
teacher_id: lesson.profile ? lesson.profile.id : undefined,
course_id,
is_complete: lesson.is_complete,
};
let updatedLesson;
let updatedLesson: any[] | null = null;
if (typeof lesson.id === 'string') {
// No need to get the result of update cause we have all in local state
@@ -85,7 +90,10 @@ export async function handleSaveLesson(lesson, course_id) {
);
}
export async function handleUpdateLessonMaterials(lesson, lessonId) {
export async function handleUpdateLessonMaterials(
lesson: any,
lessonId: Lesson['id']
) {
const materials = {
...lesson.materials,
};
@@ -171,7 +171,7 @@
<a
class="item flex items-center {(
path || $page.path
).includes(item.id) && 'active'} pl-7 py-2 {isStudent &&
).includes(item.id) && 'active'} pl-7 py-3 {isStudent &&
!item.is_complete
? 'cursor-not-allowed'
: ''}"
@@ -27,13 +27,13 @@
<div class="mt-5 flex items-center justify-between">
<PrimaryButton
className="px-6 py-2"
className="px-6 py-3"
variant={VARIANTS.OUTLINED}
label="No"
onClick={() => ($deleteMemberModal.open = false)}
/>
<PrimaryButton
className="px-6 py-2"
className="px-6 py-3"
variant={VARIANTS.OUTLINED}
label="Yes"
onClick={handleDelete}
@@ -9,7 +9,7 @@
} from '../../../../utils/services/courses';
import { invitationModal, resetForm } from './store';
import { validateForm } from '../../../Courses/functions';
import { course, setCourseData } from '../../../Course/store';
import { course, setCourse } from '../../store';
let errors = {
emails: '',
@@ -47,7 +47,7 @@
addGroupMember(membersStack).then(async (membersAdded) => {
const group = await fetchGroup($course.group.id);
setCourseData(
setCourse(
{
...$course,
group: group.data,
@@ -90,7 +90,7 @@
/>
<div class="mt-5 flex items-center">
<PrimaryButton className="px-6 py-2" label="Finish" type="submit" />
<PrimaryButton className="px-6 py-3" label="Finish" type="submit" />
</div>
</form>
</Modal>
@@ -29,8 +29,7 @@
}
$settingsDialog.open = false;
const logo = await updateCourse($course.id, {
avatar,
const logo = await updateCourse($course.id, avatar, {
title: $course.title,
description: $course.description,
});
@@ -67,13 +66,13 @@
<div class="mt-5 flex items-center justify-between">
<PrimaryButton
className="px-6 py-2"
className="px-6 py-3"
variant={VARIANTS.OUTLINED}
label="No, cancel"
type="submit"
/>
<PrimaryButton
className="px-6 py-2"
className="px-6 py-3"
variant={VARIANTS.OUTLINED}
label="Yes, delete"
onClick={handleDelete}
@@ -103,12 +102,12 @@
<div class="mt-5 flex items-center justify-between">
<PrimaryButton
className="px-6 py-2"
className="px-6 py-3"
variant={VARIANTS.CONTAINED_DANGER}
label="Delete"
onClick={() => (shouldDelete = true)}
/>
<PrimaryButton className="px-6 py-2" label="Update" type="submit" />
<PrimaryButton className="px-6 py-3" label="Update" type="submit" />
</div>
</form>
{/if}
+3 -3
View File
@@ -26,12 +26,12 @@ export function getLessonsRoute(courseId, lessonId) {
return `${path}/${lessonId}`;
}
export function getLectureNo(index) {
export function getLectureNo(index, initNo = '00') {
if (index <= 9) {
return `00${index}`;
return `${initNo}${index}`;
}
return `0${index}`;
return `${initNo.replace('0', '')}${index}`;
}
export function formatAnswers(data) {
+1 -1
View File
@@ -6,7 +6,7 @@
import PrimaryButton from '../PrimaryButton/index.svelte';
import PageBody from '../PageBody/index.svelte';
import EditContent from '../EditContent/index.svelte';
import { course } from '../Course/store';
import { course } from './store';
import { updateCourse } from '../../utils/services/courses';
let mode = MODES.view;
@@ -1,8 +1,35 @@
import { writable } from 'svelte/store';
import { writable, Writable } from 'svelte/store';
import { lessons } from './components/Lesson/store/lessons';
//@ts-ignore
import { ROLE } from '../../utils/constants/roles';
import type { Course } from '../../utils/types';
export const course = writable({ id: '' });
export const course: Writable<Course> = writable({
id: '',
title: '',
description: '',
cost: 0,
currency: '',
metadata: {
requirements: '',
description: '',
goals: '',
videoUrl: '',
showDiscount: false,
discount: 0,
reward: {
show: false,
description: '',
},
instructor: {
name: '',
role: '',
coursesNo: 0,
description: '',
imgUrl: '',
},
},
});
export const mockGroupMember = {
id: '434534534535',
@@ -27,7 +54,7 @@ export const group = writable({
people: [],
});
export async function setCourseData(data, setLesson = true) {
export async function setCourse(data: Course, setLesson = true) {
if (!data || !(Object.values(data) && Object.values(data).length)) return;
// const tutorsById = {};
@@ -38,27 +65,37 @@ export async function setCourseData(data, setLesson = true) {
people: [],
});
// @ts-ignore
if (Array.isArray(groupData.members)) {
// @ts-ignore
for (const member of groupData.members) {
if (member.role_id === ROLE.STUDENT) {
// @ts-ignore
groupData.students.push(member);
} else if (member.profile) {
// @ts-ignore
groupData.tutors.push(member.profile);
// tutorsById[member.profile.id] = member.profile;
}
}
// @ts-ignore
groupData.people = groupData.members;
}
// @ts-ignore
delete groupData.members;
// @ts-ignore
group.set(groupData);
}
if (setLesson) {
// @ts-ignore
const orderedLessons = (data.lessons || [])
// @ts-ignore
.sort((a, b) => new Date(a.created_at) - new Date(b.created_at))
// @ts-ignore
.sort((a, b) => a.order - b.order);
// .map((lesson) => ({
// ...lesson,
@@ -67,6 +104,29 @@ export async function setCourseData(data, setLesson = true) {
lessons.set(orderedLessons);
}
//@ts-ignore
delete data.lessons;
if (data.metadata && !Object.values(data.metadata)) {
data.metadata = {
requirements: '',
description: '',
goals: '',
videoUrl: '',
showDiscount: false,
discount: 0,
reward: {
show: false,
description: '',
},
instructor: {
name: '',
role: '',
coursesNo: 0,
description: '',
imgUrl: '',
},
};
}
course.set(data);
}
+2 -2
View File
@@ -1,5 +1,5 @@
<script>
import { Firework } from 'svelte-loading-spinners';
import { Moon } from 'svelte-loading-spinners';
import Navigation from '../Course/components/Navigation/index.svelte';
import Backdrop from '../../components/Backdrop/index.svelte';
// import { lessons, lesson } from '../Course/components/Lesson/store/lessons';
@@ -32,7 +32,7 @@
{#if isFetching}
<Backdrop>
<Firework size="60" color="#1d4ed8" unit="px" duration="1s" />
<Moon size="60" color="#1d4ed8" unit="px" duration="1s" />
</Backdrop>
{/if}
@@ -1,19 +0,0 @@
<script>
import TextField from '../../../Form/TextField.svelte';
import EditContent from '../../../EditContent/index.svelte';
import { landingPageStore } from '../../store';
</script>
<TextField
label="Company name"
bind:value={$landingPageStore.aboutUs.companyName}
className="mt-3 font-bold"
/>
<TextField
label="Company Logo URL"
bind:value={$landingPageStore.aboutUs.logoUrl}
className="mt-3 font-bold"
/>
<p class="mt-3 font-bold">Description</p>
<EditContent bind:value={$landingPageStore.aboutUs.description} />
@@ -4,14 +4,14 @@
import { isMobile } from '../../../../utils/store/useMobile';
import IconButton from '../../../IconButton/index.svelte';
let show = true;
export let show = true;
</script>
<section
class="root z-10 {!show && 'hide'} {$isMobile ? 'fixed shadow-xl' : 'sticky'}"
>
{#if show}
<div class="relative h-full bg-gray-100">
<div class="relative h-full bg-white">
<slot />
</div>
{/if}
@@ -46,6 +46,7 @@
min-width: 300px;
max-width: 300px;
top: 0;
left: 0;
border-right: 1px solid var(--border-color);
background-color: rgb(250, 251, 252);
@@ -1,6 +1,17 @@
<script>
import EditContent from '../../../EditContent/index.svelte';
import { landingPageStore } from '../../store';
import { set, get, cloneDeep } from 'lodash';
import TextEditor from '../../../TextEditor/index.svelte';
export let course;
</script>
<EditContent bind:value={$landingPageStore.description.content} />
<div class="h-2/5">
<TextEditor
content={get(course, 'metadata.description')}
onChange={(html) => {
const _course = cloneDeep(course);
set(_course, 'metadata.description', html);
course = _course;
}}
/>
</div>
@@ -0,0 +1,17 @@
<script>
import { set, get, cloneDeep } from 'lodash';
import TextEditor from '../../../TextEditor/index.svelte';
export let course;
</script>
<div class="h-2/5">
<TextEditor
content={get(course, 'metadata.goals')}
onChange={(html) => {
const _course = cloneDeep(course);
set(_course, 'metadata.goals', html);
course = _course;
}}
/>
</div>
@@ -1,38 +1,31 @@
<script>
import TextArea from '../../../Form/TextArea.svelte';
import TextField from '../../../Form/TextField.svelte';
import { landingPageStore } from '../../store';
export let course = {};
</script>
<TextArea
<TextField
className="mt-5"
labelClassName="font-bold"
label="Title"
bind:value={$landingPageStore.header.title}
rows="3"
className="mt-3 font-bold"
bgColor="bg-white"
bind:value={course.title}
/>
<TextArea
label="Description"
bind:value={$landingPageStore.header.description}
bind:value={course.description}
rows="6"
className="mt-3 font-bold"
bgColor="bg-white"
className="mt-5"
labelClassName="font-bold"
/>
<TextField
className="mt-3 font-bold"
label="Currency"
bind:value={$landingPageStore.header.currency}
/>
<TextField
className="mt-3 font-bold"
label="Cost"
bind:value={$landingPageStore.header.cost}
/>
<TextField
label="Button Label"
className="mt-3 font-bold"
bind:value={$landingPageStore.header.buttonLabel}
className="mt-5"
labelClassName="font-bold"
label="Short Video"
placeholder="www.youtube.com/watch?v=uYRq60G5XTk"
helperMessage="Enter a link to a youtube video"
type="text"
bind:value={course.metadata.videoUrl}
/>
@@ -0,0 +1,90 @@
<script>
import { set, cloneDeep, isEmpty, get } from 'lodash';
import TextArea from '../../../Form/TextArea.svelte';
import TextField from '../../../Form/TextField.svelte';
import UploadImage from '../../../UploadImage/index.svelte';
import { uploadAvatar } from '../../../../utils/services/courses';
export let course = {};
let name;
let role;
let imgUrl;
let description;
let courseNo;
let avatar;
let hasBeenSet = false;
function setter(value, setterKey) {
if (!value) return;
const _course = cloneDeep(course);
set(_course, setterKey, value);
course = _course;
}
async function onAvatarChange(_avatar) {
if (!_avatar) return;
const logo = await uploadAvatar(course.id, _avatar);
imgUrl = logo;
}
function setDefaults(course) {
if (isEmpty(course) || hasBeenSet) return;
hasBeenSet = true;
name = get(course, 'metadata.instructor.name');
role = get(course, 'metadata.instructor.role');
imgUrl = get(course, 'metadata.instructor.imgUrl');
description = get(course, 'metadata.instructor.description');
courseNo = get(course, 'metadata.instructor.courseNo');
}
$: setter(name, 'metadata.instructor.name');
$: setter(role, 'metadata.instructor.role');
$: setter(imgUrl, 'metadata.instructor.imgUrl');
$: setter(description, 'metadata.instructor.description');
$: setter(courseNo, 'metadata.instructor.courseNo');
$: onAvatarChange(avatar);
$: setDefaults(course);
</script>
<div class="mt-5">
<label for="upload" class="font-bold">Instructor Logo</label>
<UploadImage bind:avatar src={imgUrl} />
</div>
<TextField
className="mt-5"
labelClassName="font-bold"
label="Instructor name"
placeholder="Your company name"
bind:value={name}
/>
<TextField
className="mt-5"
labelClassName="font-bold"
label="Instructor Role"
placeholder="e.g Software developer"
bind:value={role}
/>
<TextArea
label="About instructor"
placeholder="A short background about the instructor"
rows="6"
className="mt-5"
labelClassName="font-bold"
bind:value={description}
/>
<TextField
className="mt-5"
labelClassName="font-bold"
label="Total number of courses"
type="number"
bind:value={courseNo}
/>
@@ -0,0 +1,97 @@
<script>
import { set, isEmpty, get, cloneDeep } from 'lodash';
import { Toggle } from 'carbon-components-svelte';
import TextEditor from '../../../TextEditor/index.svelte';
import TextField from '../../../Form/TextField.svelte';
export let course = {};
let discount = 0;
let showDiscount;
let giftToggled;
let hasBeenSet = false;
function setDefaults(course) {
console.log('\n\nsetDefaults course', course);
if (isEmpty(course) || hasBeenSet) return;
hasBeenSet = true;
discount = get(course, 'metadata.discount', 0);
showDiscount = get(course, 'metadata.showDiscount', false);
giftToggled = get(course, 'metadata.reward.show', false);
}
function setter(value, setterKey) {
console.log('\n\nsetter course', value);
if (typeof value !== 'boolean' && !value) return;
const _course = cloneDeep(course);
set(_course, setterKey, value);
course = _course;
}
$: setter(showDiscount, 'metadata.showDiscount');
$: setter(discount, 'metadata.discount');
$: setter(giftToggled, 'metadata.reward.show');
$: setDefaults(course);
</script>
<TextField
className="mt-5"
labelClassName="font-bold"
label="Currency"
value={course.currency}
isDisabled={true}
/>
<TextField
className="mt-5"
labelClassName="font-bold"
label="Cost"
type="number"
bind:value={course.cost}
/>
<div class="mt-5">
<!-- <label for="text-field" class="m-0 font-bold mb-2">Discount</label> -->
<Toggle labelText="Discount" bind:toggled={showDiscount}>
<span slot="labelA">No</span>
<span slot="labelB">Yes</span>
</Toggle>
</div>
{#if showDiscount}
<TextField
className="mt-5"
labelClassName="font-bold"
label="Discout Percent"
type="number"
bind:value={discount}
helperMessage="In Percentage %"
/>
{/if}
<div class="mt-5">
<!-- <label for="text-field" class="m-0 font-bold mb-2">Discount</label> -->
<Toggle labelText="Gift on Completion" bind:toggled={giftToggled}>
<span slot="labelA">No</span>
<span slot="labelB">Yes</span>
</Toggle>
</div>
{#if giftToggled}
<p class="font-bold mt-5">About Gift</p>
<div class="h-2/5">
<TextEditor
content={get(course, 'metadata.reward.description')}
onChange={(html) => {
const _course = cloneDeep(course);
set(_course, 'metadata.reward.description', html);
course = _course;
}}
/>
</div>
{/if}
@@ -1,6 +1,17 @@
<script>
import EditContent from '../../../EditContent/index.svelte';
import { landingPageStore } from '../../store';
import { set, get, cloneDeep } from 'lodash';
import TextEditor from '../../../TextEditor/index.svelte';
export let course;
</script>
<EditContent bind:value={$landingPageStore.requirement.content} />
<div class="h-2/5">
<TextEditor
content={get(course, 'metadata.requirements')}
onChange={(html) => {
const _course = cloneDeep(course);
set(_course, 'metadata.requirements', html);
course = _course;
}}
/>
</div>
@@ -1,61 +0,0 @@
<script>
import TextField from '../../../Form/TextField.svelte';
import PrimaryButton from '../../../PrimaryButton/index.svelte';
import CloseButton from '../../../Buttons/Close/index.svelte';
import { VARIANTS } from '../../../PrimaryButton/constants';
import { landingPageStore } from '../../store';
function handleAdd() {
landingPageStore.update((store) => {
const { syllabus } = store;
const lessons = [
...syllabus.lessons,
{
title: '',
},
];
return {
...store,
syllabus: {
lessons,
},
};
});
}
function handleDelete(index) {
landingPageStore.update((store) => {
const { syllabus } = store;
return {
...store,
syllabus: {
lessons: syllabus.lessons.filter(
(lesson, lessonIndex) => index !== lessonIndex
),
},
};
});
}
</script>
<h4 class="mb-3">Lessons</h4>
{#each $landingPageStore.syllabus.lessons as lesson, index}
<div class="mb-4 relative">
<TextField bind:value={lesson.title} />
<div class="absolute -top-3 -right-3">
<CloseButton
onClick={() => handleDelete(index)}
contained={true}
size="small"
/>
</div>
</div>
{/each}
<div class="mt-3">
<PrimaryButton label="Add" onClick={handleAdd} variant={VARIANTS.OUTLINED} />
</div>
@@ -8,13 +8,18 @@
import HeaderForm from './HeaderForm.svelte';
import RequirementForm from './RequirementForm.svelte';
import DescriptionForm from './DescriptionForm.svelte';
import SyllabusForm from './SyllabusForm.svelte';
import AboutUsForm from './AboutUsForm.svelte';
import PricingForm from './PricingForm.svelte';
import GoalsForm from './GoalsForm.svelte';
import InstructorForm from './InstructorForm.svelte';
import PrimaryButton from '../../../PrimaryButton/index.svelte';
import { VARIANTS } from '../../../PrimaryButton/constants';
import { updateCourse } from '../../../../utils/services/courses';
export let course;
export let courseId;
let borderBottomGrey = 'border-r-0 border-b border-l-0 border-gray-300';
let loading = false;
const sections = [
{
@@ -31,11 +36,15 @@
},
{
key: 4,
title: 'Syllabus',
title: 'Goals',
},
{
key: 5,
title: 'About Us',
title: 'Instructor',
},
{
key: 6,
title: 'Pricing',
},
];
let selectedSection = null;
@@ -55,29 +64,66 @@
};
}
function handlePublish() {}
function generateSlug(title) {
return title
.replaceAll(' ', '-')
.toLowerCase()
.concat(`-${new Date().getTime()}`);
}
async function handleSave() {
loading = true;
course.slug = course.slug || generateSlug(course.title);
await updateCourse(courseId, undefined, {
...course,
attendance: undefined,
group: undefined,
lessons: undefined,
slug: course.slug,
});
loading = false;
return `${window.location.origin}/course/${course.slug}`;
}
/**
* 1. Use all you have for editing now
* 2. Make course one interface we might need to computation
*/
async function handlePreview() {
const link = `${window.location.origin}/course/${course.slug}`;
window.open(link, '_blank');
}
</script>
<Container>
<Container show={true}>
<!-- Publish Header Section -->
{#if !selectedSection}
<div
class="flex justify-between items-center px-2 {borderBottomGrey} w-full"
>
<div class="flex justify-between items-center px-2 w-full">
<CloseButton onClick={handleClose} />
<PrimaryButton
label="Publish"
onClick={handlePublish}
disablePadding={true}
/>
<div class="flex items-center">
<PrimaryButton
label="Save"
type="button"
className="mr-1"
variant={VARIANTS.OUTLINED}
onClick={handleSave}
isLoading={loading}
/>
<PrimaryButton
label="Preview"
type="button"
onClick={handlePreview}
isDisabled={loading || !course.slug}
/>
</div>
</div>
<div
class="flex justify-between items-center px-2 bg-white {borderBottomGrey} w-full mb-2"
>
<div class="flex justify-between items-center px-2 w-full mb-2">
<h3 class="">Page Builder</h3>
</div>
{#each sections as section}
<button
class="w-full flex items-center justify-between px-2 py-3 bg-white border border-b-0 border-gray-300"
class="w-full flex items-center justify-between px-2 py-3 border border-b-0 border-gray-300"
on:click={handleSectionSelect(section.key)}
>
<p class="mr-2">{section.title} section</p>
@@ -85,24 +131,27 @@
</button>
{/each}
{:else}
<div class="flex items-center bg-white {borderBottomGrey} w-full">
<!-- Title -->
<div class="flex items-center {borderBottomGrey} w-full">
<IconButton onClick={handleClose}>
<ArrowLeft24 class="carbon-icon" title="Go back" />
</IconButton>
<h3 class="">{selectedSection.title}</h3>
</div>
<div class="p-2">
<div class="p-2 h-4/5 overflow-y">
{#if selectedSection.key === 1}
<HeaderForm />
<HeaderForm bind:course />
{:else if selectedSection.key === 2}
<RequirementForm />
<RequirementForm bind:course />
{:else if selectedSection.key === 3}
<DescriptionForm />
<DescriptionForm bind:course />
{:else if selectedSection.key === 4}
<SyllabusForm />
<GoalsForm bind:course />
{:else if selectedSection.key === 5}
<AboutUsForm />
<InstructorForm bind:course />
{:else if selectedSection.key === 6}
<PricingForm bind:course />
{/if}
</div>
{/if}
@@ -0,0 +1,147 @@
<script lang="ts">
import PrimaryButton from '../../PrimaryButton/index.svelte';
import { VARIANTS } from '../../PrimaryButton/constants';
import type { Course } from '../../../utils/types';
import { get } from 'lodash';
export let className = '';
export let courseData: Course = {
id: '',
title: '',
description: '',
};
let calculatedCost = 0;
let discount = 0;
let formatter: Intl.NumberFormat | undefined;
function calcDisc(percent: number, cost: number) {
if (!percent) return cost;
return Math.round((percent / 100) * cost) || cost;
}
function addToCart() {}
function buyNow() {}
function setFormatter(currency: string | undefined) {
if (!currency) return;
formatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency,
minimumFractionDigits: 2,
});
}
$: setFormatter(courseData.currency);
$: discount = get(courseData, 'metadata.discount', 0);
$: calculatedCost = calcDisc(discount, courseData.cost || 0);
</script>
<!-- Pricing Details -->
<aside
class="{className} price-container md:sticky md:top-0 md:shadow-2xl md:rounded-lg m-h-fit"
>
<div class="p-10">
<!-- Pricing -->
<div class="mb-6">
<p class="font-medium text-lg">
{formatter?.format(calculatedCost) || calculatedCost}
</p>
{#if courseData?.metadata?.showDiscount}
<p class="font-light text-sm text-gray-500">
{discount}% Discount.
<span class="line-through"
>{formatter?.format(courseData?.cost || 0) || courseData.cost}</span
>
</p>
{/if}
</div>
<!-- Call To Action Buttons -->
<div class="flex flex-col w-full items-center">
<PrimaryButton
label="Add to Cart"
className="w-full sm:w-full py-3 mb-3"
onClick={addToCart}
/>
<PrimaryButton
label="Buy Now"
className="w-full sm:w-full py-3 mb-3"
variant={VARIANTS.OUTLINED}
onClick={addToCart}
/>
<p class="font-light text-sm text-gray-500">Early bird offer. Buy ASAP</p>
</div>
</div>
<!-- Gift Container -->
{#if courseData?.metadata?.reward?.show}
<div
class="p-10 flex items-center flex-col border-t border-b border-gray-300"
>
{@html get(courseData, 'metadata.reward.description', '')}
</div>
{/if}
</aside>
<style lang="scss">
.banner {
background-color: #040f2d;
min-height: 472px;
}
.author {
color: #7888b7;
}
.banner-image {
max-width: 559px;
}
.backdrop {
background-color: rgba(0, 0, 0, 0.5);
}
.active {
position: relative;
display: inline-block;
}
.active::after {
position: absolute;
content: '';
width: 100%;
height: 3px;
background-color: var(--main-primary-color);
display: block;
bottom: -14px;
left: 0px;
}
.price-container {
width: 405px;
min-width: 330px;
height: fit-content;
}
@media (max-width: 768px) {
.price-container {
width: 100%;
min-width: unset;
}
}
.course-content {
max-width: 608px;
}
:global(.list ul li) {
margin-left: 1rem;
list-style-type: disc;
}
:global(.plyr) {
width: 100% !important;
}
</style>
+13 -8
View File
@@ -1,9 +1,10 @@
export const SECTION_KEYS = {
HEADER: 'header',
REQUIREMENT: 'requirement',
DESCRIPTION: 'description',
SYLLABUS: 'syllabus',
ABOUT_US: 'about-us',
REQUIREMENT: '#requirement',
DESCRIPTION: '#description',
GOALS: '#goals',
LESSONS: '#lessons',
INSTRUCTOR: '#instructor',
};
export const NAV_ITEMS = [
@@ -16,11 +17,15 @@ export const NAV_ITEMS = [
label: 'Description',
},
{
key: SECTION_KEYS.SYLLABUS,
label: 'Syllabus',
key: SECTION_KEYS.GOALS,
label: 'Goals',
},
{
key: SECTION_KEYS.ABOUT_US,
label: 'About Us',
key: SECTION_KEYS.LESSONS,
label: 'Lessons',
},
{
key: SECTION_KEYS.INSTRUCTOR,
label: 'Instructor',
},
];
+309 -125
View File
@@ -1,163 +1,347 @@
<script>
<script lang="ts">
// import paystack from 'paystack';
import get from 'lodash/get';
import { onMount } from 'svelte';
import RecentlyViewed16 from 'carbon-icons-svelte/lib/RecentlyViewed16';
import CurrencyDollar16 from 'carbon-icons-svelte/lib/CurrencyDollar16';
import Alarm16 from 'carbon-icons-svelte/lib/Alarm16';
import pluralize from 'pluralize';
import { stores } from '@sapper/app';
import Expandable from '../Expandable/index.svelte';
import Notebook from 'carbon-icons-svelte/lib/Notebook16/Notebook16.svelte'; //note
import PresentationFile from 'carbon-icons-svelte/lib/PresentationFile16/PresentationFile16.svelte'; // exercise
import Video from 'carbon-icons-svelte/lib/Video16/Video16.svelte'; //video
import PageNumber from 'carbon-icons-svelte/lib/PageNumber16/PageNumber16.svelte';
import PlayFilled from 'carbon-icons-svelte/lib/PlayFilled16/PlayFilled16.svelte';
import PrimaryButton from '../PrimaryButton/index.svelte';
import MarkdownRender from '../MarkdownRender/index.svelte';
import { NAV_ITEMS, SECTION_KEYS } from './constants';
import { landingPageStore } from './store';
import PricingSection from './components/PricingSection.svelte';
import { getLectureNo } from '../Course/function';
import { NAV_ITEMS } from './constants';
import Chip from '../Chip/index.svelte';
import { getEmbedId } from '../../utils/functions/formatYoutubeVideo';
import type { Course, Lesson } from '../../utils/types';
let activeNavItem = '';
export let courseData: Course = {
id: '',
title: '',
description: '',
};
let lessons: Array<Lesson> = [];
let player: any;
const expandableRootClass =
'max-w-4xl pt-5 my-3 border-r-0 border-b border-l-0 border-gray-300';
const { page } = stores();
// const paystackApi = paystack('');
function handleEnrol() {}
function handleNavItemClick(navItem) {
return () => {
activeNavItem = navItem;
};
let activeNav = NAV_ITEMS[0].key;
let instructor = {};
let video: string | undefined;
function playVideo() {
console.log('open modal with video');
}
function locationHashChanged() {
activeNav = window.location.hash;
}
function addToCart() {}
function buyNow() {
// if (courseData.cost) {
// paystackApi.tra;
// } else {
// // Course is free
// }
}
function initPlyr(_player: any, _video: string | undefined) {
if (!player) return;
// @ts-ignore
const plyr = new Plyr('#player', {});
// @ts-ignore
window.player = plyr;
}
onMount(() => {
activeNavItem = (window.location.hash || NAV_ITEMS[0].key).replace('#', '');
window.onhashchange = locationHashChanged;
});
$: video = get(courseData, 'metadata.videoUrl');
$: lessons = get(courseData, 'lessons', []);
$: instructor = courseData?.metadata?.instructor || {};
$: initPlyr(player, video);
</script>
<svelte:head>
<title>{$landingPageStore.header.title}</title>
<title>{get(courseData, 'title', '')}</title>
</svelte:head>
<div class="w-full bg-white flex flex-col items-center">
<section
class="w-full flex items-center justify-center bg-gray-900 text-white p-6 pb-10"
>
<div class="container flex justify-between items-center">
<div class="course-description mr-3">
<h1>
{$landingPageStore.header.title}
<!-- Header Section -->
<header class="banner w-full flex items-center justify-center p-">
<div
class="md:w-5/6 w-full flex items-center justify-between flex-col-reverse md:flex-row"
>
<!-- Course Description -->
<div class="md:w-2/5 w-11/12 py-10">
<h1 class="text-5xl text-white my-4 font-bold">
{get(courseData, 'title', '')}
</h1>
<h5>
{$landingPageStore.header.description}
</h5>
<div>
<span class="flex items-center">
<RecentlyViewed16 class="carbon-icon mr-2" />
Last updated 10/2021
</span>
</div>
</div>
<div class="action-container bg-white p-3 flex items-start flex-col">
<p class="mb-2 text-black text-md flex items-center">
<Alarm16 class="carbon-icon active mr-2" />
{$landingPageStore.header.duration}
<p class="text-md text-white mb-6">
{get(courseData, 'description', '')}
</p>
<p class="mb-2 text-black text-md flex items-center">
<CurrencyDollar16 class="carbon-icon active mr-2" />
{$landingPageStore.header.currency}{$landingPageStore.header.cost}
<p class="author my-3 text-sm">
{get(courseData, 'metadata.instructor.name', '')}
</p>
<PrimaryButton
label={$landingPageStore.header.buttonLabel}
onClick={handleEnrol}
className="md:w-full"
label="Start Course"
className="px-6 py-5 mt-6 sm:w-fit"
/>
</div>
<!-- Banner Image getEmbedId(videoUrl) -->
{#if video}
<div class="banner-image md:w-2/3 w-full flex">
<div
bind:this={player}
id="player"
data-plyr-provider="youtube"
data-plyr-embed-id={getEmbedId(video)}
/>
</div>
<!-- <div class="container">
<div
bind:this={player}
id="player"
class="banner-image w-2/3 h-96 relative"
data-plyr-provider="youtube"
data-plyr-embed-id="bTqVqk7FSmY"
/>
</div> -->
{:else}
<!-- <div
class="banner-image w-2/3 h-96 relative cursor-pointer"
on:click={playVideo}
>
<div
aria-hidden="true"
class="absolute top-0 left-0 backdrop w-full h-full"
/>
<img
src="/images/course-banner-image.png"
alt="banner"
class="w-full h-full"
/>
<div
class="absolute top-0 left-0 w-full h-full z-50 flex items-center justify-center"
>
<img src="/images/play_video.png" alt="play video" />
</div>
</div> -->
{/if}
</div>
</section>
</header>
<PricingSection className="md:hidden" {courseData} />
<!-- Body -->
<div class="bg-white w-full">
<div class="py-8 lg:w-10/12 w-full m-2 lg:m-auto flex justify-between">
<!-- Course Details -->
<div class="course-content w-full p-3 md:w-9/12 md:mr-14">
<!-- Navigation -->
<nav class="flex items-center border-b border-gray-300 pb-3">
{#each NAV_ITEMS as navItem}
<a
href="{$page.path}{navItem.key}"
class="{navItem.key === activeNav &&
'active text-blue-700'} rounded-lg px-2 mr-6 text-slate-700 font-normal hover:bg-slate-100 hover:text-slate-900"
>
{navItem.label}
</a>
{/each}
</nav>
<nav
class="w-full z-10 max-w-4xl flex items-center justify-evenly py-4 px-4 bg-gray-100 mt-4 sticky top-0"
>
{#each NAV_ITEMS as navItem}
<p
class="text-md text-gray-900 {activeNavItem === navItem.key &&
'font-bold'}"
on:click={handleNavItemClick(navItem.key)}
>
<a href="{$page.path}#{navItem.key}">{navItem.label}</a>
</p>
{/each}
</nav>
<!-- Sections - Requirement -->
<section id="requirement" class="border-b border-gray-300 mt-8 pb-10">
<h3 class="text-2xl font-bold mt-0 mb-3">Requirement</h3>
<ul class="list font-light">
{@html get(courseData, 'metadata.requirements', '')}
</ul>
</section>
<Expandable
id={SECTION_KEYS.REQUIREMENT}
title="Requirement"
titleClass="text-2xl"
rootClass={expandableRootClass}
supportsLink={true}
>
<MarkdownRender
content={$landingPageStore.requirement.content}
disableMaxWidth={true}
/>
</Expandable>
<!-- Sections - Course Description -->
<section id="description" class="border-b border-gray-300 mt-8 pb-10">
<h3 class="text-2xl font-bold mt-0 mb-3">Course Description</h3>
<p class="text-sm font-light">
{@html get(courseData, 'metadata.description', '')}
</p>
</section>
<Expandable
id={SECTION_KEYS.DESCRIPTION}
title="Description"
titleClass="text-2xl"
rootClass={expandableRootClass}
supportsLink={true}
>
<MarkdownRender
content={$landingPageStore.description.content}
disableMaxWidth={true}
/>
</Expandable>
<!-- Sections - Goal -->
<section id="goals" class="mt-8 pb-10">
<h3 class="text-2xl font-bold mt-0 mb-3">What you'll learn</h3>
<ul class="list font-light">
{@html get(courseData, 'metadata.goals', '')}
</ul>
</section>
<Expandable
id={SECTION_KEYS.SYLLABUS}
title="Syllabus"
titleClass="text-2xl"
rootClass={expandableRootClass}
supportsLink={true}
>
{#each $landingPageStore.syllabus.lessons as lesson, index}
<div class="flex items-center my-2">
<strong class="mr-2">Lesson {index + 1}. </strong>
{lesson.title}
<!-- Sections - Certificate -->
<section class="border-b border-gray-300 mt-8 pb-10">
<h3 class="text-2xl font-bold mt-0">Certificate</h3>
<p class="text-sm font-light mb-3">
When you complete all of the courses in the program, you'll earn a
Certificate to share with your professional network.
</p>
<img
src="/images/certificate-template.svg"
alt="certificate template"
/>
</section>
<!-- Sections - Lessons -->
<section id="lessons" class="border-b border-gray-300 mt-8 pb-10">
<div class="flex items-center justify-between w-full mb-3">
<h3 class="text-2xl font-bold mt-0 mb-3">Course Content</h3>
<p class="text-sm font-light">
<!-- {lessons.length} lessons -->
{pluralize('lesson', lessons.length, true)}
</p>
</div>
{#each lessons as lesson, index}
<div
class="w-full flex md:items-center justify-between mb-6 flex-col md:flex-row"
>
<div class="flex items-center mb-3">
<Chip
value={getLectureNo(index + 1, '0')}
className="bg-blue-100 text-blue-700"
/>
<p class="ml-2 text-sm font-light">{lesson.title}</p>
</div>
<div class="w-2/4">
<div class="flex items-center">
{#if lesson.slide_url}
<span class="text-sm font-light flex w-2/4"
><PresentationFile class="mr-1" />1 slide</span
>
{/if}
{#if lesson.note}
<span class="text-sm font-light flex w-2/4"
><Notebook class="mr-1" />1 note</span
>
{/if}
</div>
<div class="flex items-center">
{#if lesson.video_url}
<span class="text-sm font-light flex w-2/4"
><Video class="mr-1" />1 video</span
>
{/if}
{#if get(lesson, 'totalExercises[0].count')}
<span class="text-sm font-light flex w-2/4"
><PageNumber class="mr-1" />{pluralize(
'exercise',
get(lesson, 'totalExercises[0].count', 0),
true
)}</span
>
{/if}
</div>
</div>
</div>
{/each}
</section>
<!-- Sections - Instructor -->
<section id="instructor" class="mt-8 pb-10">
<h3 class="text-2xl font-bold mt-0 mb-3">Instructor</h3>
<div class="flex items-center mb-4">
<img
alt="Author Avatar"
class="block rounded-full h-20 w-20 mr-3"
src={get(instructor, 'imgUrl', '')}
/>
<div>
<p class="text-md font-light">{get(instructor, 'name', '')}</p>
<p class="text-xs font-light">{get(instructor, 'role', '')}</p>
<p class="text-md font-light flex items-center">
<PlayFilled class="text-blue-700" />
<span class="ml-1"
>{get(instructor, 'courseNo', '')} courses</span
>
</p>
</div>
</div>
<p class="text-sm font-light">
{get(instructor, 'description', '')}
</p>
</section>
</div>
{/each}
</Expandable>
<Expandable
id={SECTION_KEYS.ABOUT_US}
title="About Us - {$landingPageStore.aboutUs.companyName}"
titleClass="text-2xl"
rootClass={expandableRootClass}
supportsLink={true}
>
<div class="w-full">
<img
src={$landingPageStore.aboutUs.logoUrl}
alt={$landingPageStore.aboutUs.companyName}
height="260"
width="260"
class="rounded-full m-auto"
/>
<!-- Pricing Details -->
<PricingSection className="hidden md:block" {courseData} />
</div>
<MarkdownRender
content={$landingPageStore.aboutUs.description}
disableMaxWidth={true}
/>
</Expandable>
</div>
</div>
<style lang="scss">
.container {
max-width: 800px;
.banner {
background-color: #040f2d;
min-height: 472px;
}
.course-description {
max-width: 500px;
}
.author {
color: #7888b7;
}
.action-container {
height: fit-content;
width: 200px;
}
.banner-image {
max-width: 559px;
}
.backdrop {
background-color: rgba(0, 0, 0, 0.5);
}
.active {
position: relative;
display: inline-block;
}
.active::after {
position: absolute;
content: '';
width: 100%;
height: 3px;
background-color: var(--main-primary-color);
display: block;
bottom: -14px;
left: 0px;
}
.price-container {
width: 405px;
min-width: 330px;
height: fit-content;
position: sticky;
top: 0;
}
.course-content {
max-width: 608px;
}
nav {
overflow: auto;
margin: 0;
overflow-y: hidden;
width: 100%;
}
:global(.list ul li) {
margin-left: 1rem;
list-style-type: disc;
}
:global(.plyr) {
width: 100% !important;
}
</style>
@@ -35,7 +35,7 @@
</script>
<div
class="root w-72 border bg-white px-2 py-2 mr-4 mb-4 rounded-md hover:shadow-2xl transition ease-in-out relative"
class="root w-72 border bg-white px-2 py-3 mr-4 mb-4 rounded-md hover:shadow-2xl transition ease-in-out relative"
>
<Dropdown
options={getOptions()}
@@ -57,7 +57,7 @@
<div class="mt-5 flex items-center flex-row-reverse">
<PrimaryButton
className="px-6 py-2"
className="px-6 py-3"
label="Create"
type="submit"
isLoading={$copyCourseModal.isSaving}
@@ -128,7 +128,7 @@
/>
<div class="mt-5 flex items-center">
<PrimaryButton className="px-6 py-2" label="Finish" type="submit" />
<PrimaryButton className="px-6 py-3" label="Finish" type="submit" />
</div>
</form>
</Modal>
+2 -2
View File
@@ -1,6 +1,6 @@
<script>
import Box from '../Box/index.svelte';
import { Firework } from 'svelte-loading-spinners';
import { Moon } from 'svelte-loading-spinners';
import Card from './components/Card/index.svelte';
import CopyCourseModal from './components/CopyCourseModal/index.svelte';
import { courses, courseMetaDeta } from './store';
@@ -13,7 +13,7 @@
>
{#if $courseMetaDeta.isLoading}
<Box>
<Firework size="60" color="#1d4ed8" unit="px" duration="1s" />
<Moon size="60" color="#1d4ed8" unit="px" duration="1s" />
</Box>
{:else}
{#each $courses as courseData}
+3 -3
View File
@@ -8,7 +8,7 @@
let open = false;
const notIconClass =
'border rounded-lg border-grey py-2 px-3 focus:outline-none focus:border-gray-400 focus:bg-gray-200';
'border rounded-lg border-grey py-3 px-3 focus:outline-none focus:border-gray-400 focus:bg-gray-200';
function isOptionLast(index) {
return index === options.length - 1;
@@ -41,11 +41,11 @@
{#if open}
<div
class="dropdown w-48 bg-white border border-grey rounded-lg mt-2 py-2 absolute"
class="dropdown w-48 bg-white border border-grey rounded-lg mt-2 py-3 absolute"
>
{#each options as option, index}
<button
class="block px-4 py-2 {!isOptionLast(index) && 'border-b'} w-full"
class="block px-4 py-3 {!isOptionLast(index) && 'border-b'} w-full"
on:click={onClick(option)}
>
{option.label}
+9 -6
View File
@@ -1,18 +1,21 @@
<script>
</script>
<style>
/* your styles go here */
</style>
<!-- markup (zero or more items) goes here -->
<div class="my-1">
<hr />
<div class="flex justify-center w-full">
<span class="text-gray-700 text-sm py-2">Created with love by
<span class="text-gray-700 text-sm py-3"
>Created with love by
<a
class="underline text-blue-700"
href="https://twitter.com/rotimi_best"
target="_blank">Rotimi Best</a></span>
target="_blank">Rotimi Best</a
></span
>
</div>
</div>
<style>
/* your styles go here */
</style>
+3 -2
View File
@@ -5,7 +5,8 @@
export let rows = '3';
export let maxRows = 3;
export let className = '';
export let bgColor = 'bg-gray-100';
export let labelClassName = '';
export let bgColor = 'bg-gray-200';
export let helperMessage = '';
export let errorMessage = '';
export let isRequired = false;
@@ -17,7 +18,7 @@
<label class="block w-full relative {className}">
{#if label}
<p for="text-field" class="">
<p for="text-field" class={labelClassName}>
{label}
{#if isRequired}
+4 -2
View File
@@ -19,6 +19,7 @@
export let helperMessage = '';
export let autoComplete = true;
export let onChange = () => {}; // This is to know if element is 'dirty'
export let onInputChange = () => {};
let ref;
let fieldNode;
@@ -61,10 +62,11 @@
{/if}
<input
use:typeAction
class="form-input p-3 mt-1 block w-full border-none bg-gray-100 {inputClassName} {errorMessage
class="form-input p-3 mt-1 block w-full border-none bg-gray-200 {inputClassName} {errorMessage
? 'border-red-500'
: ''}"
on:keydown={onKeyDown}
on:change={onInputChange}
{placeholder}
bind:value
bind:this={ref}
@@ -92,7 +94,7 @@
<style>
.password-eye {
position: absolute;
top: 33px;
top: 23px;
right: 6px;
}
</style>
+2 -2
View File
@@ -93,13 +93,13 @@
<PrimaryButton
label="Login"
variant={VARIANTS.NONE}
className="py-2 px-6 mx-2 app-background-color"
className="py-3 px-6 mx-2 app-background-color"
onClick={() => goto('/login')}
/>
<PrimaryButton
label="Sign Up"
variant={VARIANTS.CONTAINED}
className="py-2 px-6 mx-2"
className="py-3 px-6 mx-2"
onClick={() => goto('/signup')}
/>
</div>
+4 -4
View File
@@ -13,7 +13,7 @@
export let isLoading = false;
$: variant = isLoading ? VARIANTS.OUTLINED : variant;
$: isDisabled = isLoading ? true : isDisabled;
$: isDisabled = isLoading ? true : false;
</script>
<button
@@ -21,15 +21,15 @@
'bg-opacity-25 hover:bg-opacity-25 cursor-not-allowed'} flex items-center h-auto {VARIANTS_CLASS[
variant
]} {!disablePadding &&
'py-1 px-5'} rounded-none {className} py-2 w-full justify-center sm:w-auto {isLoading &&
'py-2 border-blue-700'} hover:shadow-xl transition delay-150 duration-300 ease-in-out"
'py-1 px-5'} rounded-none {className} py-3 w-fit justify-center sm:w-auto {isLoading &&
'py-3 border-blue-700'} hover:shadow-xl transition delay-150 duration-300 ease-in-out"
on:click={onClick}
{name}
{type}
disabled={isDisabled}
>
{#if isLoading}
<Moon size="30" color="#1d4ed8" unit="px" duration="1s" />
<Moon size="20" color="#1d4ed8" unit="px" duration="1s" />
{:else if !!label}
{label}
{:else}
+1
View File
@@ -20,6 +20,7 @@
*::-webkit-scrollbar {
width: 10px;
height: 10px;
}
*::-webkit-scrollbar-track {
+70 -82
View File
@@ -1,89 +1,77 @@
<script>
import marked from 'marked';
import PrimaryButton from '../PrimaryButton/index.svelte';
import { onMount } from 'svelte';
let isWriteMode = true;
export let value = ``;
export let placeholder = ``;
const activeClassName = 'border-b-2 border-blue-700';
export let onChange;
export let content;
export let disable;
export let docId;
export let placeholder = 'Type something...';
const handleTabClick = (tab) => (event) => {
isWriteMode = tab === 1;
const settings = {
modules: {
toolbar: {
modules: {
syntax: true,
toolbar: [
[{ header: [1, 2, 3, false] }],
['bold', 'italic', 'underline', 'strike'],
['link', 'code-block'],
],
},
placeholder: 'Type something...',
theme: 'snow',
},
},
theme: 'snow',
placeholder,
};
let quill = null;
let container = null;
function resetContent(docId) {
if (quill && quill.setText) {
if (content) {
quill.setContents(JSON.parse(content));
} else {
quill.setText('');
}
}
}
onMount(async () => {
const { default: Quill } = await import('quill');
Quill.prototype.getHTML = function () {
return this.container.querySelector('.ql-editor').innerHTML;
};
Quill.prototype.setHTML = function (html) {
this.container.querySelector('.ql-editor').innerHTML = html;
};
quill = new Quill(container, settings);
quill.setHTML(content || '');
quill.on('text-change', function () {
onChange(quill.getHTML());
});
});
$: {
if (quill && quill.setText) {
if (disable) {
quill.disable(disable);
} else {
quill.enable(true);
}
}
}
$: resetContent(docId);
</script>
<div class="root mt-5 border border-gray rounded-md">
<div class="p-2 pb-0">
<button
on:click={handleTabClick(1)}
class="focus:outline-none p-2 {isWriteMode && `${activeClassName}`}"
>
Write
</button>
<button
on:click={handleTabClick(2)}
class="focus:outline-none p-2 {!isWriteMode && `${activeClassName}`}"
>
Preview
</button>
</div>
<div class="m-2 mb-0 p-0">
{#if isWriteMode}
<textarea
bind:value
{placeholder}
class="border border-gray rounded-md p-2"
/>
{:else}
<article class="m-auto preview prose prose-sm sm:prose p-2">
{@html marked(value)}
</article>
{/if}
</div>
{#if !isWriteMode}
<hr />
{/if}
<div class="flex justify-between items-center px-2 pb-1">
<div class="flex items-center text-sm">
<svg
class="octicon mr-2"
height="16"
viewBox="0 0 16 16"
version="1.1"
width="16"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M14.85 3H1.15C.52 3 0 3.52 0 4.15v7.69C0 12.48.52 13 1.15 13h13.69c.64 0 1.15-.52 1.15-1.15v-7.7C16 3.52 15.48 3 14.85 3zM9 11H7V8L5.5 9.92 4 8v3H2V5h2l1.5 2L7 5h2v6zm2.99.5L9.5 8H11V5h2v3h1.5l-2.51 3.5z"
/>
</svg>
<span>Styling with Markdown is supported</span>
</div>
<PrimaryButton label="Answer" />
</div>
<div class="p-2 w-full h-full">
<div bind:this={container} />
</div>
<style>
.root {
width: 100%;
min-height: 200px;
}
textarea {
width: 100%;
height: 150px;
}
.preview {
min-height: 150px;
}
.octicon {
display: inline-block;
vertical-align: text-top;
fill: currentColor;
}
</style>
+4
View File
@@ -16,6 +16,10 @@
<svelte:head>
<title>{status}</title>
<link
rel="stylesheet"
href="https://unpkg.com/carbon-components-svelte/css/g90.css"
/>
</svelte:head>
<div class="w-3/5 h-3/5 m-auto flex flex-col items-center justify-center">
+7 -3
View File
@@ -9,7 +9,7 @@
import { derived } from 'svelte/store';
import { stores, goto } from '@sapper/app';
import * as Sentry from '@sentry/browser';
import { Firework } from 'svelte-loading-spinners';
import { Moon } from 'svelte-loading-spinners';
import Tailwindcss from '../components/Tailwindcss.svelte';
import Navigation from '../components/Navigation/index.svelte';
import Snackbar from '../components/Snackbar/index.svelte';
@@ -91,7 +91,7 @@
// If user coming to login page, then
if (!$profile.role) {
goto(ROUTE.ONBOARDING);
} else {
} else if (['login', 'signup', 'onboarding'].includes(path)) {
goto('/dashboard');
}
}
@@ -161,6 +161,10 @@
</script>
<svelte:window on:resize={handleResize} />
<svelte:head>
<link href="/css/carbon.css" rel="stylesheet" />
</svelte:head>
<Tailwindcss />
<!-- <Nav {segment} /> -->
@@ -169,7 +173,7 @@
<main>
{#if $preloading && $delayedPreloading}
<Backdrop>
<Firework size="60" color="#1d4ed8" unit="px" duration="1s" />
<Moon size="60" color="#1d4ed8" unit="px" duration="1s" />
</Backdrop>
{/if}
{#if !ROUTES_TO_HIDE_NAV.includes($page.path) && !isCoursePage(path)}
+3 -3
View File
@@ -88,7 +88,7 @@
<div class="flex items-center justify-evenly max-w-6xl m-auto">
{#each boxes as box}
<div
class="bg-white rounded-md py-4 px-16 active shadow-md hover:shadow-2xl border-2 hover:border-blue-700 cursor-pointer {animate}"
class="bg-white rounded-md py-3 px-16 active shadow-md hover:shadow-2xl border-2 hover:border-blue-700 cursor-pointer {animate}"
>
{#if box.icon === 'students'}
<UserSpeaker32 class="carbon-icon" />
@@ -106,7 +106,7 @@
<div class="flex items-center justify-evenly mt-16">
<div class="table rounded-md bg-white shadow-md">
<div class="flex border-t-0 {borderBottomGrey} p-3">
<h3 class="m-0 py-2 pl-2 text-lg">Outstanding students</h3>
<h3 class="m-0 py-3 pl-2 text-lg">Outstanding students</h3>
</div>
{#each topStudents as topStudent, index}
@@ -123,7 +123,7 @@
<div class="table rounded-md bg-white shadow-md">
<div class="flex border-t-0 {borderBottomGrey} p-3">
<h3 class="m-0 py-2 pl-2 text-lg">Top rated courses</h3>
<h3 class="m-0 py-3 pl-2 text-lg">Top rated courses</h3>
</div>
{#each topCourses as topCourse, index}
+17 -17
View File
@@ -1,22 +1,22 @@
<script>
import ChevronDown24 from "carbon-icons-svelte/lib/ChevronDown24";
import TextEditor from "../components/TextEditor/index.svelte";
import ChevronDown24 from 'carbon-icons-svelte/lib/ChevronDown24';
import TextEditor from '../components/TextEditor/index.svelte';
let questionTitle = "";
let questionDescription = "";
let questionTitle = '';
let questionDescription = '';
let facultyFilterOpened = false;
const faculties = [
"ИЭКСУ",
"ИМБТ",
"ИМИ",
"ИПТДМ",
"ИИИР",
"ГФ",
"ХТФ",
"ИДЗО",
"УИИ",
"УНИ",
'ИЭКСУ',
'ИМБТ',
'ИМИ',
'ИПТДМ',
'ИИИР',
'ГФ',
'ХТФ',
'ИДЗО',
'УИИ',
'УНИ',
];
let checkbox = faculties.reduce((acc, cur) => {
acc[cur] = false;
@@ -38,7 +38,7 @@
<div class="main-title my-1 flex">
<div class="faculty-filter mr-1">
<button
class="flex border rounded-lg border-grey py-2 px-3 focus:outline-none focus:border-gray-400 focus:bg-gray-200 relative"
class="flex border rounded-lg border-grey py-3 px-3 focus:outline-none focus:border-gray-400 focus:bg-gray-200 relative"
on:click={handleFacultyFilters}
>
<span class="flex flex-row"
@@ -49,10 +49,10 @@
{#if facultyFilterOpened}
<div
class="w-48 bg-white border border-grey rounded-lg mt-2 py-2 absolute"
class="w-48 bg-white border border-grey rounded-lg mt-2 py-3 absolute"
>
{#each faculties as faculty}
<div class="block px-4 py-2 border-b">
<div class="block px-4 py-3 border-b">
<input
type="checkbox"
class="border border-gray checked:bg-blue-600 checked:border-transparent mr-1 cursor-pointer"
+31 -1
View File
@@ -5,9 +5,39 @@
</script>
<script>
import Backdrop from '../../components/Backdrop/index.svelte';
import { Moon } from 'svelte-loading-spinners';
import CourseLandingPage from '../../components/CourseLandingPage/index.svelte';
import { profile } from '../../utils/store/user';
import { setCourse, course } from '../../components/Course/store';
import { lessons } from '../../components/Course/components/Lesson/store/lessons';
import { fetchCourse } from '../../utils/services/courses';
export let slug;
let courseData = {};
let loading = false;
async function triggerSetCourse(_profile) {
loading = true;
const { data } = await fetchCourse(undefined, slug);
setCourse(data);
loading = false;
}
function setCourseData(course, lessons) {
courseData = { ...course, lessons };
console.log('courseData', courseData);
}
$: triggerSetCourse($profile);
$: setCourseData($course, $lessons);
</script>
<CourseLandingPage {slug} />
{#if loading}
<Backdrop>
<Moon size="60" color="#1d4ed8" unit="px" duration="1s" />
</Backdrop>
{:else}
<CourseLandingPage {courseData} />
{/if}
+2 -6
View File
@@ -11,11 +11,7 @@
import PageNav from '../../../components/PageNav/index.svelte';
import PageBody from '../../../components/PageBody/index.svelte';
import Box from '../../../components/Box/index.svelte';
import {
course,
group,
setCourseData,
} from '../../../components/Course/store';
import { course, group, setCourse } from '../../../components/Course/store';
import { getLectureNo } from '../../../components/Course/function';
import { fetchCourse } from '../../../utils/services/courses';
import { lessons } from '../../../components/Course/components/Lesson/store/lessons';
@@ -120,7 +116,7 @@
}
const { data } = await fetchCourse(courseId);
setCourseData(data);
setCourse(data);
setAttendance(data);
});
+2 -2
View File
@@ -7,7 +7,7 @@
<script>
import Overview from '../../../components/Course/index.svelte';
import CourseContainer from '../../../components/CourseContainer/index.svelte';
import { setCourseData } from '../../../components/Course/store';
import { setCourse } from '../../../components/Course/store';
import { fetchCourse } from '../../../utils/services/courses';
export let courseId;
@@ -19,7 +19,7 @@
const { data } = await fetchCourse(courseId);
setCourseData(data);
setCourse(data);
prevCourseId = courseId;
}
+22 -2
View File
@@ -9,8 +9,28 @@
import CourseLandingPage from '../../../components/CourseLandingPage/index.svelte';
import Editor from '../../../components/CourseLandingPage/components/Editor/index.svelte';
import { isMobile } from '../../../utils/store/useMobile';
import { profile } from '../../../utils/store/user';
import { setCourse, course } from '../../../components/Course/store';
import { lessons } from '../../../components/Course/components/Lesson/store/lessons';
import { fetchCourse } from '../../../utils/services/courses';
export let courseId;
let courseData = {};
async function triggerSetCourse(_profile) {
if ($course.id) return;
const { data } = await fetchCourse(courseId);
setCourse(data);
}
function setCourseData(course, lessons) {
courseData = { ...course, lessons };
}
$: triggerSetCourse($profile);
$: setCourseData($course, $lessons);
$: console.log('courseData changed', courseData);
</script>
<div
@@ -18,9 +38,9 @@
in:fly={{ y: 500, duration: 500 }}
out:fly={{ y: 500, duration: 500 }}
>
<Editor {courseId} />
<Editor {courseId} bind:course={courseData} />
<div class="rightBar" class:isMobile={$isMobile}>
<CourseLandingPage />
<CourseLandingPage bind:courseData />
</div>
</div>
@@ -29,10 +29,7 @@
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,
course,
} from '../../../../../components/Course/store';
import { setCourse, course } from '../../../../../components/Course/store';
import { lesson } from '../../../../../components/Course/components/Lesson/store/lessons';
export let courseId;
@@ -53,14 +50,10 @@
let lessonData;
if (!$course.id) {
const [_course, lesson] = await Promise.all([
fetchCourse(courseId),
fetchLesson(lessonId),
]);
const { data } = await fetchCourse(courseId);
lessonData = lesson.data;
setCourseData(_course.data);
lessonData = data.lessons.find((lesson) => lesson.id === lessonId);
setCourse(data);
} else if (prevLessonId !== lessonId) {
const lesson = await fetchLesson(lessonId);
lessonData = lesson.data;
+2 -2
View File
@@ -36,7 +36,7 @@
} from '../../../../components/Course/components/Lesson/store/lessons';
import {
course,
setCourseData,
setCourse,
group,
} from '../../../../components/Course/store';
@@ -107,7 +107,7 @@
onMount(async () => {
if ($course.id) return;
const { data } = await fetchCourse(courseId);
setCourseData(data);
setCourse(data);
});
$: {
+2 -6
View File
@@ -12,11 +12,7 @@
import PageBody from '../../../components/PageBody/index.svelte';
import Box from '../../../components/Box/index.svelte';
import { ROLE } from '../../../utils/constants/roles';
import {
setCourseData,
course,
group,
} from '../../../components/Course/store';
import { setCourse, course, group } from '../../../components/Course/store';
import { lessons } from '../../../components/Course/components/Lesson/store/lessons';
import {
fetchCourse,
@@ -50,7 +46,7 @@
onMount(async () => {
if (!$course.id) {
const { data } = await fetchCourse(courseId);
setCourseData(data);
setCourse(data);
}
const { data: marks } = await fetchMarks(courseId);
+2 -6
View File
@@ -25,11 +25,7 @@
import IconButton from '../../../components/IconButton/index.svelte';
import PageBody from '../../../components/PageBody/index.svelte';
import RoleBasedSecurity from '../../../components/RoleBasedSecurity/index.svelte';
import {
setCourseData,
course,
group,
} from '../../../components/Course/store';
import { setCourse, course, group } from '../../../components/Course/store';
import InviationModal from '../../../components/Course/components/People/InviationModal.svelte';
import DeleteConfirmation from '../../../components/Course/components/People/DeleteConfirmation.svelte';
import {
@@ -102,7 +98,7 @@
onMount(async () => {
if ($course.id) return;
const { data } = await fetchCourse(courseId);
setCourseData(data);
setCourse(data);
});
$: sortAndFilterPeople($group.people, filterBy);
+2 -2
View File
@@ -20,12 +20,12 @@
import CourseContainer from '../../../components/CourseContainer/index.svelte';
import PageNav from '../../../components/PageNav/index.svelte';
import PageBody from '../../../components/PageBody/index.svelte';
import { setCourseData } from '../../../components/Course/store';
import { setCourse } from '../../../components/Course/store';
export let courseData = {};
onMount(() => {
setCourseData(courseData);
setCourse(courseData);
});
</script>
+3 -3
View File
@@ -16,7 +16,7 @@
import PageBody from '../../../components/PageBody/index.svelte';
import CourseContainer from '../../../components/CourseContainer/index.svelte';
import { fetchCourse } from '../../../utils/services/courses';
import { setCourseData, course } from '../../../components/Course/store';
import { setCourse, course } from '../../../components/Course/store';
import {
fetchSubmissions,
updateSubmission,
@@ -182,7 +182,7 @@
onMount(async () => {
if (!$course.id) {
const { data } = await fetchCourse(courseId);
setCourseData(data);
setCourse(data);
}
const { data: submissions } = await fetchSubmissions(
@@ -297,7 +297,7 @@
<div
class="border {item.isEarly
? 'border-grey-700'
: 'border-red-700'} w-full my-2 mx-0 rounded-md bg-white py-2 px-3"
: 'border-red-700'} w-full my-2 mx-0 rounded-md bg-white py-3 px-3"
animate:flip={{ duration: flipDurationMs }}
>
<div
+2 -2
View File
@@ -20,12 +20,12 @@
import CourseContainer from '../../../components/CourseContainer/index.svelte';
import PageNav from '../../../components/PageNav/index.svelte';
import PageBody from '../../../components/PageBody/index.svelte';
import { setCourseData } from '../../../components/Course/store';
import { setCourse } from '../../../components/Course/store';
export let courseData = {};
onMount(() => {
setCourseData(courseData);
setCourse(courseData);
});
</script>
+2 -1
View File
@@ -65,7 +65,8 @@
<p class="text-md mb-6 text-center">
We have sent a confirmation email to <span class="text-blue-700"
>{fields.email}</span
>. Kindly check your inbox to reset password.
>. Kindly check your inbox to reset password or spam to reset your
password.
</p>
</div>
+1 -1
View File
@@ -97,7 +97,7 @@
<div class="flex flex-col md:flex-row">
{#each areas as area, index}
<div
class="box m-3 bg-white rounded-md py-4 px-12 active shadow-xl border-2 hover:border-blue-700 {animate}"
class="box m-3 bg-white rounded-md py-3 px-12 active shadow-xl border-2 hover:border-blue-700 {animate}"
>
<h3 class="text-3xl">
{#if index === 0}
+1 -1
View File
@@ -287,7 +287,7 @@
<PrimaryButton
label="Back"
variant={VARIANTS.NONE}
className="py-2 px-6 mr-2 text-blue-700"
className="py-3 px-6 mr-2 text-blue-700"
onClick={() => (step = step - 1)}
/>
{/if}
+3 -3
View File
@@ -183,7 +183,7 @@
This are the organizations you work at
</p>
<div class="hidden my-3 py-2 px-4 border shadow-sm rounded-md">
<div class="hidden my-3 py-3 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>
@@ -204,7 +204,7 @@
</div>
</div>
<div class="hidden my-2 py-2 px-4 border shadow-sm rounded-md">
<div class="hidden my-2 py-3 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
@@ -226,7 +226,7 @@
</div>
</div>
<PrimaryButton
className="px-6 py-2"
className="px-6 py-3"
variant={VARIANTS.CONTAINED_DANGER}
label="Log out"
onClick={logout}
+6 -3
View File
@@ -12,7 +12,8 @@
<link rel="manifest" href="manifest.json" crossorigin="use-credentials">
<link rel="icon" type="image/png" href="favicon.ico">
<link rel="icon" type="image/png" sizes="32x32" href="/logo-32.png">
<link href="/css/quill.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.plyr.io/3.7.3/plyr.css" />
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-C7WBN0S06R"></script>
<script>
@@ -23,10 +24,12 @@
gtag('config', 'G-C7WBN0S06R');
</script>
<!--Crisp Chat -->
<!--Crisp Chat
<script
type="text/javascript">window.$crisp = []; window.CRISP_WEBSITE_ID = "2158dbf9-90d4-4e50-b962-e1d8a1a6deb7"; (function () { d = document; s = d.createElement("script"); s.src = "https://client.crisp.chat/l.js"; s.async = 1; d.getElementsByTagName("head")[0].appendChild(s); })();</script>
-->
<script src="https://cdn.plyr.io/3.7.3/plyr.js">
</script>
<!-- Sapper creates a <script> tag containing `src/client.js`
and anything else it needs to hydrate the app and
initialise the router -->
+7 -1
View File
@@ -21,4 +21,10 @@ export const PUBLIC_ROUTES = [
ROUTE.RESET,
];
export const ROUTES_TO_HIDE_NAV = [...PUBLIC_ROUTES, ROUTE.ONBOARDING];
export const ROUTES_TO_HIDE_NAV = [
ROUTE.LOGIN,
ROUTE.SIGN_UP,
ROUTE.FORGOT,
ROUTE.RESET,
ROUTE.ONBOARDING,
];
+51
View File
@@ -0,0 +1,51 @@
export function formatYoutubeVideo(url: string, errors: { video: string }) {
console.log(`url`, url);
const prefix = 'https://www.youtube.com/embed';
// https://www.youtube.com/embed/qajK1J1neAM
if (url.includes('embed')) {
return url;
}
// https://youtu.be/qajK1J1neAM
if (url.includes('.be/')) {
const splittedUrlWithBe = url.split('.be/');
return `${prefix}/${splittedUrlWithBe[1]}`;
}
// https://www.youtube.com/watch?v=qajK1J1neAM
const splitedUrl = url.split('watch');
if (splitedUrl.length !== 2) {
errors.video = 'Not a valid youtube link';
return;
}
const query = new URLSearchParams(splitedUrl[1]);
return `${prefix}/${query.get('v')}`;
}
export function getEmbedId(url: string) {
// https://www.youtube.com/embed/qajK1J1neAM
if (url.includes('embed')) {
return url.split('embed/')[1];
}
// https://youtu.be/qajK1J1neAM
if (url.includes('.be/')) {
const splittedUrlWithBe = url.split('.be/');
return splittedUrlWithBe[1];
}
// https://www.youtube.com/watch?v=qajK1J1neAM
const splitedUrl = url.split('watch');
if (splitedUrl.length !== 2) {
return '';
}
const query = new URLSearchParams(splitedUrl[1]);
return query.get('v');
}
@@ -1,11 +1,13 @@
export function isUUID(uuid) {
export function isUUID(uuid: string) {
let s = '' + uuid;
s = s.match(
const m: RegExpMatchArray | null = s.match(
'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
);
if (s === null) {
if (m === null) {
return false;
}
return true;
}
View File
@@ -1,22 +1,24 @@
import { supabase } from '../../functions/supabase';
import { isUUID } from '../../functions/isUUID';
import { QUESTION_TYPE } from '../../../components/Question/constants';
import type {
Lesson,
Course,
Group,
Groupmember,
Exercise,
} from '../../../utils/types';
export async function fetchCourse(courseId, session) {
console.log(`courseId`, courseId);
let jsonUser;
export async function fetchCourse(
courseId?: Course['id'],
slug?: Course['slug']
) {
let match: { slug?: string; id?: string } = {};
if (!process.browser) {
// try {
// jsonUser = JSON.parse(session.user);
// } catch (error) {
// console.log('Token is expired probably cause user not found');
// return {
// data: [],
// cantFetch: true,
// };
// // return this.redirect(301, '/login');
// }
if (slug) {
match.slug = slug;
} else {
match.id = courseId;
}
const { data, error } = await supabase
@@ -33,14 +35,19 @@ export async function fetchCourse(courseId, session) {
profile(*)
)
),
slug,
cost,
currency,
metadata,
lessons:lesson(
id, title,public, lesson_at, call_url, is_complete, order, created_at,
note, video_url, slide_url, call_url, totalExercises:exercise(count),
profile:teacher_id(id, avatar_url, fullname)
),
attendance:group_attendance(*)
`
)
.match({ id: courseId })
.match(match)
.single();
console.log(`error`, error);
if (!data || error) {
@@ -56,7 +63,7 @@ export async function fetchCourse(courseId, session) {
};
}
export async function fetchGroup(groupId) {
export async function fetchGroup(groupId: Group['id']) {
const { data, error } = await supabase
.from('group')
.select(`*,members:groupmember(*,profile(*))`)
@@ -69,7 +76,10 @@ export async function fetchGroup(groupId) {
};
}
export async function setProfileIdOfGroupMember(email, profileId) {
export async function setProfileIdOfGroupMember(
email: string,
profileId: string
) {
const { data, error } = await supabase
.from('groupmember')
.update({
@@ -84,13 +94,41 @@ export async function setProfileIdOfGroupMember(email, profileId) {
};
}
export async function updateCourse(courseId, course) {
if (course.avatar) {
export async function uploadAvatar(courseId: string, avatar: string) {
const filename = `course/${courseId + Date.now()}.webp`;
let logo;
const { data } = await supabase.storage
.from('avatars')
.upload(filename, avatar, {
cacheControl: '3600',
upsert: false,
});
if (data && data.Key) {
const { publicURL } = supabase.storage
.from('avatars')
.getPublicUrl(filename);
if (!publicURL) return;
logo = publicURL;
}
return logo;
}
export async function updateCourse(
courseId: Course['id'],
avatar: string | undefined,
course: Course
) {
if (avatar) {
const filename = `course/${courseId + Date.now()}.webp`;
const { data } = await supabase.storage
.from('avatars')
.upload(filename, course.avatar, {
.upload(filename, avatar, {
cacheControl: '3600',
upsert: false,
});
@@ -100,12 +138,12 @@ export async function updateCourse(courseId, course) {
.from('avatars')
.getPublicUrl(filename);
if (!publicURL) return;
course.logo = publicURL;
}
}
delete course.avatar;
await supabase
.from('course')
.update(course, { returning: 'minimal' })
@@ -114,28 +152,28 @@ export async function updateCourse(courseId, course) {
return course.logo;
}
export async function deleteCourse(courseId) {
export async function deleteCourse(courseId: Course['id']) {
return await supabase.from('course').delete().match({ id: courseId });
}
export function addGroupMember(member) {
export function addGroupMember(member: any) {
return supabase.from('groupmember').insert(member, {
returning: 'minimal',
});
}
export function updatedGroupMember(update, match) {
export function updatedGroupMember(update: any, match: any) {
return supabase
.from('groupmember')
.update(update, { returning: 'minimal' })
.match(match);
}
export function deleteGroupMember(groupMemberId) {
export function deleteGroupMember(groupMemberId: Groupmember['id']) {
return supabase.from('groupmember').delete().match({ id: groupMemberId });
}
export function fetchLesson(lessonId) {
export function fetchLesson(lessonId: Lesson['id']) {
return supabase
.from('lesson')
.select(
@@ -145,35 +183,39 @@ export function fetchLesson(lessonId) {
.single();
}
export function createLesson(lesson) {
export function createLesson(lesson: any) {
return supabase.from('lesson').insert(lesson);
}
export function updateLesson(lesson, lessonId) {
export function updateLesson(lesson: any, lessonId: Lesson['id']) {
return supabase
.from('lesson')
.update({ ...lesson, id: undefined }, { returning: 'minimal' })
.match({ id: lessonId });
}
export function deleteLesson(lessonId) {
export function deleteLesson(lessonId: Lesson['id']) {
// Need to implement soft delete
return supabase.from('lesson').delete().match({ id: lessonId });
}
export function createExercise(exercise) {
export function createExercise(exercise: any) {
return supabase.from('exercise').insert(exercise);
}
export function fetchExercisesByMarks(courseId) {
export function fetchExercisesByMarks(courseId: Course['id']) {
return supabase.rpc('get_exercises').eq('course_id', courseId);
}
function isNew(item) {
function isNew(item: any) {
return isNaN(item);
}
export async function upsertExercise(questionnaire, exerciseId) {
// TODO: Add questionnaire type
export async function upsertExercise(
questionnaire: any,
exerciseId: Exercise['id']
) {
const {
questions,
title,
@@ -263,10 +305,12 @@ export async function upsertExercise(questionnaire, exerciseId) {
// Delete cause this is not a field in the table
delete newQuestion.question_type_id;
// @ts-ignore
newQuestion.question_type = { id: question_type_id };
newQuestion.id = id;
newQuestion.name = name;
newQuestion.order = order;
// @ts-ignore
newQuestion.options = [];
// Don't map options for 'Paragraph' questions
@@ -297,9 +341,11 @@ export async function upsertExercise(questionnaire, exerciseId) {
if (is_dirty || isNew(option.id)) {
const { data } = await supabase.from('option').upsert(newOption);
if (Array.isArray(data)) {
// @ts-ignore
newQuestion.options.push(data[0]);
}
} else {
// @ts-ignore
newQuestion.options.push(newOption);
}
}
@@ -312,12 +358,16 @@ export async function upsertExercise(questionnaire, exerciseId) {
return updatedQuestions;
}
interface LooseObject {
[key: string]: any;
}
export async function submitExercise(
answers,
questions,
exerciseId,
courseId,
groupMemberId
answers: Array<string>,
questions: Array<{ name: string; id: string }>,
exerciseId: Exercise['id'],
courseId: Course['id'],
groupMemberId: Groupmember['id'] | undefined
) {
if (!groupMemberId) {
return;
@@ -326,7 +376,7 @@ export async function submitExercise(
const questionsByName = questions.reduce(
(acc, q) => ({ ...acc, [q.name]: q.id }),
{}
);
) as LooseObject;
const questionAnswers = [];
const { data: submission } = await supabase.from('submission').insert({
@@ -343,7 +393,7 @@ export async function submitExercise(
question_id: questionsByName[questionName],
open_answer: '',
answers: [],
submission_id: submission[0].id,
submission_id: Array.isArray(submission) ? submission[0].id : null,
};
if (typeof value === 'string') {
@@ -359,7 +409,10 @@ export async function submitExercise(
console.log(`res`, res);
}
export async function deleteExercise(questions, exerciseId) {
export async function deleteExercise(
questions: Array<{ id: string }>,
exerciseId: Exercise['id']
) {
for (const question of questions) {
const { id } = question;
+79 -68
View File
@@ -1,25 +1,30 @@
export * from './config';
export * from './dashboard';
export interface CurrentSessionType {
access_token: string;
expires_at: number;
expires_in: number;
refresh_token: string;
token_type: string;
}
export interface UserType {
openAuthModal: boolean;
fetchingUser: boolean | null;
isLoggedIn: boolean;
currentSession: CurrentSessionType | null;
expiresAt: number;
interface CourseMetadata {
requirements?: string;
description?: string;
goals?: string;
videoUrl?: '';
showDiscount: false;
discount: 0;
reward?: {
show: boolean;
description: string;
};
instructor?: {
name: string;
role: string;
coursesNo: number;
description: string;
imgUrl: string;
};
}
// Generated from https://supabase-schema.vercel.app/
export interface Organization {
id: string /* primary key */;
name: string;
name: any; // type unknown;
}
export interface Profile {
@@ -29,27 +34,30 @@ export interface Profile {
avatar_url?: string;
created_at?: string;
updated_at?: string;
email?: string;
email?: any; // type unknown;
can_add_course?: boolean;
role?: any; // type unknown;
goal?: any; // type unknown;
source?: any; // type unknown;
}
export interface Question_type {
id: number /* primary key */;
label: string;
label: any; // type unknown;
created_at?: string;
updated_at?: string;
typename?: string;
typename?: any; // type unknown;
}
export interface Submissionstatus {
id: number /* primary key */;
label: string;
label: any; // type unknown;
updated_at?: string;
}
export interface Group {
id: string /* primary key */;
name: string;
name: any; // type unknown;
description?: string;
created_at?: string;
updated_at?: string;
@@ -58,8 +66,8 @@ export interface Group {
}
export interface Role {
type: string;
description?: string;
type: any; // type unknown;
description?: any; // type unknown;
id: number /* primary key */;
updated_at?: string;
created_at?: string;
@@ -67,24 +75,27 @@ export interface Role {
export interface Waitinglist {
id: number /* primary key */;
email: string;
email: any; // type unknown;
created_at?: string;
}
export interface Course {
created_at?: string;
description: string;
group_id?: string /* foreign key to group.id */;
group?: Group;
title: any; // type unknown;
description: any; // type unknown;
overview?: any; // type unknown;
id: string /* primary key */;
is_template?: boolean;
lessons: Lesson[];
logo?: string;
organization_id?: string /* foreign key to organization.id */;
organization?: Organization;
overview?: string;
title: string;
created_at?: string;
updated_at?: string;
group_id?: string /* foreign key to group.id */;
is_template?: boolean;
organization_id?: string /* foreign key to organization.id */;
logo?: string;
slug?: any; // type unknown;
metadata?: CourseMetadata;
cost?: number;
currency?: string;
group?: Group;
organization?: Organization;
}
export interface Groupmember {
@@ -92,9 +103,9 @@ export interface Groupmember {
group_id: string /* foreign key to group.id */;
role_id: number /* foreign key to role.id */;
profile_id?: string /* foreign key to profile.id */;
email?: string;
email?: any; // type unknown;
created_at?: string;
assigned_student_id?: string;
assigned_student_id?: any; // type unknown;
group?: Group;
role?: Role;
profile?: Profile;
@@ -123,46 +134,47 @@ export interface Group_attendance {
}
export interface Lesson {
call_url?: string;
note?: any; // type unknown;
video_url?: any; // type unknown;
slide_url?: any; // type unknown;
course_id: string /* foreign key to course.id */;
created_at: string;
exercise?: Exercise[];
id: string /* primary key */;
is_complete?: boolean;
lesson_at?: string;
note?: string;
order: number;
profile?: Profile;
public?: boolean;
slide_url?: string;
teacher_id?: string /* foreign key to profile.id */;
title: string;
created_at?: string;
updated_at?: string;
video_url?: string;
title: any; // type unknown;
public?: boolean;
lesson_at?: string;
teacher_id?: string /* foreign key to profile.id */;
is_complete?: boolean;
call_url?: string;
order?: number;
course?: Course;
profile?: Profile;
}
export interface Exercise {
title: any; // type unknown;
description?: any; // type unknown;
lesson_id?: string /* foreign key to lesson.id */;
created_at?: string;
description?: string;
due_by?: string;
id: string /* primary key */;
lesson_id: string /* foreign key */;
questions?: Question[];
title: string;
updated_at?: string;
id: string /* primary key */;
due_by?: string;
lesson?: Lesson;
}
export interface Question {
created_at?: string;
exercise_id: string /* foreign key to exercise.id */;
id: number /* primary key */;
name?: string;
options: Option[];
order?: number;
points?: string;
question_type_id: number /* foreign key to question_type.id */;
title: string;
title: any; // type unknown;
created_at?: string;
updated_at?: string;
exercise_id: string /* foreign key to exercise.id */;
name?: string;
points?: any; // type unknown;
order?: number;
question_type?: Question_type;
exercise?: Exercise;
}
export interface Submission {
@@ -182,15 +194,14 @@ export interface Submission {
}
export interface Option {
created_at?: string;
id?: number /* primary key */;
id: number /* primary key */;
label: any; // type unknown;
is_correct: boolean;
is_dirty?: boolean;
label: string;
question_id: number /* foreign key to question.id */;
question?: Question;
updated_at?: string;
value?: string;
created_at?: string;
updated_at?: string;
question?: Question;
}
export interface Question_answer {