feat: Video and Header component for a lesson

This commit is contained in:
Jannat Patel
2023-05-05 11:53:04 +05:30
parent 4336839932
commit 211c69bb41
12 changed files with 178 additions and 59 deletions

View File

@@ -3,11 +3,7 @@
--text-3-8xl: 34px; --text-3-8xl: 34px;
--text-4xl: 36px; --text-4xl: 36px;
--primary-color: var(--gray-900); --primary-color: var(--gray-900);
--accent-color: #0B9E92 --primary: var(--gray-900);
}
.btn.btn-primary {
background-color: var(--primary-color);
} }
.nav-link .course-list-count { .nav-link .course-list-count {
@@ -121,7 +117,7 @@ textarea.field-input {
} }
.outline-lesson { .outline-lesson {
padding: 0.5rem 0; padding: 0.75rem 0;
} }
.common-card-style .outline-lesson:last-of-type { .common-card-style .outline-lesson:last-of-type {
@@ -165,6 +161,31 @@ textarea.field-input {
padding-bottom: 1rem; padding-bottom: 1rem;
} }
.form-width {
width: 50%;
}
@media (max-width: 768px) {
.form-width {
width: 75%;
}
}
.clickable {
color: var(--gray-900);
font-weight: 500;
}
.clickable:hover {
color: var(--gray-900);
text-decoration: none;
}
.form-checkbox {
display: flex;
align-items: center;
}
body { body {
background-color: #FFFFFF; background-color: #FFFFFF;
} }
@@ -1487,7 +1508,7 @@ pre {
} }
.reviews-parent .progress-bar { .reviews-parent .progress-bar {
background-color: var(--accent-color); background-color: var(--primary-color);
} }
.course-home-top-container { .course-home-top-container {

View File

@@ -11,7 +11,7 @@
{% block content %} {% block content %}
<main class="common-page-style"> <main class="common-page-style">
{{ Header() }} {{ Header() }}
<div class="container w-75" id="course-outline" {% if course.name %} data-course="{{ course.name }}" {% endif %}> <div class="container form-width" id="course-outline" {% if course.name %} data-course="{{ course.name }}" {% endif %}>
{{ CreateLesson() }} {{ CreateLesson() }}
</div> </div>
</main> </main>
@@ -20,13 +20,22 @@
{% macro Header() %} {% macro Header() %}
<header class="sticky"> <header class="sticky">
<div class="container w-75"> <div class="container form-width">
<button class="btn btn-primary btn-sm pull-right mt-1" id="save-lesson"> <button class="btn btn-primary btn-sm pull-right mt-1" id="save-lesson">
<span> <span>
{{ _("Save") }} {{ _("Save") }}
</span> </span>
</button> </button>
{% if lesson.name %}
<a class="btn btn-default btn-sm pull-right mt-1 mr-2" href="{{ get_lesson_url(course.name, lesson_number) }}">
<span>
{{ _("Preview") }}
</span>
</a>
{% endif %}
<div class="page-title"> <div class="page-title">
{{ course.title if course.name else _("Course Outline") }} {{ course.title if course.name else _("Course Outline") }}
</div> </div>
@@ -46,10 +55,17 @@
</div> </div>
</div> </div>
<div class=""> <div class="">
<input id="lesson-title" type="text" class="field-input" data-index="{{ lesson_index }}" data-chapter="{{ chapter }}" data-course="{{ lesson.name }}" {% if lesson.name %} data-lesson="{{ lesson.name }}" value="{{ lesson.title }}" {% endif %}> <input id="lesson-title" type="text" class="field-input" data-index="{{ lesson_index }}" data-chapter="{{ chapter }}" data-course="{{ course.name }}" {% if lesson.name %} data-lesson="{{ lesson.name }}" value="{{ lesson.title }}" {% endif %}>
</div> </div>
</div> </div>
<div class="field-group">
<label for="published" class="form-checkbox">
<input type="checkbox" id="preview" {% if lesson.include_in_preview %} checked {% endif %}>
<span>{{ _("Show preview of this lesson to Guest users.") }}</span>
</label>
</div>
<div class="field-group"> <div class="field-group">
<div> <div>
<div class="field-label"> <div class="field-label">

View File

@@ -12,6 +12,35 @@ frappe.ready(() => {
}); });
}); });
const setup_editor = () => {
self.editor = new EditorJS({
holder: "lesson-content",
tools: {
header: {
class: Header,
inlineToolbar: ["bold", "italic", "link"],
config: {
levels: [4, 5, 6],
defaultLevel: 5,
},
},
paragraph: {
class: Paragraph,
inlineToolbar: true,
config: {
preserveBlank: true,
},
},
youtube: YouTubeVideo,
quiz: Quiz,
upload: Upload,
},
data: {
blocks: self.blocks ? self.blocks : [],
},
});
};
const parse_string_to_lesson = () => { const parse_string_to_lesson = () => {
let lesson_content = $("#current-lesson-content").html(); let lesson_content = $("#current-lesson-content").html();
let lesson_blocks = []; let lesson_blocks = [];
@@ -33,6 +62,31 @@ const parse_string_to_lesson = () => {
quiz: quiz, quiz: quiz,
}, },
}); });
} else if (block.includes("{{ Video")) {
let video = block.match(/'([^']+)'/)[1];
lesson_blocks.push({
type: "upload",
data: {
file_url: video,
},
});
} else if (block.includes("![]")) {
let image = block.match(/\((.*?)\)/)[1];
lesson_blocks.push({
type: "upload",
data: {
file_url: image,
},
});
} else if (block.includes("#")) {
let level = (block.match(/#/g) || []).length;
lesson_blocks.push({
type: "header",
data: {
text: block.replace(/#/g, "").trim(),
level: level,
},
});
} else { } else {
lesson_blocks.push({ lesson_blocks.push({
type: "paragraph", type: "paragraph",
@@ -46,26 +100,6 @@ const parse_string_to_lesson = () => {
this.blocks = lesson_blocks; this.blocks = lesson_blocks;
}; };
const setup_editor = () => {
self.editor = new EditorJS({
holder: "lesson-content",
tools: {
youtube: YouTubeVideo,
quiz: Quiz,
paragraph: {
class: Paragraph,
inlineToolbar: true,
config: {
preserveBlank: true,
},
},
},
data: {
blocks: self.blocks ? self.blocks : [],
},
});
};
const save_lesson = (e) => { const save_lesson = (e) => {
self.editor.save().then((outputData) => { self.editor.save().then((outputData) => {
parse_lesson_to_string(outputData); parse_lesson_to_string(outputData);
@@ -79,6 +113,15 @@ const parse_lesson_to_string = (data) => {
lesson_content += `{{ YouTubeVideo("${block.data.youtube}") }}\n`; lesson_content += `{{ YouTubeVideo("${block.data.youtube}") }}\n`;
} else if (block.type == "quiz") { } else if (block.type == "quiz") {
lesson_content += `{{ Quiz("${block.data.quiz}") }}\n`; lesson_content += `{{ Quiz("${block.data.quiz}") }}\n`;
} else if (block.type == "upload") {
let url = block.data.file_url;
lesson_content += block.data.is_video
? `{{ Video("${url}") }}\n`
: `![](${url})`;
} else if (block.type == "header") {
console.log(block);
lesson_content +=
"#".repeat(block.data.level) + ` ${block.data.text}\n`;
} else if (block.type == "paragraph") { } else if (block.type == "paragraph") {
lesson_content += `${block.data.text}\n`; lesson_content += `${block.data.text}\n`;
} }
@@ -95,7 +138,7 @@ const save = (lesson_content) => {
title: $("#lesson-title").val(), title: $("#lesson-title").val(),
body: lesson_content, body: lesson_content,
chapter: $("#lesson-title").data("chapter"), chapter: $("#lesson-title").data("chapter"),
preview: 0, preview: $("#preview").prop("checked") ? 1 : 0,
idx: $("#lesson-title").data("index"), idx: $("#lesson-title").data("index"),
lesson: lesson ? lesson : "", lesson: lesson ? lesson : "",
}, },
@@ -120,6 +163,12 @@ const fetch_quiz_list = () => {
}); });
}; };
const is_video = (url) => {
let video_types = ["mov", "mp4", "mkv"];
let video_extension = url.split(".").pop();
return video_types.indexOf(video_extension) >= 0;
};
class YouTubeVideo { class YouTubeVideo {
constructor({ data }) { constructor({ data }) {
this.data = data; this.data = data;
@@ -242,29 +291,58 @@ class Quiz {
} }
} }
class Video { class Upload {
static get toolbox() { static get toolbox() {
return { return {
title: "Video", title: "Upload",
}; };
} }
constructor({ data }) {
this.data = data;
}
render() { render() {
this.wrapper = document.createElement("div"); this.wrapper = document.createElement("div");
if (this.data && this.data.file_url) {
$(this.wrapper).html(this.render_upload(this.data.file_url));
} else {
this.render_upload_dialog();
}
return this.wrapper;
}
render_upload_dialog() {
let self = this;
new frappe.ui.FileUploader({ new frappe.ui.FileUploader({
disable_file_browser: true, disable_file_browser: true,
folder: "Home/Attachments", folder: "Home/Attachments",
make_attachments_public: true, make_attachments_public: true,
restrictions: { restrictions: {
allowed_file_types: ["video/*"], allowed_file_types: ["image/*", "video/*"],
}, },
on_success: (file_doc) => { on_success: (file_doc) => {
$(e.target) self.file_url = file_doc.file_url;
.parent() $(self.wrapper).html(self.render_upload(self.file_url));
.siblings("img")
.addClass("image-preview")
.attr("src", file_doc.file_url);
}, },
}); });
} }
render_upload(url) {
this.is_video = is_video(url);
if (this.is_video) {
return `<video controls width='100%'>
<source src=${encodeURI(url)} type='video/mp4'>
</video>`;
} else {
return `<img src=${encodeURI(url)} width='100%'>`;
}
}
save(block_content) {
return {
file_url: this.data.file_url || this.file_url,
is_video: this.is_video,
};
}
} }

View File

@@ -10,6 +10,7 @@ def get_context(context):
lesson_index = frappe.form_dict.get("lesson") lesson_index = frappe.form_dict.get("lesson")
lesson_number = f"{chapter_index}.{lesson_index}" lesson_number = f"{chapter_index}.{lesson_index}"
context.lesson_index = lesson_index context.lesson_index = lesson_index
context.lesson_number = lesson_number
context.chapter = frappe.db.get_value( context.chapter = frappe.db.get_value(
"Chapter Reference", {"idx": chapter_index, "parent": context.course.name}, "chapter" "Chapter Reference", {"idx": chapter_index, "parent": context.course.name}, "chapter"
) )

View File

@@ -81,7 +81,9 @@
<!-- Edit Button --> <!-- Edit Button -->
{% if (is_instructor or has_course_moderator_role()) %} {% if (is_instructor or has_course_moderator_role()) %}
<button class="btn btn-secondary btn-sm ml-2 btn-edit"> {{ _("Edit") }} </button> <a class="btn btn-secondary btn-sm ml-2" href="{{ get_lesson_url(course.name, lesson_number) }}/edit">
{{ _("Edit") }}
</a>
{% endif %} {% endif %}
</div> </div>

View File

@@ -78,10 +78,6 @@ frappe.ready(() => {
mark_active_question(); mark_active_question();
}); });
$(".btn-edit").click((e) => {
window.location.href = `${window.location.href}?edit=1`;
});
$(".btn-back").click((e) => { $(".btn-back").click((e) => {
window.location.href = window.location.href.split("?")[0]; window.location.href = window.location.href.split("?")[0];
}); });

View File

@@ -16,6 +16,7 @@ def get_context(context):
chapter_index = frappe.form_dict.get("chapter") chapter_index = frappe.form_dict.get("chapter")
lesson_index = frappe.form_dict.get("lesson") lesson_index = frappe.form_dict.get("lesson")
lesson_number = f"{chapter_index}.{lesson_index}" lesson_number = f"{chapter_index}.{lesson_index}"
context.lesson_number = lesson_number
context.lesson_index = lesson_index context.lesson_index = lesson_index
context.chapter = frappe.db.get_value( context.chapter = frappe.db.get_value(
"Chapter Reference", {"idx": chapter_index, "parent": context.course.name}, "chapter" "Chapter Reference", {"idx": chapter_index, "parent": context.course.name}, "chapter"

View File

@@ -6,7 +6,7 @@
{% block content %} {% block content %}
<main class="common-page-style"> <main class="common-page-style">
{{ Header() }} {{ Header() }}
<div class="container w-75"> <div class="container form-width">
{{ CreateCourse() }} {{ CreateCourse() }}
</div> </div>
</main> </main>
@@ -15,11 +15,17 @@
{% macro Header() %} {% macro Header() %}
<header class="sticky"> <header class="sticky">
<div class="container w-75"> <div class="container form-width">
<button class="btn btn-primary btn-sm btn-save-course pull-right mt-1"> <button class="btn btn-primary btn-sm btn-save-course pull-right mt-1">
{{ _("Save") }} {{ _("Save") }}
</button> </button>
{% if course.name %}
<a class="btn btn-default btn-sm pull-right mt-1 mr-2" href="/courses/{{ course.name }}">
{{ _("Preview") }}
</a>
{% endif %}
<div class="page-title"> {{ _("Course Details") }} </div> <div class="page-title"> {{ _("Course Details") }} </div>
</div> </div>
</header> </header>

View File

@@ -92,9 +92,7 @@ const make_editor = () => {
], ],
body: $("#description").get(0), body: $("#description").get(0),
}); });
console.log(this.description);
this.description.make(); this.description.make();
console.log(this.description);
$("#description .form-section:last").removeClass("empty-section"); $("#description .form-section:last").removeClass("empty-section");
$("#description .frappe-control").removeClass("hide-control"); $("#description .frappe-control").removeClass("hide-control");
$("#description .form-column").addClass("p-0"); $("#description .form-column").addClass("p-0");

View File

@@ -32,7 +32,7 @@
{% endif %} {% endif %}
{% if show_creators_section %} {% if show_creators_section %}
<a class="btn btn-default btn-sm ml-2" href="/courses/new-course"> <a class="btn btn-default btn-sm ml-2" href="/courses/new-course/edit">
{{ _("Create a Course") }} {{ _("Create a Course") }}
</a> </a>
{% endif %} {% endif %}

View File

@@ -8,7 +8,7 @@
{% block page_content %} {% block page_content %}
<main class="common-page-style"> <main class="common-page-style">
{{ Header() }} {{ Header() }}
<div class="container w-75" id="course-outline" {% if course.name %} data-course="{{ course.name }}" {% endif %}> <div class="container form-width" id="course-outline" {% if course.name %} data-course="{{ course.name }}" {% endif %}>
{% if chapters | length %} {% if chapters | length %}
{{ Outline(chapters) }} {{ Outline(chapters) }}
{% else %} {% else %}
@@ -22,8 +22,8 @@
{% macro Header() %} {% macro Header() %}
<header class="sticky"> <header class="sticky">
<div class="container w-75"> <div class="container form-width">
<button class="btn btn-primary btn-sm pull-right mt-1" id="add-chapter"> <button class="btn btn-primary btn-sm pull-right mt-1 btn-add-chapter">
<span> <span>
{{ _("Add Chapter") }} {{ _("Add Chapter") }}
</span> </span>
@@ -69,7 +69,7 @@
<use class="" href="#{{ lesson.icon }}"> <use class="" href="#{{ lesson.icon }}">
</svg> </svg>
</div> --> </div> -->
<a class="button-links" href="/courses/{{ course.name }}/learn/{{ chapter_index }}.{{ loop.index }}/edit"> <a class="clickable" href="/courses/{{ course.name }}/learn/{{ chapter_index }}.{{ loop.index }}/edit">
{{ lesson.title }} {{ lesson.title }}
</a> </a>
</div> </div>
@@ -158,7 +158,7 @@
{% macro EmptyState() %} {% macro EmptyState() %}
<article class="empty-state"> <article class="empty-state my-5">
<div class="text-center"> <div class="text-center">
<div class="bold-heading"> <div class="bold-heading">
{{ _("You have not added any chapter yet") }} {{ _("You have not added any chapter yet") }}
@@ -167,10 +167,10 @@
{{ _("Create and manage your chapters from here.") }} {{ _("Create and manage your chapters from here.") }}
</div> </div>
<div class="mt-4"> <div class="mt-4">
<button class="btn btn-default btn-sm"> <button class="btn btn-default btn-sm btn-add-chapter">
<svg class="icon icon-xs"> <!-- <svg class="icon icon-xs">
<use class="" href="#icon-add"></use> <use class="" href="#icon-add"></use>
</svg> </svg> -->
<span> <span>
{{ _("Add Chapter") }} {{ _("Add Chapter") }}
</span> </span>

View File

@@ -1,5 +1,5 @@
frappe.ready(() => { frappe.ready(() => {
$("#add-chapter").click((e) => { $(".btn-add-chapter").click((e) => {
show_chapter_modal(e); show_chapter_modal(e);
}); });
@@ -35,7 +35,7 @@ const save_chapter = (e) => {
course: $("#course-outline").data("course"), course: $("#course-outline").data("course"),
title: $("#chapter-title").val(), title: $("#chapter-title").val(),
chapter_description: $("#chapter-description").val(), chapter_description: $("#chapter-description").val(),
idx: parent.data("idx") || $(".chapter-container").length + 1, idx: parent.data("idx") || $(".chapter-container").length,
chapter: parent.data("chapter") || null, chapter: parent.data("chapter") || null,
}, },
callback: (data) => { callback: (data) => {