style: workspace and profile setting revamp (#2193)

* chore: custom theme mode svg added

* style: workspace settings ui revamp

* style: project settings and image upload modal improvement

* style: profile setting ui revamp

* chore: settings ui improvement and bug fixes
This commit is contained in:
Anmol Singh Bhatia 2023-09-15 15:03:32 +05:30 committed by GitHub
parent 9bfdcff44d
commit ccffbe1b4e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 1299 additions and 1072 deletions

View File

@ -103,8 +103,8 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
{projectDetails?.close_in !== 0 && ( {projectDetails?.close_in !== 0 && (
<div className="ml-12"> <div className="ml-12">
<div className="flex flex-col gap-4"> <div className="flex flex-col rounded bg-custom-background-90 border border-custom-border-200 p-2">
<div className="flex items-center justify-between rounded px-5 py-4 bg-custom-background-90 border border-custom-border-200 gap-2 w-full"> <div className="flex items-center justify-between px-5 py-4 gap-2 w-full">
<div className="w-1/2 text-sm font-medium"> <div className="w-1/2 text-sm font-medium">
Auto-close issues that are inactive for Auto-close issues that are inactive for
</div> </div>
@ -138,7 +138,7 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
</div> </div>
</div> </div>
<div className="flex items-center justify-between rounded px-5 py-4 bg-custom-background-90 border border-custom-border-200 gap-2 w-full"> <div className="flex items-center justify-between px-5 py-4 gap-2 w-full">
<div className="w-1/2 text-sm font-medium">Auto-close Status</div> <div className="w-1/2 text-sm font-medium">Auto-close Status</div>
<div className="w-1/2 "> <div className="w-1/2 ">
<CustomSearchSelect <CustomSearchSelect

View File

@ -90,14 +90,14 @@ const activityDetails: {
</> </>
); );
}, },
icon: <Icon iconName="group" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="group" className="!text-2xl" aria-hidden="true" />,
}, },
archived_at: { archived_at: {
message: (activity) => { message: (activity) => {
if (activity.new_value === "restore") return "restored the issue."; if (activity.new_value === "restore") return "restored the issue.";
else return "archived the issue."; else return "archived the issue.";
}, },
icon: <Icon iconName="archive" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="archive" className="!text-2xl" aria-hidden="true" />,
}, },
attachment: { attachment: {
message: (activity, showIssue) => { message: (activity, showIssue) => {
@ -136,7 +136,7 @@ const activityDetails: {
</> </>
); );
}, },
icon: <Icon iconName="attach_file" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="attach_file" className="!text-2xl" aria-hidden="true" />,
}, },
blocking: { blocking: {
message: (activity) => { message: (activity) => {
@ -224,7 +224,7 @@ const activityDetails: {
</> </>
); );
}, },
icon: <Icon iconName="contrast" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="contrast" className="!text-2xl" aria-hidden="true" />,
}, },
description: { description: {
message: (activity, showIssue) => ( message: (activity, showIssue) => (
@ -239,7 +239,7 @@ const activityDetails: {
. .
</> </>
), ),
icon: <Icon iconName="chat" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="chat" className="!text-2xl" aria-hidden="true" />,
}, },
estimate_point: { estimate_point: {
message: (activity, showIssue) => { message: (activity, showIssue) => {
@ -271,14 +271,14 @@ const activityDetails: {
</> </>
); );
}, },
icon: <Icon iconName="change_history" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="change_history" className="!text-2xl" aria-hidden="true" />,
}, },
issue: { issue: {
message: (activity) => { message: (activity) => {
if (activity.verb === "created") return "created the issue."; if (activity.verb === "created") return "created the issue.";
else return "deleted an issue."; else return "deleted an issue.";
}, },
icon: <Icon iconName="stack" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="stack" className="!text-2xl" aria-hidden="true" />,
}, },
labels: { labels: {
message: (activity, showIssue) => { message: (activity, showIssue) => {
@ -327,7 +327,7 @@ const activityDetails: {
</> </>
); );
}, },
icon: <Icon iconName="sell" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="sell" className="!text-2xl" aria-hidden="true" />,
}, },
link: { link: {
message: (activity, showIssue) => { message: (activity, showIssue) => {
@ -398,7 +398,7 @@ const activityDetails: {
</> </>
); );
}, },
icon: <Icon iconName="link" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="link" className="!text-2xl" aria-hidden="true" />,
}, },
modules: { modules: {
message: (activity, showIssue, workspaceSlug) => { message: (activity, showIssue, workspaceSlug) => {
@ -448,7 +448,7 @@ const activityDetails: {
</> </>
); );
}, },
icon: <Icon iconName="dataset" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="dataset" className="!text-2xl" aria-hidden="true" />,
}, },
name: { name: {
message: (activity, showIssue) => ( message: (activity, showIssue) => (
@ -463,7 +463,7 @@ const activityDetails: {
. .
</> </>
), ),
icon: <Icon iconName="chat" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="chat" className="!text-2xl" aria-hidden="true" />,
}, },
parent: { parent: {
message: (activity, showIssue) => { message: (activity, showIssue) => {
@ -496,7 +496,7 @@ const activityDetails: {
</> </>
); );
}, },
icon: <Icon iconName="supervised_user_circle" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="supervised_user_circle" className="!text-2xl" aria-hidden="true" />,
}, },
priority: { priority: {
message: (activity, showIssue) => ( message: (activity, showIssue) => (
@ -514,7 +514,7 @@ const activityDetails: {
. .
</> </>
), ),
icon: <Icon iconName="signal_cellular_alt" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="signal_cellular_alt" className="!text-2xl" aria-hidden="true" />,
}, },
start_date: { start_date: {
message: (activity, showIssue) => { message: (activity, showIssue) => {
@ -548,7 +548,7 @@ const activityDetails: {
</> </>
); );
}, },
icon: <Icon iconName="calendar_today" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="calendar_today" className="!text-2xl" aria-hidden="true" />,
}, },
state: { state: {
message: (activity, showIssue) => ( message: (activity, showIssue) => (
@ -564,7 +564,7 @@ const activityDetails: {
. .
</> </>
), ),
icon: <Squares2X2Icon className="h-3 w-3" aria-hidden="true" />, icon: <Squares2X2Icon className="h-6 w-6 text-custom-sidebar-200" aria-hidden="true" />,
}, },
target_date: { target_date: {
message: (activity, showIssue) => { message: (activity, showIssue) => {
@ -598,7 +598,7 @@ const activityDetails: {
</> </>
); );
}, },
icon: <Icon iconName="calendar_today" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="calendar_today" className="!text-2xl" aria-hidden="true" />,
}, },
}; };

View File

