forked from github/plane
Merge branch 'develop' of https://github.com/makeplane/plane into style/project_settings
This commit is contained in:
commit
ad7b691b2b
152
apps/app/components/core/image-picker-popover.tsx
Normal file
152
apps/app/components/core/image-picker-popover.tsx
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
import React, { useEffect, useState, useRef } from "react";
|
||||||
|
|
||||||
|
// next
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
// swr
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
// headless ui
|
||||||
|
import { Tab, Transition, Popover } from "@headlessui/react";
|
||||||
|
|
||||||
|
// services
|
||||||
|
import fileService from "services/file.service";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { Input, Spinner } from "components/ui";
|
||||||
|
import { PrimaryButton } from "components/ui/button/primary-button";
|
||||||
|
// hooks
|
||||||
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
|
|
||||||
|
const tabOptions = [
|
||||||
|
{
|
||||||
|
key: "unsplash",
|
||||||
|
title: "Unsplash",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "upload",
|
||||||
|
title: "Upload",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
label: string | React.ReactNode;
|
||||||
|
value: string | null;
|
||||||
|
onChange: (data: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ImagePickerPopover: React.FC<Props> = ({ label, value, onChange }) => {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [searchParams, setSearchParams] = useState("");
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
search: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: images } = useSWR(`UNSPLASH_IMAGES_${searchParams}`, () =>
|
||||||
|
fileService.getUnsplashImages(1, searchParams)
|
||||||
|
);
|
||||||
|
|
||||||
|
useOutsideClickDetector(ref, () => {
|
||||||
|
setIsOpen(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!images || value !== null) return;
|
||||||
|
onChange(images[0].urls.regular);
|
||||||
|
}, [value, onChange, images]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover className="relative z-[2]" ref={ref}>
|
||||||
|
<Popover.Button
|
||||||
|
className="rounded-md border border-gray-500 bg-white px-2 py-1 text-xs text-gray-700"
|
||||||
|
onClick={() => setIsOpen((prev) => !prev)}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Popover.Button>
|
||||||
|
<Transition
|
||||||
|
show={isOpen}
|
||||||
|
enter="transition ease-out duration-100"
|
||||||
|
enterFrom="transform opacity-0 scale-95"
|
||||||
|
enterTo="transform opacity-100 scale-100"
|
||||||
|
leave="transition ease-in duration-75"
|
||||||
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
|
leaveTo="transform opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<Popover.Panel className="absolute right-0 z-10 mt-2 rounded-md bg-white shadow-lg">
|
||||||
|
<div className="h-96 w-80 overflow-auto rounded border bg-white p-5 shadow-2xl sm:max-w-2xl md:w-96 lg:w-[40rem]">
|
||||||
|
<Tab.Group>
|
||||||
|
<Tab.List as="span" className="inline-block rounded bg-gray-200 p-1">
|
||||||
|
{tabOptions.map((tab) => (
|
||||||
|
<Tab
|
||||||
|
key={tab.key}
|
||||||
|
className={({ selected }) =>
|
||||||
|
`rounded py-1 px-4 text-center text-sm outline-none transition-colors ${
|
||||||
|
selected ? "bg-theme text-white" : "text-black"
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{tab.title}
|
||||||
|
</Tab>
|
||||||
|
))}
|
||||||
|
</Tab.List>
|
||||||
|
<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">
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setSearchParams(formData.search);
|
||||||
|
}}
|
||||||
|
className="flex gap-x-2 pt-7"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
name="search"
|
||||||
|
className="text-sm"
|
||||||
|
id="search"
|
||||||
|
value={formData.search}
|
||||||
|
onChange={(e) => setFormData({ ...formData, search: e.target.value })}
|
||||||
|
placeholder="Search for images"
|
||||||
|
/>
|
||||||
|
<PrimaryButton className="bg-indigo-600" size="sm">
|
||||||
|
Search
|
||||||
|
</PrimaryButton>
|
||||||
|
</form>
|
||||||
|
{images ? (
|
||||||
|
<div className="grid grid-cols-4 gap-4">
|
||||||
|
{images.map((image) => (
|
||||||
|
<div
|
||||||
|
key={image.id}
|
||||||
|
className="relative col-span-2 aspect-video md:col-span-1"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={image.urls.small}
|
||||||
|
alt={image.alt_description}
|
||||||
|
layout="fill"
|
||||||
|
objectFit="cover"
|
||||||
|
className="cursor-pointer rounded"
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
onChange(image.urls.regular);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex justify-center pt-20">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Tab.Panel>
|
||||||
|
<Tab.Panel className="flex h-full w-full flex-col items-center justify-center">
|
||||||
|
<p>Coming Soon...</p>
|
||||||
|
</Tab.Panel>
|
||||||
|
</Tab.Panels>
|
||||||
|
</Tab.Group>
|
||||||
|
</div>
|
||||||
|
</Popover.Panel>
|
||||||
|
</Transition>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
@ -9,4 +9,4 @@ export * from "./issues-view";
|
|||||||
export * from "./link-modal";
|
export * from "./link-modal";
|
||||||
export * from "./not-authorized-view";
|
export * from "./not-authorized-view";
|
||||||
export * from "./multi-level-select";
|
export * from "./multi-level-select";
|
||||||
export * from "./unsplash-modal";
|
export * from "./image-picker-popover";
|
||||||
|
@ -1,108 +0,0 @@
|
|||||||
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> = (props) => {
|
|
||||||
const { isOpen, handleClose, onSelect } = props;
|
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useState<string>("");
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
search: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: images } = useSWR(`UNSPLASH_IMAGES_${searchQuery.toUpperCase()}`, () =>
|
|
||||||
fileService.getUnsplashImages(1, searchQuery)
|
|
||||||
);
|
|
||||||
|
|
||||||
const onClose = () => {
|
|
||||||
handleClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
|
||||||
<Dialog as="div" className="relative z-20" onClose={onClose}>
|
|
||||||
<Transition.Child
|
|
||||||
as={React.Fragment}
|
|
||||||
enter="ease-out duration-300"
|
|
||||||
enterFrom="opacity-0"
|
|
||||||
enterTo="opacity-100"
|
|
||||||
leave="ease-in duration-200"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
>
|
|
||||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
|
||||||
</Transition.Child>
|
|
||||||
|
|
||||||
<div className="fixed inset-0 z-20 overflow-y-auto">
|
|
||||||
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
|
||||||
<Transition.Child
|
|
||||||
as={React.Fragment}
|
|
||||||
enter="ease-out duration-300"
|
|
||||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
||||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
|
||||||
leave="ease-in duration-200"
|
|
||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
||||||
>
|
|
||||||
<Dialog.Panel className="relative h-[35rem] space-y-5 transform overflow-y-auto rounded-lg bg-white px-5 py-8 text-left shadow-xl transition-all sm:w-full sm:max-w-2xl sm:p-6">
|
|
||||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900">
|
|
||||||
Select an image
|
|
||||||
</Dialog.Title>
|
|
||||||
|
|
||||||
<form
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setSearchQuery(formData.search);
|
|
||||||
}}
|
|
||||||
className="flex items-center"
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
name="search"
|
|
||||||
value={formData.search}
|
|
||||||
onChange={(e) => setFormData({ ...formData, search: e.target.value })}
|
|
||||||
placeholder="Search for images"
|
|
||||||
/>
|
|
||||||
<Button type="submit">Search</Button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{images ? (
|
|
||||||
<div className="grid grid-cols-3 gap-5">
|
|
||||||
{images.map((image) => (
|
|
||||||
<button
|
|
||||||
key={image.id}
|
|
||||||
className="w-full relative h-32 bg-gray-200 rounded-md overflow-hidden"
|
|
||||||
type="button"
|
|
||||||
onClick={() => onSelect(image.urls.small)}
|
|
||||||
>
|
|
||||||
<Image src={image.urls.small} layout="fill" objectFit="cover" />
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex h-96 w-full items-center justify-center">
|
|
||||||
<Spinner />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Dialog.Panel>
|
|
||||||
</Transition.Child>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
</Transition.Root>
|
|
||||||
);
|
|
||||||
};
|
|
@ -44,7 +44,7 @@ const EmojiIconPicker: React.FC<Props> = ({ label, value, onChange }) => {
|
|||||||
return (
|
return (
|
||||||
<Popover className="relative z-[1]" ref={ref}>
|
<Popover className="relative z-[1]" ref={ref}>
|
||||||
<Popover.Button
|
<Popover.Button
|
||||||
className="rounded-md border border-gray-300 p-2 outline-none sm:text-sm"
|
className="rounded-full bg-gray-100 p-2 outline-none sm:text-sm"
|
||||||
onClick={() => setIsOpen((prev) => !prev)}
|
onClick={() => setIsOpen((prev) => !prev)}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
import Image from "next/image";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import useSWR, { mutate } from "swr";
|
import useSWR, { mutate } from "swr";
|
||||||
@ -7,17 +8,19 @@ import useSWR, { mutate } from "swr";
|
|||||||
import { useForm, Controller } from "react-hook-form";
|
import { useForm, Controller } from "react-hook-form";
|
||||||
|
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
|
||||||
// services
|
// services
|
||||||
import projectServices from "services/project.service";
|
import projectServices from "services/project.service";
|
||||||
import workspaceService from "services/workspace.service";
|
import workspaceService from "services/workspace.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// ui
|
// ui
|
||||||
|
import { PrimaryButton } from "components/ui/button/primary-button";
|
||||||
import { Button, Input, TextArea, CustomSelect } from "components/ui";
|
import { Button, Input, TextArea, CustomSelect } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { PhotoIcon } from "@heroicons/react/24/outline";
|
import { XMarkIcon } from "@heroicons/react/24/outline";
|
||||||
// components
|
// components
|
||||||
import { UnsplashImageModal } from "components/core";
|
import { ImagePickerPopover } from "components/core";
|
||||||
import EmojiIconPicker from "components/emoji-icon-picker";
|
import EmojiIconPicker from "components/emoji-icon-picker";
|
||||||
// helpers
|
// helpers
|
||||||
import { getRandomEmoji } from "helpers/common.helper";
|
import { getRandomEmoji } from "helpers/common.helper";
|
||||||
@ -62,7 +65,6 @@ const IsGuestCondition: React.FC<{
|
|||||||
export const CreateProjectModal: React.FC<Props> = (props) => {
|
export const CreateProjectModal: React.FC<Props> = (props) => {
|
||||||
const { isOpen, setIsOpen } = props;
|
const { isOpen, setIsOpen } = props;
|
||||||
|
|
||||||
const [isUnsplashModalOpen, setIsUnsplashModalOpen] = useState(false);
|
|
||||||
const [isChangeIdentifierRequired, setIsChangeIdentifierRequired] = useState(true);
|
const [isChangeIdentifierRequired, setIsChangeIdentifierRequired] = useState(true);
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
@ -177,55 +179,60 @@ export const CreateProjectModal: React.FC<Props> = (props) => {
|
|||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white px-5 py-8 text-left shadow-xl transition-all sm:w-full sm:max-w-2xl sm:p-6">
|
<Dialog.Panel className="transform rounded-lg bg-white text-left shadow-xl transition-all sm:w-full sm:max-w-2xl">
|
||||||
<div className="absolute border-2 rounded-md p-2 bg-red-a500 right-4 top-4">
|
<div className="relative h-36 w-full rounded-t-lg bg-gray-300">
|
||||||
<button
|
{watch("cover_image") !== null && (
|
||||||
type="button"
|
<Image
|
||||||
className="w-full h-full flex items-center justify-center outline-none"
|
src={watch("cover_image")!}
|
||||||
onClick={() => setIsUnsplashModalOpen(true)}
|
layout="fill"
|
||||||
>
|
alt="cover image"
|
||||||
<PhotoIcon className="w-5 h-5 text-gray-700" />
|
objectFit="cover"
|
||||||
</button>
|
className="rounded-t-lg"
|
||||||
</div>
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
|
||||||
<div className="space-y-5">
|
|
||||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900">
|
|
||||||
Create Project
|
|
||||||
</Dialog.Title>
|
|
||||||
<div className="mt-2">
|
|
||||||
<p className="text-sm text-gray-500">
|
|
||||||
Create a new project to start working on it.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex gap-3">
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<label htmlFor="icon" className="mb-2 text-gray-500">
|
|
||||||
Icon
|
|
||||||
</label>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="icon"
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<EmojiIconPicker
|
|
||||||
label={
|
|
||||||
value ? String.fromCodePoint(parseInt(value)) : "Select Icon"
|
|
||||||
}
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div className="absolute right-2 top-2 p-2">
|
||||||
|
<button type="button" onClick={handleClose}>
|
||||||
|
<XMarkIcon className="h-5 w-5 text-white" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="absolute bottom-0 left-0 flex w-full justify-between px-6 py-5">
|
||||||
|
<div className="absolute left-0 bottom-0 h-16 w-full bg-gradient-to-t from-black opacity-60" />
|
||||||
|
<h3 className="z-[1] text-xl text-white">Create Project</h3>
|
||||||
|
<div>
|
||||||
|
<ImagePickerPopover
|
||||||
|
label="Change Cover"
|
||||||
|
onChange={(image) => {
|
||||||
|
setValue("cover_image", image);
|
||||||
|
}}
|
||||||
|
value={watch("cover_image")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full">
|
</div>
|
||||||
|
</div>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<div className="mt-5 space-y-4 px-4 py-3">
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
<div>
|
||||||
|
<EmojiIconPicker
|
||||||
|
label={String.fromCodePoint(parseInt(watch("icon")))}
|
||||||
|
onChange={(emoji) => {
|
||||||
|
setValue("icon", emoji);
|
||||||
|
}}
|
||||||
|
value={watch("icon")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-shrink-0 flex-grow">
|
||||||
<Input
|
<Input
|
||||||
id="name"
|
id="name"
|
||||||
label="Name"
|
|
||||||
name="name"
|
name="name"
|
||||||
type="name"
|
type="name"
|
||||||
placeholder="Enter name"
|
placeholder="Enter name"
|
||||||
error={errors.name}
|
error={errors.name}
|
||||||
register={register}
|
register={register}
|
||||||
|
className="text-xl"
|
||||||
|
mode="transparent"
|
||||||
validations={{
|
validations={{
|
||||||
required: "Name is required",
|
required: "Name is required",
|
||||||
maxLength: {
|
maxLength: {
|
||||||
@ -236,49 +243,14 @@ export const CreateProjectModal: React.FC<Props> = (props) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<h6 className="text-gray-500">Network</h6>
|
|
||||||
<Controller
|
|
||||||
name="network"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<CustomSelect
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
label={
|
|
||||||
Object.keys(NETWORK_CHOICES).find((k) => k === value.toString())
|
|
||||||
? NETWORK_CHOICES[
|
|
||||||
value.toString() as keyof typeof NETWORK_CHOICES
|
|
||||||
]
|
|
||||||
: "Select network"
|
|
||||||
}
|
|
||||||
input
|
|
||||||
>
|
|
||||||
{Object.keys(NETWORK_CHOICES).map((key) => (
|
|
||||||
<CustomSelect.Option key={key} value={key}>
|
|
||||||
{NETWORK_CHOICES[key as keyof typeof NETWORK_CHOICES]}
|
|
||||||
</CustomSelect.Option>
|
|
||||||
))}
|
|
||||||
</CustomSelect>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<TextArea
|
|
||||||
id="description"
|
|
||||||
name="description"
|
|
||||||
label="Description"
|
|
||||||
placeholder="Enter description"
|
|
||||||
error={errors.description}
|
|
||||||
register={register}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<Input
|
<Input
|
||||||
id="identifier"
|
id="identifier"
|
||||||
label="Identifier"
|
|
||||||
name="identifier"
|
name="identifier"
|
||||||
type="text"
|
type="text"
|
||||||
|
mode="transparent"
|
||||||
|
className="text-sm"
|
||||||
placeholder="Enter Project Identifier"
|
placeholder="Enter Project Identifier"
|
||||||
error={errors.identifier}
|
error={errors.identifier}
|
||||||
register={register}
|
register={register}
|
||||||
@ -298,29 +270,58 @@ export const CreateProjectModal: React.FC<Props> = (props) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<TextArea
|
||||||
|
id="description"
|
||||||
|
name="description"
|
||||||
|
mode="transparent"
|
||||||
|
className="text-sm"
|
||||||
|
placeholder="Enter description"
|
||||||
|
error={errors.description}
|
||||||
|
register={register}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-40">
|
||||||
|
<Controller
|
||||||
|
name="network"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<CustomSelect
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
label={
|
||||||
|
Object.keys(NETWORK_CHOICES).find((k) => k === value.toString())
|
||||||
|
? NETWORK_CHOICES[value.toString() as keyof typeof NETWORK_CHOICES]
|
||||||
|
: "Select network"
|
||||||
|
}
|
||||||
|
input
|
||||||
|
>
|
||||||
|
{Object.keys(NETWORK_CHOICES).map((key) => (
|
||||||
|
<CustomSelect.Option key={key} value={key}>
|
||||||
|
{NETWORK_CHOICES[key as keyof typeof NETWORK_CHOICES]}
|
||||||
|
</CustomSelect.Option>
|
||||||
|
))}
|
||||||
|
</CustomSelect>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 flex justify-end gap-2">
|
|
||||||
|
<div className="mt-5 flex justify-end gap-2 border-t-2 px-4 py-3">
|
||||||
<Button theme="secondary" onClick={handleClose}>
|
<Button theme="secondary" onClick={handleClose}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" disabled={isSubmitting}>
|
<PrimaryButton type="submit" size="sm" loading={isSubmitting}>
|
||||||
{isSubmitting ? "Creating Project..." : "Create Project"}
|
{isSubmitting ? "Adding project..." : "Add Project"}
|
||||||
</Button>
|
</PrimaryButton>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<UnsplashImageModal
|
|
||||||
handleClose={() => setIsUnsplashModalOpen(false)}
|
|
||||||
isOpen={isUnsplashModalOpen}
|
|
||||||
onSelect={(imageUrl) => {
|
|
||||||
setValue("cover_image", imageUrl);
|
|
||||||
setIsUnsplashModalOpen(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</Transition.Root>
|
</Transition.Root>
|
||||||
);
|
);
|
||||||
|
@ -7,15 +7,13 @@ import useSWR, { mutate } from "swr";
|
|||||||
|
|
||||||
// react-hook-form
|
// react-hook-form
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
// react-dropzone
|
|
||||||
import Dropzone from "react-dropzone";
|
|
||||||
// icons
|
// icons
|
||||||
import { LinkIcon } from "@heroicons/react/24/outline";
|
import { LinkIcon } from "@heroicons/react/24/outline";
|
||||||
// lib
|
// lib
|
||||||
import { requiredWorkspaceAdmin } from "lib/auth";
|
import { requiredWorkspaceAdmin } from "lib/auth";
|
||||||
// services
|
// services
|
||||||
import workspaceService from "services/workspace.service";
|
import workspaceService from "services/workspace.service";
|
||||||
import fileServices from "services/file.service";
|
|
||||||
// layouts
|
// layouts
|
||||||
import AppLayout from "layouts/app-layout";
|
import AppLayout from "layouts/app-layout";
|
||||||
// hooks
|
// hooks
|
||||||
@ -52,7 +50,6 @@ type TWorkspaceSettingsProps = {
|
|||||||
|
|
||||||
const WorkspaceSettings: NextPage<TWorkspaceSettingsProps> = (props) => {
|
const WorkspaceSettings: NextPage<TWorkspaceSettingsProps> = (props) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [image, setImage] = useState<File | null>(null);
|
|
||||||
const [isImageUploading, setIsImageUploading] = useState(false);
|
const [isImageUploading, setIsImageUploading] = useState(false);
|
||||||
const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false);
|
const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false);
|
||||||
|
|
||||||
@ -122,9 +119,11 @@ const WorkspaceSettings: NextPage<TWorkspaceSettingsProps> = (props) => {
|
|||||||
<ImageUploadModal
|
<ImageUploadModal
|
||||||
isOpen={isImageUploadModalOpen}
|
isOpen={isImageUploadModalOpen}
|
||||||
onClose={() => setIsImageUploadModalOpen(false)}
|
onClose={() => setIsImageUploadModalOpen(false)}
|
||||||
onSuccess={() => {
|
onSuccess={(imageUrl) => {
|
||||||
|
setIsImageUploading(true);
|
||||||
|
setValue("logo", imageUrl);
|
||||||
setIsImageUploadModalOpen(false);
|
setIsImageUploadModalOpen(false);
|
||||||
handleSubmit(onSubmit)();
|
handleSubmit(onSubmit)().then(() => setIsImageUploading(false));
|
||||||
}}
|
}}
|
||||||
value={watch("logo")}
|
value={watch("logo")}
|
||||||
/>
|
/>
|
||||||
@ -147,24 +146,11 @@ const WorkspaceSettings: NextPage<TWorkspaceSettingsProps> = (props) => {
|
|||||||
<div className="col-span lg:col-span-5">
|
<div className="col-span lg:col-span-5">
|
||||||
<h4 className="text-md mb-1 leading-6 text-gray-900">Logo</h4>
|
<h4 className="text-md mb-1 leading-6 text-gray-900">Logo</h4>
|
||||||
<div className="flex w-full gap-2">
|
<div className="flex w-full gap-2">
|
||||||
<Dropzone
|
<button type="button" onClick={() => setIsImageUploadModalOpen(true)}>
|
||||||
multiple={false}
|
{watch("logo") && watch("logo") !== null && watch("logo") !== "" ? (
|
||||||
accept={{
|
|
||||||
"image/*": [],
|
|
||||||
}}
|
|
||||||
onDrop={(files) => {
|
|
||||||
setImage(files[0]);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{({ getRootProps, getInputProps }) => (
|
|
||||||
<div>
|
|
||||||
<input {...getInputProps()} />
|
|
||||||
<div {...getRootProps()}>
|
|
||||||
{(watch("logo") && watch("logo") !== null && watch("logo") !== "") ||
|
|
||||||
(image && image !== null) ? (
|
|
||||||
<div className="relative mx-auto flex h-12 w-12">
|
<div className="relative mx-auto flex h-12 w-12">
|
||||||
<Image
|
<Image
|
||||||
src={image ? URL.createObjectURL(image) : watch("logo") ?? ""}
|
src={watch("logo")!}
|
||||||
alt="Workspace Logo"
|
alt="Workspace Logo"
|
||||||
objectFit="cover"
|
objectFit="cover"
|
||||||
layout="fill"
|
layout="fill"
|
||||||
@ -177,32 +163,14 @@ const WorkspaceSettings: NextPage<TWorkspaceSettingsProps> = (props) => {
|
|||||||
{activeWorkspace?.name?.charAt(0) ?? "N"}
|
{activeWorkspace?.name?.charAt(0) ?? "N"}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</button>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Dropzone>
|
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-2 text-sm text-gray-500">
|
<p className="mb-2 text-sm text-gray-500">
|
||||||
Max file size is 5MB. Supported file types are .jpg and .png.
|
Max file size is 5MB. Supported file types are .jpg and .png.
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (image === null) return;
|
setIsImageUploadModalOpen(true);
|
||||||
setIsImageUploading(true);
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("asset", image);
|
|
||||||
formData.append("attributes", JSON.stringify({}));
|
|
||||||
fileServices
|
|
||||||
.uploadFile(workspaceSlug as string, formData)
|
|
||||||
.then((response) => {
|
|
||||||
const imageUrl = response.asset;
|
|
||||||
setValue("logo", imageUrl);
|
|
||||||
handleSubmit(onSubmit)();
|
|
||||||
setIsImageUploading(false);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setIsImageUploading(false);
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isImageUploading ? "Uploading..." : "Upload"}
|
{isImageUploading ? "Uploading..." : "Upload"}
|
||||||
|
Loading…
Reference in New Issue
Block a user