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:
committed by
Space Cloud
co-authored by
Rotimi Best
parent
589a61fe44
commit
804e5cdeb9
+1
-1
@@ -28,7 +28,6 @@
|
|||||||
"@types/paystack": "^2.0.1",
|
"@types/paystack": "^2.0.1",
|
||||||
"@types/pluralize": "0.0.30",
|
"@types/pluralize": "0.0.30",
|
||||||
"ai": "^2.1.31",
|
"ai": "^2.1.31",
|
||||||
"axios": "^1.4.0",
|
|
||||||
"body-parser": "^1.20.2",
|
"body-parser": "^1.20.2",
|
||||||
"canvas-confetti": "^1.6.0",
|
"canvas-confetti": "^1.6.0",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
@@ -43,6 +42,7 @@
|
|||||||
"pluralize": "^8.0.0",
|
"pluralize": "^8.0.0",
|
||||||
"sirv": "^2.0.3",
|
"sirv": "^2.0.3",
|
||||||
"svelte-email": "^0.0.4",
|
"svelte-email": "^0.0.4",
|
||||||
|
"unsplash-js": "^7.0.18",
|
||||||
"wait-on": "^7.0.1",
|
"wait-on": "^7.0.1",
|
||||||
"zod": "^3.21.4"
|
"zod": "^3.21.4"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -73,7 +73,7 @@
|
|||||||
type
|
type
|
||||||
? 'border-primary-400'
|
? 'border-primary-400'
|
||||||
: `border-gray-200 dark:border-gray-400 ${
|
: `border-gray-200 dark:border-gray-400 ${
|
||||||
!option.isDisabled && 'hover:scale-90'
|
!option.isDisabled && 'hover:scale-95'
|
||||||
}`} flex flex-col {option.isDisabled &&
|
}`} flex flex-col {option.isDisabled &&
|
||||||
'cursor-not-allowed opacity-60'} transition-all ease-in-out"
|
'cursor-not-allowed opacity-60'} transition-all ease-in-out"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -145,7 +145,7 @@
|
|||||||
<div class="flex flex-wrap items-start gap-2">
|
<div class="flex flex-wrap items-start gap-2">
|
||||||
{#each TEMPLATES[selectedTag] as template}
|
{#each TEMPLATES[selectedTag] as template}
|
||||||
<button
|
<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
|
selectedTemplateId
|
||||||
? 'border-primary-400'
|
? 'border-primary-400'
|
||||||
: `border-gray-200 dark:border-gray-400`} flex flex-col transition-all ease-in-out"
|
: `border-gray-200 dark:border-gray-400`} flex flex-col transition-all ease-in-out"
|
||||||
|
|||||||
@@ -211,16 +211,16 @@
|
|||||||
hideSortIcon: true
|
hideSortIcon: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Landing Page',
|
label: 'Settings',
|
||||||
to: getNavItemRoute($course.id, 'landingpage'),
|
to: getNavItemRoute($course.id, 'settings'),
|
||||||
hideSortIcon: true,
|
hideSortIcon: true,
|
||||||
show() {
|
show() {
|
||||||
return !isStudent;
|
return !isStudent;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Settings',
|
label: 'Landing Page',
|
||||||
to: getNavItemRoute($course.id, 'settings'),
|
to: getNavItemRoute($course.id, 'landingpage'),
|
||||||
hideSortIcon: true,
|
hideSortIcon: true,
|
||||||
show() {
|
show() {
|
||||||
return !isStudent;
|
return !isStudent;
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
import { lessons } from '../Lesson/store/lessons';
|
import { lessons } from '../Lesson/store/lessons';
|
||||||
import { currentOrg } from '$lib/utils/store/org';
|
import { currentOrg } from '$lib/utils/store/org';
|
||||||
import { snackbar } from '$lib/components/Snackbar/store';
|
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 uploadingImage = false;
|
||||||
let isSaving = false;
|
let isSaving = false;
|
||||||
@@ -29,37 +31,10 @@
|
|||||||
let avatar;
|
let avatar;
|
||||||
let imagebuffer;
|
let imagebuffer;
|
||||||
|
|
||||||
// FILE UPLOAD
|
// controls widget open and close
|
||||||
const uploadImage = async (image: File) => {
|
function widgetControl() {
|
||||||
if (image) {
|
$handleOpenWidget.open = true;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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) {
|
function getLessonOrder(id) {
|
||||||
const index = $lessons.findIndex((lesson) => lesson.id === id);
|
const index = $lessons.findIndex((lesson) => lesson.id === id);
|
||||||
@@ -205,17 +180,17 @@
|
|||||||
<Grid class="border-c rounded border-gray-200">
|
<Grid class="border-c rounded border-gray-200">
|
||||||
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
|
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
|
||||||
<Column sm={8} md={8} lg={8}>
|
<Column sm={8} md={8} lg={8}>
|
||||||
<SectionTitle>Banner Image</SectionTitle>
|
<SectionTitle>Cover Image</SectionTitle>
|
||||||
<p>
|
<p>
|
||||||
This optional image will show up on your welcome page. If you include one, it should be at
|
This optional image will show up on your welcome page. If you include one, it should be at
|
||||||
least 280 x 200"
|
least 280 x 200"
|
||||||
</p>
|
</p>
|
||||||
<span class="flex items-center justify-start">
|
<span class="flex items-center justify-start">
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
variant={VARIANTS.LINK}
|
variant={VARIANTS.OUTLINED}
|
||||||
label="Replace Banner image"
|
label="Replace"
|
||||||
className="-ml-6"
|
className="mr-2"
|
||||||
onClick={() => fileInput.click()}
|
onClick={widgetControl}
|
||||||
/>
|
/>
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
variant={VARIANTS.CONTAINED_DANGER}
|
variant={VARIANTS.CONTAINED_DANGER}
|
||||||
@@ -223,22 +198,18 @@
|
|||||||
onClick={deleteBannerImage}
|
onClick={deleteBannerImage}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
{#if $handleOpenWidget.open}
|
||||||
|
<UploadWidget />
|
||||||
|
{/if}
|
||||||
</Column>
|
</Column>
|
||||||
|
|
||||||
<Column sm={8} md={8} lg={6}>
|
<Column sm={8} md={8} lg={6}>
|
||||||
<div class="w-fit relative">
|
<div class="w-fit relative z-[20]">
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
accept=".jpg, .jpeg, .png"
|
|
||||||
style="display: none;"
|
|
||||||
bind:this={fileInput}
|
|
||||||
on:change={onFileSelected}
|
|
||||||
/>
|
|
||||||
<img
|
<img
|
||||||
style="min-width:280px; min-height:200px"
|
style="min-width:280px; min-height:200px"
|
||||||
alt="About us"
|
alt="About us"
|
||||||
src={$settings.image ? $settings.image : '/images/classroomio-course-img-template.jpg'}
|
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}
|
{#if uploadingImage}
|
||||||
<Loading withOverlay={true} small class="absolute top-0 w-[280px]" />
|
<Loading withOverlay={true} small class="absolute top-0 w-[280px]" />
|
||||||
|
|||||||
@@ -1,55 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { settings } from '$lib/components/Course/components/Settings/store';
|
|
||||||
import TextArea from '$lib/components/Form/TextArea.svelte';
|
import TextArea from '$lib/components/Form/TextArea.svelte';
|
||||||
import TextField from '$lib/components/Form/TextField.svelte';
|
import TextField from '$lib/components/Form/TextField.svelte';
|
||||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||||
import { supabase } from '$lib/utils/functions/supabase';
|
import { handleOpenWidget } from '../../store';
|
||||||
import { updateCourse } from '$lib/utils/services/courses';
|
|
||||||
import { landingPage } from '../../store';
|
|
||||||
|
|
||||||
export let course = {};
|
export let course = {};
|
||||||
let fileInput;
|
|
||||||
let imageBuffer;
|
|
||||||
let avatar;
|
|
||||||
|
|
||||||
const uploadImage = async (image: File) => {
|
function widgetControl() {
|
||||||
if (image) {
|
$handleOpenWidget.open = true;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
<TextField className="mt-5" labelClassName="font-bold" label="Title" bind:value={course.title} />
|
<TextField className="mt-5" labelClassName="font-bold" label="Title" bind:value={course.title} />
|
||||||
@@ -71,18 +31,7 @@
|
|||||||
type="text"
|
type="text"
|
||||||
bind:value={course.metadata.videoUrl}
|
bind:value={course.metadata.videoUrl}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div class="mt-7">
|
||||||
<PrimaryButton
|
<p class="font-bold mb-3">Replace cover image</p>
|
||||||
variant={VARIANTS.LINK}
|
<PrimaryButton label="Replace" variant={VARIANTS.OUTLINED} onClick={widgetControl} />
|
||||||
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>
|
</div>
|
||||||
|
|||||||
@@ -1,29 +1,33 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import UploadWidget from '$lib/components/UploadWidget/index.svelte';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import { observeIntersection } from './components/IntersectionObserver';
|
|
||||||
import { onMount, onDestroy } from 'svelte';
|
import { onMount, onDestroy } from 'svelte';
|
||||||
import pluralize from 'pluralize';
|
import pluralize from 'pluralize';
|
||||||
import { page } from '$app/stores';
|
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 Notebook from 'carbon-icons-svelte/lib/Notebook.svelte'; //note
|
||||||
import PresentationFile from 'carbon-icons-svelte/lib/PresentationFile.svelte'; // exercise
|
import PresentationFile from 'carbon-icons-svelte/lib/PresentationFile.svelte'; // exercise
|
||||||
import Video from 'carbon-icons-svelte/lib/Video.svelte'; //video
|
import Video from 'carbon-icons-svelte/lib/Video.svelte'; //video
|
||||||
import PageNumber from 'carbon-icons-svelte/lib/PageNumber.svelte';
|
import PageNumber from 'carbon-icons-svelte/lib/PageNumber.svelte';
|
||||||
import PlayFilled from 'carbon-icons-svelte/lib/PlayFilled.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 PricingSection from './components/PricingSection.svelte';
|
||||||
|
import { observeIntersection } from './components/IntersectionObserver';
|
||||||
import { getLectureNo } from '../Course/function';
|
import { getLectureNo } from '../Course/function';
|
||||||
import { NAV_ITEMS } from './constants';
|
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 Modal from '../Modal/index.svelte';
|
||||||
import { reviewsModalStore } from './store';
|
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 { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||||
import HtmlRender from '$lib/components/HTMLRender/HTMLRender.svelte';
|
import HtmlRender from '$lib/components/HTMLRender/HTMLRender.svelte';
|
||||||
import { calDateDiff } from '$lib/utils/functions/date';
|
import { calDateDiff } from '$lib/utils/functions/date';
|
||||||
import { landingPage } from './store';
|
import { handleOpenWidget } from './store';
|
||||||
import { Loading } from 'carbon-components-svelte';
|
|
||||||
export let editMode = false;
|
export let editMode = false;
|
||||||
export let courseData: Course;
|
export let courseData: Course;
|
||||||
const ratingsImg = [
|
const ratingsImg = [
|
||||||
@@ -124,6 +128,10 @@
|
|||||||
startCoursePayment = true;
|
startCoursePayment = true;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{#if $handleOpenWidget.open}
|
||||||
|
<UploadWidget />
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Banner Image getEmbedId(videoUrl) -->
|
<!-- Banner Image getEmbedId(videoUrl) -->
|
||||||
@@ -146,12 +154,12 @@
|
|||||||
/>
|
/>
|
||||||
</div> -->
|
</div> -->
|
||||||
{:else}
|
{: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
|
<img
|
||||||
style="min-width:280px; min-height:200px"
|
style="min-width:280px; min-height:200px"
|
||||||
alt="About us"
|
alt="About us"
|
||||||
src={bannerImage ? bannerImage : '/images/classroomio-course-img-template.jpg'}
|
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}
|
{#if $landingPage.uploadingImage}
|
||||||
<Loading withOverlay={true} small class="absolute top-0 w-full h-full" />
|
<Loading withOverlay={true} small class="absolute top-0 w-full h-full" />
|
||||||
|
|||||||
@@ -64,3 +64,7 @@ export const landingPage = writable({
|
|||||||
uploadingImage: false,
|
uploadingImage: false,
|
||||||
imageUrl: ''
|
imageUrl: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export let handleOpenWidget = writable({
|
||||||
|
open: false
|
||||||
|
});
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<a
|
<a
|
||||||
rel="prefetch"
|
rel="prefetch"
|
||||||
href={isOnLandingPage ? `/course/${slug}` : `/courses/${id}`}
|
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="p-4">
|
||||||
<div class=" mb-5">
|
<div class=" mb-5">
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
export let onKeyDown = () => {};
|
export let onKeyDown = () => {};
|
||||||
export let className = '';
|
export let className = '';
|
||||||
export let inputClassName = '';
|
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 bgColor = 'bg-gray-100 focus:bg-primary-50 dark:bg-neutral-800 dark:text-white';
|
||||||
export let type = 'text';
|
export let type = 'text';
|
||||||
export let isPassword = false;
|
export let isPassword = false;
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
|
|
||||||
<label class="block relative {className}">
|
<label class="block relative {className}">
|
||||||
{#if label}
|
{#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}
|
{label}
|
||||||
|
|
||||||
{#if isRequired}
|
{#if isRequired}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="">
|
||||||
<slot name="content" />
|
<slot name="content" />
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
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)}
|
on:click={() => goto(path)}
|
||||||
>
|
>
|
||||||
<CourseIcon color="#fff" />
|
<CourseIcon />
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
<button
|
<button
|
||||||
|
|||||||
Reference in New Issue
Block a user