diff --git a/apps/web/components/Dashboard/Course/EditCourseGeneral/ThumbnailUpdate.tsx b/apps/web/components/Dashboard/Course/EditCourseGeneral/ThumbnailUpdate.tsx
index 980fd6bb..ba264bd2 100644
--- a/apps/web/components/Dashboard/Course/EditCourseGeneral/ThumbnailUpdate.tsx
+++ b/apps/web/components/Dashboard/Course/EditCourseGeneral/ThumbnailUpdate.tsx
@@ -3,10 +3,11 @@ import { useOrg } from '@components/Contexts/OrgContext'
import { getAPIUrl } from '@services/config/config'
import { updateCourseThumbnail } from '@services/courses/courses'
import { getCourseThumbnailMediaDirectory } from '@services/media/media'
-import { ArrowBigUpDash, UploadCloud } from 'lucide-react'
+import { ArrowBigUpDash, UploadCloud, Image as ImageIcon } from 'lucide-react'
import { useLHSession } from '@components/Contexts/LHSessionContext'
-import React from 'react'
+import React, { useState } from 'react'
import { mutate } from 'swr'
+import UnsplashImagePicker from './UnsplashImagePicker'
function ThumbnailUpdate() {
const course = useCourse() as any
@@ -15,10 +16,24 @@ function ThumbnailUpdate() {
const [localThumbnail, setLocalThumbnail] = React.useState(null) as any
const [isLoading, setIsLoading] = React.useState(false) as any
const [error, setError] = React.useState('') as any
+ const [showUnsplashPicker, setShowUnsplashPicker] = useState(false)
const handleFileChange = async (event: any) => {
const file = event.target.files[0]
setLocalThumbnail(file)
+ await updateThumbnail(file)
+ }
+
+ const handleUnsplashSelect = async (imageUrl: string) => {
+ setIsLoading(true)
+ const response = await fetch(imageUrl)
+ const blob = await response.blob()
+ const file = new File([blob], 'unsplash_image.jpg', { type: 'image/jpeg' })
+ setLocalThumbnail(file)
+ await updateThumbnail(file)
+ }
+
+ const updateThumbnail = async (file: File) => {
setIsLoading(true)
const res = await updateCourseThumbnail(
course.courseStructure.course_uuid,
@@ -49,8 +64,7 @@ function ThumbnailUpdate() {
{localThumbnail ? (
) : (
{isLoading ? (
-
-
) : (
-
+
+
)}
+ {showUnsplashPicker && (
+ setShowUnsplashPicker(false)}
+ />
+ )}
)
}
-export default ThumbnailUpdate
+export default ThumbnailUpdate
\ No newline at end of file
diff --git a/apps/web/components/Dashboard/Course/EditCourseGeneral/UnsplashImagePicker.tsx b/apps/web/components/Dashboard/Course/EditCourseGeneral/UnsplashImagePicker.tsx
new file mode 100644
index 00000000..662974af
--- /dev/null
+++ b/apps/web/components/Dashboard/Course/EditCourseGeneral/UnsplashImagePicker.tsx
@@ -0,0 +1,166 @@
+import React, { useState, useEffect, useCallback } from 'react';
+import { createApi } from 'unsplash-js';
+import { Search, X, Cpu, Briefcase, GraduationCap, Heart, Palette, Plane, Utensils,
+ Dumbbell, Music, Shirt, Book, Building, Bike, Camera, Microscope, Coins, Coffee, Gamepad,
+ Flower} from 'lucide-react';
+
+const unsplash = createApi({
+ accessKey: process.env.NEXT_PUBLIC_UNSPLASH_ACCESS_KEY as string,
+});
+
+const IMAGES_PER_PAGE = 20;
+
+const predefinedLabels = [
+ { name: 'Nature', icon: Flower },
+ { name: 'Technology', icon: Cpu },
+ { name: 'Business', icon: Briefcase },
+ { name: 'Education', icon: GraduationCap },
+ { name: 'Health', icon: Heart },
+ { name: 'Art', icon: Palette },
+ { name: 'Science', icon: Microscope },
+ { name: 'Travel', icon: Plane },
+ { name: 'Food', icon: Utensils },
+ { name: 'Sports', icon: Dumbbell },
+ { name: 'Music', icon: Music },
+ { name: 'Fashion', icon: Shirt },
+ { name: 'History', icon: Book },
+ { name: 'Architecture', icon: Building },
+ { name: 'Fitness', icon: Bike },
+ { name: 'Photography', icon: Camera },
+ { name: 'Biology', icon: Microscope },
+ { name: 'Finance', icon: Coins },
+ { name: 'Lifestyle', icon: Coffee },
+ { name: 'Gaming', icon: Gamepad },
+];
+
+interface UnsplashImagePickerProps {
+ onSelect: (imageUrl: string) => void;
+ onClose: () => void;
+}
+
+const UnsplashImagePicker: React.FC = ({ onSelect, onClose }) => {
+ const [query, setQuery] = useState('');
+ const [images, setImages] = useState([]);
+ const [page, setPage] = useState(1);
+ const [loading, setLoading] = useState(false);
+
+ const fetchImages = useCallback(async (searchQuery: string, pageNum: number) => {
+ setLoading(true);
+ try {
+ const result = await unsplash.search.getPhotos({
+ query: searchQuery,
+ page: pageNum,
+ perPage: IMAGES_PER_PAGE,
+ });
+ if (result && result.response) {
+ setImages(prevImages => pageNum === 1 ? result.response.results : [...prevImages, ...result.response.results]);
+ } else {
+ console.error('Unexpected response structure:', result);
+ }
+ } catch (error) {
+ console.error('Error fetching images:', error);
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ const debouncedFetchImages = useCallback(
+ debounce((searchQuery: string) => {
+ setPage(1);
+ fetchImages(searchQuery, 1);
+ }, 300),
+ [fetchImages]
+ );
+
+ useEffect(() => {
+ if (query) {
+ debouncedFetchImages(query);
+ }
+ }, [query, debouncedFetchImages]);
+
+ const handleSearch = (e: React.ChangeEvent) => {
+ setQuery(e.target.value);
+ };
+
+ const handleLabelClick = (label: string) => {
+ setQuery(label);
+ };
+
+ const handleLoadMore = () => {
+ const nextPage = page + 1;
+ setPage(nextPage);
+ fetchImages(query, nextPage);
+ };
+
+ const handleImageSelect = (imageUrl: string) => {
+ onSelect(imageUrl);
+ onClose();
+ };
+
+ return (
+
+
+
+
Choose an image from Unsplash
+
+
+
+
+
+
+
+ {predefinedLabels.map(label => (
+
+ ))}
+
+
+ {images.map(image => (
+
+

handleImageSelect(image.urls.full)}
+ />
+
+ ))}
+
+ {loading &&
Loading...
}
+ {!loading && images.length > 0 && (
+
+ )}
+
+
+ );
+};
+
+// Custom debounce function
+const debounce = (func: Function, delay: number) => {
+ let timeoutId: NodeJS.Timeout;
+ return (...args: any[]) => {
+ clearTimeout(timeoutId);
+ timeoutId = setTimeout(() => func(...args), delay);
+ };
+};
+
+export default UnsplashImagePicker;
\ No newline at end of file
diff --git a/apps/web/components/Dashboard/UserAccount/UserEditGeneral/UserEditGeneral.tsx b/apps/web/components/Dashboard/UserAccount/UserEditGeneral/UserEditGeneral.tsx
index ce8ef647..691783d4 100644
--- a/apps/web/components/Dashboard/UserAccount/UserEditGeneral/UserEditGeneral.tsx
+++ b/apps/web/components/Dashboard/UserAccount/UserEditGeneral/UserEditGeneral.tsx
@@ -175,7 +175,7 @@ function UserEditGeneral() {
}
>
- Change Thumbnail
+ Change Avatar
)}
diff --git a/apps/web/package.json b/apps/web/package.json
index 7a1a0b8c..67aede28 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -67,6 +67,7 @@
"tailwind-merge": "^2.5.3",
"tailwind-scrollbar": "^3.1.0",
"tailwindcss-animate": "^1.0.7",
+ "unsplash-js": "^7.0.19",
"uuid": "^9.0.1",
"y-indexeddb": "^9.0.12",
"y-prosemirror": "^1.2.12",
diff --git a/apps/web/pnpm-lock.yaml b/apps/web/pnpm-lock.yaml
index 7c8be69d..81acb2c2 100644
--- a/apps/web/pnpm-lock.yaml
+++ b/apps/web/pnpm-lock.yaml
@@ -176,6 +176,9 @@ importers:
tailwindcss-animate:
specifier: ^1.0.7
version: 1.0.7(tailwindcss@3.4.13)
+ unsplash-js:
+ specifier: ^7.0.19
+ version: 7.0.19
uuid:
specifier: ^9.0.1
version: 9.0.1
@@ -3818,6 +3821,10 @@ packages:
unplugin@1.0.1:
resolution: {integrity: sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA==}
+ unsplash-js@7.0.19:
+ resolution: {integrity: sha512-j6qT2floy5Q2g2d939FJpwey1yw/GpQecFiSouyJtsHQPj3oqmqq3K4rI+GF8vU1zwGCT7ZwIGQd2dtCQLjYJw==}
+ engines: {node: '>=10'}
+
update-browserslist-db@1.1.1:
resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==}
hasBin: true
@@ -6271,8 +6278,8 @@ snapshots:
'@typescript-eslint/parser': 8.8.1(eslint@8.57.1)(typescript@5.4.4)
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
- eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.4.4))(eslint@8.57.1))(eslint@8.57.1)
- eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.4.4))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
+ eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1)
+ eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1)
eslint-plugin-jsx-a11y: 6.10.0(eslint@8.57.1)
eslint-plugin-react: 7.37.1(eslint@8.57.1)
eslint-plugin-react-hooks: 4.6.2(eslint@8.57.1)
@@ -6291,37 +6298,37 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.4.4))(eslint@8.57.1))(eslint@8.57.1):
+ eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1):
dependencies:
'@nolyfill/is-core-module': 1.0.39
debug: 4.3.7
enhanced-resolve: 5.17.1
eslint: 8.57.1
- eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.4.4))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
+ eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1)
fast-glob: 3.3.2
get-tsconfig: 4.8.1
is-bun-module: 1.2.1
is-glob: 4.0.3
optionalDependencies:
- eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.4.4))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
+ eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1)
transitivePeerDependencies:
- '@typescript-eslint/parser'
- eslint-import-resolver-node
- eslint-import-resolver-webpack
- supports-color
- eslint-module-utils@2.12.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.4.4))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
+ eslint-module-utils@2.12.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 8.8.1(eslint@8.57.1)(typescript@5.4.4)
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
- eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.4.4))(eslint@8.57.1))(eslint@8.57.1)
+ eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1)
transitivePeerDependencies:
- supports-color
- eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.4.4))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
+ eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.8
@@ -6332,7 +6339,7 @@ snapshots:
doctrine: 2.1.0
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
- eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.4.4))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
+ eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1)
hasown: 2.0.2
is-core-module: 2.15.1
is-glob: 4.0.3
@@ -8034,6 +8041,8 @@ snapshots:
webpack-sources: 3.2.3
webpack-virtual-modules: 0.5.0
+ unsplash-js@7.0.19: {}
+
update-browserslist-db@1.1.1(browserslist@4.24.0):
dependencies:
browserslist: 4.24.0