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,7 +183,13 @@ 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">
<div className="flex items-center">
<DangerButton onClick={handleDelete} outline disabled={!value}>
{isRemoving ? "Removing..." : "Remove"}
</DangerButton>
</div>
<div className="flex items-center gap-2">
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton> <SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
<PrimaryButton <PrimaryButton
onClick={handleSubmit} onClick={handleSubmit}
@ -192,6 +199,7 @@ export const ImageUploadModal: React.FC<Props> = ({
{isImageUploading ? "Uploading..." : "Upload & Save"} {isImageUploading ? "Uploading..." : "Upload & Save"}
</PrimaryButton> </PrimaryButton>
</div> </div>
</div>
</Dialog.Panel> </Dialog.Panel>
</Transition.Child> </Transition.Child>
</div> </div>

View File

@ -46,15 +46,16 @@ 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="flex item-center gap-2.5">
<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}
@ -63,15 +64,20 @@ const IntegrationGuide = () => {
alt={`${service.title} Logo`} alt={`${service.title} Logo`}
/> />
</div> </div>
<div className="w-full"> <div>
<h3>{service.title}</h3> <h3 className="flex items-center gap-4 text-sm font-medium">
<p className="text-sm text-custom-text-200">{service.description}</p> {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,10 +135,11 @@ const IntegrationGuide = () => {
<Icon iconName="keyboard_arrow_right" className="!text-lg" /> <Icon iconName="keyboard_arrow_right" className="!text-lg" />
</button> </button>
</div> </div>
</h3> </div>
<div className="flex flex-col">
{exporterServices && exporterServices?.results ? ( {exporterServices && exporterServices?.results ? (
exporterServices?.results?.length > 0 ? ( exporterServices?.results?.length > 0 ? (
<div className="space-y-2"> <div>
<div className="divide-y divide-custom-border-200"> <div className="divide-y divide-custom-border-200">
{exporterServices?.results.map((service) => ( {exporterServices?.results.map((service) => (
<SingleExport key={service.id} service={service} refreshing={refreshing} /> <SingleExport key={service.id} service={service} refreshing={refreshing} />
@ -150,6 +158,7 @@ const IntegrationGuide = () => {
</Loader> </Loader>
)} )}
</div> </div>
</div>
</> </>
{provider && ( {provider && (
<Exporter <Exporter

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,14 +77,13 @@ 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="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 gap-4">
<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}
@ -94,27 +92,27 @@ const IntegrationGuide = () => {
alt={`${service.title} Logo`} alt={`${service.title} Logo`}
/> />
</div> </div>
<div className="w-full"> <div>
<h3>{service.title}</h3> <h3 className="flex items-center gap-4 text-sm font-medium">{service.title}</h3>
<p className="text-sm text-custom-text-200">{service.description}</p> <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 <Link href={`/${workspaceSlug}/settings/imports?provider=${service.provider}`}>
href={`/${workspaceSlug}/settings/imports?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>
</div> </div>
</div> </div>
</div>
))} ))}
</div> <div>
<div className="rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4"> <div className="flex items-center pt-7 pb-3.5 border-b border-custom-border-200">
<h3 className="mb-2 flex gap-2 text-lg font-medium"> <h3 className="flex gap-2 text-xl font-medium">
Previous Imports Previous Imports
<button <button
type="button" type="button"
@ -130,6 +128,8 @@ const IntegrationGuide = () => {
{refreshing ? "Refreshing..." : "Refresh status"} {refreshing ? "Refreshing..." : "Refresh status"}
</button> </button>
</h3> </h3>
</div>
<div className="flex flex-col px-4 py-6">
{importerServices ? ( {importerServices ? (
importerServices.length > 0 ? ( importerServices.length > 0 ? (
<div className="space-y-2"> <div className="space-y-2">
@ -158,6 +158,7 @@ const IntegrationGuide = () => {
</Loader> </Loader>
)} )}
</div> </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={
<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">
<span className="capitalize"> <span className="capitalize">
{field.value ? ROLE[field.value] : "Select role"} {field.value ? ROLE[field.value] : "Select role"}
</span> </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,11 +47,61 @@ 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">
<div className="flex flex-col gap-2">
<span className="text-xs text-custom-sidebar-text-400 font-semibold">SETTINGS</span> <span className="text-xs text-custom-sidebar-text-400 font-semibold">SETTINGS</span>
<div className="flex flex-col gap-1 w-full"> <div className="flex flex-col gap-1 w-full">
{projectLinks.map((link) => ( {(projectId ? projectLinks : workspaceLinks).map((link) => (
<Link key={link.href} href={link.href}> <Link key={link.href} href={link.href}>
<a> <a>
<div <div
@ -68,5 +122,32 @@ export const SettingsSidebar = () => {
))} ))}
</div> </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>
); );
}; };

View File

@ -184,6 +184,8 @@ export const SingleState: React.FC<Props> = ({
<ArrowDownIcon className="h-4 w-4" /> <ArrowDownIcon className="h-4 w-4" />
</button> </button>
)} )}
<div className=" items-center gap-2.5 hidden group-hover:flex">
{state.default ? ( {state.default ? (
<span className="text-xs text-custom-text-200">Default</span> <span className="text-xs text-custom-text-200">Default</span>
) : ( ) : (
@ -196,7 +198,6 @@ export const SingleState: React.FC<Props> = ({
Mark as default Mark as default
</button> </button>
)} )}
<div className=" items-center gap-2.5 hidden group-hover:flex">
<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,18 +38,17 @@ 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 ? ( {userActivity ? (
<div> <section 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={`flex flex-col gap-2 py-4 w-full`}>
<ul role="list" className="-mb-4"> <ul role="list" className="-mb-4">
{userActivity.results.map((activityItem: any, activityIdx: number) => { {userActivity.results.map((activityItem: any, activityIdx: number) => {
if (activityItem.field === "comment") { if (activityItem.field === "comment") {
@ -78,9 +77,9 @@ const ProfileActivity = () => {
</div> </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"> <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 <ChatBubbleLeftEllipsisIcon
className="h-3.5 w-3.5 text-custom-text-200" className="h-6 w-6 !text-2xl text-custom-text-200"
aria-hidden="true" aria-hidden="true"
/> />
</span> </span>
@ -143,24 +142,17 @@ const ProfileActivity = () => {
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
className="absolute top-5 left-5 -ml-px h-full w-0.5 bg-custom-background-80"
aria-hidden="true"
/>
) : null}
<div className="relative flex items-start space-x-2">
<> <>
<div> <div>
<div className="relative px-1.5"> <div className="relative px-1.5">
<div className="mt-1.5"> <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"> <div className="flex h-6 w-6 items-center justify-center">
{activityItem.field ? ( {activityItem.field ? (
activityItem.new_value === "restore" ? ( activityItem.new_value === "restore" ? (
<Icon <Icon
iconName="history" iconName="history"
className="text-sm text-custom-text-200" className="!text-2xl text-custom-text-200"
/> />
) : ( ) : (
<ActivityIcon activity={activityItem} /> <ActivityIcon activity={activityItem} />
@ -185,8 +177,8 @@ const ProfileActivity = () => {
</div> </div>
</div> </div>
</div> </div>
<div className="min-w-0 flex-1 py-3"> <div className="min-w-0 flex-1 py-4 border-b border-custom-border-200">
<div className="text-xs text-custom-text-200 break-words"> <div className="text-sm 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>
@ -218,6 +210,7 @@ const ProfileActivity = () => {
})} })}
</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">
<div className="flex items-center justify-center bg-custom-background-90 h-16 w-16 rounded-lg">
<button type="button" onClick={() => setIsImageUploadModalOpen(true)}> <button type="button" onClick={() => setIsImageUploadModalOpen(true)}>
{!watch("avatar") || watch("avatar") === "" ? ( {!watch("avatar") || watch("avatar") === "" ? (
<div className="h-12 w-12 rounded-md bg-custom-background-80 p-2"> <div className="h-16 w-16 rounded-md bg-custom-background-80 p-2">
<UserIcon className="h-full w-full text-custom-text-200" /> <UserIcon className="h-full w-full text-custom-text-200" />
</div> </div>
) : ( ) : (
<div className="relative h-12 w-12 overflow-hidden"> <div className="relative h-16 w-16 overflow-hidden">
<img <img
src={watch("avatar")} src={watch("avatar")}
className="absolute top-0 left-0 h-full w-full object-cover rounded-md" className="absolute top-0 left-0 h-full w-full object-cover rounded-lg"
onClick={() => setIsImageUploadModalOpen(true)} onClick={() => setIsImageUploadModalOpen(true)}
alt={myProfile.display_name} alt={myProfile.display_name}
/> />
</div> </div>
)} )}
</button> </button>
<div className="flex items-center gap-2">
<SecondaryButton
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>
<div className="grid grid-cols-12 gap-4 sm:gap-16"> <div className="flex absolute right-3 bottom-3">
<div className="col-span-12 sm:col-span-6"> <Controller
<h4 className="text-lg font-semibold">Cover Photo</h4> control={control}
<p className="text-sm text-custom-text-200"> name="cover_image"
Select your cover photo from the given library. render={() => (
</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,24 +219,45 @@ const Profile: NextPage = () => {
"https://images.unsplash.com/photo-1506383796573-caf02b4a79ab" "https://images.unsplash.com/photo-1506383796573-caf02b4a79ab"
} }
/> />
)}
/>
</div> </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> </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>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6"> <div className="grid grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3 gap-6 px-8">
<h4 className="text-lg font-semibold text-custom-text-100">Full Name</h4> <div className="flex flex-col gap-1">
</div> <h4 className="text-sm">First Name</h4>
<div className="col-span-12 flex items-center gap-2 sm:col-span-6">
<Input <Input
name="first_name" name="first_name"
id="first_name" id="first_name"
register={register} register={register}
error={errors.first_name} error={errors.first_name}
placeholder="Enter your first name" placeholder="Enter your first name"
className="!px-3 !py-2 rounded-md font-medium"
autoComplete="off" autoComplete="off"
/> />
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">Last Name</h4>
<Input <Input
name="last_name" name="last_name"
register={register} register={register}
@ -274,18 +265,56 @@ const Profile: NextPage = () => {
id="last_name" id="last_name"
placeholder="Enter your last name" placeholder="Enter your last name"
autoComplete="off" autoComplete="off"
className="!px-3 !py-2 rounded-md font-medium"
/> />
</div> </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>
<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-1">
<h4 className="text-lg font-semibold text-custom-text-100">Display Name</h4> <h4 className="text-sm">Role</h4>
<p className="text-sm text-custom-text-200"> <Controller
This could be your first name, or a nickname however you{"'"}d like people to name="role"
refer to you in Plane. control={control}
</p> 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>
<div className="col-span-12 sm:col-span-6">
<div className="flex flex-col gap-1">
<h4 className="text-sm">Display name </h4>
<Input <Input
id="display_name" id="display_name"
name="display_name" name="display_name"
@ -313,63 +342,10 @@ const Profile: NextPage = () => {
}} }}
/> />
</div> </div>
</div>
<div className="grid grid-cols-12 gap-4 sm:gap-16"> <div className="flex flex-col gap-1">
<div className="col-span-12 sm:col-span-6"> <h4 className="text-sm">Timezone </h4>
<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 <Controller
name="user_timezone" name="user_timezone"
control={control} control={control}
@ -390,16 +366,20 @@ const Profile: NextPage = () => {
/> />
)} )}
/> />
{errors.role && <span className="text-xs text-red-500">Please select a role</span>} {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 className="sm:text-right">
<SecondaryButton type="submit" loading={isSubmitting}>
{isSubmitting ? "Updating..." : "Update profile"}
</SecondaryButton>
</div> </div>
</form> </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> </div>
<SettingsNavbar profilePage />
<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>
<div className="space-y-8 sm:space-y-12"> <div className="grid grid-cols-12 gap-4 sm:gap-16 py-6">
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<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,7 +388,7 @@ const GeneralSettings: NextPage = () => {
)} )}
</div> </div>
</div> </div>
{isAdmin && (
<Disclosure as="div" className="border-t border-custom-border-400"> <Disclosure as="div" className="border-t border-custom-border-400">
{({ open }) => ( {({ open }) => (
<div className="w-full"> <div className="w-full">
@ -397,8 +397,8 @@ const GeneralSettings: NextPage = () => {
type="button" type="button"
className="flex items-center justify-between w-full py-4" className="flex items-center justify-between w-full py-4"
> >
<span className="text-xl tracking-tight">Danger Zone</span> <span className="text-xl tracking-tight">Delete Project</span>
<Icon iconName={open ? "expand_more" : "expand_less"} className="!text-2xl" /> <Icon iconName={open ? "expand_less" : "expand_more"} className="!text-2xl" />
</Disclosure.Button> </Disclosure.Button>
<Transition <Transition
@ -414,9 +414,9 @@ const GeneralSettings: NextPage = () => {
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-8">
<span className="text-sm tracking-tight"> <span className="text-sm tracking-tight">
The danger zone of the project delete page is a critical area that The danger zone of the project delete page is a critical area that
requires careful consideration and attention. When deleting a project, all requires careful consideration and attention. When deleting a project,
of the data and resources within that project will be permanently removed all of the data and resources within that project will be permanently
and cannot be recovered. removed and cannot be recovered.
</span> </span>
<div> <div>
{projectDetails ? ( {projectDetails ? (
@ -441,6 +441,7 @@ const GeneralSettings: NextPage = () => {
</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>
<h3 className="text-2xl font-semibold leading-6">Billing & Plans</h3>
<p className="mt-4 text-sm text-custom-text-200">Free launch preview</p>
</div> </div>
<div className="space-y-8 md:w-2/3"> <section className="pr-9 py-8 w-full">
<div>
<div className="flex items-center py-3.5 border-b border-custom-border-200">
<h3 className="text-xl font-medium">Billing & Plan</h3>
</div>
</div>
<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,11 +40,17 @@ 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 />
</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 /> <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,26 +183,21 @@ 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">
{activeWorkspace ? ( <SettingsSidebar />
<div className={`space-y-8 sm:space-y-12 ${isAdmin ? "" : "opacity-60"}`}>
<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">Logo</h4>
<p className="text-sm text-custom-text-200">
Max file size is 5MB. Supported file types are .jpg and .png.
</p>
</div> </div>
<div className="col-span-12 sm:col-span-6"> {activeWorkspace ? (
<div className="flex items-center gap-4"> <div className={`pr-9 py-8 w-full ${isAdmin ? "" : "opacity-60"}`}>
<div className="flex gap-5 items-center pb-7 border-b border-custom-border-200">
<div className="flex flex-col gap-1">
<button <button
type="button" type="button"
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"> <div className="relative mx-auto flex h-14 w-14">
<img <img
src={watch("logo")!} src={watch("logo")!}
className="absolute top-0 left-0 h-full w-full object-cover rounded-md" className="absolute top-0 left-0 h-full w-full object-cover rounded-md"
@ -205,81 +205,41 @@ const WorkspaceSettings: NextPage = () => {
/> />
</div> </div>
) : ( ) : (
<div className="relative flex h-12 w-12 items-center justify-center rounded bg-gray-700 p-4 uppercase text-white"> <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"} {activeWorkspace?.name?.charAt(0) ?? "N"}
</div> </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 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">URL</h4>
<p className="text-sm text-custom-text-200">Your workspace URL.</p>
</div>
<div className="col-span-12 flex items-center gap-2 sm:col-span-6">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<Input <h3 className="text-lg font-semibold leading-6">{watch("name")}</h3>
id="url" <span className="text-sm tracking-tight">{`${
name="url"
autoComplete="off"
register={register}
error={errors.url}
className="w-full"
value={`${
typeof window !== "undefined" && typeof window !== "undefined" &&
window.location.origin.replace("http://", "").replace("https://", "") window.location.origin.replace("http://", "").replace("https://", "")
}/${activeWorkspace.slug}`} }/${activeWorkspace.slug}`}</span>
disabled <div className="flex item-center gap-2.5">
/> <button
</div> className="flex items-center gap-1.5 text-xs text-left text-custom-primary-100 font-medium"
<SecondaryButton onClick={() => setIsImageUploadModalOpen(true)}
className="h-min" disabled={!isAdmin}
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]" /> {watch("logo") && watch("logo") !== null && watch("logo") !== "" ? (
</SecondaryButton> <>
<Pencil className="h-3 w-3" />
Edit logo
</>
) : (
"Upload logo"
)}
</button>
</div> </div>
</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">Name</h4>
<p className="text-sm text-custom-text-200">Give a name to your workspace.</p>
</div> </div>
<div className="col-span-12 sm:col-span-6">
<div className="flex flex-col gap-8 my-10">
<div className="grid grid-col grid-cols-1 xl:grid-cols-2 2xl:grid-cols-3 items-center justify-between gap-10 w-full">
<div className="flex flex-col gap-1 ">
<h4 className="text-sm">Workspace Name</h4>
<Input <Input
id="name" id="name"
name="name" name="name"
@ -297,13 +257,9 @@ const WorkspaceSettings: NextPage = () => {
disabled={!isAdmin} disabled={!isAdmin}
/> />
</div> </div>
</div>
<div className="grid grid-cols-12 gap-4 sm:gap-16"> <div className="flex flex-col gap-1 ">
<div className="col-span-12 sm:col-span-6"> <h4 className="text-sm">Company Size</h4>
<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 <Controller
name="organization_size" name="organization_size"
control={control} control={control}
@ -327,40 +283,83 @@ const WorkspaceSettings: NextPage = () => {
)} )}
/> />
</div> </div>
<div className="flex flex-col gap-1 ">
<h4 className="text-sm">Workspace URL</h4>
<Input
id="url"
name="url"
autoComplete="off"
register={register}
error={errors.url}
className="w-full"
value={`${
typeof window !== "undefined" &&
window.location.origin.replace("http://", "").replace("https://", "")
}/${activeWorkspace.slug}`}
disabled
/>
</div>
</div> </div>
{isAdmin && ( <div className="flex items-center justify-between py-2">
<> <PrimaryButton
<div className="sm:text-right">
<SecondaryButton
onClick={handleSubmit(onSubmit)} onClick={handleSubmit(onSubmit)}
loading={isSubmitting} loading={isSubmitting}
disabled={!isAdmin} disabled={!isAdmin}
> >
{isSubmitting ? "Updating..." : "Update Workspace"} {isSubmitting ? "Updating..." : "Update Workspace"}
</SecondaryButton> </PrimaryButton>
</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">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>
<div className="col-span-12 sm:col-span-6">
<DangerButton onClick={() => setIsOpen(true)} outline> <Disclosure as="div" className="border-t border-custom-border-400">
Delete the workspace {({ 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 Workspace</span>
<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> </DangerButton>
</div> </div>
</div> </div>
</> </Disclosure.Panel>
</Transition>
</div>
)} )}
</Disclosure>
</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,8 +143,7 @@ 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}`}>
<a className="relative flex h-10 w-10 items-center justify-center rounded-lg p-4 capitalize text-white">
<img <img
src={member.avatar} src={member.avatar}
className="absolute top-0 left-0 h-full w-full object-cover rounded-lg" className="absolute top-0 left-0 h-full w-full object-cover rounded-lg"
alt={member.display_name || member.email} 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}`}>
<a className="relative flex h-10 w-10 items-center justify-center rounded-lg p-4 capitalize bg-gray-700 text-white">
{(member.display_name || member.email)?.charAt(0)} {(member.display_name || member.email)?.charAt(0)}
</div> </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 = () => {
} }
}} }}
> >
<span className="flex items-center justify-start gap-2">
<XMarkIcon className="h-4 w-4" />
<span>
{" "}
{user?.id === member.memberId ? "Leave" : "Remove member"} {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