CLASSROOMIO-T-30:Developed the UI for the video upload

CLASSROOMIO-T-30: UI cleanup

CLASSROOMIO-T-30: Improve UX

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

CLASSROOMIO-T-30:Made the input clear after upload and also chnged the icons

CLASSROOMIO-T-30: Remove console log

CLASSROOMIO-T-30: Better trim out white spaces in url

CLASSROOMIO-T-30:Fixed the bug between the save and edit

CLASSROOMIO-T-30:Trying to fix the bug between edit and save

Merge branch 'main' of https://gitlab.com/thenextgreat/classroomio

CLASSROOMIO-T-30:Stil working on the reviews

CLASSROOMIO-T-30:Still working on some of the reviews and updates

CLASSROOMIO-T-30: Remove unnessecary values in store

CLASSROOMIO-T-30: Change button to div in modal

CLASSROOMIO-T-100: Select only courses belonging to current org and current user is a member

CLASSROOMIO-T-100: Update condition for fetchCourses

CLASSROOMIO-T-100: Select only courses belonging to current org and current user is a member


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


Merge-request: CLASSROOMIO-MR-35
Merged-by: Best Admin <[email protected]>
Remove netlify app from redirect rule

Add redirect to layout's load

Redirect to about domain

CLASSROOMIO-T-97: UX again? 😇

CLASSROOMIO-T-97: Better course UX 😍😍

CLASSROOMIO-T-97: Better button style and fix route not found

CLASSROOMIO-T-97: Course UI improvement 😁😁😁

CLASSROOMIO-T-30: Add plyr

CLASSROOMIO-T-30:Merged main into branch

Merge branch 'main' of https://gitlab.com/thenextgreat/classroomio

CLASSROOMIO-T-30:Wotking on the lms dashboard

CLASSROOMIO-T-30:Made some changes based on the reviews

Merge branch 'main' of https://gitlab.com/thenextgreat/classroomio

CLASSROOMIO-T-30:Made some refactoring to keep the code DRY

CLASSROOMIO-T-30:Developed the UI for the video upload

Merge branch 'main' of https://gitlab.com/thenextgreat/classroomio

Merge branch 'main' of https://gitlab.com/thenextgreat/classroomio

CLASSROOMIO-T-27: Review Section for Instructors on landing page

CLASSROOMIO-T-27: Remove unused variable

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

CLASSROOMIO-T-27: Complete reviews functionality

CLASSROOMIO-T-27: Review Render Update

CLASSROOMIO-T-27:
- Resolved Feedbacks

CLASSROOMIO-T-27: Fixed Ratings Data

CLASSROOMIO-T-27:
- Added Ratings Svg
- Fixed avatar_url feedback

CLASSROOMIO-T-27: Import Adjustment

CLASSROOMIO-T-27: Code Review

CLASSROOM-T-27: Added Add Review Functionality

CLASSROOMIO-T-27:
- Unresolved Review
- Enhance Code Redability

CLASSROOMIO-T-27: Completed the delete functionality

CLASSROOMIO-T-27: Hide/Show functionality

CLASSROOMIO-T-27: Double Quotation Update

CLASSROOMIO-T-27: Testing toggle functinality

CLASSROOMIO-T-27: Added the icons

CLASSROOMIO-T-27: Completion of the hide/show functionality

CLASSROOMIO-T-27:
Display the store data on the review page for styling

CLASSROOMIO-T-27:
Styling of the review component & connecting it's data

CLASSROOMIO-T-27:
- Add ReviewsForm
- Completion of the Reviews Form Structure/Styling

CLASSROOMIO-T-27: Added a reviews modal

CLASSROOMIO-T-27: Logic update for review section

CLASSROOMIO-T-50: Logic for overlapping reviews

CLASSROOMIO-T-27:
- Completion of the structure & styling with mock data.
- Addition of custom ratings assets

CLASSROOMIO-T-27:
- Review Section Code Structure
- Import Profile Picture Assets


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


Merge-request: CLASSROOMIO-MR-22
Merged-by: Best Admin <[email protected]>
CLASSROOMIO-T-62: Dark Mode Bug Fix