@ -1,6 +1,5 @@
import React, { useCallback, useState } from "react"; import React, { useCallback, useState } from "react";
import NextImage from "next/image";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// react-dropzone // react-dropzone
@ -12,7 +11,7 @@ import fileServices from "services/file.service";
// hooks // hooks
import useWorkspaceDetails from "hooks/use-workspace-details"; import useWorkspaceDetails from "hooks/use-workspace-details";
// ui // ui
import { PrimaryButton, SecondaryButton } from "components/ui"; import { DangerButton, PrimaryButton, SecondaryButton } from "components/ui";
// icons // icons
import { UserCircleIcon } from "components/icons"; import { UserCircleIcon } from "components/icons";
@ -21,6 +20,8 @@ type Props = {
onClose: () => void; onClose: () => void;
isOpen: boolean; isOpen: boolean;
onSuccess: (url: string) => void; onSuccess: (url: string) => void;
isRemoving: boolean;
handleDelete: () => void;
userImage?: boolean; userImage?: boolean;
}; };
@ -29,6 +30,8 @@ export const ImageUploadModal: React.FC<Props> = ({
onSuccess, onSuccess,
isOpen, isOpen,
onClose, onClose,
isRemoving,
handleDelete,
userImage, userImage,
}) => { }) => {
const [image, setImage] = useState<File | null>(null); const [image, setImage] = useState<File | null>(null);
@ -148,12 +151,10 @@ export const ImageUploadModal: React.FC<Props> = ({
> >
Edit Edit
</button> </button>
<NextImage <img
layout="fill"
objectFit="cover"
src={image ? URL.createObjectURL(image) : value ? value : ""} src={image ? URL.createObjectURL(image) : value ? value : ""}
alt="image" alt="image"
className="rounded-lg" className="absolute top-0 left-0 h-full w-full object-cover rounded-md"
/> />
</> </>
) : ( ) : (
@ -182,15 +183,22 @@ export const ImageUploadModal: React.FC<Props> = ({
<p className="my-4 text-custom-text-200 text-sm"> <p className="my-4 text-custom-text-200 text-sm">
File formats supported- .jpeg, .jpg, .png, .webp, .svg File formats supported- .jpeg, .jpg, .png, .webp, .svg
</p> </p>
<div className="flex items-center justify-end gap-2"> <div className="flex items-center justify-between">
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton> <div className="flex items-center">
<PrimaryButton <DangerButton onClick={handleDelete} outline disabled={!value}>
onClick={handleSubmit} {isRemoving ? "Removing..." : "Remove"}
disabled={!image} </DangerButton>
loading={isImageUploading} </div>
> <div className="flex items-center gap-2">
{isImageUploading ? "Uploading..." : "Upload & Save"} <SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
</PrimaryButton> <PrimaryButton
onClick={handleSubmit}
disabled={!image}
loading={isImageUploading}
>
{isImageUploading ? "Uploading..." : "Upload & Save"}
</PrimaryButton>
</div>
</div> </div>
</Dialog.Panel> </Dialog.Panel>
</Transition.Child> </Transition.Child>

View File

@ -46,32 +46,38 @@ const IntegrationGuide = () => {
return ( return (
<> <>
<div className="h-full space-y-2"> <div className="h-full w-full">
<> <>
<div className="space-y-2"> <div>
{EXPORTERS_LIST.map((service) => ( {EXPORTERS_LIST.map((service) => (
<div <div
key={service.provider} key={service.provider}
className="rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4" className="flex items-center justify-between gap-2 border-b border-custom-border-200 bg-custom-background-100 px-4 py-6"
> >
<div className="flex items-center gap-4 whitespace-nowrap"> <div className="flex items-start justify-between gap-4 w-full">
<div className="relative h-10 w-10 flex-shrink-0"> <div className="flex item-center gap-2.5">
<Image <div className="relative h-10 w-10 flex-shrink-0">
src={service.logo} <Image
layout="fill" src={service.logo}
objectFit="cover" layout="fill"
alt={`${service.title} Logo`} objectFit="cover"
/> alt={`${service.title} Logo`}
</div> />
<div className="w-full"> </div>
<h3>{service.title}</h3> <div>
<p className="text-sm text-custom-text-200">{service.description}</p> <h3 className="flex items-center gap-4 text-sm font-medium">
{service.title}
</h3>
<p className="text-sm text-custom-text-200 tracking-tight">
{service.description}
</p>
</div>
</div> </div>
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<Link href={`/${workspaceSlug}/settings/exports?provider=${service.provider}`}> <Link href={`/${workspaceSlug}/settings/exports?provider=${service.provider}`}>
<a> <a>
<PrimaryButton> <PrimaryButton>
<span className="capitalize">{service.type}</span> now <span className="capitalize">{service.type}</span>
</PrimaryButton> </PrimaryButton>
</a> </a>
</Link> </Link>
@ -80,10 +86,11 @@ const IntegrationGuide = () => {
</div> </div>
))} ))}
</div> </div>
<div className="rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4"> <div>
<h3 className="mb-2 flex gap-2 text-lg font-medium justify-between"> <div className="flex items-center justify-between pt-7 pb-3.5 border-b border-custom-border-200">
<div className="flex gap-2"> <div className="flex gap-2 items-center">
<div className="">Previous Exports</div> <h3 className="flex gap-2 text-xl font-medium">Previous Exports</h3>
<button <button
type="button" type="button"
className="flex flex-shrink-0 items-center gap-1 rounded bg-custom-background-80 py-1 px-1.5 text-xs outline-none" className="flex flex-shrink-0 items-center gap-1 rounded bg-custom-background-80 py-1 px-1.5 text-xs outline-none"
@ -128,27 +135,29 @@ const IntegrationGuide = () => {
<Icon iconName="keyboard_arrow_right" className="!text-lg" /> <Icon iconName="keyboard_arrow_right" className="!text-lg" />
</button> </button>
</div> </div>
</h3> </div>
{exporterServices && exporterServices?.results ? ( <div className="flex flex-col">
exporterServices?.results?.length > 0 ? ( {exporterServices && exporterServices?.results ? (
<div className="space-y-2"> exporterServices?.results?.length > 0 ? (
<div className="divide-y divide-custom-border-200"> <div>
{exporterServices?.results.map((service) => ( <div className="divide-y divide-custom-border-200">
<SingleExport key={service.id} service={service} refreshing={refreshing} /> {exporterServices?.results.map((service) => (
))} <SingleExport key={service.id} service={service} refreshing={refreshing} />
))}
</div>
</div> </div>
</div> ) : (
<p className="py-2 text-sm text-custom-text-200">No previous export available.</p>
)
) : ( ) : (
<p className="py-2 text-sm text-custom-text-200">No previous export available.</p> <Loader className="mt-6 grid grid-cols-1 gap-3">
) <Loader.Item height="40px" width="100%" />
) : ( <Loader.Item height="40px" width="100%" />
<Loader className="mt-6 grid grid-cols-1 gap-3"> <Loader.Item height="40px" width="100%" />
<Loader.Item height="40px" width="100%" /> <Loader.Item height="40px" width="100%" />
<Loader.Item height="40px" width="100%" /> </Loader>
<Loader.Item height="40px" width="100%" /> )}
<Loader.Item height="40px" width="100%" /> </div>
</Loader>
)}
</div> </div>
</> </>
{provider && ( {provider && (

View File

@ -23,7 +23,7 @@ export const SingleExport: React.FC<Props> = ({ service, refreshing }) => {
}; };
return ( return (
<div className="flex items-center justify-between gap-2 py-3"> <div className="flex items-center justify-between gap-2 px-4 py-3">
<div> <div>
<h4 className="flex items-center gap-2 text-sm"> <h4 className="flex items-center gap-2 text-sm">
<span> <span>

View File

@ -21,7 +21,6 @@ import {
import { Loader, PrimaryButton } from "components/ui"; import { Loader, PrimaryButton } from "components/ui";
// icons // icons
import { ArrowPathIcon } from "@heroicons/react/24/outline"; import { ArrowPathIcon } from "@heroicons/react/24/outline";
import { ArrowRightIcon } from "components/icons";
// types // types
import { IImporterService } from "types"; import { IImporterService } from "types";
// fetch-keys // fetch-keys
@ -57,10 +56,10 @@ const IntegrationGuide = () => {
data={importToDelete} data={importToDelete}
user={user} user={user}
/> />
<div className="h-full space-y-2"> <div className="h-full">
{(!provider || provider === "csv") && ( {(!provider || provider === "csv") && (
<> <>
<div className="mb-5 flex items-center gap-2"> {/* <div className="mb-5 flex items-center gap-2">
<div className="h-full w-full space-y-1"> <div className="h-full w-full space-y-1">
<div className="text-lg font-medium">Relocation Guide</div> <div className="text-lg font-medium">Relocation Guide</div>
<div className="text-sm"> <div className="text-sm">
@ -78,85 +77,87 @@ const IntegrationGuide = () => {
<ArrowRightIcon width={"18px"} color={"#3F76FF"} /> <ArrowRightIcon width={"18px"} color={"#3F76FF"} />
</div> </div>
</a> </a>
</div> </div> */}
<div className="space-y-2"> {IMPORTERS_EXPORTERS_LIST.map((service) => (
{IMPORTERS_EXPORTERS_LIST.map((service) => ( <div
<div key={service.provider}
key={service.provider} className="flex items-center justify-between gap-2 border-b border-custom-border-200 bg-custom-background-100 px-4 py-6"
className="rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4" >
> <div className="flex items-start gap-4">
<div className="flex items-center gap-4 whitespace-nowrap"> <div className="relative h-10 w-10 flex-shrink-0">
<div className="relative h-10 w-10 flex-shrink-0"> <Image
<Image src={service.logo}
src={service.logo} layout="fill"
layout="fill" objectFit="cover"
objectFit="cover" alt={`${service.title} Logo`}
alt={`${service.title} Logo`} />
/> </div>
</div> <div>
<div className="w-full"> <h3 className="flex items-center gap-4 text-sm font-medium">{service.title}</h3>
<h3>{service.title}</h3> <p className="text-sm text-custom-text-200 tracking-tight">
<p className="text-sm text-custom-text-200">{service.description}</p> {service.description}
</div> </p>
<div className="flex-shrink-0">
<Link
href={`/${workspaceSlug}/settings/imports?provider=${service.provider}`}
>
<a>
<PrimaryButton>
<span className="capitalize">{service.type}</span> now
</PrimaryButton>
</a>
</Link>
</div>
</div> </div>
</div> </div>
))} <div className="flex-shrink-0">
</div> <Link href={`/${workspaceSlug}/settings/imports?provider=${service.provider}`}>
<div className="rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4"> <a>
<h3 className="mb-2 flex gap-2 text-lg font-medium"> <PrimaryButton>
Previous Imports <span className="capitalize">{service.type}</span>
<button </PrimaryButton>
type="button" </a>
className="flex flex-shrink-0 items-center gap-1 rounded bg-custom-background-80 py-1 px-1.5 text-xs outline-none" </Link>
onClick={() => { </div>
setRefreshing(true); </div>
mutate(IMPORTER_SERVICES_LIST(workspaceSlug as string)).then(() => ))}
setRefreshing(false) <div>
); <div className="flex items-center pt-7 pb-3.5 border-b border-custom-border-200">
}} <h3 className="flex gap-2 text-xl font-medium">
> Previous Imports
<ArrowPathIcon className={`h-3 w-3 ${refreshing ? "animate-spin" : ""}`} />{" "} <button
{refreshing ? "Refreshing..." : "Refresh status"} type="button"
</button> className="flex flex-shrink-0 items-center gap-1 rounded bg-custom-background-80 py-1 px-1.5 text-xs outline-none"
</h3> onClick={() => {
{importerServices ? ( setRefreshing(true);
importerServices.length > 0 ? ( mutate(IMPORTER_SERVICES_LIST(workspaceSlug as string)).then(() =>
<div className="space-y-2"> setRefreshing(false)
<div className="divide-y divide-custom-border-200"> );
{importerServices.map((service) => ( }}
<SingleImport >
key={service.id} <ArrowPathIcon className={`h-3 w-3 ${refreshing ? "animate-spin" : ""}`} />{" "}
service={service} {refreshing ? "Refreshing..." : "Refresh status"}
refreshing={refreshing} </button>
handleDelete={() => handleDeleteImport(service)} </h3>
/> </div>
))} <div className="flex flex-col px-4 py-6">
{importerServices ? (
importerServices.length > 0 ? (
<div className="space-y-2">
<div className="divide-y divide-custom-border-200">
{importerServices.map((service) => (
<SingleImport
key={service.id}
service={service}
refreshing={refreshing}
handleDelete={() => handleDeleteImport(service)}
/>
))}
</div>
</div> </div>
</div> ) : (
<p className="py-2 text-sm text-custom-text-200">
No previous imports available.
</p>
)
) : ( ) : (
<p className="py-2 text-sm text-custom-text-200"> <Loader className="mt-6 grid grid-cols-1 gap-3">
No previous imports available. <Loader.Item height="40px" width="100%" />
</p> <Loader.Item height="40px" width="100%" />
) <Loader.Item height="40px" width="100%" />
) : ( <Loader.Item height="40px" width="100%" />
<Loader className="mt-6 grid grid-cols-1 gap-3"> </Loader>
<Loader.Item height="40px" width="100%" /> )}
<Loader.Item height="40px" width="100%" /> </div>
<Loader.Item height="40px" width="100%" />
<Loader.Item height="40px" width="100%" />
</Loader>
)}
</div> </div>
</> </>
)} )}

View File

@ -16,7 +16,7 @@ type Props = {
}; };
export const SingleImport: React.FC<Props> = ({ service, refreshing, handleDelete }) => ( export const SingleImport: React.FC<Props> = ({ service, refreshing, handleDelete }) => (
<div className="flex items-center justify-between gap-2 py-3"> <div className="flex items-center justify-between gap-2 px-4 py-3">
<div> <div>
<h4 className="flex items-center gap-2 text-sm"> <h4 className="flex items-center gap-2 text-sm">
<span> <span>

View File

@ -15,6 +15,7 @@ import { DangerButton, Loader, PrimaryButton } from "components/ui";
// icons // icons
import GithubLogo from "public/services/github.png"; import GithubLogo from "public/services/github.png";
import SlackLogo from "public/services/slack.png"; import SlackLogo from "public/services/slack.png";
import { CheckCircle2 } from "lucide-react";
// types // types
import { IAppIntegration, IWorkspaceIntegration } from "types"; import { IAppIntegration, IWorkspaceIntegration } from "types";
// fetch-keys // fetch-keys
@ -27,13 +28,12 @@ type Props = {
const integrationDetails: { [key: string]: any } = { const integrationDetails: { [key: string]: any } = {
github: { github: {
logo: GithubLogo, logo: GithubLogo,
installed: installed: "Activate GitHub on individual projects to sync with specific repositories.",
"Activate GitHub integrations on individual projects to sync with specific repositories.",
notInstalled: "Connect with GitHub with your Plane workspace to sync project issues.", notInstalled: "Connect with GitHub with your Plane workspace to sync project issues.",
}, },
slack: { slack: {
logo: SlackLogo, logo: SlackLogo,
installed: "Activate Slack integrations on individual projects to sync with specific channels.", installed: "Activate Slack on individual projects to sync with specific channels.",
notInstalled: "Connect with Slack with your Plane workspace to sync project issues.", notInstalled: "Connect with Slack with your Plane workspace to sync project issues.",
}, },
}; };
@ -99,31 +99,22 @@ export const SingleIntegrationCard: React.FC<Props> = ({ integration }) => {
); );
return ( return (
<div className="flex items-center justify-between gap-2 rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-5"> <div className="flex items-center justify-between gap-2 border-b border-custom-border-200 bg-custom-background-100 px-4 py-6">
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<div className="h-12 w-12 flex-shrink-0"> <div className="h-10 w-10 flex-shrink-0">
<Image <Image
src={integrationDetails[integration.provider].logo} src={integrationDetails[integration.provider].logo}
alt={`${integration.title} Logo`} alt={`${integration.title} Logo`}
/> />
</div> </div>
<div> <div>
<h3 className="flex items-center gap-4 text-xl font-semibold"> <h3 className="flex items-center gap-2 text-sm font-medium">
{integration.title} {integration.title}
{workspaceIntegrations ? ( {workspaceIntegrations
isInstalled ? ( ? isInstalled && <CheckCircle2 className="h-3.5 w-3.5 text-white fill-green-500" />
<span className="flex items-center gap-1 text-sm font-normal text-green-500"> : null}
<span className="h-1.5 w-1.5 flex-shrink-0 rounded-full bg-green-500" /> Installed
</span>
) : (
<span className="flex items-center gap-1 text-sm font-normal text-custom-text-200">
<span className="h-1.5 w-1.5 flex-shrink-0 rounded-full bg-custom-background-80" />{" "}
Not Installed
</span>
)
) : null}
</h3> </h3>
<p className="text-sm text-custom-text-200"> <p className="text-sm text-custom-text-200 tracking-tight">
{workspaceIntegrations {workspaceIntegrations
? isInstalled ? isInstalled
? integrationDetails[integration.provider].installed ? integrationDetails[integration.provider].installed
@ -135,12 +126,12 @@ export const SingleIntegrationCard: React.FC<Props> = ({ integration }) => {
{workspaceIntegrations ? ( {workspaceIntegrations ? (
isInstalled ? ( isInstalled ? (
<DangerButton onClick={handleRemoveIntegration} loading={deletingIntegration}> <DangerButton onClick={handleRemoveIntegration} loading={deletingIntegration} outline>
{deletingIntegration ? "Removing..." : "Remove installation"} {deletingIntegration ? "Uninstalling..." : "Uninstall"}
</DangerButton> </DangerButton>
) : ( ) : (
<PrimaryButton onClick={startAuth} loading={isInstalling}> <PrimaryButton onClick={startAuth} loading={isInstalling}>
{isInstalling ? "Installing..." : "Add installation"} {isInstalling ? "Installing..." : "Install"}
</PrimaryButton> </PrimaryButton>
) )
) : ( ) : (

View File

@ -16,8 +16,6 @@ import { Popover, Transition } from "@headlessui/react";
import issuesService from "services/issues.service"; import issuesService from "services/issues.service";
// ui // ui
import { Input, PrimaryButton, SecondaryButton } from "components/ui"; import { Input, PrimaryButton, SecondaryButton } from "components/ui";
// icons
import { Component } from "lucide-react";
// types // types
import { IIssueLabels } from "types"; import { IIssueLabels } from "types";
// fetch-keys // fetch-keys
@ -146,10 +144,10 @@ export const CreateUpdateLabelInline = forwardRef<HTMLDivElement, Props>(
open ? "text-custom-text-100" : "text-custom-text-200" open ? "text-custom-text-100" : "text-custom-text-200"
}`} }`}
> >
<Component <span
className="h-4 w-4 text-custom-text-100 flex-shrink-0" className="h-4 w-4 rounded-full"
style={{ style={{
color: watch("color"), backgroundColor: watch("color"),
}} }}
/> />
</Popover.Button> </Popover.Button>

View File

@ -43,7 +43,7 @@ export const SingleLabel: React.FC<Props> = ({
> >
<CustomMenu.MenuItem onClick={() => addLabelToGroup(label)}> <CustomMenu.MenuItem onClick={() => addLabelToGroup(label)}>
<span className="flex items-center justify-start gap-2"> <span className="flex items-center justify-start gap-2">
<RectangleGroupIcon className="h-4 w-4" /> <Component className="h-4 w-4 leading-4 text-custom-sidebar-text-400 flex-shrink-0" />
<span>Convert to group</span> <span>Convert to group</span>
</span> </span>
</CustomMenu.MenuItem> </CustomMenu.MenuItem>

View File

@ -79,7 +79,7 @@ const ConfirmProjectMemberRemove: React.FC<Props> = ({ isOpen, onClose, data, ha
</div> </div>
</div> </div>
</div> </div>
<div className="flex justify-end gap-2 bg-custom-background-90 p-4 sm:px-6"> <div className="flex justify-end gap-2 p-4 sm:px-6">
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton> <SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
<DangerButton onClick={handleDeletion} loading={isDeleteLoading}> <DangerButton onClick={handleDeletion} loading={isDeleteLoading}>
{isDeleteLoading ? "Removing..." : "Remove"} {isDeleteLoading ? "Removing..." : "Remove"}

View File

@ -49,7 +49,7 @@ export const MemberSelect: React.FC<Props> = ({ value, onChange }) => {
{selectedOption ? ( {selectedOption ? (
selectedOption?.display_name selectedOption?.display_name
) : ( ) : (
<span className="text-sm py-0.5 text-custom-text-200">Select</span> <span className="text-sm py-0.5 text-custom-sidebar-text-400">Select</span>
)} )}
</div> </div>
} }

View File

@ -219,7 +219,9 @@ const SendProjectInvitationModal: React.FC<Props> = (props) => {
} }
</div> </div>
) : ( ) : (
<div>Select co-worker</div> <div className="flex items-center gap-2 py-0.5">
Select co-worker
</div>
)} )}
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" /> <ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
</button> </button>
@ -249,10 +251,13 @@ const SendProjectInvitationModal: React.FC<Props> = (props) => {
render={({ field }) => ( render={({ field }) => (
<CustomSelect <CustomSelect
{...field} {...field}
label={ customButton={
<span className="capitalize"> <button className="flex w-full items-center justify-between gap-1 rounded-md border border-custom-border-200 shadow-sm duration-300 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-80 focus:outline-none px-3 py-2.5 text-sm text-left">
{field.value ? ROLE[field.value] : "Select role"} <span className="capitalize">
</span> {field.value ? ROLE[field.value] : "Select role"}
</span>
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
</button>
} }
input input
width="w-full" width="w-full"

View File

@ -2,7 +2,11 @@ import React from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import Link from "next/link"; import Link from "next/link";
export const SettingsSidebar = () => { type Props = {
profilePage?: boolean;
};
export const SettingsSidebar: React.FC<Props> = ({ profilePage = false }) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
@ -43,30 +47,107 @@ export const SettingsSidebar = () => {
href: `/${workspaceSlug}/projects/${projectId}/settings/automations`, href: `/${workspaceSlug}/projects/${projectId}/settings/automations`,
}, },
]; ];
const workspaceLinks: Array<{
label: string;
href: string;
}> = [
{
label: "General",
href: `/${workspaceSlug}/settings`,
},
{
label: "Members",
href: `/${workspaceSlug}/settings/members`,
},
{
label: "Billing & Plans",
href: `/${workspaceSlug}/settings/billing`,
},
{
label: "Integrations",
href: `/${workspaceSlug}/settings/integrations`,
},
{
label: "Imports",
href: `/${workspaceSlug}/settings/imports`,
},
{
label: "Exports",
href: `/${workspaceSlug}/settings/exports`,
},
];
const profileLinks: Array<{
label: string;
href: string;
}> = [
{
label: "Profile",
href: `/${workspaceSlug}/me/profile`,
},
{
label: "Activity",
href: `/${workspaceSlug}/me/profile/activity`,
},
{
label: "Preferences",
href: `/${workspaceSlug}/me/profile/preferences`,
},
];
return ( return (
<div className="flex flex-col gap-2 w-80 px-9"> <div className="flex flex-col gap-6 w-80 px-5">
<span className="text-xs text-custom-sidebar-text-400 font-semibold">SETTINGS</span> <div className="flex flex-col gap-2">
<div className="flex flex-col gap-1 w-full"> <span className="text-xs text-custom-sidebar-text-400 font-semibold">SETTINGS</span>
{projectLinks.map((link) => ( <div className="flex flex-col gap-1 w-full">
<Link key={link.href} href={link.href}> {(projectId ? projectLinks : workspaceLinks).map((link) => (
<a> <Link key={link.href} href={link.href}>
<div <a>
className={`px-4 py-2 text-sm font-medium rounded-md ${ <div
( className={`px-4 py-2 text-sm font-medium rounded-md ${
link.label === "Import" (
? router.asPath.includes(link.href) link.label === "Import"
: router.asPath === link.href ? router.asPath.includes(link.href)
) : router.asPath === link.href
? "bg-custom-primary-100/10 text-custom-primary-100" )
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80" ? "bg-custom-primary-100/10 text-custom-primary-100"
}`} : "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
> }`}
{link.label} >
</div> {link.label}
</a> </div>
</Link> </a>
))} </Link>
))}
</div>
</div> </div>
{!projectId && (
<div className="flex flex-col gap-2">
<span className="text-xs text-custom-sidebar-text-400 font-semibold">My Account</span>
<div className="flex flex-col gap-1 w-full">
{profileLinks.map((link) => (
<Link key={link.href} href={link.href}>
<a>
<div
className={`px-4 py-2 text-sm font-medium rounded-md ${
(
link.label === "Import"
? router.asPath.includes(link.href)
: router.asPath === link.href
)
? "bg-custom-primary-100/10 text-custom-primary-100"
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
}`}
>
{link.label}
</div>
</a>
</Link>
))}
</div>
</div>
)}
</div> </div>
); );
}; };

View File

@ -184,19 +184,20 @@ export const SingleState: React.FC<Props> = ({
<ArrowDownIcon className="h-4 w-4" /> <ArrowDownIcon className="h-4 w-4" />
</button> </button>
)} )}
{state.default ? (
<span className="text-xs text-custom-text-200">Default</span>
) : (
<button
type="button"
className="hidden text-xs text-custom-sidebar-text-400 group-hover:inline-block"
onClick={handleMakeDefault}
disabled={isSubmitting}
>
Mark as default
</button>
)}
<div className=" items-center gap-2.5 hidden group-hover:flex"> <div className=" items-center gap-2.5 hidden group-hover:flex">
{state.default ? (
<span className="text-xs text-custom-text-200">Default</span>
) : (
<button
type="button"
className="hidden text-xs text-custom-sidebar-text-400 group-hover:inline-block"
onClick={handleMakeDefault}
disabled={isSubmitting}
>
Mark as default
</button>
)}
<button <button
type="button" type="button"
className="grid place-items-center group-hover:opacity-100 opacity-0" className="grid place-items-center group-hover:opacity-100 opacity-0"
@ -215,14 +216,26 @@ export const SingleState: React.FC<Props> = ({
> >
{state.default ? ( {state.default ? (
<Tooltip tooltipContent="Cannot delete the default state."> <Tooltip tooltipContent="Cannot delete the default state.">
<X className="h-3.5 w-3.5 text-red-500" /> <X
className={`h-4 w-4 ${
groupLength < 1 ? "text-custom-sidebar-text-400" : "text-red-500"
}`}
/>
</Tooltip> </Tooltip>
) : groupLength === 1 ? ( ) : groupLength === 1 ? (
<Tooltip tooltipContent="Cannot have an empty group."> <Tooltip tooltipContent="Cannot have an empty group.">
<X className="h-3.5 w-3.5 text-red-500" /> <X
className={`h-4 w-4 ${
groupLength < 1 ? "text-custom-sidebar-text-400" : "text-red-500"
}`}
/>
</Tooltip> </Tooltip>
) : ( ) : (
<X className="h-3.5 w-3.5 text-red-500" /> <X
className={`h-4 w-4 ${
groupLength < 1 ? "text-custom-sidebar-text-400" : "text-red-500"
}`}
/>
)} )}
</button> </button>
</div> </div>

View File

@ -6,7 +6,7 @@ type Props = {
}; };
export const IntegrationAndImportExportBanner: React.FC<Props> = ({ bannerName, description }) => ( export const IntegrationAndImportExportBanner: React.FC<Props> = ({ bannerName, description }) => (
<div className="flex flex-col items-start gap-3 py-3.5 border-b border-custom-border-200"> <div className="flex items-start gap-3 py-3.5 border-b border-custom-border-200">
<h3 className="text-xl font-medium">{bannerName}</h3> <h3 className="text-xl font-medium">{bannerName}</h3>
{description && ( {description && (
<div className="flex items-center gap-3 rounded-[10px] border border-custom-primary/75 bg-custom-primary/5 p-4 text-sm text-custom-text-100"> <div className="flex items-center gap-3 rounded-[10px] border border-custom-primary/75 bg-custom-primary/5 p-4 text-sm text-custom-text-100">

View File

@ -73,7 +73,7 @@ const activityDetails: {
. .
</> </>
), ),
icon: <Icon iconName="group" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="group" className="!text-2xl" aria-hidden="true" />,
}, },
archived_at: { archived_at: {
@ -81,7 +81,7 @@ const activityDetails: {
if (activity.new_value === "restore") return "restored the issue."; if (activity.new_value === "restore") return "restored the issue.";
else return "archived the issue."; else return "archived the issue.";
}, },
icon: <Icon iconName="archive" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="archive" className="!text-2xl" aria-hidden="true" />,
}, },
attachment: { attachment: {
@ -99,7 +99,7 @@ const activityDetails: {
{showIssue && <IssueLink activity={activity} />} {showIssue && <IssueLink activity={activity} />}
</> </>
), ),
icon: <Icon iconName="attach_file" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="attach_file" className="!text-2xl" aria-hidden="true" />,
}, },
blocking: { blocking: {
@ -156,7 +156,7 @@ const activityDetails: {
</button> </button>
</> </>
), ),
icon: <Icon iconName="contrast" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="contrast" className="!text-2xl" aria-hidden="true" />,
}, },
description: { description: {
@ -172,7 +172,7 @@ const activityDetails: {
. .
</> </>
), ),
icon: <Icon iconName="chat" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="chat" className="!text-2xl" aria-hidden="true" />,
}, },
estimate_point: { estimate_point: {
@ -190,7 +190,7 @@ const activityDetails: {
)} )}
</> </>
), ),
icon: <Icon iconName="change_history" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="change_history" className="!text-2xl" aria-hidden="true" />,
}, },
issue: { issue: {
@ -198,7 +198,7 @@ const activityDetails: {
if (activity.verb === "created") return "created the issue."; if (activity.verb === "created") return "created the issue.";
else return "deleted an issue."; else return "deleted an issue.";
}, },
icon: <Icon iconName="stack" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="stack" className="!text-2xl" aria-hidden="true" />,
}, },
labels: { labels: {
@ -225,7 +225,7 @@ const activityDetails: {
)} )}
</> </>
), ),
icon: <Icon iconName="sell" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="sell" className="!text-2xl" aria-hidden="true" />,
}, },
link: { link: {
@ -255,7 +255,7 @@ const activityDetails: {
. .
</> </>
), ),
icon: <Icon iconName="link" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="link" className="!text-2xl" aria-hidden="true" />,
}, },
modules: { modules: {
@ -279,7 +279,7 @@ const activityDetails: {
. .
</> </>
), ),
icon: <Icon iconName="dataset" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="dataset" className="!text-2xl" aria-hidden="true" />,
}, },
name: { name: {
@ -295,7 +295,7 @@ const activityDetails: {
. .
</> </>
), ),
icon: <Icon iconName="chat" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="chat" className="!text-2xl" aria-hidden="true" />,
}, },
parent: { parent: {
@ -314,7 +314,7 @@ const activityDetails: {
. .
</> </>
), ),
icon: <Icon iconName="supervised_user_circle" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="supervised_user_circle" className="!text-2xl" aria-hidden="true" />,
}, },
priority: { priority: {
@ -333,7 +333,7 @@ const activityDetails: {
. .
</> </>
), ),
icon: <Icon iconName="signal_cellular_alt" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="signal_cellular_alt" className="!text-2xl" aria-hidden="true" />,
}, },
start_date: { start_date: {
@ -351,7 +351,7 @@ const activityDetails: {
)} )}
</> </>
), ),
icon: <Icon iconName="calendar_today" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="calendar_today" className="!text-2xl" aria-hidden="true" />,
}, },
state: { state: {
@ -389,7 +389,7 @@ const activityDetails: {
)} )}
</> </>
), ),
icon: <Icon iconName="calendar_today" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="calendar_today" className="!text-2xl" aria-hidden="true" />,
}, },
}; };

