From cc68ea2e947bc0e55f6f89ad2f73403b7172463e Mon Sep 17 00:00:00 2001 From: swve Date: Thu, 10 Oct 2024 22:06:27 +0200 Subject: [PATCH] feat: add unsplash image chooser for courses thumbnails --- .../EditCourseGeneral/ThumbnailUpdate.tsx | 51 ++++-- .../EditCourseGeneral/UnsplashImagePicker.tsx | 166 ++++++++++++++++++ .../UserEditGeneral/UserEditGeneral.tsx | 2 +- apps/web/package.json | 1 + apps/web/pnpm-lock.yaml | 27 ++- 5 files changed, 222 insertions(+), 25 deletions(-) create mode 100644 apps/web/components/Dashboard/Course/EditCourseGeneral/UnsplashImagePicker.tsx 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 ? (
- -
+
Uploading
) : ( -
+
+
)}
+ {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 => ( +
+ {image.alt_description} 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