CLASSROOMIO-T-62: Delete Unused class

CLASSROOMIO-T-62: Dark Mode Bug Fix


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


Merge-request: CLASSROOMIO-MR-23
Merged-by: Best Admin <[email protected]>

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


Merge-request: CLASSROOMIO-MR-33
Merged-by: Best Admin <[email protected]>
This commit is contained in:
tunny17
2023-08-07 19:44:26 +00:00
committed by Space Cloud
co-authored by Rotimi Best tunny17 Ifeanyi Nwosu
parent 4910ec4f56
commit a27bc32c63
27 changed files with 495 additions and 99 deletions
@@ -0,0 +1,40 @@
<script>
import YoutubeVideo from './YoutubeVideo.svelte';
import LocalVideo from './LocalVideo.svelte';
import * as CONSTANTS from './constants';
let tabs = CONSTANTS.videoTabs;
let currentTab = tabs[0].value;
const onChange = (tab) => () => (currentTab = tab);
</script>
<section class="w-full h-full flex flex-row items-start gap-3">
<div class="flex-1 flex-col">
<p class="text-sm font-normal text-start text-[#4F4B4B] dark:text-[#b0a9a9] mb-3">Add By</p>
{#each tabs as item (item.value)}
<button
on:click={onChange(item.value)}
class={`w-full px-4 py-3 my-5 border ${
currentTab === item.value
? 'border border-[#0233BD] bg-[#F5F8FE] dark:text-black'
: 'border border-gray-200'
} rounded-md cursor-pointer flex flex-row items-center justify-start gap-2 whitespace-nowrap`}
>
<svelte:component
this={item.icon}
size={20}
color={`${currentTab === item.value ? 'dark:invert-0' : 'dark:invert'}`}
/>
<p>{item.title}</p>
</button>
{/each}
</div>
<main class="w-full h-full">
{#if currentTab === 1}
<YoutubeVideo />
{:else}
<LocalVideo />
{/if}
</main>
</section>
@@ -0,0 +1,88 @@
<script>
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { deleteLessonVideo } from '$lib/components/Course/components/Lesson/store/lessons';
let isWrongFormat = false;
let isUploaded = false;
let isLoaded = false;
let isBig = false;
const uploadVideo = () => {
// if the file is too big or not the correct format
isWrongFormat = true;
// when the upload has been sucessfully completed
// here the you set the video_url to something and that would be displayed on the page
isUploaded = false;
// closes the modal
// $uploadCourseVideo.isModalOpen = false;
};
const onUpload = () => {
// PROBABLY GET THE SNAPSHOT OR SOMETHING
isLoaded = true;
};
</script>
{#if isLoaded == false}
<button
on:click={onUpload}
class="h-full w-full flex flex-col items-center justify-center border border-blue-300 border-dashed rounded-xl"
>
<img src="/upload-video.svg" alt="upload" />
<span class="pt-3">
<h3 class="text-center text-xl font-normal dark:text-white">Upload video</h3>
<p class=" text-center text-sm font-normal">
Select file( Mp4, MOV, AVI) to upload or drag and drop video here.<br /> Size must be less than
20 MB.
</p>
</span>
</button>
{:else if isBig && isUploaded == false}
<div class="h-full w-full flex flex-col items-center justify-center rounded-xl">
<img src="/video-upload-error.svg" alt="upload error" />
<span class="pt-3 pb-2">
<h3 class="text-center text-base font-normal dark:text-white">
Oops 😬, couldn’t upload video
</h3>
<p class="text-center text-xs font-normal text-[#ADADAD]">
Sorry we video wasn’t uploaded. The file size is too big,<br /> maximum size is 20 MB. Try again!
</p>
</span>
<PrimaryButton label="Try again" />
</div>
{:else if isWrongFormat && isUploaded == false}
<div class="h-full w-full flex flex-col items-center justify-center rounded-xl">
<img src="/video-upload-error.svg" alt="upload error" />
<span class="pt-3 pb-2">
<h3 class="text-center text-base font-normal dark:text-white">
Oops 😬, couldn’t upload video
</h3>
<p class="text-center text-xs font-normal text-[#ADADAD]">
Sorry we video wasn’t uploaded, the file format isn’t supported.<br /> Upload MP4, MOV and AVI
files.
</p>
</span>
<PrimaryButton label="Try again" />
</div>
{:else}
<div class="flex flex-col w-full h-full items-start justify-between">
<div class="h-auto w-full border rounded-md px-8 py-3">
<div class="flex items-center justify-between">
<span class="flex items-center gap-2">
<div class="rounded-sm overflow-hidden">
<img src="/course-video.png" alt="coursevideo" class="object-cover" />
</div>
<p>Video.png</p>
</span>
<button on:click={deleteLessonVideo}>
<img src="/delete-video.svg" alt="deletevideo" class="dark:invert" />
</button>
</div>
</div>
<div class="ml-auto">
<PrimaryButton label="upload" onClick={uploadVideo} />
</div>
</div>
{/if}
@@ -0,0 +1,79 @@
<script>
import { CopyButton, Tag } from 'carbon-components-svelte';
import TrashCanIcon from 'carbon-icons-svelte/lib/TrashCan.svelte';
import TextField from '$lib/components/Form/TextField.svelte';
import { lesson, isLessonDirty } from '$lib/components/Course/components/Lesson/store/lessons';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import Chip from '$lib/components/Chip/Text.svelte';
import IconButton from '$lib/components/IconButton/index.svelte';
let youtubeLinks = '';
let error = '';
function addVideo() {
const links = youtubeLinks.split(',').map((link) => link.trim());
const validLinks = links.filter(isValidYouTubeLink);
if (validLinks.length === 0) {
error = 'Invalid YouTube link(s)';
} else {
const existingLinks = $lesson.materials.video_url
.split(',')
.map((link) => link.trim())
.filter(Boolean);
const updatedLinks = [
...existingLinks,
...validLinks.filter((link) => !existingLinks.includes(link))
];
$lesson.materials.video_url = updatedLinks.join(',').trim();
youtubeLinks = '';
error = '';
}
}
function isValidYouTubeLink(link) {
const youtubeRegex =
/^(https?:\/\/)?(www\.)?(youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/;
return youtubeRegex.test(link.trim());
}
function removeVideo(index) {
const links = $lesson.materials.video_url.split(',').map((link) => link.trim());
links.splice(index, 1);
$lesson.materials.video_url = links.join(',').trim();
}
</script>
<div class="w-full flex items-{error ? 'center' : 'end'} justify-between gap-5">
<TextField
label="Youtube link"
bind:value={youtubeLinks}
className="flex-1 text-left "
inputClassName="text-sm"
onChange={() => ($isLessonDirty = true)}
placeholder="https://www.youtube.com/watch?v="
errorMessage={error}
/>
<PrimaryButton label="Add Video" className="rounded-md" onClick={addVideo} />
</div>
<p class="mt-4 pl-2 text-sm">
Added : <strong
>{($lesson?.materials?.video_url && $lesson.materials.video_url.trim().split(',').length) || 0} Videos</strong
>
</p>
<div class="">
{#each $lesson?.materials?.video_url && $lesson.materials.video_url
.trim()
.split(',') as videoLink, index}
<div class="flex items-center gap-1">
<Tag type="blue">
{videoLink}
</Tag>
<CopyButton text={videoLink} feedback="Copied to clipboard" />
<IconButton value="delete-video" onClick={() => removeVideo(index)}>
<TrashCanIcon size={16} class="carbon-icon dark:text-white" />
</IconButton>
</div>
{/each}
</div>
@@ -0,0 +1,14 @@
import Link from 'carbon-icons-svelte/lib/Link.svelte';
import Video from 'carbon-icons-svelte/lib/Video.svelte';
export const videoTabs = [
{
value: 1,
title: 'Youtube Link',
icon: Link
},
{
value: 2,
title: 'Video',
icon: Video
}
];
@@ -1,11 +1,13 @@
<script>
<script lang="ts">
import { useCompletion } from 'ai/svelte';
import MODES from '$lib/utils/constants/mode.js';
import { getEmbedId } from '$lib/utils/functions/formatYoutubeVideo';
import Modal from '$lib/components/Modal/index.svelte';
import { Popover } from 'carbon-components-svelte';
import AlignBoxTopLeftIcon from 'carbon-icons-svelte/lib/AlignBoxTopLeft.svelte';
import TrashCan from 'carbon-icons-svelte/lib/TrashCan.svelte';
import ListIcon from 'carbon-icons-svelte/lib/List.svelte';
import IbmWatsonKnowledgeStudioIcon from 'carbon-icons-svelte/lib/IbmWatsonKnowledgeStudio.svelte';
import { formatYoutubeVideo } from '$lib/utils/functions/formatYoutubeVideo';
import MachineLearningModel from 'carbon-icons-svelte/lib/MachineLearningModel.svelte';
import Tabs from '$lib/components/Tabs/index.svelte';
import TabContent from '$lib/components/TabContent/index.svelte';
@@ -14,24 +16,50 @@
import EditContent from '$lib/components/EditContent/index.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import TextField from '$lib/components/Form/TextField.svelte';
import { lesson, lessons, handleUpdateLessonMaterials, isLessonDirty } from '../store/lessons';
import {
lesson,
lessons,
handleUpdateLessonMaterials,
isLessonDirty,
uploadCourseVideoStore,
deleteLessonVideo
} from '$lib/components/Course/components/Lesson/store/lessons';
import VideoUploader from '$lib/components/Course/components/Lesson/Materials/Video/Index.svelte';
import { course } from '$lib/components/Course/store';
import * as CONSTANTS from './constants';
import { browser } from '$app/environment';
export let mode = MODES.view;
export let prevMode = null;
export let lessonId;
export let prevMode = '';
export let lessonId = '';
let tabs = CONSTANTS.tabs;
let currentTab = tabs[0].value;
let errors = {};
let player = {};
let textareaRef = {};
let aiButtonRef = {};
let openPopover = false;
let videoUrls: Array<string> = [];
let aiButtonClass =
'flex items-center px-5 py-2 border border-gray-300 hover:bg-gray-200 rounded-md w-full mb-2';
const onChange = (tab) => () => (currentTab = tab);
const onChange =
(tab = 0) =>
() =>
(currentTab = tab);
function initPlyr(_player, videoUrl = '') {
if (!browser) return;
const players = Array.from(document.querySelectorAll('.plyr-video-trigger')).map((p) => {
// @ts-ignore
return new Plyr(p);
});
// @ts-ignore
window.players = players;
}
function handleSave(prevMode) {
if (prevMode === MODES.edit) {
@@ -58,6 +86,10 @@
});
}
const openAddVideoModal = () => {
$uploadCourseVideoStore.isModalOpen = true;
};
const { input, handleSubmit, completion, isLoading } = useCompletion({
api: '/api/completion'
});
@@ -81,13 +113,31 @@
}, 500);
}
const onClose = () => {
handleUpdateLessonMaterials($lesson, lessonId);
$uploadCourseVideoStore.isModalOpen = false;
};
$: handleSave(prevMode);
$: initPlyr(player, $lesson.materials.video_url);
$: addBadgeValueToTab($lesson.materials);
$: updateNoteByCompletion($completion);
$: videoUrls = ($lesson?.materials?.video_url || '').split(',').filter((url) => !!url.trim());
</script>
<Modal
{onClose}
bind:open={$uploadCourseVideoStore.isModalOpen}
width="w-4/5 h-[566px]"
modalHeading="Add a Video"
>
<VideoUploader {onClose} />
</Modal>
<Tabs {tabs} {currentTab} {onChange}>
<slot:fragment slot="content">
<TabContent value={tabs[0].value} index={currentTab}>
@@ -186,37 +236,48 @@
</TabContent>
<TabContent value={tabs[2].value} index={currentTab}>
{#if mode === MODES.edit}
<TextField
label="Youtube link"
bind:value={$lesson.materials.video_url}
onChange={() => ($isLessonDirty = true)}
/>
{:else if !!$lesson.materials.video_url && $lesson.materials.video_url.includes('youtube')}
{#each $lesson.materials.video_url.split(',') as url}
<div class="mb-5">
<iframe
width="100%"
height="569"
class="iframe"
src={formatYoutubeVideo(url, errors)}
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
/>
</div>
{/each}
{#if mode === MODES.edit && videoUrls.length}
<PrimaryButton label="Add/Edit Video" onClick={openAddVideoModal} className="mb-2" />
{/if}
<!-- Line 230 would run if its in edit mode or the video is truthy but then in line 235 we are mapping for an empty string -->
{#if videoUrls.length}
<div class="flex flex-col items-start w-full h-full">
{#each videoUrls as url, index}
<div class="w-full h-full flex flex-col gap-2 overflow-hidden">
{#if url.includes('youtube')}
<div class="mb-5">
<div
bind:this={player}
class="plyr-video-trigger"
data-plyr-provider="youtube"
data-plyr-embed-id={getEmbedId(url)}
/>
</div>
{:else}
<video bind:this={player} class="plyr-video-trigger" playsinline controls>
<source
src="https://pub-bd38ff7251aa402a93762181a947f7ea.r2.dev/generate-lesson-draft.mp4"
type="video/mp4"
/>
<!-- <source src="/path/to/video.webm" type="video/webm" /> -->
<!-- Captions are optional -->
<track kind="captions" />
</video>
{/if}
</div>
{/each}
</div>
{:else}
<Box>
<div class="flex justify-between flex-col items-center w-96">
<img src="/images/empty-video-icon.svg" alt="No Video" class="my-2.5 mx-auto" />
<h2 class="text-xl my-1.5 font-normal">No videos yet</h2>
<p class="text-sm text-center text-slate-500">
Share your knowledge with the world by creating engaging videos. Add your video link
now.
</p>
</div>
<img src="/no-video.svg" alt="Video not found" />
<h3 class="text-xl font-normal dark:text-white py-2">
No youtube video added for this lesson yet
</h3>
<p class="text-sm text-center font-normal py-2">
Share your knowledge with the world by creating engaging lessons.<br />
Start by clicking on New Lesson button.
</p>
<PrimaryButton label="Add Video" className="rounded-md" onClick={openAddVideoModal} />
</Box>
{/if}
</TabContent>
@@ -1,18 +1,21 @@
import { writable } from 'svelte/store';
import type { Writable, Updater } from 'svelte/store';
import { createLesson, updateLesson, deleteLesson } from '$lib/utils/services/courses';
import type { Lesson, Course } from '$lib/utils/types';
export const uploadCourseVideoStore = writable({
isModalOpen: false
});
export const lessons: Writable<Lesson[]> = writable([]);
export const lesson = writable({
id: null,
totalExercises: 0,
is_complete: false,
materials: {
note: null,
slide_url: null,
video_url: null
note: '',
slide_url: '',
video_url: ''
},
exercises: []
});
@@ -92,7 +95,23 @@ export async function handleUpdateLessonMaterials(lesson: any, lessonId: Lesson[
...lesson.materials
};
delete materials.profile;
delete materials.totalExercises;
return await updateLesson(materials, lessonId);
}
export const deleteLessonVideo = (index: any) => {
lesson.update((currentLesson) => {
const links = currentLesson.materials.video_url.split(',').map((link) => link.trim());
links.splice(index, 1);
const updatedVideoUrl = links.join(',').trim();
return {
...currentLesson,
materials: {
...currentLesson.materials,
video_url: updatedVideoUrl
}
};
});
};
@@ -172,7 +172,7 @@
</RoleBasedSecurity>
</slot:fragment>
</PageNav> -->
<div class="h-full mt-2 overflow-y-auto">
<div class="h-full py-3 overflow-y-auto">
{#each navItems as navItem}
{#if !navItem.show || (typeof navItem.show === 'function' && navItem.show())}
<NavExpandable
@@ -270,7 +270,6 @@
max-width: 15px;
}
}
.footer {
border-top: 1px solid var(--border-color);
}