View File

@ -79,7 +79,7 @@ const ConfirmWorkspaceMemberRemove: React.FC<Props> = ({ isOpen, onClose, data,
</div> </div>
</div> </div>
</div> </div>
<div className="flex justify-end gap-2 bg-custom-background-90 p-4 sm:px-6"> <div className="flex justify-end gap-2 p-4 sm:px-6">
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton> <SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
<DangerButton onClick={handleDeletion} loading={isDeleteLoading}> <DangerButton onClick={handleDeletion} loading={isDeleteLoading}>
{isDeleteLoading ? "Removing..." : "Remove"} {isDeleteLoading ? "Removing..." : "Remove"}

View File

@ -6,7 +6,6 @@ export * from "./help-section";
export * from "./issues-list"; export * from "./issues-list";
export * from "./issues-pie-chart"; export * from "./issues-pie-chart";
export * from "./issues-stats"; export * from "./issues-stats";
export * from "./settings-header";
export * from "./sidebar-dropdown"; export * from "./sidebar-dropdown";
export * from "./sidebar-menu"; export * from "./sidebar-menu";
export * from "./sidebar-quick-action"; export * from "./sidebar-quick-action";

View File

@ -1,13 +0,0 @@
import SettingsNavbar from "layouts/settings-navbar";
export const SettingsHeader = () => (
<div className="mb-8 space-y-6">
<div>
<h3 className="text-2xl font-semibold">Workspace Settings</h3>
<p className="mt-1 text-sm text-custom-text-200">
This information will be displayed to every member of the workspace.
</p>
</div>
<SettingsNavbar />
</div>
);

View File

@ -1,127 +0,0 @@
import Link from "next/link";
import { useRouter } from "next/router";
type Props = {
profilePage?: boolean;
};
const SettingsNavbar: React.FC<Props> = ({ profilePage = false }) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const workspaceLinks: Array<{
label: string;
href: string;
}> = [
{
label: "General",
href: `/${workspaceSlug}/settings`,
},
{
label: "Members",
href: `/${workspaceSlug}/settings/members`,
},
{
label: "Billing & Plans",
href: `/${workspaceSlug}/settings/billing`,
},
{
label: "Integrations",
href: `/${workspaceSlug}/settings/integrations`,
},
{
label: "Imports",
href: `/${workspaceSlug}/settings/imports`,
},
{
label: "Exports",
href: `/${workspaceSlug}/settings/exports`,
},
];
const projectLinks: Array<{
label: string;
href: string;
}> = [
{
label: "General",
href: `/${workspaceSlug}/projects/${projectId}/settings`,
},
{
label: "Control",
href: `/${workspaceSlug}/projects/${projectId}/settings/control`,
},
{
label: "Members",
href: `/${workspaceSlug}/projects/${projectId}/settings/members`,
},
{
label: "Features",
href: `/${workspaceSlug}/projects/${projectId}/settings/features`,
},
{
label: "States",
href: `/${workspaceSlug}/projects/${projectId}/settings/states`,
},
{
label: "Labels",
href: `/${workspaceSlug}/projects/${projectId}/settings/labels`,
},
{
label: "Integrations",
href: `/${workspaceSlug}/projects/${projectId}/settings/integrations`,
},
{
label: "Estimates",
href: `/${workspaceSlug}/projects/${projectId}/settings/estimates`,
},
{
label: "Automations",
href: `/${workspaceSlug}/projects/${projectId}/settings/automations`,
},
];
const profileLinks: Array<{
label: string;
href: string;
}> = [
{
label: "General",
href: `/${workspaceSlug}/me/profile`,
},
{
label: "Activity",
href: `/${workspaceSlug}/me/profile/activity`,
},
{
label: "Preferences",
href: `/${workspaceSlug}/me/profile/preferences`,
},
];
return (
<div className="flex flex-wrap gap-4">
{(profilePage ? profileLinks : projectId ? projectLinks : workspaceLinks).map((link) => (
<Link key={link.href} href={link.href}>
<a>
<div
className={`rounded-full border px-5 py-1.5 text-sm outline-none ${
(
link.label === "Import"
? router.asPath.includes(link.href)
: router.asPath === link.href
)
? "border-custom-primary bg-custom-primary text-white"
: "border-custom-border-200 bg-custom-background-100 hover:bg-custom-background-90"
}`}
>
{link.label}
</div>
</a>
</Link>
))}
</div>
);
};
export default SettingsNavbar;

View File

@ -7,7 +7,6 @@ import Link from "next/link";
import userService from "services/user.service"; import userService from "services/user.service";
// layouts // layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import SettingsNavbar from "layouts/settings-navbar";
// components // components
import { ActivityIcon, ActivityMessage } from "components/core"; import { ActivityIcon, ActivityMessage } from "components/core";
import { TipTapEditor } from "components/tiptap"; import { TipTapEditor } from "components/tiptap";
@ -20,6 +19,7 @@ import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
import { USER_ACTIVITY } from "constants/fetch-keys"; import { USER_ACTIVITY } from "constants/fetch-keys";
// helper // helper
import { timeAgo } from "helpers/date-time.helper"; import { timeAgo } from "helpers/date-time.helper";
import { SettingsSidebar } from "components/project";
const ProfileActivity = () => { const ProfileActivity = () => {
const router = useRouter(); const router = useRouter();
@ -38,186 +38,179 @@ const ProfileActivity = () => {
</Breadcrumbs> </Breadcrumbs>
} }
> >
<div className="p-8"> <div className="flex flex-row gap-2">
<div className="mb-8 space-y-6"> <div className="w-80 py-8">
<div> <SettingsSidebar />
<h3 className="text-3xl font-semibold">Profile Settings</h3>
<p className="mt-1 text-custom-text-200">
This information will be visible to only you.
</p>
</div>
<SettingsNavbar profilePage />
</div> </div>
{userActivity ? (
<div>
<ul role="list" className="-mb-4">
{userActivity.results.map((activityItem: any, activityIdx: number) => {
if (activityItem.field === "comment") {
return (
<div key={activityItem.id} className="mt-2">
<div className="relative flex items-start space-x-3">
<div className="relative px-1">
{activityItem.field ? (
activityItem.new_value === "restore" && (
<Icon iconName="history" className="text-sm text-custom-text-200" />
)
) : activityItem.actor_detail.avatar &&
activityItem.actor_detail.avatar !== "" ? (
<img
src={activityItem.actor_detail.avatar}
alt={activityItem.actor_detail.display_name}
height={30}
width={30}
className="grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white"
/>
) : (
<div
className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white`}
>
{activityItem.actor_detail.display_name?.charAt(0)}
</div>
)}
<span className="ring-6 flex h-7 w-7 items-center justify-center rounded-full bg-custom-background-80 text-custom-text-200 ring-white"> {userActivity ? (
<ChatBubbleLeftEllipsisIcon <section className="pr-9 py-8 w-full">
className="h-3.5 w-3.5 text-custom-text-200" <div className="flex items-center py-3.5 border-b border-custom-border-200">
aria-hidden="true" <h3 className="text-xl font-medium">Acitivity</h3>
/> </div>
</span> <div className={`flex flex-col gap-2 py-4 w-full`}>
</div> <ul role="list" className="-mb-4">
<div className="min-w-0 flex-1"> {userActivity.results.map((activityItem: any, activityIdx: number) => {
<div> if (activityItem.field === "comment") {
<div className="text-xs"> return (
{activityItem.actor_detail.is_bot <div key={activityItem.id} className="mt-2">
? activityItem.actor_detail.first_name + " Bot" <div className="relative flex items-start space-x-3">
: activityItem.actor_detail.display_name} <div className="relative px-1">
</div> {activityItem.field ? (
<p className="mt-0.5 text-xs text-custom-text-200"> activityItem.new_value === "restore" && (
Commented {timeAgo(activityItem.created_at)} <Icon iconName="history" className="text-sm text-custom-text-200" />
</p> )
) : activityItem.actor_detail.avatar &&
activityItem.actor_detail.avatar !== "" ? (
<img
src={activityItem.actor_detail.avatar}
alt={activityItem.actor_detail.display_name}
height={30}
width={30}
className="grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white"
/>
) : (
<div
className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white`}
>
{activityItem.actor_detail.display_name?.charAt(0)}
</div>
)}
<span className="ring-6 flex h-6 w-6 items-center justify-center rounded-full bg-custom-background-80 text-custom-text-200 ring-white">
<ChatBubbleLeftEllipsisIcon
className="h-6 w-6 !text-2xl text-custom-text-200"
aria-hidden="true"
/>
</span>
</div> </div>
<div className="issue-comments-section p-0"> <div className="min-w-0 flex-1">
<TipTapEditor <div>
workspaceSlug={workspaceSlug as string} <div className="text-xs">
value={ {activityItem.actor_detail.is_bot
activityItem?.new_value !== "" ? activityItem.actor_detail.first_name + " Bot"
? activityItem.new_value : activityItem.actor_detail.display_name}
: activityItem.old_value </div>
} <p className="mt-0.5 text-xs text-custom-text-200">
customClassName="text-xs border border-custom-border-200 bg-custom-background-100" Commented {timeAgo(activityItem.created_at)}
noBorder </p>
borderOnFocus={false} </div>
editable={false} <div className="issue-comments-section p-0">
/> <TipTapEditor
workspaceSlug={workspaceSlug as string}
value={
activityItem?.new_value !== ""
? activityItem.new_value
: activityItem.old_value
}
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
noBorder
borderOnFocus={false}
editable={false}
/>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> );
); }
}
const message = const message =
activityItem.verb === "created" && activityItem.verb === "created" &&
activityItem.field !== "cycles" && activityItem.field !== "cycles" &&
activityItem.field !== "modules" && activityItem.field !== "modules" &&
activityItem.field !== "attachment" && activityItem.field !== "attachment" &&
activityItem.field !== "link" && activityItem.field !== "link" &&
activityItem.field !== "estimate" ? ( activityItem.field !== "estimate" ? (
<span className="text-custom-text-200"> <span className="text-custom-text-200">
created{" "} created{" "}
<Link <Link
href={`/${workspaceSlug}/projects/${activityItem.project}/issues/${activityItem.issue}`} href={`/${workspaceSlug}/projects/${activityItem.project}/issues/${activityItem.issue}`}
> >
<a className="inline-flex items-center hover:underline"> <a className="inline-flex items-center hover:underline">
this issue. <ArrowTopRightOnSquareIcon className="ml-1 h-3.5 w-3.5" /> this issue. <ArrowTopRightOnSquareIcon className="ml-1 h-3.5 w-3.5" />
</a> </a>
</Link> </Link>
</span> </span>
) : activityItem.field ? ( ) : activityItem.field ? (
<ActivityMessage activity={activityItem} showIssue /> <ActivityMessage activity={activityItem} showIssue />
) : ( ) : (
"created the issue." "created the issue."
); );
if ("field" in activityItem && activityItem.field !== "updated_by") { if ("field" in activityItem && activityItem.field !== "updated_by") {
return ( return (
<li key={activityItem.id}> <li key={activityItem.id}>
<div className="relative pb-1"> <div className="relative pb-1">
{userActivity.results.length > 1 && <div className="relative flex items-center space-x-2">
activityIdx !== userActivity.results.length - 1 ? ( <>
<span <div>
className="absolute top-5 left-5 -ml-px h-full w-0.5 bg-custom-background-80" <div className="relative px-1.5">
aria-hidden="true" <div className="mt-1.5">
/> <div className="flex h-6 w-6 items-center justify-center">
) : null} {activityItem.field ? (
<div className="relative flex items-start space-x-2"> activityItem.new_value === "restore" ? (
<> <Icon
<div> iconName="history"
<div className="relative px-1.5"> className="!text-2xl text-custom-text-200"
<div className="mt-1.5"> />
<div className="ring-6 flex h-7 w-7 items-center justify-center rounded-full bg-custom-background-80 text-custom-text-200 ring-white"> ) : (
{activityItem.field ? ( <ActivityIcon activity={activityItem} />
activityItem.new_value === "restore" ? ( )
<Icon ) : activityItem.actor_detail.avatar &&
iconName="history" activityItem.actor_detail.avatar !== "" ? (
className="text-sm text-custom-text-200" <img
src={activityItem.actor_detail.avatar}
alt={activityItem.actor_detail.display_name}
height={24}
width={24}
className="rounded-full"
/> />
) : ( ) : (
<ActivityIcon activity={activityItem} /> <div
) className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-700 text-xs text-white`}
) : activityItem.actor_detail.avatar && >
activityItem.actor_detail.avatar !== "" ? ( {activityItem.actor_detail.display_name?.charAt(0)}
<img </div>
src={activityItem.actor_detail.avatar} )}
alt={activityItem.actor_detail.display_name} </div>
height={24}
width={24}
className="rounded-full"
/>
) : (
<div
className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-700 text-xs text-white`}
>
{activityItem.actor_detail.display_name?.charAt(0)}
</div>
)}
</div> </div>
</div> </div>
</div> </div>
</div> <div className="min-w-0 flex-1 py-4 border-b border-custom-border-200">
<div className="min-w-0 flex-1 py-3"> <div className="text-sm text-custom-text-200 break-words">
<div className="text-xs text-custom-text-200 break-words"> {activityItem.field === "archived_at" &&
{activityItem.field === "archived_at" && activityItem.new_value !== "restore" ? (
activityItem.new_value !== "restore" ? ( <span className="text-gray font-medium">Plane</span>
<span className="text-gray font-medium">Plane</span> ) : activityItem.actor_detail.is_bot ? (
) : activityItem.actor_detail.is_bot ? ( <span className="text-gray font-medium">
<span className="text-gray font-medium"> {activityItem.actor_detail.first_name} Bot
{activityItem.actor_detail.first_name} Bot </span>
) : (
<Link
href={`/${workspaceSlug}/profile/${activityItem.actor_detail.id}`}
>
<a className="text-gray font-medium">
{activityItem.actor_detail.display_name}
</a>
</Link>
)}{" "}
{message}{" "}
<span className="whitespace-nowrap">
{timeAgo(activityItem.created_at)}
</span> </span>
) : ( </div>
<Link
href={`/${workspaceSlug}/profile/${activityItem.actor_detail.id}`}
>
<a className="text-gray font-medium">
{activityItem.actor_detail.display_name}
</a>
</Link>
)}{" "}
{message}{" "}
<span className="whitespace-nowrap">
{timeAgo(activityItem.created_at)}
</span>
</div> </div>
</div> </>
</> </div>
</div> </div>
</div> </li>
</li> );
); }
} })}
})} </ul>
</ul> </div>
</div> </section>
) : ( ) : (
<Loader className="space-y-5"> <Loader className="space-y-5">
<Loader.Item height="40px" /> <Loader.Item height="40px" />

View File

@ -1,4 +1,6 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
import Link from "next/link";
// react-hook-form // react-hook-form
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
@ -10,21 +12,15 @@ import useUserAuth from "hooks/use-user-auth";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// layouts // layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import SettingsNavbar from "layouts/settings-navbar";
// components // components
import { ImagePickerPopover, ImageUploadModal } from "components/core"; import { ImagePickerPopover, ImageUploadModal } from "components/core";
import { SettingsSidebar } from "components/project";
// ui // ui
import { import { CustomSearchSelect, CustomSelect, Input, PrimaryButton, Spinner } from "components/ui";
CustomSearchSelect,
CustomSelect,
DangerButton,
Input,
SecondaryButton,
Spinner,
} from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons // icons
import { UserIcon } from "@heroicons/react/24/outline"; import { UserIcon } from "@heroicons/react/24/outline";
import { UserCircle } from "lucide-react";
// types // types
import type { NextPage } from "next"; import type { NextPage } from "next";
import type { IUser } from "types"; import type { IUser } from "types";
@ -46,6 +42,9 @@ const Profile: NextPage = () => {
const [isRemoving, setIsRemoving] = useState(false); const [isRemoving, setIsRemoving] = useState(false);
const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false); const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false);
const router = useRouter();
const { workspaceSlug } = router.query;
const { const {
register, register,
handleSubmit, handleSubmit,
@ -126,6 +125,7 @@ const Profile: NextPage = () => {
if (!prevData) return prevData; if (!prevData) return prevData;
return { ...prevData, avatar: "" }; return { ...prevData, avatar: "" };
}, false); }, false);
setIsRemoving(false);
}) })
.catch(() => { .catch(() => {
setToastAlert({ setToastAlert({
@ -155,6 +155,8 @@ const Profile: NextPage = () => {
<ImageUploadModal <ImageUploadModal
isOpen={isImageUploadModalOpen} isOpen={isImageUploadModalOpen}
onClose={() => setIsImageUploadModalOpen(false)} onClose={() => setIsImageUploadModalOpen(false)}
isRemoving={isRemoving}
handleDelete={() => handleDelete(myProfile?.avatar, true)}
onSuccess={(url) => { onSuccess={(url) => {
setValue("avatar", url); setValue("avatar", url);
handleSubmit(onSubmit)(); handleSubmit(onSubmit)();
@ -164,81 +166,49 @@ const Profile: NextPage = () => {
userImage userImage
/> />
{myProfile ? ( {myProfile ? (
<div className="p-8"> <form onSubmit={handleSubmit(onSubmit)}>
<div className="mb-8 space-y-6"> <div className="flex flex-row gap-2">
<div> <div className="w-80 py-8">
<h3 className="text-3xl font-semibold">Profile Settings</h3> <SettingsSidebar />
<p className="mt-1 text-custom-text-200">
This information will be visible to only you.
</p>
</div> </div>
<SettingsNavbar profilePage /> <div className={`flex flex-col gap-8 pr-9 py-9 w-full`}>
</div> <div className="relative h-44 w-full mt-6">
<form onSubmit={handleSubmit(onSubmit)} className="space-y-8 sm:space-y-12"> <img
<div className="grid grid-cols-12 gap-4 sm:gap-16"> src={
<div className="col-span-12 sm:col-span-6"> watch("cover_image") ??
<h4 className="text-lg font-semibold text-custom-text-100">Profile Picture</h4> "https://images.unsplash.com/photo-1506383796573-caf02b4a79ab"
<p className="text-sm text-custom-text-200"> }
Max file size is 5MB. Supported file types are .jpg and .png. className="h-44 w-full rounded-lg object-cover"
</p> alt={myProfile?.name ?? "Cover image"}
</div> />
<div className="col-span-12 sm:col-span-6"> <div className="flex items-end justify-between absolute left-8 -bottom-6">
<div className="flex items-center gap-4"> <div className="flex gap-3">
<button type="button" onClick={() => setIsImageUploadModalOpen(true)}> <div className="flex items-center justify-center bg-custom-background-90 h-16 w-16 rounded-lg">
{!watch("avatar") || watch("avatar") === "" ? ( <button type="button" onClick={() => setIsImageUploadModalOpen(true)}>
<div className="h-12 w-12 rounded-md bg-custom-background-80 p-2"> {!watch("avatar") || watch("avatar") === "" ? (
<UserIcon className="h-full w-full text-custom-text-200" /> <div className="h-16 w-16 rounded-md bg-custom-background-80 p-2">
</div> <UserIcon className="h-full w-full text-custom-text-200" />
) : ( </div>
<div className="relative h-12 w-12 overflow-hidden"> ) : (
<img <div className="relative h-16 w-16 overflow-hidden">
src={watch("avatar")} <img
className="absolute top-0 left-0 h-full w-full object-cover rounded-md" src={watch("avatar")}
onClick={() => setIsImageUploadModalOpen(true)} className="absolute top-0 left-0 h-full w-full object-cover rounded-lg"
alt={myProfile.display_name} onClick={() => setIsImageUploadModalOpen(true)}
/> alt={myProfile.display_name}
</div> />
)} </div>
</button> )}
<div className="flex items-center gap-2"> </button>
<SecondaryButton </div>
onClick={() => {
setIsImageUploadModalOpen(true);
}}
>
Upload
</SecondaryButton>
{myProfile.avatar && myProfile.avatar !== "" && (
<DangerButton
onClick={() => handleDelete(myProfile.avatar, true)}
loading={isRemoving}
>
{isRemoving ? "Removing..." : "Remove"}
</DangerButton>
)}
</div> </div>
</div> </div>
</div>
</div> <div className="flex absolute right-3 bottom-3">
<div className="grid grid-cols-12 gap-4 sm:gap-16"> <Controller
<div className="col-span-12 sm:col-span-6"> control={control}
<h4 className="text-lg font-semibold">Cover Photo</h4> name="cover_image"
<p className="text-sm text-custom-text-200"> render={() => (
Select your cover photo from the given library.
</p>
</div>
<div className="col-span-12 sm:col-span-6">
<div className="h-32 w-full rounded border border-custom-border-200 p-1">
<div className="relative h-full w-full rounded">
<img
src={
watch("cover_image") ??
"https://images.unsplash.com/photo-1506383796573-caf02b4a79ab"
}
className="absolute top-0 left-0 h-full w-full object-cover rounded"
alt={myProfile?.name ?? "Cover image"}
/>
<div className="absolute bottom-0 flex w-full justify-end">
<ImagePickerPopover <ImagePickerPopover
label={"Change cover"} label={"Change cover"}
onChange={(imageUrl) => { onChange={(imageUrl) => {
@ -249,157 +219,167 @@ const Profile: NextPage = () => {
"https://images.unsplash.com/photo-1506383796573-caf02b4a79ab" "https://images.unsplash.com/photo-1506383796573-caf02b4a79ab"
} }
/> />
</div> )}
/>
</div>
</div>
<div className="flex item-center justify-between px-8 mt-4">
<div className="flex flex-col">
<div className="flex item-center text-lg font-semibold text-custom-text-100">
<span>{`${watch("first_name")} ${watch("last_name")}`}</span>
</div> </div>
<span className="text-sm tracking-tight">{watch("email")}</span>
</div>
<Link href={`/${workspaceSlug}/profile/${myProfile.id}`}>
<a className="flex item-center cursor-pointer gap-2 h-4 leading-4 text-sm text-custom-primary-100">
<span className="h-4 w-4">
<UserCircle className="h-4 w-4" />
</span>
View Profile
</a>
</Link>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3 gap-6 px-8">
<div className="flex flex-col gap-1">
<h4 className="text-sm">First Name</h4>
<Input
name="first_name"
id="first_name"
register={register}
error={errors.first_name}
placeholder="Enter your first name"
className="!px-3 !py-2 rounded-md font-medium"
autoComplete="off"
/>
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">Last Name</h4>
<Input
name="last_name"
register={register}
error={errors.last_name}
id="last_name"
placeholder="Enter your last name"
autoComplete="off"
className="!px-3 !py-2 rounded-md font-medium"
/>
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">Email</h4>
<Input
id="email"
name="email"
autoComplete="off"
register={register}
className="!px-3 !py-2 rounded-md font-medium"
error={errors.name}
disabled
/>
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">Role</h4>
<Controller
name="role"
control={control}
rules={{ required: "This field is required" }}
render={({ field: { value, onChange } }) => (
<CustomSelect
value={value}
onChange={onChange}
label={value ? value.toString() : "Select your role"}
buttonClassName={errors.role ? "border-red-500 bg-red-500/10" : ""}
width="w-full"
input
verticalPosition="top"
position="right"
>
{USER_ROLES.map((item) => (
<CustomSelect.Option key={item.value} value={item.value}>
{item.label}
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
{errors.role && (
<span className="text-xs text-red-500">Please select a role</span>
)}
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">Display name </h4>
<Input
id="display_name"
name="display_name"
autoComplete="off"
register={register}
error={errors.display_name}
className="w-full"
placeholder="Enter your display name"
validations={{
required: "Display name is required.",
validate: (value) => {
if (value.trim().length < 1) return "Display name can't be empty.";
if (value.split(" ").length > 1)
return "Display name can't have two consecutive spaces.";
if (value.replace(/\s/g, "").length < 1)
return "Display name must be at least 1 characters long.";
if (value.replace(/\s/g, "").length > 20)
return "Display name must be less than 20 characters long.";
return true;
},
}}
/>
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">Timezone </h4>
<Controller
name="user_timezone"
control={control}
rules={{ required: "This field is required" }}
render={({ field: { value, onChange } }) => (
<CustomSearchSelect
value={value}
label={
value
? TIME_ZONES.find((t) => t.value === value)?.label ?? value
: "Select a timezone"
}
options={timeZoneOptions}
onChange={onChange}
verticalPosition="top"
optionsClassName="w-full"
input
/>
)}
/>
{errors.role && (
<span className="text-xs text-red-500">Please select a role</span>
)}
</div>
<div className="flex items-center justify-between py-2">
<PrimaryButton type="submit" loading={isSubmitting}>
{isSubmitting ? "Updating Project..." : "Update Project"}
</PrimaryButton>
</div> </div>
</div> </div>
</div> </div>
<div className="grid grid-cols-12 gap-4 sm:gap-16"> </div>
<div className="col-span-12 sm:col-span-6"> </form>
<h4 className="text-lg font-semibold text-custom-text-100">Full Name</h4>
</div>
<div className="col-span-12 flex items-center gap-2 sm:col-span-6">
<Input
name="first_name"
id="first_name"
register={register}
error={errors.first_name}
placeholder="Enter your first name"
autoComplete="off"
/>
<Input
name="last_name"
register={register}
error={errors.last_name}
id="last_name"
placeholder="Enter your last name"
autoComplete="off"
/>
</div>
</div>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold text-custom-text-100">Display Name</h4>
<p className="text-sm text-custom-text-200">
This could be your first name, or a nickname however you{"'"}d like people to
refer to you in Plane.
</p>
</div>
<div className="col-span-12 sm:col-span-6">
<Input
id="display_name"
name="display_name"
autoComplete="off"
register={register}
error={errors.display_name}
className="w-full"
placeholder="Enter your display name"
validations={{
required: "Display name is required.",
validate: (value) => {
if (value.trim().length < 1) return "Display name can't be empty.";
if (value.split(" ").length > 1)
return "Display name can't have two consecutive spaces.";
if (value.replace(/\s/g, "").length < 1)
return "Display name must be at least 1 characters long.";
if (value.replace(/\s/g, "").length > 20)
return "Display name must be less than 20 characters long.";
return true;
},
}}
/>
</div>
</div>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold text-custom-text-100">Email</h4>
<p className="text-sm text-custom-text-200">
The email address that you are using.
</p>
</div>
<div className="col-span-12 sm:col-span-6">
<Input
id="email"
name="email"
autoComplete="off"
register={register}
error={errors.name}
className="w-full"
disabled
/>
</div>
</div>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold text-custom-text-100">Role</h4>
<p className="text-sm text-custom-text-200">Add your role.</p>
</div>
<div className="col-span-12 sm:col-span-6">
<Controller
name="role"
control={control}
rules={{ required: "This field is required" }}
render={({ field: { value, onChange } }) => (
<CustomSelect
value={value}
onChange={onChange}
label={value ? value.toString() : "Select your role"}
buttonClassName={errors.role ? "border-red-500 bg-red-500/10" : ""}
width="w-full"
input
position="right"
>
{USER_ROLES.map((item) => (
<CustomSelect.Option key={item.value} value={item.value}>
{item.label}
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
{errors.role && <span className="text-xs text-red-500">Please select a role</span>}
</div>
</div>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold text-custom-text-100">Timezone</h4>
<p className="text-sm text-custom-text-200">Select a timezone</p>
</div>
<div className="col-span-12 sm:col-span-6">
<Controller
name="user_timezone"
control={control}
rules={{ required: "This field is required" }}
render={({ field: { value, onChange } }) => (
<CustomSearchSelect
value={value}
label={
value
? TIME_ZONES.find((t) => t.value === value)?.label ?? value
: "Select a timezone"
}
options={timeZoneOptions}
onChange={onChange}
verticalPosition="top"
optionsClassName="w-full"
input
/>
)}
/>
{errors.role && <span className="text-xs text-red-500">Please select a role</span>}
</div>
</div>
<div className="sm:text-right">
<SecondaryButton type="submit" loading={isSubmitting}>
{isSubmitting ? "Updating..." : "Update profile"}
</SecondaryButton>
</div>
</form>
</div>
) : ( ) : (
<div className="grid h-full w-full place-items-center px-4 sm:px-0"> <div className="grid h-full w-full place-items-center px-4 sm:px-0">
<Spinner /> <Spinner />

View File

@ -3,7 +3,6 @@ import { useEffect, useState } from "react";
import useUserAuth from "hooks/use-user-auth"; import useUserAuth from "hooks/use-user-auth";
// layouts // layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import SettingsNavbar from "layouts/settings-navbar";
// components // components
import { CustomThemeSelector, ThemeSwitch } from "components/core"; import { CustomThemeSelector, ThemeSwitch } from "components/core";
// ui // ui
@ -15,6 +14,7 @@ import { ICustomTheme } from "types";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
import { SettingsSidebar } from "components/project";
const ProfilePreferences = observer(() => { const ProfilePreferences = observer(() => {
const { user: myProfile } = useUserAuth(); const { user: myProfile } = useUserAuth();
@ -59,18 +59,16 @@ const ProfilePreferences = observer(() => {
} }
> >
{myProfile ? ( {myProfile ? (
<div className="p-8"> <div className="flex flex-row gap-2">
<div className="mb-8 space-y-6"> <div className="w-80 py-8">
<div> <SettingsSidebar />
<h3 className="text-3xl font-semibold">Profile Settings</h3>
<p className="mt-1 text-custom-text-200">
This information will be visible to only you.
</p>
</div>
<SettingsNavbar profilePage />
</div> </div>
<div className="space-y-8 sm:space-y-12">
<div className="grid grid-cols-12 gap-4 sm:gap-16"> <div className="pr-9 py-8 w-full">
<div className="flex items-center py-3.5 border-b border-custom-border-200">
<h3 className="text-xl font-medium">Acitivity</h3>
</div>
<div className="grid grid-cols-12 gap-4 sm:gap-16 py-6">
<div className="col-span-12 sm:col-span-6"> <div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold text-custom-text-100">Theme</h4> <h4 className="text-lg font-semibold text-custom-text-100">Theme</h4>
<p className="text-sm text-custom-text-200"> <p className="text-sm text-custom-text-200">

View File

@ -19,7 +19,8 @@ import { ToggleSwitch } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons // icons
import { ModuleIcon } from "components/icons"; import { ModuleIcon } from "components/icons";
import { Contrast, FileText, Inbox, Layers } from "lucide-react"; import { FileText, Inbox, Layers } from "lucide-react";
import { ContrastOutlined } from "@mui/icons-material";
// types // types
import { IProject } from "types"; import { IProject } from "types";
import type { NextPage } from "next"; import type { NextPage } from "next";
@ -33,7 +34,10 @@ const featuresList = [
title: "Cycles", title: "Cycles",
description: description:
"Cycles are enabled for all the projects in this workspace. Access them from the sidebar.", "Cycles are enabled for all the projects in this workspace. Access them from the sidebar.",
icon: <Contrast className="h-4 w-4 text-custom-primary-100 flex-shrink-0" />, icon: (
<ContrastOutlined className="!text-base !leading-4 text-purple-500 flex-shrink-0 rotate-180" />
),
property: "cycle_view", property: "cycle_view",
}, },
{ {
@ -61,7 +65,7 @@ const featuresList = [
title: "Inbox", title: "Inbox",
description: description:
"Inbox are enabled for all the projects in this workspace. Access it from the issues views page.", "Inbox are enabled for all the projects in this workspace. Access it from the issues views page.",
icon: <Inbox className="h-4 w-4 text-cyan-500 flex-shrink-0" />, icon: <Inbox className="h-4 w-4 text-fuchsia-500 flex-shrink-0" />,
property: "inbox_view", property: "inbox_view",
}, },
]; ];

View File

@ -25,7 +25,6 @@ import {
TextArea, TextArea,
Loader, Loader,
CustomSelect, CustomSelect,
SecondaryButton,
DangerButton, DangerButton,
Icon, Icon,
PrimaryButton, PrimaryButton,
@ -67,7 +66,7 @@ const GeneralSettings: NextPage = () => {
: null : null
); );
const { data: memberDetails, error } = useSWR( const { data: memberDetails } = useSWR(
workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId.toString()) : null, workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId.toString()) : null,
workspaceSlug && projectId workspaceSlug && projectId
? () => projectService.projectMemberMe(workspaceSlug.toString(), projectId.toString()) ? () => projectService.projectMemberMe(workspaceSlug.toString(), projectId.toString())
@ -168,6 +167,7 @@ const GeneralSettings: NextPage = () => {
}; };
const currentNetwork = NETWORK_CHOICES.find((n) => n.key === projectDetails?.network); const currentNetwork = NETWORK_CHOICES.find((n) => n.key === projectDetails?.network);
const selectedNetwork = NETWORK_CHOICES.find((n) => n.key === watch("network"));
const isAdmin = memberDetails?.role === 20; const isAdmin = memberDetails?.role === 20;
@ -350,7 +350,7 @@ const GeneralSettings: NextPage = () => {
<CustomSelect <CustomSelect
value={value} value={value}
onChange={onChange} onChange={onChange}
label={currentNetwork?.label ?? "Select network"} label={selectedNetwork?.label ?? "Select network"}
className="!border-custom-border-200 !shadow-none" className="!border-custom-border-200 !shadow-none"
input input
disabled={!isAdmin} disabled={!isAdmin}
@ -388,59 +388,60 @@ const GeneralSettings: NextPage = () => {
)} )}
</div> </div>
</div> </div>
{isAdmin && (
<Disclosure as="div" className="border-t border-custom-border-400">
{({ open }) => (
<div className="w-full">
<Disclosure.Button
as="button"
type="button"
className="flex items-center justify-between w-full py-4"
>
<span className="text-xl tracking-tight">Delete Project</span>
<Icon iconName={open ? "expand_less" : "expand_more"} className="!text-2xl" />
</Disclosure.Button>
<Disclosure as="div" className="border-t border-custom-border-400"> <Transition
{({ open }) => ( show={open}
<div className="w-full"> enter="transition duration-100 ease-out"
<Disclosure.Button enterFrom="transform opacity-0"
as="button" enterTo="transform opacity-100"
type="button" leave="transition duration-75 ease-out"
className="flex items-center justify-between w-full py-4" leaveFrom="transform opacity-100"
> leaveTo="transform opacity-0"
<span className="text-xl tracking-tight">Danger Zone</span> >
<Icon iconName={open ? "expand_more" : "expand_less"} className="!text-2xl" /> <Disclosure.Panel>
</Disclosure.Button> <div className="flex flex-col gap-8">
<span className="text-sm tracking-tight">
<Transition The danger zone of the project delete page is a critical area that
show={open} requires careful consideration and attention. When deleting a project,
enter="transition duration-100 ease-out" all of the data and resources within that project will be permanently
enterFrom="transform opacity-0" removed and cannot be recovered.
enterTo="transform opacity-100" </span>
leave="transition duration-75 ease-out" <div>
leaveFrom="transform opacity-100" {projectDetails ? (
leaveTo="transform opacity-0" <div>
> <DangerButton
<Disclosure.Panel> onClick={() => setSelectedProject(projectDetails.id ?? null)}
<div className="flex flex-col gap-8"> className="!text-sm"
<span className="text-sm tracking-tight"> outline
The danger zone of the project delete page is a critical area that >
requires careful consideration and attention. When deleting a project, all Delete my project
of the data and resources within that project will be permanently removed </DangerButton>
and cannot be recovered. </div>
</span> ) : (
<div> <Loader className="mt-2 w-full">
{projectDetails ? ( <Loader.Item height="38px" width="144px" />
<div> </Loader>
<DangerButton )}
onClick={() => setSelectedProject(projectDetails.id ?? null)} </div>
className="!text-sm"
outline
>
Delete my project
</DangerButton>
</div>
) : (
<Loader className="mt-2 w-full">
<Loader.Item height="38px" width="144px" />
</Loader>
)}
</div> </div>
</div> </Disclosure.Panel>
</Disclosure.Panel> </Transition>
</Transition> </div>
</div> )}
)} </Disclosure>
</Disclosure> )}
</div> </div>
</div> </div>
</form> </form>

View File

@ -113,11 +113,11 @@ const LabelsSettings: NextPage = () => {
</Breadcrumbs> </Breadcrumbs>
} }
> >
<div className="flex flex-row gap-2"> <div className="flex flex-row gap-2 h-full w-full">
<div className="w-80 py-8"> <div className="w-80 py-8">
<SettingsSidebar /> <SettingsSidebar />
</div> </div>
<section className="pr-9 py-8 gap-10 w-full"> <section className="pr-9 py-8 gap-10 h-full w-full">
<div className="flex items-center justify-between pt-2 pb-3.5 border-b border-custom-border-200"> <div className="flex items-center justify-between pt-2 pb-3.5 border-b border-custom-border-200">
<h3 className="text-xl font-medium">Labels</h3> <h3 className="text-xl font-medium">Labels</h3>
@ -129,7 +129,7 @@ const LabelsSettings: NextPage = () => {
Add label Add label
</PrimaryButton> </PrimaryButton>
</div> </div>
<div className="space-y-3 py-6"> <div className="space-y-3 py-6 h-full w-full">
{labelForm && ( {labelForm && (
<CreateUpdateLabelInline <CreateUpdateLabelInline
labelForm={labelForm} labelForm={labelForm}

View File

@ -334,7 +334,7 @@ const MembersSettings: NextPage = () => {
</div> </div>
<div className="flex items-center justify-between gap-4 py-3.5 border-b border-custom-border-200"> <div className="flex items-center justify-between gap-4 py-3.5 border-b border-custom-border-200">
<h4 className="text-xl font-medium border-b border-custom-border-100">Members</h4> <h4 className="text-xl font-medium">Members</h4>
<PrimaryButton onClick={() => setInviteModal(true)}>Add Member</PrimaryButton> <PrimaryButton onClick={() => setInviteModal(true)}>Add Member</PrimaryButton>
</div> </div>
{!projectMembers || !projectInvitations ? ( {!projectMembers || !projectInvitations ? (
@ -386,11 +386,13 @@ const MembersSettings: NextPage = () => {
<h4 className="text-sm">{member.display_name || member.email}</h4> <h4 className="text-sm">{member.display_name || member.email}</h4>
)} )}
{isOwner && ( {isOwner && (
<p className="mt-0.5 text-xs text-custom-text-200">{member.email}</p> <p className="mt-0.5 text-xs text-custom-sidebar-text-300">
{member.email}
</p>
)} )}
</div> </div>
</div> </div>
<div className="flex items-center gap-2 text-xs"> <div className="flex items-center gap-3 text-xs">
{!member.member && ( {!member.member && (
<div className="mr-2 flex items-center justify-center rounded-full bg-yellow-500/20 px-2 py-1 text-center text-xs text-yellow-500"> <div className="mr-2 flex items-center justify-center rounded-full bg-yellow-500/20 px-2 py-1 text-center text-xs text-yellow-500">
Pending Pending

View File

@ -8,7 +8,8 @@ import useSWR from "swr";
import workspaceService from "services/workspace.service"; import workspaceService from "services/workspace.service";
// layouts // layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { SettingsHeader } from "components/workspace"; // component
import { SettingsSidebar } from "components/project";
// ui // ui
import { SecondaryButton } from "components/ui"; import { SecondaryButton } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
@ -42,14 +43,17 @@ const BillingSettings: NextPage = () => {
</Breadcrumbs> </Breadcrumbs>
} }
> >
<div className="p-8"> <div className="flex flex-row gap-2 h-full overflow-hidden">
<SettingsHeader /> <div className="w-80 py-8">
<section className="space-y-8"> <SettingsSidebar />
</div>
<section className="pr-9 py-8 w-full">
<div> <div>
<h3 className="text-2xl font-semibold leading-6">Billing & Plans</h3> <div className="flex items-center py-3.5 border-b border-custom-border-200">
<p className="mt-4 text-sm text-custom-text-200">Free launch preview</p> <h3 className="text-xl font-medium">Billing & Plan</h3>
</div>
</div> </div>
<div className="space-y-8 md:w-2/3"> <div className="px-4 py-6">
<div> <div>
<h4 className="text-md mb-1 leading-6">Current plan</h4> <h4 className="text-md mb-1 leading-6">Current plan</h4>
<p className="mb-3 text-sm text-custom-text-200"> <p className="mb-3 text-sm text-custom-text-200">

View File

@ -6,10 +6,9 @@ import useSWR from "swr";
import workspaceService from "services/workspace.service"; import workspaceService from "services/workspace.service";
// layouts // layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { SettingsHeader } from "components/workspace";
// components // components
import ExportGuide from "components/exporter/guide"; import ExportGuide from "components/exporter/guide";
import { IntegrationAndImportExportBanner } from "components/ui"; import { SettingsSidebar } from "components/project";
// ui // ui
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// types // types
@ -41,10 +40,16 @@ const ImportExport: NextPage = () => {
</Breadcrumbs> </Breadcrumbs>
} }
> >
<div className="p-8 space-y-4"> <div className="flex flex-row gap-2">
<SettingsHeader /> <div className="w-80 py-8">
<IntegrationAndImportExportBanner bannerName="Export" /> <SettingsSidebar />
<ExportGuide /> </div>
<div className="pr-9 py-8 overflow-y-auto w-full">
<div className="flex items-center py-3.5 border-b border-custom-border-200">
<h3 className="text-xl font-medium">Exports</h3>
</div>
<ExportGuide />
</div>
</div> </div>
</WorkspaceAuthorizationLayout> </WorkspaceAuthorizationLayout>
); );

View File

@ -6,10 +6,9 @@ import useSWR from "swr";
import workspaceService from "services/workspace.service"; import workspaceService from "services/workspace.service";
// layouts // layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { SettingsHeader } from "components/workspace";
// components // components
import IntegrationGuide from "components/integration/guide"; import IntegrationGuide from "components/integration/guide";
import { IntegrationAndImportExportBanner } from "components/ui"; import { SettingsSidebar } from "components/project";
// ui // ui
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// types // types
@ -41,15 +40,19 @@ const ImportExport: NextPage = () => {
</Breadcrumbs> </Breadcrumbs>
} }
> >
<div className="p-8 space-y-4"> <div className="flex flex-row gap-2 h-full overflow-hidden">
<SettingsHeader /> <div className="w-80 py-8">
<IntegrationAndImportExportBanner <SettingsSidebar />
bannerName="Import/ Export" </div>
description="Integrations and importers are only available on the cloud version. We plan to open-source <section className="pr-9 py-8 w-full">
our SDKs in the near future so that the community can request or contribute integrations as <div className="flex items-center py-3.5 border-b border-custom-border-200">
needed." <h3 className="text-xl font-medium">Imports</h3>
/> </div>
<IntegrationGuide /> <IntegrationGuide />
<div className="flex items-center py-3.5 border-b border-custom-border-200">
<h3 className="text-xl font-medium">Previous Imports</h3>
</div>
</section>
</div> </div>
</WorkspaceAuthorizationLayout> </WorkspaceAuthorizationLayout>
); );

View File

@ -16,14 +16,16 @@ import useUserAuth from "hooks/use-user-auth";
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
// components // components
import { ImageUploadModal } from "components/core"; import { ImageUploadModal } from "components/core";
import { DeleteWorkspaceModal, SettingsHeader } from "components/workspace"; import { DeleteWorkspaceModal } from "components/workspace";
import { SettingsSidebar } from "components/project";
// ui // ui
import { Spinner, Input, CustomSelect, SecondaryButton, DangerButton } from "components/ui"; import { Disclosure, Transition } from "@headlessui/react";
import { Spinner, Input, CustomSelect, DangerButton, PrimaryButton, Icon } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons // icons
import { LinkIcon } from "@heroicons/react/24/outline"; import { Pencil } from "lucide-react";
// helpers // helpers
import { copyTextToClipboard, truncateText } from "helpers/string.helper"; import { truncateText } from "helpers/string.helper";
// types // types
import type { IWorkspace } from "types"; import type { IWorkspace } from "types";
import type { NextPage } from "next"; import type { NextPage } from "next";
@ -135,6 +137,7 @@ const WorkspaceSettings: NextPage = () => {
logo: "", logo: "",
}; };
}); });
setIsImageUploadModalOpen(false);
}) })
.catch(() => { .catch(() => {
setToastAlert({ setToastAlert({
@ -162,6 +165,8 @@ const WorkspaceSettings: NextPage = () => {
<ImageUploadModal <ImageUploadModal
isOpen={isImageUploadModalOpen} isOpen={isImageUploadModalOpen}
onClose={() => setIsImageUploadModalOpen(false)} onClose={() => setIsImageUploadModalOpen(false)}
isRemoving={isImageRemoving}
handleDelete={() => handleDelete(activeWorkspace?.logo)}
onSuccess={(imageUrl) => { onSuccess={(imageUrl) => {
setIsImageUploading(true); setIsImageUploading(true);
setValue("logo", imageUrl); setValue("logo", imageUrl);
@ -178,67 +183,109 @@ const WorkspaceSettings: NextPage = () => {
data={activeWorkspace ?? null} data={activeWorkspace ?? null}
user={user} user={user}
/> />
<div className="p-8"> <div className="flex flex-row gap-2 h-full w-full">
<SettingsHeader /> <div className="w-80 py-8">
<SettingsSidebar />
</div>
{activeWorkspace ? ( {activeWorkspace ? (
<div className={`space-y-8 sm:space-y-12 ${isAdmin ? "" : "opacity-60"}`}> <div className={`pr-9 py-8 w-full ${isAdmin ? "" : "opacity-60"}`}>
<div className="grid grid-cols-12 gap-4 sm:gap-16"> <div className="flex gap-5 items-center pb-7 border-b border-custom-border-200">
<div className="col-span-12 sm:col-span-6"> <div className="flex flex-col gap-1">
<h4 className="text-lg font-semibold">Logo</h4> <button
<p className="text-sm text-custom-text-200"> type="button"
Max file size is 5MB. Supported file types are .jpg and .png. onClick={() => setIsImageUploadModalOpen(true)}
</p> disabled={!isAdmin}
>
{watch("logo") && watch("logo") !== null && watch("logo") !== "" ? (
<div className="relative mx-auto flex h-14 w-14">
<img
src={watch("logo")!}
className="absolute top-0 left-0 h-full w-full object-cover rounded-md"
alt="Workspace Logo"
/>
</div>
) : (
<div className="relative flex h-14 w-14 items-center justify-center rounded bg-gray-700 p-4 uppercase text-white">
{activeWorkspace?.name?.charAt(0) ?? "N"}
</div>
)}
</button>
</div> </div>
<div className="col-span-12 sm:col-span-6"> <div className="flex flex-col gap-1">
<div className="flex items-center gap-4"> <h3 className="text-lg font-semibold leading-6">{watch("name")}</h3>
<span className="text-sm tracking-tight">{`${
typeof window !== "undefined" &&
window.location.origin.replace("http://", "").replace("https://", "")
}/${activeWorkspace.slug}`}</span>
<div className="flex item-center gap-2.5">
<button <button
type="button" className="flex items-center gap-1.5 text-xs text-left text-custom-primary-100 font-medium"
onClick={() => setIsImageUploadModalOpen(true)} onClick={() => setIsImageUploadModalOpen(true)}
disabled={!isAdmin} disabled={!isAdmin}
> >
{watch("logo") && watch("logo") !== null && watch("logo") !== "" ? ( {watch("logo") && watch("logo") !== null && watch("logo") !== "" ? (
<div className="relative mx-auto flex h-12 w-12"> <>
<img <Pencil className="h-3 w-3" />
src={watch("logo")!} Edit logo
className="absolute top-0 left-0 h-full w-full object-cover rounded-md" </>
alt="Workspace Logo"
/>
</div>
) : ( ) : (
<div className="relative flex h-12 w-12 items-center justify-center rounded bg-gray-700 p-4 uppercase text-white"> "Upload logo"
{activeWorkspace?.name?.charAt(0) ?? "N"}
</div>
)} )}
</button> </button>
{isAdmin && (
<div className="flex gap-4">
<SecondaryButton
onClick={() => {
setIsImageUploadModalOpen(true);
}}
>
{isImageUploading ? "Uploading..." : "Upload"}
</SecondaryButton>
{activeWorkspace.logo && activeWorkspace.logo !== "" && (
<DangerButton
onClick={() => handleDelete(activeWorkspace.logo)}
loading={isImageRemoving}
>
{isImageRemoving ? "Removing..." : "Remove"}
</DangerButton>
)}
</div>
)}
</div> </div>
</div> </div>
</div> </div>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6"> <div className="flex flex-col gap-8 my-10">
<h4 className="text-lg font-semibold">URL</h4> <div className="grid grid-col grid-cols-1 xl:grid-cols-2 2xl:grid-cols-3 items-center justify-between gap-10 w-full">
<p className="text-sm text-custom-text-200">Your workspace URL.</p> <div className="flex flex-col gap-1 ">
</div> <h4 className="text-sm">Workspace Name</h4>
<div className="col-span-12 flex items-center gap-2 sm:col-span-6"> <Input
<div className="flex flex-col gap-1"> id="name"
name="name"
placeholder="Name"
autoComplete="off"
register={register}
error={errors.name}
validations={{
required: "Name is required",
maxLength: {
value: 80,
message: "Workspace name should not exceed 80 characters",
},
}}
disabled={!isAdmin}
/>
</div>
<div className="flex flex-col gap-1 ">
<h4 className="text-sm">Company Size</h4>
<Controller
name="organization_size"
control={control}
render={({ field: { value, onChange } }) => (
<CustomSelect
value={value}
onChange={onChange}
label={
ORGANIZATION_SIZE.find((c) => c === value) ?? "Select organization size"
}
width="w-full"
input
disabled={!isAdmin}
>
{ORGANIZATION_SIZE?.map((item) => (
<CustomSelect.Option key={item} value={item}>
{item}
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
</div>
<div className="flex flex-col gap-1 ">
<h4 className="text-sm">Workspace URL</h4>
<Input <Input
id="url" id="url"
name="url" name="url"
@ -253,114 +300,66 @@ const WorkspaceSettings: NextPage = () => {
disabled disabled
/> />
</div> </div>
<SecondaryButton
className="h-min"
onClick={() =>
copyTextToClipboard(
`${typeof window !== "undefined" && window.location.origin}/${
activeWorkspace.slug
}`
).then(() => {
setToastAlert({
type: "success",
title: "Link Copied!",
message: "Workspace link copied to clipboard.",
});
})
}
outline
>
<LinkIcon className="h-[18px] w-[18px]" />
</SecondaryButton>
</div> </div>
</div>
<div className="grid grid-cols-12 gap-4 sm:gap-16"> <div className="flex items-center justify-between py-2">
<div className="col-span-12 sm:col-span-6"> <PrimaryButton
<h4 className="text-lg font-semibold">Name</h4> onClick={handleSubmit(onSubmit)}
<p className="text-sm text-custom-text-200">Give a name to your workspace.</p> loading={isSubmitting}
</div>
<div className="col-span-12 sm:col-span-6">
<Input
id="name"
name="name"
placeholder="Name"
autoComplete="off"
register={register}
error={errors.name}
validations={{
required: "Name is required",
maxLength: {
value: 80,
message: "Workspace name should not exceed 80 characters",
},
}}
disabled={!isAdmin} disabled={!isAdmin}
/> >
</div> {isSubmitting ? "Updating..." : "Update Workspace"}
</div> </PrimaryButton>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold">Organization Size</h4>
<p className="text-sm text-custom-text-200">What size is your organization?</p>
</div>
<div className="col-span-12 sm:col-span-6">
<Controller
name="organization_size"
control={control}
render={({ field: { value, onChange } }) => (
<CustomSelect
value={value}
onChange={onChange}
label={
ORGANIZATION_SIZE.find((c) => c === value) ?? "Select organization size"
}
width="w-full"
input
disabled={!isAdmin}
>
{ORGANIZATION_SIZE?.map((item) => (
<CustomSelect.Option key={item} value={item}>
{item}
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
</div> </div>
</div> </div>
{isAdmin && ( <Disclosure as="div" className="border-t border-custom-border-400">
<> {({ open }) => (
<div className="sm:text-right"> <div className="w-full">
<SecondaryButton <Disclosure.Button
onClick={handleSubmit(onSubmit)} as="button"
loading={isSubmitting} type="button"
disabled={!isAdmin} className="flex items-center justify-between w-full py-4"
> >
{isSubmitting ? "Updating..." : "Update Workspace"} <span className="text-xl tracking-tight">Delete Workspace</span>
</SecondaryButton> <Icon iconName={open ? "expand_less" : "expand_more"} className="!text-2xl" />
</Disclosure.Button>
<Transition
show={open}
enter="transition duration-100 ease-out"
enterFrom="transform opacity-0"
enterTo="transform opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform opacity-100"
leaveTo="transform opacity-0"
>
<Disclosure.Panel>
<div className="flex flex-col gap-8">
<span className="text-sm tracking-tight">
The danger zone of the project delete page is a critical area that
requires careful consideration and attention. When deleting a project, all
of the data and resources within that project will be permanently removed
and cannot be recovered.
</span>
<div>
<DangerButton
onClick={() => setIsOpen(true)}
className="!text-sm"
outline
>
Delete my project
</DangerButton>
</div>
</div>
</Disclosure.Panel>
</Transition>
</div> </div>
<div className="grid grid-cols-12 gap-4 sm:gap-16"> )}
<div className="col-span-12 sm:col-span-6"> </Disclosure>
<h4 className="text-lg font-semibold">Danger Zone</h4>
<p className="text-sm text-custom-text-200">
The danger zone of the workspace delete page is a critical area that requires
careful consideration and attention. When deleting a workspace, all of the
data and resources within that workspace will be permanently removed and
cannot be recovered.
</p>
</div>
<div className="col-span-12 sm:col-span-6">
<DangerButton onClick={() => setIsOpen(true)} outline>
Delete the workspace
</DangerButton>
</div>
</div>
</>
)}
</div> </div>
) : ( ) : (
<div className="grid h-full w-full place-items-center px-4 sm:px-0"> <div className="flex items-center justify-center h-full w-full px-4 sm:px-0">
<Spinner /> <Spinner />
</div> </div>
)} )}

View File

@ -9,9 +9,9 @@ import workspaceService from "services/workspace.service";
import IntegrationService from "services/integration"; import IntegrationService from "services/integration";
// layouts // layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { SettingsHeader } from "components/workspace";
// components // components
import { SingleIntegrationCard } from "components/integration"; import { SingleIntegrationCard } from "components/integration";
import { SettingsSidebar } from "components/project";
// ui // ui
import { IntegrationAndImportExportBanner, Loader } from "components/ui"; import { IntegrationAndImportExportBanner, Loader } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
@ -48,19 +48,21 @@ const WorkspaceIntegrations: NextPage = () => {
</Breadcrumbs> </Breadcrumbs>
} }
> >
<div className="p-8"> <div className="flex flex-row gap-2">
<SettingsHeader /> <div className="w-80 py-8">
<section className="space-y-5"> <SettingsSidebar />
</div>
<section className="pr-9 py-8 w-full">
<IntegrationAndImportExportBanner bannerName="Integrations" /> <IntegrationAndImportExportBanner bannerName="Integrations" />
<div className="space-y-5"> <div>
{appIntegrations ? ( {appIntegrations ? (
appIntegrations.map((integration) => ( appIntegrations.map((integration) => (
<SingleIntegrationCard key={integration.id} integration={integration} /> <SingleIntegrationCard key={integration.id} integration={integration} />
)) ))
) : ( ) : (
<Loader className="space-y-5"> <Loader className="space-y-1">
<Loader.Item height="60px" /> <Loader.Item height="89px" />
<Loader.Item height="60px" /> <Loader.Item height="89px" />
</Loader> </Loader>
)} )}
</div> </div>

View File

@ -13,15 +13,15 @@ import useUser from "hooks/use-user";
import useWorkspaceMembers from "hooks/use-workspace-members"; import useWorkspaceMembers from "hooks/use-workspace-members";
// layouts // layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { SettingsHeader } from "components/workspace";
// components // components
import ConfirmWorkspaceMemberRemove from "components/workspace/confirm-workspace-member-remove"; import ConfirmWorkspaceMemberRemove from "components/workspace/confirm-workspace-member-remove";
import SendWorkspaceInvitationModal from "components/workspace/send-workspace-invitation-modal"; import SendWorkspaceInvitationModal from "components/workspace/send-workspace-invitation-modal";
import { SettingsSidebar } from "components/project";
// ui // ui
import { CustomMenu, CustomSelect, Loader } from "components/ui"; import { CustomMenu, CustomSelect, Icon, Loader, PrimaryButton } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons // icons
import { PlusIcon } from "@heroicons/react/24/outline"; import { XMarkIcon } from "components/icons";
// types // types
import type { NextPage } from "next"; import type { NextPage } from "next";
// fetch-keys // fetch-keys
@ -143,9 +143,8 @@ const MembersSettings: NextPage = () => {
}); });
}) })
.finally(() => { .finally(() => {
mutateMembers( mutateMembers((prevData: any) =>
(prevData: any) => prevData?.filter((item: any) => item.id !== selectedRemoveMember)
prevData?.filter((item: any) => item.id !== selectedRemoveMember)
); );
}); });
} }
@ -187,19 +186,14 @@ const MembersSettings: NextPage = () => {
user={user} user={user}
onSuccess={handleInviteModalSuccess} onSuccess={handleInviteModalSuccess}
/> />
<div className="p-8"> <div className="flex flex-row gap-2">
<SettingsHeader /> <div className="w-80 py-8">
<section className="space-y-5"> <SettingsSidebar />
<div className="flex items-end justify-between gap-4"> </div>
<h3 className="text-2xl font-semibold">Members</h3> <section className="pr-9 py-8 w-full">
<button <div className="flex items-center justify-between gap-4 pt-2 pb-3.5 border-b border-custom-border-200">
type="button" <h4 className="text-xl font-medium">Members</h4>
className="flex items-center gap-2 text-custom-primary outline-none" <PrimaryButton onClick={() => setInviteModal(true)}>Add Member</PrimaryButton>
onClick={() => setInviteModal(true)}
>
<PlusIcon className="h-4 w-4" />
Add Member
</button>
</div> </div>
{!workspaceMembers || !workspaceInvitations ? ( {!workspaceMembers || !workspaceInvitations ? (
<Loader className="space-y-5"> <Loader className="space-y-5">
@ -209,23 +203,30 @@ const MembersSettings: NextPage = () => {
<Loader.Item height="40px" /> <Loader.Item height="40px" />
</Loader> </Loader>
) : ( ) : (
<div className="divide-y divide-custom-border-200 rounded-[10px] border border-custom-border-200 bg-custom-background-100 px-6"> <div className="divide-y divide-custom-border-200">
{members.length > 0 {members.length > 0
? members.map((member) => ( ? members.map((member) => (
<div key={member.id} className="flex items-center justify-between py-6"> <div
key={member.id}
className="group flex items-center justify-between px-3.5 py-[18px]"
>
<div className="flex items-center gap-x-8 gap-y-2"> <div className="flex items-center gap-x-8 gap-y-2">
{member.avatar && member.avatar !== "" ? ( {member.avatar && member.avatar !== "" ? (
<div className="relative flex h-10 w-10 items-center justify-center rounded-lg p-4 capitalize text-white"> <Link href={`/${workspaceSlug}/profile/${member.memberId}`}>
<img <a className="relative flex h-10 w-10 items-center justify-center rounded-lg p-4 capitalize text-white">
src={member.avatar} <img
className="absolute top-0 left-0 h-full w-full object-cover rounded-lg" src={member.avatar}
alt={member.display_name || member.email} className="absolute top-0 left-0 h-full w-full object-cover rounded-lg"
/> alt={member.display_name || member.email}
</div> />
</a>
</Link>
) : member.display_name || member.email ? ( ) : member.display_name || member.email ? (
<div className="relative flex h-10 w-10 items-center justify-center rounded-lg p-4 capitalize bg-gray-700 text-white"> <Link href={`/${workspaceSlug}/profile/${member.memberId}`}>
{(member.display_name || member.email)?.charAt(0)} <a className="relative flex h-10 w-10 items-center justify-center rounded-lg p-4 capitalize bg-gray-700 text-white">
</div> {(member.display_name || member.email)?.charAt(0)}
</a>
</Link>
) : ( ) : (
<div className="relative flex h-10 w-10 items-center justify-center rounded-lg p-4 capitalize bg-gray-700 text-white"> <div className="relative flex h-10 w-10 items-center justify-center rounded-lg p-4 capitalize bg-gray-700 text-white">
? ?
@ -244,14 +245,18 @@ const MembersSettings: NextPage = () => {
</a> </a>
</Link> </Link>
) : ( ) : (
<h4 className="text-sm">{member.display_name || member.email}</h4> <h4 className="text-sm cursor-default">
{member.display_name || member.email}
</h4>
)} )}
{isOwner && ( {isOwner && (
<p className="text-xs text-custom-text-200">{member.email}</p> <p className="mt-0.5 text-xs text-custom-sidebar-text-300">
{member.email}
</p>
)} )}
</div> </div>
</div> </div>
<div className="flex items-center gap-2 text-xs"> <div className="flex items-center gap-3 text-xs">
{!member?.status && ( {!member?.status && (
<div className="mr-2 flex items-center justify-center rounded-full bg-yellow-500/20 px-2 py-1 text-center text-xs text-yellow-500"> <div className="mr-2 flex items-center justify-center rounded-full bg-yellow-500/20 px-2 py-1 text-center text-xs text-yellow-500">
<p>Pending</p> <p>Pending</p>
@ -263,9 +268,22 @@ const MembersSettings: NextPage = () => {
</div> </div>
)} )}
<CustomSelect <CustomSelect
label={ROLE[member.role as keyof typeof ROLE]} customButton={
<button className="flex item-center gap-1">
<span
className={`flex items-center text-sm font-medium ${
member.memberId !== user?.id ? "" : "text-custom-sidebar-text-400"
}`}
>
{ROLE[member.role as keyof typeof ROLE]}
</span>
{member.memberId !== user?.id && (
<Icon iconName="expand_more" className="text-lg font-medium" />
)}
</button>
}
value={member.role} value={member.role}
onChange={(value: any) => { onChange={(value: 5 | 10 | 15 | 20 | undefined) => {
if (!workspaceSlug) return; if (!workspaceSlug) return;
mutateMembers( mutateMembers(
@ -323,7 +341,14 @@ const MembersSettings: NextPage = () => {
} }
}} }}
> >
{user?.id === member.memberId ? "Leave" : "Remove member"} <span className="flex items-center justify-start gap-2">
<XMarkIcon className="h-4 w-4" />
<span>
{" "}
{user?.id === member.memberId ? "Leave" : "Remove member"}
</span>
</span>
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
</CustomMenu> </CustomMenu>
</div> </div>

View File

@ -0,0 +1,54 @@
<svg width="201" height="150" viewBox="0 0 201 150" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_2267_36125" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="101" height="150">
<rect width="101" height="150" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_2267_36125)">
<path d="M1.25 4.00001C1.25 1.92894 2.92893 0.25 5 0.25H225C227.071 0.25 228.75 1.92893 228.75 4V189.75H1.25V4.00001Z" fill="#231035" stroke="#3F2B58" stroke-width="0.5"/>
<path d="M2 4C2 2.34315 3.34315 1 5 1H204C205.657 1 207 2.34315 207 4V10H2V4Z" fill="#32184C"/>
<line x1="51.25" y1="10" x2="51.25" y2="190" stroke="#3F2B58" stroke-width="0.5"/>
<line x1="1" y1="9.75" x2="211" y2="9.75002" stroke="#3F2B58" stroke-width="0.5"/>
<rect x="5" y="14" width="36" height="6" rx="1" fill="#572D81"/>
<rect x="5" y="26" width="39" height="4" rx="1" fill="#371B52"/>
<rect x="5" y="35" width="31" height="4" rx="1" fill="#371B52"/>
<rect x="5" y="53" width="26" height="4" rx="1" fill="#371B52"/>
<rect x="5" y="80" width="34" height="4" rx="1" fill="#371B52"/>
<rect x="5" y="44" width="35" height="4" rx="1" fill="#371B52"/>
<rect x="5" y="62" width="29" height="4" rx="1" fill="#371B52"/>
<rect x="5" y="71" width="38" height="4" rx="1" fill="#371B52"/>
<rect x="43" y="14" width="6" height="6" rx="3" fill="#572D81"/>
<rect x="66" y="44" width="51" height="4" rx="1" fill="#572D81"/>
<rect x="66" y="53" width="132" height="4" rx="1" fill="#371B52"/>
<rect x="66" y="60" width="97" height="4" rx="1" fill="#371B52"/>
<rect x="75" y="76" width="52" height="4" rx="1" fill="#371B52"/>
<rect x="75" y="87" width="78" height="4" rx="1" fill="#371B52"/>
<rect x="75" y="98" width="71" height="4" rx="1" fill="#371B52"/>
<rect x="75" y="109" width="58" height="4" rx="1" fill="#371B52"/>
<rect x="66" y="75" width="6" height="6" rx="3" fill="#401E60"/>
<rect x="66" y="86" width="6" height="6" rx="3" fill="#401E60"/>
<rect x="66" y="97" width="6" height="6" rx="3" fill="#401E60"/>
<rect x="66" y="108" width="6" height="6" rx="3" fill="#401E60"/>
<rect x="66" y="26" width="12" height="12" rx="6" fill="#572D81"/>
<rect x="5" y="4" width="3" height="3" rx="1.5" fill="#EF4444"/>
<rect x="9" y="4" width="3" height="3" rx="1.5" fill="#FCD34D"/>
<rect x="13" y="4" width="3" height="3" rx="1.5" fill="#4ADE80"/>
</g>
<mask id="mask1_2267_36125" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="101" y="0" width="100" height="150">
<rect x="101" width="100" height="150" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask1_2267_36125)">
<path d="M0.5 4.00001C0.5 2.06701 2.067 0.5 4 0.5H224C225.933 0.5 227.5 2.067 227.5 4V189.5H0.5V4.00001Z" fill="#000C1B" stroke="#172534"/>
<path d="M1 4C1 2.34315 2.34315 1 4 1H203C204.657 1 206 2.34315 206 4V10H1V4Z" fill="#001936"/>
<line x1="2.18557e-08" y1="9.75" x2="210" y2="9.75002" stroke="#172534" stroke-width="0.5"/>
<rect x="65" y="44" width="51" height="4" rx="1" fill="#172B52"/>
<rect x="65" y="53" width="132" height="4" rx="1" fill="#151E3D"/>
<rect x="65" y="60" width="97" height="4" rx="1" fill="#151E3D"/>
<rect x="74" y="76" width="52" height="4" rx="1" fill="#151E3D"/>
<rect x="74" y="87" width="78" height="4" rx="1" fill="#151E3D"/>
<rect x="74" y="98" width="71" height="4" rx="1" fill="#151E3D"/>
<rect x="74" y="109" width="58" height="4" rx="1" fill="#151E3D"/>
<rect x="129" y="76" width="6" height="4" rx="1" fill="#151E3D"/>
<rect x="155" y="87" width="6" height="4" rx="1" fill="#151E3D"/>
<rect x="148" y="98" width="6" height="4" rx="1" fill="#151E3D"/>
<rect x="135" y="109" width="6" height="4" rx="1" fill="#151E3D"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -0,0 +1,33 @@
<svg width="462" height="536" viewBox="0 0 462 536" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.827922 13.2468C0.827922 6.38804 6.38802 0.827922 13.2468 0.827922H741.818C748.677 0.827922 754.237 6.38802 754.237 13.2468V628.393H0.827922V13.2468Z" fill="white" stroke="#E5E5E5" stroke-width="1.65584"/>
<path d="M0.827922 13.2468C0.827922 6.38802 6.38802 0.827922 13.2468 0.827922H672.273C677.302 0.827922 681.38 4.90533 681.38 9.93507V32.289H0.827922V13.2468Z" fill="#F5F5F5" stroke="#E5E5E5" stroke-width="1.65584"/>
<line x1="166.414" y1="33.1162" x2="166.414" y2="629.22" stroke="#E5E5E5" stroke-width="1.65584"/>
<line x1="-7.23793e-08" y1="32.2883" x2="695.455" y2="32.2882" stroke="#E5E5E5" stroke-width="1.65584"/>
<rect x="13.25" y="46.3633" width="119.221" height="19.8701" rx="3.31169" fill="#E5E5E5"/>
<rect x="13.25" y="86.1035" width="129.156" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
<rect x="13.25" y="115.908" width="102.662" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
<rect x="13.25" y="175.519" width="86.1039" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
<rect x="13.25" y="264.935" width="112.597" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
<rect x="13.25" y="145.714" width="115.909" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
<rect x="13.25" y="205.324" width="96.039" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
<rect x="13.25" y="235.129" width="125.844" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
<rect x="139.094" y="46.3633" width="19.8701" height="19.8701" rx="9.93506" fill="#E5E5E5"/>
<rect x="215.266" y="145.715" width="168.896" height="13.2468" rx="3.31169" fill="#E5E5E5"/>
<rect x="397.406" y="145.715" width="43.0519" height="13.2468" rx="3.31169" fill="#3F76FF"/>
<rect x="215.266" y="175.521" width="437.143" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
<rect x="215.266" y="198.702" width="321.234" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
<rect x="245.07" y="251.688" width="172.208" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
<rect x="245.07" y="288.117" width="258.312" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
<rect x="245.07" y="324.546" width="235.13" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
<rect x="245.07" y="360.975" width="192.078" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
<rect x="427.219" y="251.688" width="19.8701" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
<rect x="447.078" y="360.975" width="19.8701" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
<rect x="215.266" y="248.378" width="19.8701" height="19.8701" rx="9.93506" fill="#E5E5E5"/>
<rect x="215.266" y="284.806" width="19.8701" height="19.8701" rx="9.93506" fill="#E5E5E5"/>
<rect x="215.266" y="321.234" width="19.8701" height="19.8701" rx="9.93506" fill="#E5E5E5"/>
<rect x="215.266" y="357.663" width="19.8701" height="19.8701" rx="9.93506" fill="#E5E5E5"/>
<rect x="215.266" y="86.1045" width="39.7403" height="39.7403" rx="19.8701" fill="#D4D4D4"/>
<rect x="13.2422" y="13.2471" width="9.93506" height="9.93506" rx="4.96753" fill="#EF4444"/>
<rect x="26.4922" y="13.2471" width="9.93506" height="9.93506" rx="4.96753" fill="#FCD34D"/>
<rect x="39.7422" y="13.2471" width="9.93506" height="9.93506" rx="4.96753" fill="#4ADE80"/>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -0,0 +1,34 @@
<svg width="201" height="151" viewBox="0 0 201 151" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.25 4.00001C0.25 1.92894 1.92893 0.25 4 0.25H224C226.071 0.25 227.75 1.92893 227.75 4V189.75H0.25V4.00001Z" fill="#171717" stroke="white" stroke-width="0.5"/>
<path d="M1 4C1 2.34315 2.34315 1 4 1H203C204.657 1 206 2.34315 206 4V10H1V4Z" fill="#171717"/>
<line x1="50.25" y1="10" x2="50.25" y2="190" stroke="white" stroke-width="0.5"/>
<line x1="2.18557e-08" y1="9.75" x2="210" y2="9.75002" stroke="white" stroke-width="0.5"/>
<rect x="4" y="14" width="36" height="6" rx="1" fill="white"/>
<rect x="4" y="26" width="39" height="4" rx="1" fill="#D4D4D4"/>
<rect x="4" y="35" width="31" height="4" rx="1" fill="#D4D4D4"/>
<rect x="4" y="53" width="26" height="4" rx="1" fill="#D4D4D4"/>
<rect x="4" y="80" width="34" height="4" rx="1" fill="#D4D4D4"/>
<rect x="4" y="44" width="35" height="4" rx="1" fill="#D4D4D4"/>
<rect x="4" y="62" width="29" height="4" rx="1" fill="#D4D4D4"/>
<rect x="4" y="71" width="38" height="4" rx="1" fill="#D4D4D4"/>
<rect x="42" y="14" width="6" height="6" rx="3" fill="white"/>
<rect x="65" y="44" width="51" height="4" rx="1" fill="#F1F1F1"/>
<rect x="65" y="53" width="132" height="4" rx="1" fill="#D4D4D4"/>
<rect x="65" y="60" width="97" height="4" rx="1" fill="#D4D4D4"/>
<rect x="74" y="76" width="52" height="4" rx="1" fill="#D4D4D4"/>
<rect x="74" y="87" width="78" height="4" rx="1" fill="#D4D4D4"/>
<rect x="74" y="98" width="71" height="4" rx="1" fill="#D4D4D4"/>
<rect x="74" y="109" width="58" height="4" rx="1" fill="#D4D4D4"/>
<rect x="129" y="76" width="6" height="4" rx="1" fill="#D4D4D4"/>
<rect x="155" y="87" width="6" height="4" rx="1" fill="#D4D4D4"/>
<rect x="148" y="98" width="6" height="4" rx="1" fill="#D4D4D4"/>
<rect x="135" y="109" width="6" height="4" rx="1" fill="#D4D4D4"/>
<rect x="65" y="75" width="6" height="6" rx="3" fill="white"/>
<rect x="65" y="86" width="6" height="6" rx="3" fill="white"/>
<rect x="65" y="97" width="6" height="6" rx="3" fill="white"/>
<rect x="65" y="108" width="6" height="6" rx="3" fill="white"/>
<rect x="65" y="26" width="12" height="12" rx="6" fill="#F1F1F1"/>
<rect x="4" y="4" width="3" height="3" rx="1.5" fill="#EF4444"/>
<rect x="8" y="4" width="3" height="3" rx="1.5" fill="#FCD34D"/>
<rect x="12" y="4" width="3" height="3" rx="1.5" fill="#4ADE80"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,41 @@
<svg width="201" height="154" viewBox="0 0 201 154" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2273_36428)">
<path d="M0.25 4.00001C0.25 1.92894 1.92893 0.25 4 0.25H224C226.071 0.25 227.75 1.92893 227.75 4V189.75H0.25V4.00001Z" fill="#121212" stroke="#262626" stroke-width="0.5"/>
<path d="M0.25 4C0.25 1.92893 1.92893 0.25 4 0.25H203C204.519 0.25 205.75 1.48122 205.75 3V9.75H0.25V4Z" fill="#222222" stroke="#222222" stroke-width="0.5"/>
<line x1="50.25" y1="10" x2="50.25" y2="190" stroke="#262626" stroke-width="0.5"/>
<rect x="4" y="14" width="36" height="6" rx="1" fill="#404040"/>
<rect x="4" y="26" width="39" height="4" rx="1" fill="#262626"/>
<rect x="4" y="35" width="31" height="4" rx="1" fill="#262626"/>
<rect x="4" y="53" width="26" height="4" rx="1" fill="#262626"/>
<rect x="4" y="80" width="34" height="4" rx="1" fill="#262626"/>
<rect x="4" y="44" width="35" height="4" rx="1" fill="#262626"/>
<rect x="4" y="62" width="29" height="4" rx="1" fill="#262626"/>
<rect x="4" y="71" width="38" height="4" rx="1" fill="#262626"/>
<rect x="42" y="14" width="6" height="6" rx="3" fill="#404040"/>
<rect x="65" y="44" width="51" height="4" rx="1" fill="#404040"/>
<rect x="65" y="53" width="132" height="4" rx="1" fill="#262626"/>
<rect x="65" y="60" width="97" height="4" rx="1" fill="#262626"/>
<rect x="74" y="76" width="52" height="4" rx="1" fill="#262626"/>
<rect x="74" y="87" width="78" height="4" rx="1" fill="#262626"/>
<rect x="74" y="98" width="71" height="4" rx="1" fill="#262626"/>
<rect x="74" y="109" width="58" height="4" rx="1" fill="#262626"/>
<rect x="129" y="76" width="6" height="4" rx="1" fill="#262626"/>
<rect x="155" y="87" width="6" height="4" rx="1" fill="#262626"/>
<rect x="148" y="98" width="6" height="4" rx="1" fill="#262626"/>
<rect x="135" y="109" width="6" height="4" rx="1" fill="#262626"/>
<rect x="65" y="75" width="6" height="6" rx="3" fill="#222222"/>
<rect x="65" y="86" width="6" height="6" rx="3" fill="#222222"/>
<rect x="65" y="97" width="6" height="6" rx="3" fill="#222222"/>
<rect x="65" y="108" width="6" height="6" rx="3" fill="#222222"/>
<rect x="65" y="26" width="12" height="12" rx="6" fill="#404040"/>
<rect x="4" y="4" width="3" height="3" rx="1.5" fill="#EF4444"/>
<rect x="8" y="4" width="3" height="3" rx="1.5" fill="#FCD34D"/>
<rect x="12" y="4" width="3" height="3" rx="1.5" fill="#4ADE80"/>
<rect x="121" y="44" width="13" height="4" rx="1" fill="#3F76FF"/>
</g>
<defs>
<clipPath id="clip0_2273_36428">
<rect width="201" height="154" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,42 @@
<svg width="204" height="154" viewBox="0 0 204 154" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2273_36429)">
<path d="M0.25 4.00001C0.25 1.92894 1.92893 0.25 4 0.25H224C226.071 0.25 227.75 1.92893 227.75 4V189.75H0.25V4.00001Z" fill="white" stroke="black" stroke-width="0.5"/>
<path d="M1 4C1 2.34315 2.34315 1 4 1H203C204.657 1 206 2.34315 206 4V10H1V4Z" fill="white"/>
<line x1="50.25" y1="10" x2="50.25" y2="190" stroke="black" stroke-width="0.5"/>
<line x1="-2.18557e-08" y1="9.75" x2="210" y2="9.74998" stroke="black" stroke-width="0.5"/>
<rect x="4" y="14" width="36" height="6" rx="1" fill="#2E2E2E"/>
<rect x="4" y="26" width="39" height="4" rx="1" fill="#404040"/>
<rect x="4" y="35" width="31" height="4" rx="1" fill="#404040"/>
<rect x="4" y="53" width="26" height="4" rx="1" fill="#404040"/>
<rect x="4" y="80" width="34" height="4" rx="1" fill="#404040"/>
<rect x="4" y="44" width="35" height="4" rx="1" fill="#404040"/>
<rect x="4" y="62" width="29" height="4" rx="1" fill="#404040"/>
<rect x="4" y="71" width="38" height="4" rx="1" fill="#404040"/>
<rect x="42" y="14" width="6" height="6" rx="3" fill="#2E2E2E"/>
<rect x="65" y="44" width="51" height="4" rx="1" fill="#2E2E2E"/>
<rect x="65" y="53" width="132" height="4" rx="1" fill="#404040"/>
<rect x="65" y="60" width="97" height="4" rx="1" fill="#404040"/>
<rect x="74" y="76" width="52" height="4" rx="1" fill="#404040"/>
<rect x="74" y="87" width="78" height="4" rx="1" fill="#404040"/>
<rect x="74" y="98" width="71" height="4" rx="1" fill="#404040"/>
<rect x="74" y="109" width="58" height="4" rx="1" fill="#404040"/>
<rect x="129" y="76" width="6" height="4" rx="1" fill="#404040"/>
<rect x="155" y="87" width="6" height="4" rx="1" fill="#404040"/>
<rect x="148" y="98" width="6" height="4" rx="1" fill="#404040"/>
<rect x="135" y="109" width="6" height="4" rx="1" fill="#404040"/>
<rect x="65" y="75" width="6" height="6" rx="3" fill="#3A3A3A"/>
<rect x="65" y="86" width="6" height="6" rx="3" fill="#3A3A3A"/>
<rect x="65" y="97" width="6" height="6" rx="3" fill="#3A3A3A"/>
<rect x="65" y="108" width="6" height="6" rx="3" fill="#3A3A3A"/>
<rect x="65" y="26" width="12" height="12" rx="6" fill="#262626"/>
<rect x="4" y="4" width="3" height="3" rx="1.5" fill="#EF4444"/>
<rect x="8" y="4" width="3" height="3" rx="1.5" fill="#FCD34D"/>
<rect x="12" y="4" width="3" height="3" rx="1.5" fill="#4ADE80"/>
<rect x="121" y="44" width="13" height="4" rx="1" fill="#3F76FF"/>
</g>
<defs>
<clipPath id="clip0_2273_36429">
<rect width="204" height="154" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,42 @@
<svg width="201" height="154" viewBox="0 0 201 154" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2273_36427)">
<path d="M0.25 4.00001C0.25 1.92894 1.92893 0.25 4 0.25H224C226.071 0.25 227.75 1.92893 227.75 4V189.75H0.25V4.00001Z" fill="white" stroke="#E5E5E5" stroke-width="0.5"/>
<path d="M0.25 4C0.25 1.92893 1.92893 0.25 4 0.25H203C204.519 0.25 205.75 1.48122 205.75 3V9.75H0.25V4Z" fill="#F5F5F5" stroke="#E5E5E5" stroke-width="0.5"/>
<line x1="50.25" y1="10" x2="50.25" y2="190" stroke="#E5E5E5" stroke-width="0.5"/>
<line x1="-2.18557e-08" y1="9.75" x2="210" y2="9.74998" stroke="#E5E5E5" stroke-width="0.5"/>
<rect x="4" y="14" width="36" height="6" rx="1" fill="#E5E5E5"/>
<rect x="4" y="26" width="39" height="4" rx="1" fill="#F1F1F1"/>
<rect x="4" y="35" width="31" height="4" rx="1" fill="#F1F1F1"/>
<rect x="4" y="53" width="26" height="4" rx="1" fill="#F1F1F1"/>
<rect x="4" y="80" width="34" height="4" rx="1" fill="#F1F1F1"/>
<rect x="4" y="44" width="35" height="4" rx="1" fill="#F1F1F1"/>
<rect x="4" y="62" width="29" height="4" rx="1" fill="#F1F1F1"/>
<rect x="4" y="71" width="38" height="4" rx="1" fill="#F1F1F1"/>
<rect x="42" y="14" width="6" height="6" rx="3" fill="#E5E5E5"/>
<rect x="65" y="44" width="51" height="4" rx="1" fill="#E5E5E5"/>
<rect x="120" y="44" width="13" height="4" rx="1" fill="#3F76FF"/>
<rect x="65" y="53" width="132" height="4" rx="1" fill="#F1F1F1"/>
<rect x="65" y="60" width="97" height="4" rx="1" fill="#F1F1F1"/>
<rect x="74" y="76" width="52" height="4" rx="1" fill="#F1F1F1"/>
<rect x="74" y="87" width="78" height="4" rx="1" fill="#F1F1F1"/>
<rect x="74" y="98" width="71" height="4" rx="1" fill="#F1F1F1"/>
<rect x="74" y="109" width="58" height="4" rx="1" fill="#F1F1F1"/>
<rect x="129" y="76" width="6" height="4" rx="1" fill="#F1F1F1"/>
<rect x="155" y="87" width="6" height="4" rx="1" fill="#F1F1F1"/>
<rect x="148" y="98" width="6" height="4" rx="1" fill="#F1F1F1"/>
<rect x="135" y="109" width="6" height="4" rx="1" fill="#F1F1F1"/>
<rect x="65" y="75" width="6" height="6" rx="3" fill="#E5E5E5"/>
<rect x="65" y="86" width="6" height="6" rx="3" fill="#E5E5E5"/>
<rect x="65" y="97" width="6" height="6" rx="3" fill="#E5E5E5"/>
<rect x="65" y="108" width="6" height="6" rx="3" fill="#E5E5E5"/>
<rect x="65" y="26" width="12" height="12" rx="6" fill="#D4D4D4"/>
<rect x="4" y="4" width="3" height="3" rx="1.5" fill="#EF4444"/>
<rect x="8" y="4" width="3" height="3" rx="1.5" fill="#FCD34D"/>
<rect x="12" y="4" width="3" height="3" rx="1.5" fill="#4ADE80"/>
</g>
<defs>
<clipPath id="clip0_2273_36427">
<rect width="201" height="154" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB