diff --git a/apps/app/components/core/index.ts b/apps/app/components/core/index.ts index 01a190d07..e7fffedde 100644 --- a/apps/app/components/core/index.ts +++ b/apps/app/components/core/index.ts @@ -9,3 +9,4 @@ export * from "./issues-view"; export * from "./link-modal"; export * from "./not-authorized-view"; export * from "./multi-level-select"; +export * from "./unsplash-modal"; diff --git a/apps/app/components/core/unsplash-modal.tsx b/apps/app/components/core/unsplash-modal.tsx new file mode 100644 index 000000000..1a2270ea8 --- /dev/null +++ b/apps/app/components/core/unsplash-modal.tsx @@ -0,0 +1,108 @@ +import React, { useState } from "react"; + +import Image from "next/image"; + +import useSWR from "swr"; + +import { Dialog, Transition } from "@headlessui/react"; + +// services +import fileService from "services/file.service"; +// ui +import { Button, Input, Spinner } from "components/ui"; + +type Props = { + isOpen: boolean; + handleClose: () => void; + onSelect: (url: string) => void; +}; + +export const UnsplashImageModal: React.FC = (props) => { + const { isOpen, handleClose, onSelect } = props; + + const [searchQuery, setSearchQuery] = useState(""); + const [formData, setFormData] = useState({ + search: "", + }); + + const { data: images } = useSWR(`UNSPLASH_IMAGES_${searchQuery.toUpperCase()}`, () => + fileService.getUnsplashImages(1, searchQuery) + ); + + const onClose = () => { + handleClose(); + }; + + return ( + + + +
+ + +
+
+ + + + Select an image + + +
{ + e.preventDefault(); + setSearchQuery(formData.search); + }} + className="flex items-center" + > + setFormData({ ...formData, search: e.target.value })} + placeholder="Search for images" + /> + +
+ + {images ? ( +
+ {images.map((image) => ( + + ))} +
+ ) : ( +
+ +
+ )} +
+
+
+
+
+
+ ); +}; diff --git a/apps/app/components/project/create-project-modal.tsx b/apps/app/components/project/create-project-modal.tsx index 08a085e49..7a1fa7a92 100644 --- a/apps/app/components/project/create-project-modal.tsx +++ b/apps/app/components/project/create-project-modal.tsx @@ -14,7 +14,10 @@ import workspaceService from "services/workspace.service"; import useToast from "hooks/use-toast"; // ui import { Button, Input, TextArea, CustomSelect } from "components/ui"; +// icons +import { PhotoIcon } from "@heroicons/react/24/outline"; // components +import { UnsplashImageModal } from "components/core"; import EmojiIconPicker from "components/emoji-icon-picker"; // helpers import { getRandomEmoji } from "helpers/common.helper"; @@ -36,6 +39,7 @@ const defaultValues: Partial = { description: "", network: 2, icon: getRandomEmoji(), + cover_image: null, }; const IsGuestCondition: React.FC<{ @@ -58,6 +62,7 @@ const IsGuestCondition: React.FC<{ export const CreateProjectModal: React.FC = (props) => { const { isOpen, setIsOpen } = props; + const [isUnsplashModalOpen, setIsUnsplashModalOpen] = useState(false); const [isChangeIdentifierRequired, setIsChangeIdentifierRequired] = useState(true); const { setToastAlert } = useToast(); @@ -173,6 +178,15 @@ export const CreateProjectModal: React.FC = (props) => { leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" > +
+ +
@@ -299,6 +313,14 @@ export const CreateProjectModal: React.FC = (props) => {
+ setIsUnsplashModalOpen(false)} + isOpen={isUnsplashModalOpen} + onSelect={(imageUrl) => { + setValue("cover_image", imageUrl); + setIsUnsplashModalOpen(false); + }} + /> ); diff --git a/apps/app/next.config.js b/apps/app/next.config.js index ed883e597..2b3583fa4 100644 --- a/apps/app/next.config.js +++ b/apps/app/next.config.js @@ -9,6 +9,7 @@ const nextConfig = { "vinci-web.s3.amazonaws.com", "planefs-staging.s3.ap-south-1.amazonaws.com", "planefs.s3.amazonaws.com", + "images.unsplash.com", ], }, output: "standalone", diff --git a/apps/app/services/file.service.ts b/apps/app/services/file.service.ts index efb9a8cb2..5f96a663d 100644 --- a/apps/app/services/file.service.ts +++ b/apps/app/services/file.service.ts @@ -3,6 +3,30 @@ import APIService from "services/api.service"; const { NEXT_PUBLIC_API_BASE_URL } = process.env; +interface UnSplashImage { + id: string; + created_at: Date; + updated_at: Date; + promoted_at: Date; + width: number; + height: number; + color: string; + blur_hash: string; + description: null; + alt_description: string; + urls: UnSplashImageUrls; + [key: string]: any; +} + +interface UnSplashImageUrls { + raw: string; + full: string; + regular: string; + small: string; + thumb: string; + small_s3: string; +} + class FileServices extends APIService { constructor() { super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); @@ -15,6 +39,24 @@ class FileServices extends APIService { throw error?.response?.data; }); } + + async getUnsplashImages(page: number = 1, query?: string): Promise { + const clientId = process.env.NEXT_PUBLIC_UNSPLASH_ACCESS; + const url = query + ? `https://api.unsplash.com/search/photos/?client_id=${clientId}&query=${query}&page=${page}&per_page=20` + : `https://api.unsplash.com/photos/?client_id=${clientId}&page=${page}&per_page=20`; + + return this.request({ + method: "get", + url, + }) + .then((response) => { + return response?.data?.results ?? response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } } export default new FileServices(); diff --git a/apps/app/types/projects.d.ts b/apps/app/types/projects.d.ts index 5abb32d2a..7364682a8 100644 --- a/apps/app/types/projects.d.ts +++ b/apps/app/types/projects.d.ts @@ -1,6 +1,7 @@ import type { IUserLite, IWorkspace } from "./"; export interface IProject { + cover_image: string | null; created_at: Date; created_by: string; cycle_view: boolean;