mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
feat: default project cover images tab on the change cover popover (#2375)
* feat: default project cover images tab * chore: remove unnecessary env vars from turbo.json
This commit is contained in:
parent
64af5e2e75
commit
77c1b90e6b
@ -74,24 +74,6 @@ class FileServices extends APIService {
|
|||||||
throw error?.response?.data;
|
throw error?.response?.data;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUnsplashImages(page: number = 1, query?: string): Promise<UnSplashImage[]> {
|
|
||||||
const url = "/api/unsplash";
|
|
||||||
|
|
||||||
return this.request({
|
|
||||||
method: "get",
|
|
||||||
url,
|
|
||||||
params: {
|
|
||||||
page,
|
|
||||||
per_page: 20,
|
|
||||||
query,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((response) => response?.data?.results ?? response?.data)
|
|
||||||
.catch((error) => {
|
|
||||||
throw error?.response?.data;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileServices = new FileServices();
|
const fileServices = new FileServices();
|
||||||
|
@ -12,8 +12,6 @@
|
|||||||
"NEXT_PUBLIC_GITHUB_APP_NAME",
|
"NEXT_PUBLIC_GITHUB_APP_NAME",
|
||||||
"NEXT_PUBLIC_ENABLE_SENTRY",
|
"NEXT_PUBLIC_ENABLE_SENTRY",
|
||||||
"NEXT_PUBLIC_ENABLE_OAUTH",
|
"NEXT_PUBLIC_ENABLE_OAUTH",
|
||||||
"NEXT_PUBLIC_UNSPLASH_ACCESS",
|
|
||||||
"NEXT_PUBLIC_UNSPLASH_ENABLED",
|
|
||||||
"NEXT_PUBLIC_TRACK_EVENTS",
|
"NEXT_PUBLIC_TRACK_EVENTS",
|
||||||
"NEXT_PUBLIC_PLAUSIBLE_DOMAIN",
|
"NEXT_PUBLIC_PLAUSIBLE_DOMAIN",
|
||||||
"NEXT_PUBLIC_CRISP_ID",
|
"NEXT_PUBLIC_CRISP_ID",
|
||||||
|
@ -1,32 +1,23 @@
|
|||||||
import React, { useEffect, useState, useRef, useCallback } from "react";
|
import React, { useEffect, useState, useRef, useCallback } from "react";
|
||||||
|
|
||||||
// next
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
// swr
|
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
// react-dropdown
|
|
||||||
import { useDropzone } from "react-dropzone";
|
import { useDropzone } from "react-dropzone";
|
||||||
|
|
||||||
// headless ui
|
|
||||||
import { Tab, Transition, Popover } from "@headlessui/react";
|
import { Tab, Transition, Popover } from "@headlessui/react";
|
||||||
|
|
||||||
// services
|
// services
|
||||||
import fileService from "services/file.service";
|
import fileService from "services/file.service";
|
||||||
|
|
||||||
// components
|
|
||||||
import { Input, Spinner, PrimaryButton, SecondaryButton } from "components/ui";
|
|
||||||
// hooks
|
// hooks
|
||||||
import useWorkspaceDetails from "hooks/use-workspace-details";
|
import useWorkspaceDetails from "hooks/use-workspace-details";
|
||||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
|
// components
|
||||||
const unsplashEnabled =
|
import { Input, PrimaryButton, SecondaryButton, Loader } from "components/ui";
|
||||||
process.env.NEXT_PUBLIC_UNSPLASH_ENABLED === "true" ||
|
|
||||||
process.env.NEXT_PUBLIC_UNSPLASH_ENABLED === "1";
|
|
||||||
|
|
||||||
const tabOptions = [
|
const tabOptions = [
|
||||||
|
{
|
||||||
|
key: "unsplash",
|
||||||
|
title: "Unsplash",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "images",
|
key: "images",
|
||||||
title: "Images",
|
title: "Images",
|
||||||
@ -64,8 +55,22 @@ export const ImagePickerPopover: React.FC<Props> = ({
|
|||||||
search: "",
|
search: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: images } = useSWR(`UNSPLASH_IMAGES_${searchParams}`, () =>
|
const { data: unsplashImages, error: unsplashError } = useSWR(
|
||||||
fileService.getUnsplashImages(1, searchParams)
|
`UNSPLASH_IMAGES_${searchParams}`,
|
||||||
|
() => fileService.getUnsplashImages(searchParams),
|
||||||
|
{
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
revalidateOnReconnect: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: projectCoverImages } = useSWR(
|
||||||
|
`PROJECT_COVER_IMAGES`,
|
||||||
|
() => fileService.getProjectCoverImages(),
|
||||||
|
{
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
revalidateOnReconnect: false,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const imagePickerRef = useRef<HTMLDivElement>(null);
|
const imagePickerRef = useRef<HTMLDivElement>(null);
|
||||||
@ -115,18 +120,17 @@ export const ImagePickerPopover: React.FC<Props> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!images || value !== null) return;
|
if (!unsplashImages || value !== null) return;
|
||||||
onChange(images[0].urls.regular);
|
|
||||||
}, [value, onChange, images]);
|
onChange(unsplashImages[0].urls.regular);
|
||||||
|
}, [value, onChange, unsplashImages]);
|
||||||
|
|
||||||
useOutsideClickDetector(imagePickerRef, () => setIsOpen(false));
|
useOutsideClickDetector(imagePickerRef, () => setIsOpen(false));
|
||||||
|
|
||||||
if (!unsplashEnabled) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover className="relative z-[2]" ref={ref}>
|
<Popover className="relative z-[2]" ref={ref}>
|
||||||
<Popover.Button
|
<Popover.Button
|
||||||
className="rounded-sm border border-custom-border-300 bg-custom-background-100 px-2 py-1 text-xs text-custom-text-200 hover:text-custom-text-100"
|
className="rounded border border-custom-border-300 bg-custom-background-100 px-2 py-1 text-xs text-custom-text-200 hover:text-custom-text-100"
|
||||||
onClick={() => setIsOpen((prev) => !prev)}
|
onClick={() => setIsOpen((prev) => !prev)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
@ -141,15 +145,19 @@ export const ImagePickerPopover: React.FC<Props> = ({
|
|||||||
leaveFrom="transform opacity-100 scale-100"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Popover.Panel className="absolute right-0 z-10 mt-2 rounded-md border border-custom-border-200 bg-custom-background-80 shadow-lg">
|
<Popover.Panel className="absolute right-0 z-10 mt-2 rounded-md border border-custom-border-200 bg-custom-background-100 shadow-custom-shadow-sm">
|
||||||
<div
|
<div
|
||||||
ref={imagePickerRef}
|
ref={imagePickerRef}
|
||||||
className="h-96 md:h-[28rem] w-80 md:w-[36rem] flex flex-col overflow-auto rounded border border-custom-border-300 bg-custom-background-100 p-3 shadow-2xl"
|
className="h-96 md:h-[28rem] w-80 md:w-[36rem] flex flex-col overflow-auto rounded border border-custom-border-300 bg-custom-background-100 p-3 shadow-2xl"
|
||||||
>
|
>
|
||||||
<Tab.Group>
|
<Tab.Group>
|
||||||
<div>
|
<Tab.List as="span" className="inline-block rounded bg-custom-background-80 p-1">
|
||||||
<Tab.List as="span" className="inline-block rounded bg-custom-background-80 p-1">
|
{tabOptions.map((tab) => {
|
||||||
{tabOptions.map((tab) => (
|
if (!unsplashImages && unsplashError && tab.key === "unsplash") return null;
|
||||||
|
if (projectCoverImages && projectCoverImages.length === 0 && tab.key === "images")
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return (
|
||||||
<Tab
|
<Tab
|
||||||
key={tab.key}
|
key={tab.key}
|
||||||
className={({ selected }) =>
|
className={({ selected }) =>
|
||||||
@ -160,50 +168,106 @@ export const ImagePickerPopover: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
{tab.title}
|
{tab.title}
|
||||||
</Tab>
|
</Tab>
|
||||||
))}
|
);
|
||||||
</Tab.List>
|
})}
|
||||||
</div>
|
</Tab.List>
|
||||||
<Tab.Panels className="h-full w-full flex-1 overflow-y-auto overflow-x-hidden">
|
<Tab.Panels className="h-full w-full flex-1 overflow-y-auto overflow-x-hidden">
|
||||||
<Tab.Panel className="h-full w-full space-y-4">
|
{(unsplashImages || !unsplashError) && (
|
||||||
<div className="flex gap-x-2 pt-7">
|
<Tab.Panel className="h-full w-full space-y-4 mt-4">
|
||||||
<Input
|
<div className="flex gap-x-2">
|
||||||
name="search"
|
<Input
|
||||||
className="text-sm"
|
name="search"
|
||||||
id="search"
|
className="text-sm"
|
||||||
value={formData.search}
|
id="search"
|
||||||
onChange={(e) => setFormData({ ...formData, search: e.target.value })}
|
value={formData.search}
|
||||||
placeholder="Search for images"
|
onChange={(e) => setFormData({ ...formData, search: e.target.value })}
|
||||||
/>
|
placeholder="Search for images"
|
||||||
<PrimaryButton onClick={() => setSearchParams(formData.search)} size="sm">
|
/>
|
||||||
Search
|
<PrimaryButton onClick={() => setSearchParams(formData.search)} size="sm">
|
||||||
</PrimaryButton>
|
Search
|
||||||
</div>
|
</PrimaryButton>
|
||||||
{images ? (
|
</div>
|
||||||
<div className="grid grid-cols-4 gap-4">
|
{unsplashImages ? (
|
||||||
{images.map((image) => (
|
unsplashImages.length > 0 ? (
|
||||||
<div
|
<div className="grid grid-cols-4 gap-4">
|
||||||
key={image.id}
|
{unsplashImages.map((image) => (
|
||||||
className="relative col-span-2 aspect-video md:col-span-1"
|
<div
|
||||||
>
|
key={image.id}
|
||||||
<img
|
className="relative col-span-2 aspect-video md:col-span-1"
|
||||||
src={image.urls.small}
|
onClick={() => {
|
||||||
alt={image.alt_description}
|
setIsOpen(false);
|
||||||
className="cursor-pointer rounded absolute top-0 left-0 h-full w-full object-cover"
|
onChange(image.urls.regular);
|
||||||
onClick={() => {
|
}}
|
||||||
setIsOpen(false);
|
>
|
||||||
onChange(image.urls.regular);
|
<img
|
||||||
}}
|
src={image.urls.small}
|
||||||
/>
|
alt={image.alt_description}
|
||||||
|
className="cursor-pointer rounded absolute top-0 left-0 h-full w-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
) : (
|
||||||
</div>
|
<p className="text-center text-custom-text-300 text-xs pt-7">
|
||||||
) : (
|
No images found.
|
||||||
<div className="flex justify-center pt-20">
|
</p>
|
||||||
<Spinner />
|
)
|
||||||
</div>
|
) : (
|
||||||
)}
|
<Loader className="grid grid-cols-4 gap-4">
|
||||||
</Tab.Panel>
|
<Loader.Item height="80px" width="100%" />
|
||||||
<Tab.Panel className="h-full w-full pt-5">
|
<Loader.Item height="80px" width="100%" />
|
||||||
|
<Loader.Item height="80px" width="100%" />
|
||||||
|
<Loader.Item height="80px" width="100%" />
|
||||||
|
<Loader.Item height="80px" width="100%" />
|
||||||
|
<Loader.Item height="80px" width="100%" />
|
||||||
|
<Loader.Item height="80px" width="100%" />
|
||||||
|
<Loader.Item height="80px" width="100%" />
|
||||||
|
</Loader>
|
||||||
|
)}
|
||||||
|
</Tab.Panel>
|
||||||
|
)}
|
||||||
|
{(!projectCoverImages || projectCoverImages.length !== 0) && (
|
||||||
|
<Tab.Panel className="h-full w-full space-y-4 mt-4">
|
||||||
|
{projectCoverImages ? (
|
||||||
|
projectCoverImages.length > 0 ? (
|
||||||
|
<div className="grid grid-cols-4 gap-4">
|
||||||
|
{projectCoverImages.map((image, index) => (
|
||||||
|
<div
|
||||||
|
key={image}
|
||||||
|
className="relative col-span-2 aspect-video md:col-span-1"
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
onChange(image);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={image}
|
||||||
|
alt={`Default project cover image- ${index}`}
|
||||||
|
className="cursor-pointer rounded absolute top-0 left-0 h-full w-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-center text-custom-text-300 text-xs pt-7">
|
||||||
|
No images found.
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<Loader className="grid grid-cols-4 gap-4 pt-4">
|
||||||
|
<Loader.Item height="80px" width="100%" />
|
||||||
|
<Loader.Item height="80px" width="100%" />
|
||||||
|
<Loader.Item height="80px" width="100%" />
|
||||||
|
<Loader.Item height="80px" width="100%" />
|
||||||
|
<Loader.Item height="80px" width="100%" />
|
||||||
|
<Loader.Item height="80px" width="100%" />
|
||||||
|
<Loader.Item height="80px" width="100%" />
|
||||||
|
<Loader.Item height="80px" width="100%" />
|
||||||
|
</Loader>
|
||||||
|
)}
|
||||||
|
</Tab.Panel>
|
||||||
|
)}
|
||||||
|
<Tab.Panel className="h-full w-full mt-4">
|
||||||
<div className="w-full h-full flex flex-col gap-y-2">
|
<div className="w-full h-full flex flex-col gap-y-2">
|
||||||
<div className="flex items-center gap-3 w-full flex-1">
|
<div className="flex items-center gap-3 w-full flex-1">
|
||||||
<div
|
<div
|
||||||
|
@ -393,7 +393,7 @@ export const CreateProjectModal: React.FC<Props> = ({
|
|||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
options={options}
|
options={options}
|
||||||
buttonClassName="!px-2 shadow-md"
|
buttonClassName="border-[0.5px] !px-2 shadow-md"
|
||||||
label={
|
label={
|
||||||
<div className="flex items-center justify-center gap-2 py-[1px]">
|
<div className="flex items-center justify-center gap-2 py-[1px]">
|
||||||
{value ? (
|
{value ? (
|
||||||
|
@ -15,6 +15,7 @@ const nextConfig = {
|
|||||||
"vinci-web.s3.amazonaws.com",
|
"vinci-web.s3.amazonaws.com",
|
||||||
"planefs-staging.s3.ap-south-1.amazonaws.com",
|
"planefs-staging.s3.ap-south-1.amazonaws.com",
|
||||||
"planefs.s3.amazonaws.com",
|
"planefs.s3.amazonaws.com",
|
||||||
|
"planefs-staging.s3.amazonaws.com",
|
||||||
"images.unsplash.com",
|
"images.unsplash.com",
|
||||||
"avatars.githubusercontent.com",
|
"avatars.githubusercontent.com",
|
||||||
"localhost",
|
"localhost",
|
||||||
|
@ -76,21 +76,23 @@ class FileServices extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUnsplashImages(page: number = 1, query?: string): Promise<UnSplashImage[]> {
|
async getUnsplashImages(query?: string): Promise<UnSplashImage[]> {
|
||||||
const url = "/api/unsplash";
|
return this.get(`/api/unsplash/`, {
|
||||||
|
|
||||||
return this.request({
|
|
||||||
method: "get",
|
|
||||||
url,
|
|
||||||
params: {
|
params: {
|
||||||
page,
|
|
||||||
per_page: 20,
|
|
||||||
query,
|
query,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((response) => response?.data?.results ?? response?.data)
|
.then((res) => res?.data?.results ?? res?.data)
|
||||||
.catch((error) => {
|
.catch((err) => {
|
||||||
throw error?.response?.data;
|
throw err?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getProjectCoverImages(): Promise<string[]> {
|
||||||
|
return this.get(`/api/project-covers/`)
|
||||||
|
.then((res) => res?.data)
|
||||||
|
.catch((err) => {
|
||||||
|
throw err?.response?.data;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user