CLASSROOMIO-T-21: Course landing page + editor
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
+17
-9
@@ -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}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user