CLASSROOMIO-T-122: Auto saving lesson changes

CLASSROOMIO-T-122: Autosave improvements

CLASSROOMIO-T-122: Add text for Autosave

CLASSROOMIO-T-122: Auto save feature

Merge branch 'CLASSROOMIO-T-122' of https://gitlab.com/thenextgreat/classroomio into CLASSROOMIO-T-122

CLASSROOMIO-T-122: Add Autosave

CLASSROOMIO-T-122: Update savelesson comment

CLASSROOMIO-T-122: Remove autosave from video upload to enable general autosave


Co-authored-by: Rotimi Best <[email protected]>
Co-authored-by: Tunji Adeyemi <[email protected]>


Merge-request: CLASSROOMIO-MR-49
Merged-by: Best Admin <[email protected]>
This commit is contained in:
Rotimi Best
2023-08-18 19:28:41 +00:00
committed by Space Cloud
co-authored by Rotimi Best Tunji Adeyemi
parent 726b513ad0
commit 910d9c50a9
10 changed files with 130 additions and 69 deletions
+4 -1
View File
@@ -32,7 +32,7 @@
} }
</script> </script>
<div class="w-full min-h-screen flex items-center justify-center"> <div class="root w-full min-h-screen flex items-center justify-center">
<div class="container border border-gray bg-white dark:bg-gray-800"> <div class="container border border-gray bg-white dark:bg-gray-800">
<div class="flex items-center flex-col p-2 lg:px-8 lg:py-3"> <div class="flex items-center flex-col p-2 lg:px-8 lg:py-3">
{#if !showOnlyContent || showLogo} {#if !showOnlyContent || showLogo}
@@ -85,6 +85,9 @@
</div> </div>
<style> <style>
.root {
background: radial-gradient(circle at 10% 20%, rgb(239, 246, 249) 0%, rgb(206, 239, 253) 90%);
}
.container { .container {
width: 450px; width: 450px;
} }
@@ -4,7 +4,6 @@
import * as CONSTANTS from './constants'; import * as CONSTANTS from './constants';
export let lessonId = ''; export let lessonId = '';
export let saveLesson = () => {};
let tabs = CONSTANTS.videoTabs; let tabs = CONSTANTS.videoTabs;
let currentTab = tabs[0].value; let currentTab = tabs[0].value;
@@ -37,7 +36,7 @@
{#if currentTab === 1} {#if currentTab === 1}
<YoutubeVideo /> <YoutubeVideo />
{:else} {:else}
<LocalVideo {lessonId} {saveLesson} /> <LocalVideo {lessonId} />
{/if} {/if}
</main> </main>
</section> </section>
@@ -6,11 +6,9 @@
deleteLessonVideo, deleteLessonVideo,
uploadCourseVideoStore uploadCourseVideoStore
} from '$lib/components/Course/components/Lesson/store/lessons'; } from '$lib/components/Course/components/Lesson/store/lessons';
import { enhance } from '$app/forms';
import { Moon } from 'svelte-loading-spinners'; import { Moon } from 'svelte-loading-spinners';
export let lessonId = ''; export let lessonId = '';
export let saveLesson = () => {};
let formRes; let formRes;
let isLoaded = false; let isLoaded = false;
@@ -18,6 +16,15 @@
let submit; let submit;
let uploadedFileUrl = ''; let uploadedFileUrl = '';
let isLoading = false; let isLoading = false;
let timeoutkey;
const uploadingTexts = [
'Sending your video to our server...',
'Generating a link to your video...',
'Generating transcription for your video...',
'Wrapping things up'
];
let uploadingLoadingText = uploadingTexts[0];
function isVideoAdded(link) { function isVideoAdded(link) {
return $lesson.materials?.videos?.find((v) => v.link === link); return $lesson.materials?.videos?.find((v) => v.link === link);
@@ -27,10 +34,8 @@
isLoading = true; isLoading = true;
if (!fileInput) return; if (!fileInput) return;
console.log('file', fileInput.files[0]);
const formData = new FormData(); const formData = new FormData();
formData.append('videoFile', fileInput.files[0]); formData.append('videoFile', fileInput.files[0]);
console.log('formData', formData);
try { try {
const response = await axios({ const response = await axios({
@@ -68,16 +73,35 @@
{ {
type: 'muse', type: 'muse',
link: uploadedFileUrl, link: uploadedFileUrl,
metadata: {} metadata: response?.metadata || {}
} }
]; ];
saveLesson();
} }
} }
function tryAgain() { function tryAgain() {
formRes = null; formRes = null;
isLoaded = false; isLoaded = false;
isLoading = false;
}
$: {
if (isLoading) {
timeoutkey = setTimeout(() => {
console.log('timeout');
const i = uploadingTexts.findIndex((text) => text === uploadingLoadingText);
const next = uploadingTexts[i + 1];
if (next) {
console.log('uploadingLoadingText', next);
uploadingLoadingText = next;
}
}, 5000);
} else if (timeoutkey) {
console.log('clearTimeout');
clearTimeout(timeoutkey);
uploadingLoadingText = uploadingTexts[0];
}
} }
$: isDoneUploading(formRes); $: isDoneUploading(formRes);
@@ -96,7 +120,7 @@
> >
{#if isLoading} {#if isLoading}
<Moon size="40" color="#1d4ed8" unit="px" duration="1s" /> <Moon size="40" color="#1d4ed8" unit="px" duration="1s" />
<p class="mt-5">Uploading Video</p> <p class="mt-5">{uploadingLoadingText}</p>
{:else} {:else}
<img src="/upload-video.svg" alt="upload" /> <img src="/upload-video.svg" alt="upload" />
<span class="pt-3"> <span class="pt-3">
@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import { useCompletion } from 'ai/svelte'; import { useCompletion } from 'ai/svelte';
import merge from 'lodash/merge';
import MODES from '$lib/utils/constants/mode.js'; import MODES from '$lib/utils/constants/mode.js';
import TrashCanIcon from 'carbon-icons-svelte/lib/TrashCan.svelte'; import TrashCanIcon from 'carbon-icons-svelte/lib/TrashCan.svelte';
import IconButton from '$lib/components/IconButton/index.svelte'; import IconButton from '$lib/components/IconButton/index.svelte';
@@ -30,12 +29,14 @@
import VideoUploader from '$lib/components/Course/components/Lesson/Materials/Video/Index.svelte'; import VideoUploader from '$lib/components/Course/components/Lesson/Materials/Video/Index.svelte';
import { course } from '$lib/components/Course/store'; import { course } from '$lib/components/Course/store';
import * as CONSTANTS from './constants'; import * as CONSTANTS from './constants';
import { fetchLesson } from '$lib/utils/services/courses';
export let mode = MODES.view; export let mode = MODES.view;
export let prevMode = ''; export let prevMode = '';
export let lessonId = ''; export let lessonId = '';
export let isSaving = false;
let initAutoSave = false;
let timeoutId: NodeJS.Timeout;
let tabs = CONSTANTS.tabs; let tabs = CONSTANTS.tabs;
let currentTab = tabs[0].value; let currentTab = tabs[0].value;
let errors = {}; let errors = {};
@@ -51,18 +52,14 @@
() => () =>
(currentTab = tab); (currentTab = tab);
// problem: after we save an uploaded video, async function saveLesson(materials = undefined) {
async function saveLesson() { const _lesson = !!materials
const { data } = await fetchLesson(lessonId); ? {
if (data?.videos?.length === $lesson.materials.videos.length) { ...$lesson,
const materials = merge(data, $lesson.materials);
lesson.update((l) => ({
...l,
materials materials
}));
} }
handleUpdateLessonMaterials($lesson, lessonId); : $lesson;
handleUpdateLessonMaterials(_lesson, lessonId);
} }
function handleSave(prevMode: string) { function handleSave(prevMode: string) {
@@ -132,12 +129,38 @@
window.players = players; window.players = players;
} }
function autoSave(updatedMaterials) {
if (timeoutId) clearTimeout(timeoutId);
if (!initAutoSave) {
initAutoSave = true;
return;
}
isSaving = true;
timeoutId = setTimeout(async () => {
await saveLesson(updatedMaterials);
isSaving = false;
}, 1500);
}
function handleInputChange() {
$isLessonDirty = true;
}
function onLessonIdChange() {
initAutoSave = false;
}
const onClose = () => { const onClose = () => {
saveLesson();
$uploadCourseVideoStore.isModalOpen = false; $uploadCourseVideoStore.isModalOpen = false;
$uploadCourseVideoStore.formRes = null;
}; };
$: autoSave($lesson.materials);
$: onLessonIdChange(lessonId);
$: handleSave(prevMode); $: handleSave(prevMode);
$: addBadgeValueToTab($lesson.materials); $: addBadgeValueToTab($lesson.materials);
@@ -153,7 +176,7 @@
width="w-4/5 h-[566px]" width="w-4/5 h-[566px]"
modalHeading="Add a Video" modalHeading="Add a Video"
> >
<VideoUploader {lessonId} {saveLesson} /> <VideoUploader {lessonId} />
</Modal> </Modal>
<Tabs {tabs} {currentTab} {onChange}> <Tabs {tabs} {currentTab} {onChange}>
@@ -164,11 +187,11 @@
writeLabel="Note" writeLabel="Note"
bind:value={$lesson.materials.note} bind:value={$lesson.materials.note}
placeholder="Start typing your lesson" placeholder="Start typing your lesson"
onInputChange={() => ($isLessonDirty = true)} on:change={handleInputChange}
bind:textareaRef bind:textareaRef
bind:buttonRef={aiButtonRef} bind:buttonRef={aiButtonRef}
> >
<div slot="buttons"> <div slot="buttons" class="flex items-center">
<PrimaryButton <PrimaryButton
className="flex items-center relative" className="flex items-center relative"
onClick={() => { onClick={() => {
@@ -179,7 +202,7 @@
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
> >
<MachineLearningModel size={20} class="carbon-icon mr-3" /> <MachineLearningModel size={20} class="carbon-icon mr-3" />
AI Assistant AI
<Popover <Popover
caret caret
align="left" align="left"
@@ -226,7 +249,7 @@
<TextField <TextField
label="Slide link" label="Slide link"
bind:value={$lesson.materials.slide_url} bind:value={$lesson.materials.slide_url}
onChange={() => ($isLessonDirty = true)} onInputChange={handleInputChange}
/> />
{:else if $lesson.materials.slide_url} {:else if $lesson.materials.slide_url}
<iframe <iframe
@@ -312,11 +335,12 @@
<Box> <Box>
<img src="/no-video.svg" alt="Video not found" /> <img src="/no-video.svg" alt="Video not found" />
<h3 class="text-xl font-normal dark:text-white py-2"> <h3 class="text-xl font-normal dark:text-white py-2">
No youtube video added for this lesson yet No video added for this lesson yet
</h3> </h3>
<p class="text-sm text-center font-normal py-2"> <p class="text-sm text-center font-normal py-2">
Share your knowledge with the world by creating engaging lessons.<br /> Share videos with your students and get auto transcription and in video search out of
Start by clicking on New Lesson button. the box.<br />
Start by clicking on Add Video button.
</p> </p>
<PrimaryButton label="Add Video" className="rounded-md" onClick={openAddVideoModal} /> <PrimaryButton label="Add Video" className="rounded-md" onClick={openAddVideoModal} />
</Box> </Box>
@@ -4,8 +4,7 @@ import { createLesson, updateLesson, deleteLesson } from '$lib/utils/services/co
import type { Lesson, Course, LessonPage } from '$lib/utils/types'; import type { Lesson, Course, LessonPage } from '$lib/utils/types';
export const uploadCourseVideoStore = writable({ export const uploadCourseVideoStore = writable({
isModalOpen: false, isModalOpen: false
formRes: null
}); });
export const lessons: Writable<Lesson[]> = writable([]); export const lessons: Writable<Lesson[]> = writable([]);
@@ -14,6 +13,7 @@ export const lesson = writable<LessonPage>({
id: null, id: null,
totalExercises: 0, totalExercises: 0,
is_complete: false, is_complete: false,
isSaving: false,
materials: { materials: {
note: '', note: '',
slide_url: '', slide_url: '',
@@ -104,14 +104,11 @@ export async function handleUpdateLessonMaterials(lesson: any, lessonId: Lesson[
} }
export const deleteLessonVideo = (index: any) => { export const deleteLessonVideo = (index: any) => {
lesson.update((currentLesson) => { lesson.update((currentLesson) => ({
const videos = currentLesson.materials.videos || [];
return {
...currentLesson, ...currentLesson,
materials: { materials: {
...currentLesson.materials, ...currentLesson.materials,
videos: videos?.filter((video, i) => i !== index) videos: currentLesson.materials.videos.filter((video, i) => i !== index)
} }
}; }));
});
}; };
+11 -2
View File
@@ -6,7 +6,7 @@
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants'; import { VARIANTS } from '$lib/components/PrimaryButton/constants';
export let disableLogin; export let disableSignup = false;
export let logo; export let logo;
export let orgName; export let orgName;
export let isOrgSite = false; export let isOrgSite = false;
@@ -60,7 +60,6 @@
{/if} {/if}
{:else} {:else}
<li> <li>
{#if !disableLogin}
<div class="flex"> <div class="flex">
<PrimaryButton <PrimaryButton
label="Login" label="Login"
@@ -68,6 +67,16 @@
onClick={() => goto('/login' + redirect)} onClick={() => goto('/login' + redirect)}
/> />
</div> </div>
</li>
<li>
{#if !disableSignup}
<div class="flex">
<PrimaryButton
label="Signup"
variant={VARIANTS.CONTAINED}
onClick={() => goto('/signup' + redirect)}
/>
</div>
{/if} {/if}
</li> </li>
{/if} {/if}
@@ -141,7 +141,7 @@
<main> <main>
<!-- Header Section --> <!-- Header Section -->
<header id="header" class="banner w-full h-[80vh] mb-10 relative"> <header id="header" class="banner w-full h-[80vh] mb-10 relative">
<Navigation logo={$currentOrg.avatar_url} orgName={$currentOrg.name} disableLogin={false} /> <Navigation logo={$currentOrg.avatar_url} orgName={$currentOrg.name} disableSignup={true} />
<div class="absolute h-[80vh] top-0 w-full opacity-80 z-10 bg-white" /> <div class="absolute h-[80vh] top-0 w-full opacity-80 z-10 bg-white" />
{#if video} {#if video}
+1
View File
@@ -144,6 +144,7 @@ export interface LessonPage {
id?: string | null; id?: string | null;
totalExercises: Number; totalExercises: Number;
is_complete: Boolean; is_complete: Boolean;
isSaving: Boolean;
materials: { materials: {
note: string; note: string;
slide_url: string; slide_url: string;
+2 -2
View File
@@ -283,7 +283,7 @@
isOrgSite={data.isOrgSite} isOrgSite={data.isOrgSite}
logo={data.isOrgSite ? $currentOrg.avatar_url : undefined} logo={data.isOrgSite ? $currentOrg.avatar_url : undefined}
orgName={data.isOrgSite ? $currentOrg.name : undefined} orgName={data.isOrgSite ? $currentOrg.name : undefined}
disableLogin={false} disableSignup={false}
/> />
{/if} {/if}
{/if} {/if}
@@ -322,7 +322,7 @@
<style> <style>
main { main {
background: radial-gradient(circle at 10% 20%, rgb(239, 246, 249) 0%, rgb(206, 239, 253) 90%); background-color: white;
box-sizing: border-box; box-sizing: border-box;
} }
@@ -5,6 +5,7 @@
import CheckmarkOutlineIcon from 'carbon-icons-svelte/lib/CheckmarkOutline.svelte'; import CheckmarkOutlineIcon from 'carbon-icons-svelte/lib/CheckmarkOutline.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants.js'; import { VARIANTS } from '$lib/components/PrimaryButton/constants.js';
import { Loading } from 'carbon-components-svelte';
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte'; import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
import PageNav from '$lib/components/PageNav/index.svelte'; import PageNav from '$lib/components/PageNav/index.svelte';
import PageBody from '$lib/components/PageBody/index.svelte'; import PageBody from '$lib/components/PageBody/index.svelte';
@@ -12,11 +13,12 @@
import Exercises from '$lib/components/Course/components/Lesson/Exercises/index.svelte'; import Exercises from '$lib/components/Course/components/Lesson/Exercises/index.svelte';
import MODES from '$lib/utils/constants/mode.js'; import MODES from '$lib/utils/constants/mode.js';
import { setCourse, course } from '$lib/components/Course/store'; import { setCourse, course } from '$lib/components/Course/store';
import Download from 'carbon-icons-svelte/lib/Download.svelte';
import { import {
lesson, lesson,
lessons, lessons,
handleSaveLesson, handleSaveLesson
uploadCourseVideoStore
} from '$lib/components/Course/components/Lesson/store/lessons'; } from '$lib/components/Course/components/Lesson/store/lessons';
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import { currentOrg } from '$lib/utils/store/org'; import { currentOrg } from '$lib/utils/store/org';
@@ -25,8 +27,6 @@
export let data; export let data;
export let form;
let path = ''; let path = '';
let mode = MODES.view; let mode = MODES.view;
let prevMode = ''; let prevMode = '';
@@ -34,6 +34,7 @@
let isFetching = false; let isFetching = false;
let isMarkingComplete = false; let isMarkingComplete = false;
let isLoading = false; let isLoading = false;
let isSaving = false;
function getLessonOrder(id) { function getLessonOrder(id) {
const index = $lessons.findIndex((lesson) => lesson.id === id); const index = $lessons.findIndex((lesson) => lesson.id === id);
@@ -153,8 +154,6 @@
mode = MODES.view; mode = MODES.view;
fetchReqData(data.lessonId); fetchReqData(data.lessonId);
} }
$: $uploadCourseVideoStore.formRes = form;
</script> </script>
<CourseContainer {isFetching} {path} isExercisePage={!data.isMaterialsTabActive && data.exerciseId}> <CourseContainer {isFetching} {path} isExercisePage={!data.isMaterialsTabActive && data.exerciseId}>
@@ -179,17 +178,22 @@
<div class="flex items-center"> <div class="flex items-center">
<PrimaryButton <PrimaryButton
className="mr-2" className="mr-2"
label="Download PDF"
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
onClick={downloadLesson} onClick={downloadLesson}
{isLoading} {isLoading}
/> >
<PrimaryButton <Download size={16} class="mr-2" />
className="mr-2" Download PDF
label={mode === MODES.edit ? 'Save' : 'Edit'} </PrimaryButton>
variant={VARIANTS.OUTLINED}
onClick={toggleMode} <PrimaryButton className="mr-2" variant={VARIANTS.OUTLINED} onClick={toggleMode}>
/> {#if isSaving}
<Loading withOverlay={false} small />
<span class="text-sm ml-2 italic">Autosaving...</span>
{:else}
{mode === MODES.edit ? 'Done' : 'Edit'}
{/if}
</PrimaryButton>
</div> </div>
{/if} {/if}
</RoleBasedSecurity> </RoleBasedSecurity>
@@ -200,7 +204,7 @@
<Exercises lessonId={data.lessonId} exerciseId={data.exerciseId} path={`${path}/exercises`} /> <Exercises lessonId={data.lessonId} exerciseId={data.exerciseId} path={`${path}/exercises`} />
{:else if !!data.lessonId} {:else if !!data.lessonId}
<PageBody> <PageBody>
<Materials lessonId={data.lessonId} {mode} {prevMode} /> <Materials lessonId={data.lessonId} {mode} {prevMode} bind:isSaving />
<div class="w-full flex flex-row-reverse"> <div class="w-full flex flex-row-reverse">
<PrimaryButton <PrimaryButton