CLASSROOMIO-T-177: Upload Widget

improve banner image

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

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

CLASSROOMIO-T-177: Moved API call to backend

general ux improvements

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

CLASSROOMIO-T-177: Resolved Feedbacks

CLASSROOMIO-T-177: UI improvements

CLASSROOMIO-T-177: fix styling or image

CLASSROOMIO-T-177: Resolved Feedbacks

CLASSROOMIO-T-177: Landing Page Max Width

CLASSROOMIO-T-177: Refactor Upload Widget

Merge branch 'main' into CLASSROOMIO-T-177

CLASSROOMIO-T-177: Completion of Upload Widget

Merge branch 'main' into CLASSROOMIO-T-177

CLASSROOMIO-T-177: Widget Test


Co-authored-by: Rotimi Best <rotimiibitoyeemma@gmail.com>
Co-authored-by: Tunji Adeyemi <tunji@digdippa.com>


Merge-request: CLASSROOMIO-MR-86
Merged-by: Best Admin <best@digdippa.com>
This commit is contained in:
tunny17
2023-10-05 09:22:39 +00:00
committed by Space Cloud
co-authored by Rotimi Best
parent 589a61fe44
commit 804e5cdeb9
14 changed files with 287 additions and 125 deletions
+1 -1
View File
@@ -28,7 +28,6 @@
"@types/paystack": "^2.0.1",
"@types/pluralize": "0.0.30",
"ai": "^2.1.31",
"axios": "^1.4.0",
"body-parser": "^1.20.2",
"canvas-confetti": "^1.6.0",
"cookie-parser": "^1.4.6",
@@ -43,6 +42,7 @@
"pluralize": "^8.0.0",
"sirv": "^2.0.3",
"svelte-email": "^0.0.4",
"unsplash-js": "^7.0.18",
"wait-on": "^7.0.1",
"zod": "^3.21.4"
},
@@ -73,7 +73,7 @@
type
? 'border-primary-400'
: `border-gray-200 dark:border-gray-400 ${
!option.isDisabled && 'hover:scale-90'
!option.isDisabled && 'hover:scale-95'
}`} flex flex-col {option.isDisabled &&
'cursor-not-allowed opacity-60'} transition-all ease-in-out"
type="button"
@@ -145,7 +145,7 @@
<div class="flex flex-wrap items-start gap-2">
{#each TEMPLATES[selectedTag] as template}
<button
class="w-[161px] h-[140px] p-5 rounded-md dark:bg-neutral-700 border {template.id ===
class="w-[161px] h-[140px] hover:scale-95 p-5 rounded-md dark:bg-neutral-700 border-2 {template.id ===
selectedTemplateId
? 'border-primary-400'
: `border-gray-200 dark:border-gray-400`} flex flex-col transition-all ease-in-out"
@@ -211,16 +211,16 @@
hideSortIcon: true
},
{
label: 'Landing Page',
to: getNavItemRoute($course.id, 'landingpage'),
label: 'Settings',
to: getNavItemRoute($course.id, 'settings'),
hideSortIcon: true,
show() {
return !isStudent;
}
},
{
label: 'Settings',
to: getNavItemRoute($course.id, 'settings'),
label: 'Landing Page',
to: getNavItemRoute($course.id, 'landingpage'),
hideSortIcon: true,
show() {
return !isStudent;
@@ -19,6 +19,8 @@
import { lessons } from '../Lesson/store/lessons';
import { currentOrg } from '$lib/utils/store/org';
import { snackbar } from '$lib/components/Snackbar/store';
import UploadWidget from '$lib/components/UploadWidget/index.svelte';
import { handleOpenWidget } from '$lib/components/CourseLandingPage/store';
let uploadingImage = false;
let isSaving = false;
@@ -29,37 +31,10 @@
let avatar;
let imagebuffer;
// FILE UPLOAD
const uploadImage = async (image: File) => {
if (image) {
const filename = `bannerimage/${Date.now()}` + image.name;
const { data } = await supabase.storage.from('avatars').upload(filename, image, {
cacheControl: '3600',
upsert: false
});
if (data) {
const { data: response } = await supabase.storage.from('avatars').getPublicUrl(filename);
$settings.image = response.publicUrl;
uploadingImage = false;
// controls widget open and close
function widgetControl() {
$handleOpenWidget.open = true;
}
}
};
const onFileSelected = () => {
uploadingImage = true;
const image = fileInput.files[0];
if (image) {
let reader = new FileReader();
reader.readAsDataURL(image);
reader.onload = (e) => {
imagebuffer = image;
uploadImage(image);
};
}
};
function getLessonOrder(id) {
const index = $lessons.findIndex((lesson) => lesson.id === id);
@@ -205,17 +180,17 @@
<Grid class="border-c rounded border-gray-200">
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
<Column sm={8} md={8} lg={8}>
<SectionTitle>Banner Image</SectionTitle>
<SectionTitle>Cover Image</SectionTitle>
<p>
This optional image will show up on your welcome page. If you include one, it should be at
least 280 x 200"
</p>
<span class="flex items-center justify-start">
<PrimaryButton
variant={VARIANTS.LINK}
label="Replace Banner image"
className="-ml-6"
onClick={() => fileInput.click()}
variant={VARIANTS.OUTLINED}
label="Replace"
className="mr-2"
onClick={widgetControl}
/>
<PrimaryButton
variant={VARIANTS.CONTAINED_DANGER}
@@ -223,22 +198,18 @@
onClick={deleteBannerImage}
/>
</span>
{#if $handleOpenWidget.open}
<UploadWidget />
{/if}
</Column>
<Column sm={8} md={8} lg={6}>
<div class="w-fit relative">
<input
type="file"
accept=".jpg, .jpeg, .png"
style="display: none;"
bind:this={fileInput}
on:change={onFileSelected}
/>
<div class="w-fit relative z-[20]">
<img
style="min-width:280px; min-height:200px"
alt="About us"
src={$settings.image ? $settings.image : '/images/classroomio-course-img-template.jpg'}
class="mt-2 md:mt-0 rounded-md w-full relative"
class="mt-2 md:mt-0 w-[280px] h-[200px] rounded-md relative"
/>
{#if uploadingImage}
<Loading withOverlay={true} small class="absolute top-0 w-[280px]" />
@@ -1,55 +1,15 @@
<script lang="ts">
import { settings } from '$lib/components/Course/components/Settings/store';
import TextArea from '$lib/components/Form/TextArea.svelte';
import TextField from '$lib/components/Form/TextField.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { supabase } from '$lib/utils/functions/supabase';
import { updateCourse } from '$lib/utils/services/courses';
import { landingPage } from '../../store';
import { handleOpenWidget } from '../../store';
export let course = {};
let fileInput;
let imageBuffer;
let avatar;
const uploadImage = async (image: File) => {
if (image) {
const filename = `landingpage/${Date.now()}` + image.name;
const { data } = await supabase.storage.from('avatars').upload(filename, image, {
cacheControl: '3600',
upsert: false
});
if (data) {
const { data: response } = await supabase.storage.from('avatars').getPublicUrl(filename);
$landingPage.imageUrl = response.publicUrl;
$settings.image = $landingPage.imageUrl;
await updateCourse(course.id, avatar, {
logo: $settings.image
});
course.logo = $settings.image;
$landingPage.uploadingImage = false;
function widgetControl() {
$handleOpenWidget.open = true;
}
}
};
const onFileSelected = () => {
$landingPage.uploadingImage = true;
const image = fileInput.files[0];
if (image) {
let reader = new FileReader();
reader.readAsDataURL(image);
reader.onload = (e) => {
imageBuffer = image;
uploadImage(image);
};
}
};
</script>
<TextField className="mt-5" labelClassName="font-bold" label="Title" bind:value={course.title} />
@@ -71,18 +31,7 @@
type="text"
bind:value={course.metadata.videoUrl}
/>
<div>
<PrimaryButton
variant={VARIANTS.LINK}
label="Replace Banner image"
className="-ml-6"
onClick={() => fileInput.click()}
/>
<input
type="file"
accept=".jpg, .jpeg, .png"
style="display: none;"
bind:this={fileInput}
on:change={onFileSelected}
/>
<div class="mt-7">
<p class="font-bold mb-3">Replace cover image</p>
<PrimaryButton label="Replace" variant={VARIANTS.OUTLINED} onClick={widgetControl} />
</div>
@@ -1,29 +1,33 @@
<script lang="ts">
import UploadWidget from '$lib/components/UploadWidget/index.svelte';
import get from 'lodash/get';
import { observeIntersection } from './components/IntersectionObserver';
import { onMount, onDestroy } from 'svelte';
import pluralize from 'pluralize';
import { page } from '$app/stores';
import { landingPage } from './store';
import { Loading } from 'carbon-components-svelte';
import Notebook from 'carbon-icons-svelte/lib/Notebook.svelte'; //note
import PresentationFile from 'carbon-icons-svelte/lib/PresentationFile.svelte'; // exercise
import Video from 'carbon-icons-svelte/lib/Video.svelte'; //video
import PageNumber from 'carbon-icons-svelte/lib/PageNumber.svelte';
import PlayFilled from 'carbon-icons-svelte/lib/PlayFilled.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import PricingSection from './components/PricingSection.svelte';
import { observeIntersection } from './components/IntersectionObserver';
import { getLectureNo } from '../Course/function';
import { NAV_ITEMS } from './constants';
import Avatar from '$lib/components/Avatar/index.svelte';
import Chip from '../Chip/index.svelte';
import { getEmbedId } from '$lib/utils/functions/formatYoutubeVideo';
import type { Course, Lesson, Review } from '$lib/utils/types';
import Modal from '../Modal/index.svelte';
import { reviewsModalStore } from './store';
import Chip from '../Chip/index.svelte';
import Avatar from '$lib/components/Avatar/index.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { getEmbedId } from '$lib/utils/functions/formatYoutubeVideo';
import type { Course, Lesson, Review } from '$lib/utils/types';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import HtmlRender from '$lib/components/HTMLRender/HTMLRender.svelte';
import { calDateDiff } from '$lib/utils/functions/date';
import { landingPage } from './store';
import { Loading } from 'carbon-components-svelte';
import { handleOpenWidget } from './store';
export let editMode = false;
export let courseData: Course;
const ratingsImg = [
@@ -124,6 +128,10 @@
startCoursePayment = true;
}}
/>
{#if $handleOpenWidget.open}
<UploadWidget />
{/if}
</div>
<!-- Banner Image getEmbedId(videoUrl) -->
@@ -146,12 +154,12 @@
/>
</div> -->
{:else}
<div class="w-2/3 relative rounded-md overflow-hidden">
<div class="banner-image md:w-2/3 relative rounded-md overflow-hidden">
<img
style="min-width:280px; min-height:200px"
alt="About us"
src={bannerImage ? bannerImage : '/images/classroomio-course-img-template.jpg'}
class="mt-2 md:mt-0 rounded-md w-full relative"
class="mt-2 md:mt-0 rounded-md w-full h-full max-w-[500px] max-h-[400px] relative"
/>
{#if $landingPage.uploadingImage}
<Loading withOverlay={true} small class="absolute top-0 w-full h-full" />
@@ -64,3 +64,7 @@ export const landingPage = writable({
uploadingImage: false,
imageUrl: ''
});
export let handleOpenWidget = writable({
open: false
});
@@ -23,7 +23,7 @@
<a
rel="prefetch"
href={isOnLandingPage ? `/course/${slug}` : `/courses/${id}`}
class="text-black border border-gray rounded w-full max-w-[320px] relative hover:shadow-lg transition-all ease-in-out"
class="text-black border border-gray rounded w-full max-w-[320px] relative hover:scale-95 transition-all ease-in-out"
>
<div class="p-4">
<div class=" mb-5">
+2 -2
View File
@@ -10,7 +10,7 @@
export let onKeyDown = () => {};
export let className = '';
export let inputClassName = '';
export let labelClassName = '';
export let labelClassName = 'font-light';
export let bgColor = 'bg-gray-100 focus:bg-primary-50 dark:bg-neutral-800 dark:text-white';
export let type = 'text';
export let isPassword = false;
@@ -57,7 +57,7 @@
<label class="block relative {className}">
{#if label}
<p for="text-field" class="text-sm dark:text-white text-left m-0 font-light {labelClassName}">
<p for="text-field" class="text-sm dark:text-white text-left m-0 {labelClassName}">
{label}
{#if isRequired}
+1 -1
View File
@@ -58,7 +58,7 @@
{/each}
</div>
<div class="mt-4">
<div class="">
<slot name="content" />
</div>
</div>
@@ -0,0 +1,173 @@
<script lang="ts">
import { onMount } from 'svelte';
import Tabs from '$lib/components/Tabs/index.svelte';
import { course } from '$lib/components/Course/store';
import { getSupabase } from '$lib/utils/functions/supabase';
import TabContent from '$lib/components/TabContent/index.svelte';
import { updateCourse } from '$lib/utils/services/courses';
import { snackbar } from '../Snackbar/store';
import { isObject } from '$lib/utils/functions/isObject';
import Modal from '$lib/components/Modal/index.svelte';
import { handleOpenWidget } from '$lib/components/CourseLandingPage/store';
import { settings } from '../Course/components/Settings/store';
import { queryUnsplash } from './utils';
export let selectedImgUrl = '';
let tabs = [
{ label: 'Unsplash', value: 'unsplash' },
{ label: 'Upload', value: 'upload' }
];
let currentTab = tabs[0].value;
let searchQuery = '';
let unsplashImages = [];
let avatar;
let fileInput;
let imagebuffer;
const supabase = getSupabase();
const onChange = (tabValue) => {
currentTab = tabValue;
};
async function handleImageClick(imageUrl) {
selectedImgUrl = imageUrl;
uploadImage(selectedImgUrl);
}
const onFileSelected = () => {
const image = fileInput.files[0];
if (image) {
let reader = new FileReader();
reader.readAsDataURL(image);
reader.onload = (e) => {
imagebuffer = image;
uploadImage(image);
};
}
};
const uploadImage = async (image) => {
if (image) {
if (typeof image === 'string') {
$settings.image = image;
} else if (image instanceof File) {
const filename = `bannerimage/${Date.now()}` + image.name;
const { data } = await supabase.storage.from('avatars').upload(filename, image, {
cacheControl: '3600',
upsert: false
});
if (data) {
const { data: response } = await supabase.storage.from('avatars').getPublicUrl(filename);
$settings.image = response.publicUrl;
}
}
try {
const { image } = $settings;
await updateCourse($course.id, avatar, {
logo: image,
metadata: {
...(isObject($course.metadata) ? $course.metadata : {})
}
});
$course.logo = image;
$course.metadata = {
...(isObject($course.metadata) ? $course.metadata : {})
};
snackbar.success('Saved successfully');
$handleOpenWidget.open = false;
} catch (error) {
snackbar.error();
console.log(error);
}
}
};
function handleUpload() {
fileInput.click();
}
async function handleSubmit() {
try {
unsplashImages = await queryUnsplash(searchQuery || 'rocks');
} catch (error) {
console.error('Error fetching images from Unsplash:', error);
}
}
onMount(handleSubmit);
</script>
<Modal
onClose={() => ($handleOpenWidget.open = false)}
bind:open={$handleOpenWidget.open}
width="w-3/5"
maxWidth=""
modalHeading="Banner Widget"
>
<div class="w-full bg-white dark:bg-inherit p-5">
<Tabs {tabs} {currentTab} {onChange}>
<slot:fragment slot="content">
<TabContent value={tabs[1].value} index={currentTab}>
<!-- Your Upload content here -->
<div class="w-full mt-2">
<input
type="file"
style="display: none;"
bind:this={fileInput}
on:change={onFileSelected}
/>
<button
on:click={handleUpload}
class="py-2 w-full font-semibold hover:bg-gray-200 border-[1px]">Upload Image</button
>
<p class="text-center text-sm text-gray-500 my-2">
Images wider than 1500 pixels work best.
</p>
<p class="text-center text-sm text-gray-500">The maximum size per file is 5 MB.</p>
</div>
</TabContent>
<TabContent value={tabs[0].value} index={currentTab}>
<!-- Your Images content here -->
<div class="h-full py-5 overflow-y-scroll">
<form on:submit|preventDefault={handleSubmit} class="flex gap-2">
<input
type="text"
bind:value={searchQuery}
name=""
id=""
class="rounded-lg w-[87%] ml-2 dark:text-black"
/>
<button
type="submit"
class="px-3 py-1 bg-white rounded-lg border-[1px] border-gray-500 text-black"
>Submit</button
>
</form>
{#if unsplashImages && unsplashImages.length > 0}
<div class="flex flex-row items-center flex-wrap gap-2 py-4 px-[10px] max-h-[300px]">
{#each unsplashImages as unsplashImages (unsplashImages.id)}
<div class="w-[190px] h-[130px] overflow-hidden relative">
<img
src={unsplashImages.urls.regular}
alt={unsplashImages.alt_description}
class="w-full h-full object-cover rounded-md hover:opacity-80 cursor-pointer"
on:click={() => handleImageClick(unsplashImages.urls.regular)}
/>
</div>
{/each}
</div>
{:else}
<p class="text-center pt-7">No images available.</p>
{/if}
</div>
</TabContent>
</slot:fragment>
</Tabs>
</div>
</Modal>
+24
View File
@@ -0,0 +1,24 @@
import { supabase } from '$lib/utils/functions/supabase';
export async function queryUnsplash(searchQuery: string) {
const { data } = await supabase.auth.getSession();
const accessToken = data.session?.access_token || '';
try {
const response = await fetch('/api/unsplash', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: accessToken
},
body: JSON.stringify({ searchQuery })
});
if (!response.ok) {
throw new Error(await response.text());
}
const result = await response.json();
return result.photos;
} catch (error) {
console.error('Error sending fetch request', error);
}
}
+33
View File
@@ -0,0 +1,33 @@
import { json } from '@sveltejs/kit';
import { UNSPLASH_API_KEY } from '$env/static/private';
const UNSPLASH_API_URL = `https://api.unsplash.com/search/photos?page=2&per_page=15&auto=format&fit=crop&w=2970&q=80&client_id=${UNSPLASH_API_KEY}`;
// API to fetch photos from Unsplash
export async function POST({ request }) {
const body = await request.json();
const { searchQuery } = body;
const query = searchQuery || 'rocks';
try {
const response = await fetch(`${UNSPLASH_API_URL}&query=${encodeURIComponent(query)}`, {
method: 'GET',
});
if (!response.ok) {
throw new Error('Unable to fetch photos from Unsplash API');
}
const data = await response.json();
return json({
success: true,
photos: data.results,
});
} catch (error) {
console.error(error);
return json({ success: false, message: 'Error fetching photos from Unsplash' }, { status: 500 });
}
}
@@ -306,7 +306,7 @@
class="px-2 my-2 pr-4 border-t-0 border-b-0 border-l-0 border border-gray-300 flex items-center"
on:click={() => goto(path)}
>
<CourseIcon color="#fff" />
<CourseIcon />
</button>
{/if}
<button