diff --git a/.cspell.json b/.cspell.json
index 9c47d01cc..b22d44a42 100644
--- a/.cspell.json
+++ b/.cspell.json
@@ -6,6 +6,7 @@
"words": [
" X",
" X ",
+ "hookform",
"accepte",
"Accordian",
"adipiscing",
@@ -24,6 +25,7 @@
"apidemodt",
"apidemodts",
"apidev",
+ "apikey",
"apisauce",
"apistage",
"apistagecivo",
@@ -92,6 +94,7 @@
"creatoe",
"dailyplan",
"Darkmode",
+ "DATACENTER",
"datas",
"dataToDisplay",
"daygrid",
@@ -240,6 +243,7 @@
"longpress",
"Lorem",
"lucide",
+ "mailchimp",
"mainconfig",
"mappagination",
"mathieudutour",
@@ -355,7 +359,6 @@
"tailess",
"Tailess",
"tailwindcss",
- "timesheet-viewMode",
"tanstack",
"taskid",
"taskstatus",
@@ -367,6 +370,7 @@
"testid",
"timegrid",
"Timesheet",
+ "timesheet-viewMode",
"Timesheets",
"Timeslot",
"tinvitations",
@@ -400,6 +404,7 @@
"VERSONS",
"vertificalline",
"vhidden",
+ "Waitlist",
"WARNING️",
"wasabisys",
"webm",
diff --git a/apps/web/.env b/apps/web/.env
index 9180c30c7..b6a667a29 100644
--- a/apps/web/.env
+++ b/apps/web/.env
@@ -137,3 +137,7 @@ NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
# Warning: IF TRUE This allows production builds to successfully complete even if
# your project has ESLint errors.
NEXT_IGNORE_ESLINT_ERROR_ON_BUILD=true
+
+# Mailchimp
+MAILCHIMP_API_KEY=
+MAILCHIMP_LIST_ID=
diff --git a/apps/web/.env.sample b/apps/web/.env.sample
index 62ae03258..1989758f4 100644
--- a/apps/web/.env.sample
+++ b/apps/web/.env.sample
@@ -87,3 +87,7 @@ MEET_JWT_APP_SECRET=
# Warning: IF TRUE This allows production builds to successfully complete even if
# your project has ESLint errors.
NEXT_IGNORE_ESLINT_ERROR_ON_BUILD=true
+
+# Mailchimp
+MAILCHIMP_API_KEY=
+MAILCHIMP_LIST_ID=
diff --git a/apps/web/app/[locale]/layout.tsx b/apps/web/app/[locale]/layout.tsx
index 8dd5ce0ce..7353f53b4 100644
--- a/apps/web/app/[locale]/layout.tsx
+++ b/apps/web/app/[locale]/layout.tsx
@@ -1,7 +1,7 @@
/* eslint-disable no-mixed-spaces-and-tabs */
'use client';
import 'react-loading-skeleton/dist/skeleton.css';
-import '../../styles/globals.css';
+import '@/styles/globals.css';
import clsx from 'clsx';
import { Provider } from 'jotai';
diff --git a/apps/web/app/[locale]/page-component.tsx b/apps/web/app/[locale]/page-component.tsx
index 68f697eed..ddb2d6b58 100644
--- a/apps/web/app/[locale]/page-component.tsx
+++ b/apps/web/app/[locale]/page-component.tsx
@@ -66,9 +66,9 @@ function MainPage() {
showTimer={headerSize <= 11.8 && isTrackingEnabled}
className="h-full"
mainHeaderSlot={
-
+
-
+
diff --git a/apps/web/app/api/subscribe/route.ts b/apps/web/app/api/subscribe/route.ts
new file mode 100644
index 000000000..e7a08d50d
--- /dev/null
+++ b/apps/web/app/api/subscribe/route.ts
@@ -0,0 +1,67 @@
+import { NextRequest, NextResponse } from 'next/server';
+
+export const POST = async (req: NextRequest) => {
+ // 1. Destructure the email address from the request body.
+ const reqData = (await req.json()) as {
+ email_address: string;
+ tags: string[];
+ captcha?: string;
+ };
+
+ if (!reqData.email_address) {
+ // 2. Throw an error if an email wasn't provided.
+ return NextResponse.json({ error: 'Email is required' }, { status: 400 });
+ }
+
+ if (!reqData.captcha) {
+ // 2. Display an error if the captcha code wasn't provided.
+ console.error('ERROR: Please provide required fields', 'STATUS: 400');
+ }
+
+ try {
+ // 3. Fetch the environment variables.
+ const LIST_ID = process.env.MAILCHIMP_LIST_ID;
+ const API_KEY = process.env.MAILCHIMP_API_KEY ? process.env.MAILCHIMP_API_KEY : '';
+ if (!LIST_ID || !API_KEY) {
+ throw new Error('Missing Mailchimp environment variables');
+ }
+ // 4. API keys are in the form
-us3.
+ const DATACENTER = API_KEY.split('-')[1];
+ const mailchimpData = {
+ email_address: reqData.email_address,
+ status: 'subscribed',
+ tags: reqData.tags ? [...reqData.tags] : ['Ever Teams']
+ };
+ // 5. Send a POST request to Mailchimp.
+ const response = await fetch(`https://${DATACENTER}.api.mailchimp.com/3.0/lists/${LIST_ID}/members`, {
+ body: JSON.stringify(mailchimpData),
+ headers: {
+ Authorization: `apikey ${API_KEY}`,
+ 'Content-Type': 'application/json'
+ },
+ method: 'POST'
+ });
+ console.log(response);
+ // 6. Swallow any errors from Mailchimp and return a better error message.
+ if (response.status >= 400) {
+ const errorResponse = await response.json();
+ return NextResponse.json(
+ {
+ error: `There was an error subscribing to the newsletter: ${errorResponse.detail}`
+ },
+ { status: 400 }
+ );
+ }
+
+ // 7. If we made it this far, it was a success! 🎉
+ return NextResponse.json({ error: '', resp: response }, { status: 201 });
+ } catch (error) {
+ return NextResponse.json(
+ {
+ error: (error as Error).message || (error as Error).toString(),
+ resp: null
+ },
+ { status: 500 }
+ );
+ }
+};
diff --git a/apps/web/components/app-sidebar.tsx b/apps/web/components/app-sidebar.tsx
index eaf3048e7..8c304672e 100644
--- a/apps/web/components/app-sidebar.tsx
+++ b/apps/web/components/app-sidebar.tsx
@@ -3,38 +3,36 @@ import {
MonitorSmartphone,
LayoutDashboard,
Heart,
- FolderKanban,
SquareActivity,
- PlusIcon,
Files,
- X
+ X,
+ Command,
+ AudioWaveform,
+ GalleryVerticalEnd
} from 'lucide-react';
-import { EverTeamsLogo, SymbolAppLogo } from '@/lib/components/svgs';
import { NavMain } from '@/components/nav-main';
import {
Sidebar,
SidebarContent,
SidebarHeader,
- SidebarMenu,
- SidebarMenuButton,
- SidebarMenuItem,
SidebarRail,
SidebarTrigger,
useSidebar,
- SidebarMenuSubButton
+ SidebarMenuSubButton,
+ SidebarFooter
} from '@/components/ui/sidebar';
import Link from 'next/link';
import { cn } from '@/lib/utils';
-import { useOrganizationAndTeamManagers } from '@/app/hooks/features/useOrganizationTeamManagers';
import { useAuthenticateUser, useModal, useOrganizationTeams } from '@/app/hooks';
import { useFavoritesTask } from '@/app/hooks/features/useFavoritesTask';
-import { Button } from '@/lib/components/button';
import { CreateTeamModal, TaskIssueStatus } from '@/lib/features';
import { useTranslations } from 'next-intl';
+import { WorkspacesSwitcher } from './workspace-switcher';
+import { SidebarOptInForm } from './sidebar-opt-in-form';
+import { NavProjects } from './nav-projects';
type AppSidebarProps = React.ComponentProps & { publicTeam: boolean | undefined };
export function AppSidebar({ publicTeam, ...props }: AppSidebarProps) {
- const { userManagedTeams } = useOrganizationAndTeamManagers();
const { user } = useAuthenticateUser();
const username = user?.name || user?.firstName || user?.lastName || user?.username;
const { isTeamManager } = useOrganizationTeams();
@@ -44,11 +42,57 @@ export function AppSidebar({ publicTeam, ...props }: AppSidebarProps) {
const t = useTranslations();
// This is sample data.
const data = {
- user: {
- name: 'evereq',
- email: 'evereq@ever.co',
- avatar: '/assets/svg/profile.svg'
- },
+ workspaces: [
+ {
+ name: 'Ever Teams',
+ logo: ({ className }: { className?: string }) => (
+
+ ),
+ plan: 'Enterprise'
+ },
+ {
+ name: 'Ever Gauzy',
+ logo: AudioWaveform,
+ plan: 'Startup'
+ },
+ {
+ name: 'Ever Cloc',
+ logo: GalleryVerticalEnd,
+ plan: 'Free'
+ },
+ {
+ name: 'Ever Rec',
+ logo: Command,
+ plan: 'Free'
+ }
+ ],
navMain: [
{
title: t('sidebar.DASHBOARD'),
@@ -138,35 +182,6 @@ export function AppSidebar({ publicTeam, ...props }: AppSidebarProps) {
}
]
},
- ...(userManagedTeams && userManagedTeams.length > 0
- ? [
- {
- title: t('sidebar.PROJECTS'),
- label: 'projects',
- url: '#',
- icon: FolderKanban,
- items: [
- {
- title: t('common.NO_PROJECT'),
- label: 'no-project',
- url: '#',
- component: (
-
-
-
- )
- }
- ]
- }
- ]
- : []),
{
title: t('sidebar.MY_WORKS'),
url: '#',
@@ -232,7 +247,8 @@ export function AppSidebar({ publicTeam, ...props }: AppSidebarProps) {
}
]
: [])
- ]
+ ],
+ projects: []
};
return (
@@ -245,30 +261,20 @@ export function AppSidebar({ publicTeam, ...props }: AppSidebarProps) {
-
-
-
-
-
-
-
- {state === 'expanded' && }
-
-
-
-
+
+
+
+
+
+
diff --git a/apps/web/components/nav-main.tsx b/apps/web/components/nav-main.tsx
index de5446bdf..79934f941 100644
--- a/apps/web/components/nav-main.tsx
+++ b/apps/web/components/nav-main.tsx
@@ -5,6 +5,7 @@ import { cn } from '@/lib/utils';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import {
SidebarGroup,
+ SidebarGroupLabel,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
@@ -64,7 +65,10 @@ export function NavMain({
};
return (
-
+ Platform
+
{items.map((item, index) => (
@@ -110,11 +114,11 @@ export function NavMain({
) : (
@@ -142,23 +146,23 @@ export function NavMain({
{item.items?.length ? (
<>
-
+
Toggle
-
+
{item.items.map((subItem, key) => (
-
+
{subItem?.component || (
handleSubMenuToggle(key)}
@@ -167,7 +171,7 @@ export function NavMain({
) {
- const { isMobile, state } = useSidebar();
+}) {
+ const { isMobile } = useSidebar();
- return (
+ const { user } = useAuthenticateUser();
+
+ const { userManagedTeams } = useOrganizationAndTeamManagers();
+ const t = useTranslations();
+ return userManagedTeams && userManagedTeams.length > 0 ? (
Projects
-
- {projects.map((item) => (
-
-
-
-
-
- {item.name}
-
-
-
-
-
-
-
- More
-
-
-
-
-
-
+ {projects && projects.length ? (
+ <>
+ {projects.map((item) => (
+
+
+
+
+ {item.name}
+
+
+
+
+
+
+ More
+
+
+
- View Project
-
-
-
-
-
- Share Project
-
-
-
-
-
-
- Delete Project
-
-
-
-
+
+
+ View Project
+
+
+
+ Share Project
+
+
+
+
+ Delete Project
+
+
+
+
+ ))}
+
+
+
+ More
+
+
+ >
+ ) : (
+
+
+
+
- ))}
-
-
-
-
- More
-
-
-
+ )}
- );
+ ) : null;
}
diff --git a/apps/web/components/nav-secondary.tsx b/apps/web/components/nav-secondary.tsx
index 3b1bd5394..b4e2aec2b 100644
--- a/apps/web/components/nav-secondary.tsx
+++ b/apps/web/components/nav-secondary.tsx
@@ -25,10 +25,10 @@ export function NavSecondary({
return (
-
+
{items.map((item) => (
-
-
+
+
- {options.map(({ label, icon: Icon, view: optionView }) => (
-
-
-
- ))}
- >
- );
+ return (
+ <>
+ {options.map(({ label, icon: Icon, view: optionView }) => (
+
+
+
+ ))}
+ >
+ );
}
diff --git a/apps/web/components/pages/main/header-tabs.tsx b/apps/web/components/pages/main/header-tabs.tsx
index 3cb3561e4..ee7fadb5c 100644
--- a/apps/web/components/pages/main/header-tabs.tsx
+++ b/apps/web/components/pages/main/header-tabs.tsx
@@ -1,69 +1,58 @@
import { clsxm } from '@app/utils';
import { Tooltip } from 'lib/components';
import LinkWrapper from '../kanban/link-wrapper';
-import {
- QueueListIcon,
- Squares2X2Icon,
- TableCellsIcon
-} from '@heroicons/react/20/solid';
+import { QueueListIcon, Squares2X2Icon, TableCellsIcon } from '@heroicons/react/20/solid';
import KanbanIcon from '@components/ui/svgs/kanban';
import { IssuesView } from '@app/constants';
import { useAtom } from 'jotai';
import { headerTabs } from '@app/stores/header-tabs';
import { DottedLanguageObjectStringPaths, useTranslations } from 'next-intl';
-const HeaderTabs = ({
- linkAll,
- kanban = false
-}: {
- linkAll: boolean;
- kanban?: boolean;
-}) => {
- const t = useTranslations();
- const options = [
- { label: 'CARDS', icon: QueueListIcon, view: IssuesView.CARDS },
- { label: 'TABLE', icon: TableCellsIcon, view: IssuesView.TABLE },
- { label: 'BLOCKS', icon: Squares2X2Icon, view: IssuesView.BLOCKS },
- { label: 'KANBAN', icon: KanbanIcon, view: IssuesView.KANBAN }
- ];
- const links = linkAll
- ? ['/', '/', '/', '/kanban']
- : [undefined, undefined, undefined, '/kanban'];
- const [view, setView] = useAtom(headerTabs);
- const activeView = kanban ? IssuesView.KANBAN : view;
- return (
- <>
- {options.map(({ label, icon: Icon, view: optionView }, index) => (
-
-
-
-
-
- ))}
- >
- );
+const HeaderTabs = ({ linkAll, kanban = false }: { linkAll: boolean; kanban?: boolean }) => {
+ const t = useTranslations();
+ const options = [
+ { label: 'CARDS', icon: QueueListIcon, view: IssuesView.CARDS },
+ { label: 'TABLE', icon: TableCellsIcon, view: IssuesView.TABLE },
+ { label: 'BLOCKS', icon: Squares2X2Icon, view: IssuesView.BLOCKS },
+ { label: 'KANBAN', icon: KanbanIcon, view: IssuesView.KANBAN }
+ ];
+ const links = linkAll ? ['/', '/', '/', '/kanban'] : [undefined, undefined, undefined, '/kanban'];
+ const [view, setView] = useAtom(headerTabs);
+ const activeView = kanban ? IssuesView.KANBAN : view;
+ return (
+ <>
+ {options.map(({ label, icon: Icon, view: optionView }, index) => (
+
+
+
+
+
+ ))}
+ >
+ );
};
export default HeaderTabs;
diff --git a/apps/web/components/pages/task/description-block/editor-components/LinkElement.tsx b/apps/web/components/pages/task/description-block/editor-components/LinkElement.tsx
index 3bc09d970..033e97093 100644
--- a/apps/web/components/pages/task/description-block/editor-components/LinkElement.tsx
+++ b/apps/web/components/pages/task/description-block/editor-components/LinkElement.tsx
@@ -53,7 +53,7 @@ const LinkElement = ({ attributes, element, children }: any) => {
href={href}
rel="noreferrer"
target="_blank"
- className=" text-[#5000B9] dark:text-primary-light truncate max-w-[240px] overflow-hidden whitespace-nowrap mr-0"
+ className=" text-[#5000B9] dark:text-primary-light truncate max-w-[230px] overflow-hidden whitespace-nowrap mr-0"
style={{ textOverflow: 'ellipsis' }}
>
{element.href}
diff --git a/apps/web/components/sidebar-opt-in-form.tsx b/apps/web/components/sidebar-opt-in-form.tsx
new file mode 100644
index 000000000..670acd645
--- /dev/null
+++ b/apps/web/components/sidebar-opt-in-form.tsx
@@ -0,0 +1,111 @@
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { SidebarInput, useSidebar } from '@/components/ui/sidebar';
+
+import { zodResolver } from '@hookform/resolvers/zod';
+import { useForm } from 'react-hook-form';
+import { z } from 'zod';
+import { Form, FormControl, FormField, FormItem, FormMessage } from '@/components/ui/form';
+import { ToastAction } from './ui/toast';
+import { toast } from './ui/use-toast';
+import { useState } from 'react';
+
+export function SidebarOptInForm() {
+ const { state } = useSidebar();
+ const [isLoading, setLoading] = useState(false);
+ const subscribeFormSchema = z
+ .object({
+ email: z.string().email()
+ })
+ .required();
+ const form = useForm>({
+ resolver: zodResolver(subscribeFormSchema)
+ });
+
+ const subscribe = async () => {
+ let tags = ['Ever Teams, Ever Teams App', 'Open', 'Cloud'];
+ setLoading((prev) => true);
+ const res = await fetch('/api/subscribe', {
+ body: JSON.stringify({
+ email_address: form.getValues('email'),
+ captcha: '',
+ tags: tags,
+ status: 'subscribed'
+ }),
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ method: 'POST'
+ });
+ const { error } = (await res.json()) as {
+ error: string;
+ status: number;
+ [key: string]: unknown;
+ };
+
+ if (error) {
+ setLoading((prev) => false);
+ toast({
+ title: 'Waiting list registration error',
+ description: `We have encountered a problem ${error} with your registration to our waiting list for Ever Teams`,
+ variant: 'destructive'
+ });
+ return;
+ }
+
+ setLoading(() => false);
+ toast({
+ title: 'Confirmation of registration on waiting list',
+ description: "Thank you for joining our waiting list! We're delighted you're interested in Ever Teams",
+ variant: 'default',
+ className: 'bg-green-50 text-green-600 border-green-500',
+ action: Undo
+ });
+ };
+
+ const onSubmit = (data: z.infer) => {
+ console.log(data);
+ (async () => await subscribe())();
+ };
+
+ return state == 'expanded' ? (
+
+ ) : null;
+}
diff --git a/apps/web/components/ui/card.tsx b/apps/web/components/ui/card.tsx
new file mode 100644
index 000000000..fbb0c3772
--- /dev/null
+++ b/apps/web/components/ui/card.tsx
@@ -0,0 +1,43 @@
+import * as React from 'react';
+
+import { cn } from '@/lib/utils';
+
+const Card = React.forwardRef>(({ className, ...props }, ref) => (
+
+));
+Card.displayName = 'Card';
+
+const CardHeader = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+);
+CardHeader.displayName = 'CardHeader';
+
+const CardTitle = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+);
+CardTitle.displayName = 'CardTitle';
+
+const CardDescription = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+);
+CardDescription.displayName = 'CardDescription';
+
+const CardContent = React.forwardRef>(
+ ({ className, ...props }, ref) =>
+);
+CardContent.displayName = 'CardContent';
+
+const CardFooter = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+);
+CardFooter.displayName = 'CardFooter';
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
diff --git a/apps/web/components/ui/form.tsx b/apps/web/components/ui/form.tsx
new file mode 100644
index 000000000..4bcc587c8
--- /dev/null
+++ b/apps/web/components/ui/form.tsx
@@ -0,0 +1,135 @@
+import * as React from 'react';
+import * as LabelPrimitive from '@radix-ui/react-label';
+import { Slot } from '@radix-ui/react-slot';
+import { Controller, ControllerProps, FieldPath, FieldValues, FormProvider, useFormContext } from 'react-hook-form';
+
+import { cn } from '@/lib/utils';
+import { Label } from 'components/ui/label';
+
+const Form = FormProvider;
+
+type FormFieldContextValue<
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath
+> = {
+ name: TName;
+};
+
+const FormFieldContext = React.createContext({} as FormFieldContextValue);
+
+const FormField = <
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath
+>({
+ ...props
+}: ControllerProps) => {
+ return (
+
+
+
+ );
+};
+
+const useFormField = () => {
+ const fieldContext = React.useContext(FormFieldContext);
+ const itemContext = React.useContext(FormItemContext);
+ const { getFieldState, formState } = useFormContext();
+
+ if (!fieldContext) {
+ throw new Error('useFormField should be used within ');
+ }
+ const fieldState = getFieldState(fieldContext.name, formState);
+
+ const { id } = itemContext;
+
+ return {
+ id,
+ name: fieldContext.name,
+ formItemId: `${id}-form-item`,
+ formDescriptionId: `${id}-form-item-description`,
+ formMessageId: `${id}-form-item-message`,
+ ...fieldState
+ };
+};
+
+type FormItemContextValue = {
+ id: string;
+};
+
+const FormItemContext = React.createContext({} as FormItemContextValue);
+
+const FormItem = React.forwardRef>(
+ ({ className, ...props }, ref) => {
+ const id = React.useId();
+
+ return (
+
+
+
+ );
+ }
+);
+FormItem.displayName = 'FormItem';
+
+const FormLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => {
+ const { error, formItemId } = useFormField();
+
+ return ;
+});
+FormLabel.displayName = 'FormLabel';
+
+const FormControl = React.forwardRef, React.ComponentPropsWithoutRef>(
+ ({ ...props }, ref) => {
+ const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
+
+ return (
+
+ );
+ }
+);
+FormControl.displayName = 'FormControl';
+
+const FormDescription = React.forwardRef>(
+ ({ className, ...props }, ref) => {
+ const { formDescriptionId } = useFormField();
+
+ return (
+
+ );
+ }
+);
+FormDescription.displayName = 'FormDescription';
+
+const FormMessage = React.forwardRef>(
+ ({ className, children, ...props }, ref) => {
+ const { error, formMessageId } = useFormField();
+ const body = error ? String(error?.message) : children;
+
+ if (!body) {
+ return null;
+ }
+
+ return (
+
+ {body}
+
+ );
+ }
+);
+FormMessage.displayName = 'FormMessage';
+
+export { useFormField, Form, FormItem, FormLabel, FormControl, FormDescription, FormMessage, FormField };
diff --git a/apps/web/components/ui/label.tsx b/apps/web/components/ui/label.tsx
new file mode 100644
index 000000000..084b15b79
--- /dev/null
+++ b/apps/web/components/ui/label.tsx
@@ -0,0 +1,19 @@
+'use client';
+
+import * as React from 'react';
+import * as LabelPrimitive from '@radix-ui/react-label';
+import { cva, type VariantProps } from 'class-variance-authority';
+
+import { cn } from '@/lib/utils';
+
+const labelVariants = cva('text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70');
+
+const Label = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & VariantProps
+>(({ className, ...props }, ref) => (
+
+));
+Label.displayName = LabelPrimitive.Root.displayName;
+
+export { Label };
diff --git a/apps/web/components/ui/sidebar.tsx b/apps/web/components/ui/sidebar.tsx
index 092b081de..824cb1469 100644
--- a/apps/web/components/ui/sidebar.tsx
+++ b/apps/web/components/ui/sidebar.tsx
@@ -13,9 +13,9 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/comp
const SIDEBAR_COOKIE_NAME = 'sidebar:state';
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
-const SIDEBAR_WIDTH = '17rem';
+const SIDEBAR_WIDTH = '16rem';
const SIDEBAR_WIDTH_MOBILE = '19rem';
-const SIDEBAR_WIDTH_ICON = '5rem';
+const SIDEBAR_WIDTH_ICON = '3rem';
const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
type SidebarContext = {
diff --git a/apps/web/components/team-switcher.tsx b/apps/web/components/workspace-switcher.tsx
similarity index 76%
rename from apps/web/components/team-switcher.tsx
rename to apps/web/components/workspace-switcher.tsx
index 858ee426e..fa6329608 100644
--- a/apps/web/components/team-switcher.tsx
+++ b/apps/web/components/workspace-switcher.tsx
@@ -1,3 +1,4 @@
+'use client';
import * as React from 'react';
import { ChevronsUpDown, Plus } from 'lucide-react';
@@ -12,17 +13,17 @@ import {
} from '@/components/ui/dropdown-menu';
import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from '@/components/ui/sidebar';
-export function TeamSwitcher({
- teams
+export function WorkspacesSwitcher({
+ workspaces
}: Readonly<{
- teams: {
+ workspaces: {
name: string;
logo: React.ElementType;
plan: string;
}[];
}>) {
const { isMobile } = useSidebar();
- const [activeTeam, setActiveTeam] = React.useState(teams[0]);
+ const [activeWorkspace, setActiveWorkspace] = React.useState(workspaces[0]);
return (
@@ -34,11 +35,11 @@ export function TeamSwitcher({
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
- {activeTeam.name}
- {activeTeam.plan}
+ {activeWorkspace.name}
+ {activeWorkspace.plan}
@@ -50,12 +51,16 @@ export function TeamSwitcher({
sideOffset={4}
>
Teams
- {teams.map((team, index) => (
- setActiveTeam(team)} className="gap-2 p-2">
+ {workspaces.map((workspace, index) => (
+ setActiveWorkspace(workspace)}
+ className="gap-2 p-2"
+ >
-
+
- {team.name}
+ {workspace.name}
⌘{index + 1}
))}
diff --git a/apps/web/lib/features/task/daily-plan/views-header-tabs.tsx b/apps/web/lib/features/task/daily-plan/views-header-tabs.tsx
index dc4c61dd5..837d33e8d 100644
--- a/apps/web/lib/features/task/daily-plan/views-header-tabs.tsx
+++ b/apps/web/lib/features/task/daily-plan/views-header-tabs.tsx
@@ -1,52 +1,48 @@
import { IssuesView } from '@app/constants';
import { dailyPlanViewHeaderTabs } from '@app/stores/header-tabs';
import { clsxm } from '@app/utils';
-import {
- QueueListIcon,
- Squares2X2Icon,
- TableCellsIcon
-} from '@heroicons/react/20/solid';
+import { QueueListIcon, Squares2X2Icon, TableCellsIcon } from '@heroicons/react/20/solid';
import { Tooltip } from 'lib/components';
import { DottedLanguageObjectStringPaths, useTranslations } from 'next-intl';
import { useAtom } from 'jotai';
export default function ViewsHeaderTabs() {
- const t = useTranslations();
- const options = [
- { label: 'CARDS', icon: QueueListIcon, view: IssuesView.CARDS },
- { label: 'TABLE', icon: TableCellsIcon, view: IssuesView.TABLE },
- { label: 'BLOCKS', icon: Squares2X2Icon, view: IssuesView.BLOCKS }
- ];
+ const t = useTranslations();
+ const options = [
+ { label: 'CARDS', icon: QueueListIcon, view: IssuesView.CARDS },
+ { label: 'TABLE', icon: TableCellsIcon, view: IssuesView.TABLE },
+ { label: 'BLOCKS', icon: Squares2X2Icon, view: IssuesView.BLOCKS }
+ ];
- const [view, setView] = useAtom(dailyPlanViewHeaderTabs);
+ const [view, setView] = useAtom(dailyPlanViewHeaderTabs);
- return (
-
- {options.map(({ label, icon: Icon, view: optionView }) => (
-
-
-
- ))}
-
- );
+ return (
+
+ {options.map(({ label, icon: Icon, view: optionView }) => (
+
+
+
+ ))}
+
+ );
}
diff --git a/apps/web/lib/features/task/task-input-kanban.tsx b/apps/web/lib/features/task/task-input-kanban.tsx
index 59000339f..2430c2580 100644
--- a/apps/web/lib/features/task/task-input-kanban.tsx
+++ b/apps/web/lib/features/task/task-input-kanban.tsx
@@ -1,72 +1,55 @@
'use client';
import {
- HostKeys,
- RTuseTaskInput,
- useCallbackRef,
- useHotkeys,
- useOutsideClick,
- useTaskInput,
- useTaskLabels,
- useTaskStatus
+ HostKeys,
+ RTuseTaskInput,
+ useCallbackRef,
+ useHotkeys,
+ useOutsideClick,
+ useTaskInput,
+ useTaskLabels,
+ useTaskStatus
} from '@app/hooks';
import { ITaskPriority, ITaskSize, ITeamTask, Nullable } from '@app/interfaces';
import { timerStatusState } from '@app/stores';
import { clsxm } from '@app/utils';
import { PlusIcon } from '@heroicons/react/20/solid';
-import {
- Button,
- Card,
- InputField,
- SpinnerLoader,
- Tooltip
-} from 'lib/components';
-import {
- MutableRefObject,
- PropsWithChildren,
- useCallback,
- useEffect,
- useMemo,
- useRef,
- useState
-} from 'react';
+import { Button, Card, InputField, SpinnerLoader, Tooltip } from 'lib/components';
+import { MutableRefObject, PropsWithChildren, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useAtomValue } from 'jotai';
import { TaskIssuesDropdown } from './task-issue';
-import {
- ActiveTaskPropertiesDropdown,
- ActiveTaskSizesDropdown
-} from './task-status';
+import { ActiveTaskPropertiesDropdown, ActiveTaskSizesDropdown } from './task-status';
import { useTranslations } from 'next-intl';
import { TaskLabels } from './task-labels';
type Props = {
- task?: Nullable;
- tasks?: ITeamTask[];
- kanbanTitle?: string;
- onTaskClick?: (task: ITeamTask) => void;
- initEditMode?: boolean;
- onCloseCombobox?: () => void;
- inputLoader?: boolean;
- onEnterKey?: (taskName: string, task: ITeamTask) => void;
- keepOpen?: boolean;
- loadingRef?: MutableRefObject;
- closeable_fc?: () => void;
- viewType?: 'input-trigger' | 'one-view';
- createOnEnterClick?: boolean;
- showTaskNumber?: boolean;
- showCombobox?: boolean;
- autoAssignTaskAuth?: boolean;
- fullWidthCombobox?: boolean;
- fullHeightCombobox?: boolean;
- placeholder?: string;
- autoFocus?: boolean;
- autoInputSelectText?: boolean;
- usersTaskCreatedAssignTo?: { id: string }[];
- onTaskCreated?: (task: ITeamTask | undefined) => void;
- cardWithoutShadow?: boolean;
- onClose: any;
-
- forParentChildRelationship?: boolean;
+ task?: Nullable;
+ tasks?: ITeamTask[];
+ kanbanTitle?: string;
+ onTaskClick?: (task: ITeamTask) => void;
+ initEditMode?: boolean;
+ onCloseCombobox?: () => void;
+ inputLoader?: boolean;
+ onEnterKey?: (taskName: string, task: ITeamTask) => void;
+ keepOpen?: boolean;
+ loadingRef?: MutableRefObject;
+ closeable_fc?: () => void;
+ viewType?: 'input-trigger' | 'one-view';
+ createOnEnterClick?: boolean;
+ showTaskNumber?: boolean;
+ showCombobox?: boolean;
+ autoAssignTaskAuth?: boolean;
+ fullWidthCombobox?: boolean;
+ fullHeightCombobox?: boolean;
+ placeholder?: string;
+ autoFocus?: boolean;
+ autoInputSelectText?: boolean;
+ usersTaskCreatedAssignTo?: { id: string }[];
+ onTaskCreated?: (task: ITeamTask | undefined) => void;
+ cardWithoutShadow?: boolean;
+ onClose: any;
+
+ forParentChildRelationship?: boolean;
} & PropsWithChildren;
/**
@@ -77,414 +60,387 @@ type Props = {
*/
export function TaskInputKanban(props: Props) {
- const t = useTranslations();
-
- const { viewType = 'input-trigger', showTaskNumber = false } = props;
-
- const datas = useTaskInput({
- task: props.task,
- initEditMode: props.initEditMode,
- tasks: props.tasks
- });
-
- const onCloseComboboxRef = useCallbackRef(props.onCloseCombobox);
- const closeable_fcRef = useCallbackRef(props.closeable_fc);
- const $onTaskCreated = useCallbackRef(props.onTaskCreated);
- const inputRef = useRef(null);
- const timerStatus = useAtomValue(timerStatusState);
- const timerRunningStatus = useMemo(() => {
- return Boolean(timerStatus?.running);
- }, [timerStatus]);
-
- const onTaskCreated = useCallback(
- (task: ITeamTask | undefined) =>
- $onTaskCreated.current && $onTaskCreated.current(task),
- [$onTaskCreated]
- );
-
- const {
- inputTask,
- setTaskIssue,
- editMode,
- setEditMode,
- setQuery,
- updateLoading,
- updateTaskTitleHandler,
- setFilter
- } = datas;
-
- const inputTaskTitle = useMemo(() => inputTask?.title || '', [
- inputTask?.title
- ]);
-
- const [taskName, setTaskName] = useState('');
-
- const { targetEl, ignoreElementRef } = useOutsideClick(
- () => !props.keepOpen && setEditMode(false)
- );
-
- useEffect(() => {
- setQuery(taskName === inputTask?.title ? '' : taskName);
- }, [taskName, inputTask, setQuery]);
-
- useEffect(() => {
- setTaskName(inputTaskTitle);
- }, [editMode, inputTaskTitle]);
-
- useEffect(() => {
- /**
- * Call onCloseCombobox only when the menu has been closed
- */
- !editMode && onCloseComboboxRef.current && onCloseComboboxRef.current();
- }, [editMode, onCloseComboboxRef]);
-
- /**
- * On update task name
- */
- const updateTaskNameHandler = useCallback(
- (task: ITeamTask, title: string) => {
- if (task.title !== title) {
- !updateLoading && updateTaskTitleHandler(task, title);
- }
- },
- [updateLoading, updateTaskTitleHandler]
- );
-
- /**
- * Signle parent about updating and close event (that can trigger close component e.g)
- */
- useEffect(() => {
- if (props.loadingRef?.current && !updateLoading) {
- closeable_fcRef.current && closeable_fcRef.current();
- }
-
- if (props.loadingRef) {
- props.loadingRef.current = updateLoading;
- }
- }, [updateLoading, props.loadingRef, closeable_fcRef]);
-
- /* Setting the filter to open when the edit mode is true. */
- useEffect(() => {
- editMode && setFilter('open');
- }, [editMode, setFilter]);
-
- /*
+ const t = useTranslations();
+
+ const { viewType = 'input-trigger', showTaskNumber = false } = props;
+
+ const datas = useTaskInput({
+ task: props.task,
+ initEditMode: props.initEditMode,
+ tasks: props.tasks
+ });
+
+ const onCloseComboboxRef = useCallbackRef(props.onCloseCombobox);
+ const closeable_fcRef = useCallbackRef(props.closeable_fc);
+ const $onTaskCreated = useCallbackRef(props.onTaskCreated);
+ const inputRef = useRef(null);
+ const timerStatus = useAtomValue(timerStatusState);
+ const timerRunningStatus = useMemo(() => {
+ return Boolean(timerStatus?.running);
+ }, [timerStatus]);
+
+ const onTaskCreated = useCallback(
+ (task: ITeamTask | undefined) => $onTaskCreated.current && $onTaskCreated.current(task),
+ [$onTaskCreated]
+ );
+
+ const {
+ inputTask,
+ setTaskIssue,
+ editMode,
+ setEditMode,
+ setQuery,
+ updateLoading,
+ updateTaskTitleHandler,
+ setFilter
+ } = datas;
+
+ const inputTaskTitle = useMemo(() => inputTask?.title || '', [inputTask?.title]);
+
+ const [taskName, setTaskName] = useState('');
+
+ const { targetEl, ignoreElementRef } = useOutsideClick(
+ () => !props.keepOpen && setEditMode(false)
+ );
+
+ useEffect(() => {
+ setQuery(taskName === inputTask?.title ? '' : taskName);
+ }, [taskName, inputTask, setQuery]);
+
+ useEffect(() => {
+ setTaskName(inputTaskTitle);
+ }, [editMode, inputTaskTitle]);
+
+ useEffect(() => {
+ /**
+ * Call onCloseCombobox only when the menu has been closed
+ */
+ !editMode && onCloseComboboxRef.current && onCloseComboboxRef.current();
+ }, [editMode, onCloseComboboxRef]);
+
+ /**
+ * On update task name
+ */
+ const updateTaskNameHandler = useCallback(
+ (task: ITeamTask, title: string) => {
+ if (task.title !== title) {
+ !updateLoading && updateTaskTitleHandler(task, title);
+ }
+ },
+ [updateLoading, updateTaskTitleHandler]
+ );
+
+ /**
+ * Signle parent about updating and close event (that can trigger close component e.g)
+ */
+ useEffect(() => {
+ if (props.loadingRef?.current && !updateLoading) {
+ closeable_fcRef.current && closeable_fcRef.current();
+ }
+
+ if (props.loadingRef) {
+ props.loadingRef.current = updateLoading;
+ }
+ }, [updateLoading, props.loadingRef, closeable_fcRef]);
+
+ /* Setting the filter to open when the edit mode is true. */
+ useEffect(() => {
+ editMode && setFilter('open');
+ }, [editMode, setFilter]);
+
+ /*
If task is passed then we don't want to set the active task for the authenticated user.
after task creation
*/
- const handleTaskCreation = useCallback(async () => {
- /* Checking if the `handleTaskCreation` is available and if the `hasCreateForm` is true. */
- datas &&
- datas.handleTaskCreation &&
- datas.hasCreateForm &&
- datas
- .handleTaskCreation({
- autoActiveTask: true,
- autoAssignTaskAuth: props.autoAssignTaskAuth,
- assignToUsers: props.usersTaskCreatedAssignTo || []
- })
- ?.then(onTaskCreated)
- .finally(async () => {
- setTaskName('');
-
- props.onClose && props.onClose();
- });
- }, [datas, props, onTaskCreated]);
-
- const updatedTaskList = useMemo(() => {
- let updatedTaskList: ITeamTask[] = [];
- if (props.forParentChildRelationship) {
- if (
- // Story can have ParentId set to Epic ID
- props.task?.issueType === 'Story'
- ) {
- updatedTaskList = datas.filteredTasks.filter(
- (item) => item.issueType === 'Epic'
- );
- } else if (
- // TASK|BUG can have ParentId to be set either to Story ID or Epic ID
- props.task?.issueType === 'Task' ||
- props.task?.issueType === 'Bug' ||
- !props.task?.issueType
- ) {
- updatedTaskList = datas.filteredTasks.filter(
- (item) => item.issueType === 'Epic' || item.issueType === 'Story'
- );
- } else {
- updatedTaskList = datas.filteredTasks;
- }
-
- if (props.task?.children && props.task?.children?.length) {
- const childrenTaskIds = props.task?.children?.map((item) => item.id);
- updatedTaskList = updatedTaskList.filter(
- (item) => !childrenTaskIds.includes(item.id)
- );
- }
- }
-
- return updatedTaskList;
- }, [props.task, datas.filteredTasks]);
-
- useEffect(() => {
- const handleClickOutside = (event: MouseEvent) => {
- if (
- inputRef.current &&
- !inputRef.current.contains(event.target as Node) &&
- editMode
- ) {
- inputTask && updateTaskNameHandler(inputTask, taskName);
- }
- };
-
- // Attach the event listener
- document.addEventListener('mousedown', handleClickOutside);
-
- // Clean up the event listener on component unmount
- return () => {
- document.removeEventListener('mousedown', handleClickOutside);
- };
- }, [inputTask, taskName, updateTaskNameHandler, editMode]);
-
- // Handling Hotkeys
- const handleCommandKeySequence = useCallback(() => {
- if (!editMode) {
- setEditMode(true);
- if (targetEl.current) {
- targetEl.current.focus();
- }
- } else {
- setEditMode(false);
- }
- }, [setEditMode, editMode, targetEl]);
- useHotkeys(HostKeys.CREATE_TASK, handleCommandKeySequence);
-
- useEffect(() => {
- if (props.autoFocus && targetEl.current) {
- targetEl.current.focus();
- }
- }, [props.autoFocus, targetEl]);
-
- const inputField = (
- {
- setEditMode(true);
- props.autoInputSelectText && setTimeout(() => e?.target?.select(), 10);
- }}
- onChange={(event) => {
- setTaskName(event.target.value);
- }}
- onKeyUp={(e) => {
- if (e.key === 'Enter' && inputTask) {
- /* If createOnEnterClick is false then updateTaskNameHandler is called. */
- !props.createOnEnterClick &&
- updateTaskNameHandler(inputTask, taskName);
-
- props.onEnterKey && props.onEnterKey(taskName, inputTask);
- }
- /* Creating a new task when the enter key is pressed. */
- if (e.key === 'Enter') {
- props.createOnEnterClick && handleTaskCreation();
- }
- }}
- trailingNode={
- /* Showing the spinner when the task is being updated. */
-
- {props.task ? (
- (updateLoading || props.inputLoader) &&
- ) : (
- <>{updateLoading && }>
- )}
-
- }
- className={clsxm(
- showTaskNumber && inputTask && ['pl-2'],
- 'dark:bg-[#1B1D22]',
- props.initEditMode && 'h-10'
- )}
- /* Showing the task number. */
- leadingNode={
- // showTaskNumber &&
- // inputTask &&
-
- setTaskIssue(v)}
- />
-
- }
- />
- );
-
- const taskCard = (
-
- );
-
- return taskCard;
+ const handleTaskCreation = useCallback(async () => {
+ /* Checking if the `handleTaskCreation` is available and if the `hasCreateForm` is true. */
+ datas &&
+ datas.handleTaskCreation &&
+ datas.hasCreateForm &&
+ datas
+ .handleTaskCreation({
+ autoActiveTask: true,
+ autoAssignTaskAuth: props.autoAssignTaskAuth,
+ assignToUsers: props.usersTaskCreatedAssignTo || []
+ })
+ ?.then(onTaskCreated)
+ .finally(async () => {
+ setTaskName('');
+
+ props.onClose && props.onClose();
+ });
+ }, [datas, props, onTaskCreated]);
+
+ const updatedTaskList = useMemo(() => {
+ let updatedTaskList: ITeamTask[] = [];
+ if (props.forParentChildRelationship) {
+ if (
+ // Story can have ParentId set to Epic ID
+ props.task?.issueType === 'Story'
+ ) {
+ updatedTaskList = datas.filteredTasks.filter((item) => item.issueType === 'Epic');
+ } else if (
+ // TASK|BUG can have ParentId to be set either to Story ID or Epic ID
+ props.task?.issueType === 'Task' ||
+ props.task?.issueType === 'Bug' ||
+ !props.task?.issueType
+ ) {
+ updatedTaskList = datas.filteredTasks.filter(
+ (item) => item.issueType === 'Epic' || item.issueType === 'Story'
+ );
+ } else {
+ updatedTaskList = datas.filteredTasks;
+ }
+
+ if (props.task?.children && props.task?.children?.length) {
+ const childrenTaskIds = props.task?.children?.map((item) => item.id);
+ updatedTaskList = updatedTaskList.filter((item) => !childrenTaskIds.includes(item.id));
+ }
+ }
+
+ return updatedTaskList;
+ }, [props.task, datas.filteredTasks]);
+
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (inputRef.current && !inputRef.current.contains(event.target as Node) && editMode) {
+ inputTask && updateTaskNameHandler(inputTask, taskName);
+ }
+ };
+
+ // Attach the event listener
+ document.addEventListener('mousedown', handleClickOutside);
+
+ // Clean up the event listener on component unmount
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, [inputTask, taskName, updateTaskNameHandler, editMode]);
+
+ // Handling Hotkeys
+ const handleCommandKeySequence = useCallback(() => {
+ if (!editMode) {
+ setEditMode(true);
+ if (targetEl.current) {
+ targetEl.current.focus();
+ }
+ } else {
+ setEditMode(false);
+ }
+ }, [setEditMode, editMode, targetEl]);
+ useHotkeys(HostKeys.CREATE_TASK, handleCommandKeySequence);
+
+ useEffect(() => {
+ if (props.autoFocus && targetEl.current) {
+ targetEl.current.focus();
+ }
+ }, [props.autoFocus, targetEl]);
+
+ const inputField = (
+ {
+ setEditMode(true);
+ props.autoInputSelectText && setTimeout(() => e?.target?.select(), 10);
+ }}
+ onChange={(event) => {
+ setTaskName(event.target.value);
+ }}
+ onKeyUp={(e) => {
+ if (e.key === 'Enter' && inputTask) {
+ /* If createOnEnterClick is false then updateTaskNameHandler is called. */
+ !props.createOnEnterClick && updateTaskNameHandler(inputTask, taskName);
+
+ props.onEnterKey && props.onEnterKey(taskName, inputTask);
+ }
+ /* Creating a new task when the enter key is pressed. */
+ if (e.key === 'Enter') {
+ props.createOnEnterClick && handleTaskCreation();
+ }
+ }}
+ trailingNode={
+ /* Showing the spinner when the task is being updated. */
+
+ {props.task ? (
+ (updateLoading || props.inputLoader) &&
+ ) : (
+ <>{updateLoading && }>
+ )}
+
+ }
+ className={clsxm(
+ showTaskNumber && inputTask && ['pl-2'],
+ 'dark:bg-[#1B1D22]',
+ props.initEditMode && 'h-10'
+ )}
+ /* Showing the task number. */
+ leadingNode={
+ // showTaskNumber &&
+ // inputTask &&
+
+ setTaskIssue(v)}
+ />
+
+ }
+ />
+ );
+
+ const taskCard = (
+
+ );
+
+ return taskCard;
}
/**
* A component that is used to render the task list.
*/
function TaskCard({
- datas,
- inputField,
- kanbanTitle,
- handleTaskCreation
+ datas,
+ inputField,
+ kanbanTitle,
+ handleTaskCreation
}: {
- datas: Partial;
- inputField?: JSX.Element;
- kanbanTitle: string;
- fullWidth?: boolean;
- fullHeight?: boolean;
- handleTaskCreation: () => void;
- cardWithoutShadow?: boolean;
- updatedTaskList?: ITeamTask[];
+ datas: Partial;
+ inputField?: JSX.Element;
+ kanbanTitle: string;
+ fullWidth?: boolean;
+ fullHeight?: boolean;
+ handleTaskCreation: () => void;
+ cardWithoutShadow?: boolean;
+ updatedTaskList?: ITeamTask[];
}) {
- const t = useTranslations();
- const activeTaskEl = useRef(null);
- const { taskLabels: taskLabelsData } = useTaskLabels();
-
- const {
- taskStatus,
- taskPriority,
- taskSize,
- taskDescription,
- taskLabels
- } = datas;
- useEffect(() => {
- if (taskStatus) {
- taskStatus.current = kanbanTitle ?? 'open';
- }
- }, [taskStatus, datas.hasCreateForm, kanbanTitle]);
- useEffect(() => {
- if (datas.editMode) {
- window.setTimeout(() => {
- activeTaskEl?.current?.scrollIntoView({
- block: 'nearest',
- inline: 'start'
- });
- }, 10);
- }
- }, [datas.editMode]);
- const taskStatusHook = useTaskStatus();
- return (
- <>
-
- {inputField}
-
- {/* Create team button */}
-
-
- {datas.hasCreateForm && (
-
-
{
- if (taskDescription) {
- taskDescription.current = e.target.value;
- }
- }}
- className={'dark:bg-[#1B1D22]'}
- />
-
-
-
{
- if (v && taskPriority) {
- taskPriority.current = v;
- }
- }}
- defaultValue={taskPriority?.current as ITaskPriority}
- task={null}
- />
-
- {
- if (v && taskSize) {
- taskSize.current = v;
- }
- }}
- defaultValue={taskSize?.current as ITaskSize}
- task={null}
- />
- {
- taskLabelsData.filter((tag) =>
- tag.name ? values?.includes(tag.name) : false
- );
-
- if (taskLabels && values?.length) {
- taskLabels.current = taskLabelsData.filter((tag) =>
- tag.name ? values?.includes(tag.name) : false
- );
- }
- }}
- task={datas.inputTask}
- />
-
-
- )}
-
-
-
-
-
-
-
-
- {/* Just some spaces at the end */}
- {'|'}
- >
- );
+ const t = useTranslations();
+ const activeTaskEl = useRef(null);
+ const { taskLabels: taskLabelsData } = useTaskLabels();
+
+ const { taskStatus, taskPriority, taskSize, taskDescription, taskLabels } = datas;
+ useEffect(() => {
+ if (taskStatus) {
+ taskStatus.current = kanbanTitle ?? 'open';
+ }
+ }, [taskStatus, datas.hasCreateForm, kanbanTitle]);
+ useEffect(() => {
+ if (datas.editMode) {
+ window.setTimeout(() => {
+ activeTaskEl?.current?.scrollIntoView({
+ block: 'nearest',
+ inline: 'start'
+ });
+ }, 10);
+ }
+ }, [datas.editMode]);
+ const taskStatusHook = useTaskStatus();
+ return (
+ <>
+
+ {inputField}
+
+ {/* Create team button */}
+
+
+ {datas.hasCreateForm && (
+
+
{
+ if (taskDescription) {
+ taskDescription.current = e.target.value;
+ }
+ }}
+ className={'dark:bg-[#1B1D22]'}
+ />
+
+
+
{
+ if (v && taskPriority) {
+ taskPriority.current = v;
+ }
+ }}
+ defaultValue={taskPriority?.current as ITaskPriority}
+ task={null}
+ />
+
+ {
+ if (v && taskSize) {
+ taskSize.current = v;
+ }
+ }}
+ defaultValue={taskSize?.current as ITaskSize}
+ task={null}
+ />
+ {
+ taskLabelsData.filter((tag) =>
+ tag.name ? values?.includes(tag.name) : false
+ );
+
+ if (taskLabels && values?.length) {
+ taskLabels.current = taskLabelsData.filter((tag) =>
+ tag.name ? values?.includes(tag.name) : false
+ );
+ }
+ }}
+ task={datas.inputTask}
+ />
+
+
+ )}
+
+
+
+
+
+
+
+
+ {/* Just some spaces at the end */}
+ {'|'}
+ >
+ );
}
diff --git a/apps/web/lib/features/task/task-input.tsx b/apps/web/lib/features/task/task-input.tsx
index f63ad9ed9..a05abd268 100644
--- a/apps/web/lib/features/task/task-input.tsx
+++ b/apps/web/lib/features/task/task-input.tsx
@@ -1,61 +1,49 @@
'use client';
import {
- HostKeys,
- RTuseTaskInput,
- useAuthenticateUser,
- useCallbackRef,
- useHotkeys,
- useIssueType,
- useOrganizationEmployeeTeams,
- useOrganizationTeams,
- useOutsideClick,
- useTaskInput,
- useTaskLabels
+ HostKeys,
+ RTuseTaskInput,
+ useAuthenticateUser,
+ useCallbackRef,
+ useHotkeys,
+ useIssueType,
+ useOrganizationEmployeeTeams,
+ useOrganizationTeams,
+ useOutsideClick,
+ useTaskInput,
+ useTaskLabels
} from '@app/hooks';
import {
- IIssueTypesItemList,
- ITaskIssue,
- ITaskPriority,
- ITaskSize,
- ITaskStatus,
- ITeamTask,
- Nullable,
- OT_Member
+ IIssueTypesItemList,
+ ITaskIssue,
+ ITaskPriority,
+ ITaskSize,
+ ITaskStatus,
+ ITeamTask,
+ Nullable,
+ OT_Member
} from '@app/interfaces';
import { activeTeamTaskId, timerStatusState } from '@app/stores';
import { clsxm } from '@app/utils';
import { Combobox, Popover, Transition } from '@headlessui/react';
import { CheckIcon, ChevronUpDownIcon, PlusIcon } from '@heroicons/react/20/solid';
-import {
- Button,
- Card,
- Divider,
- InputField,
- OutlineBadge,
- SpinnerLoader,
- Tooltip
-} from 'lib/components';
+import { Button, Card, Divider, InputField, OutlineBadge, SpinnerLoader, Tooltip } from 'lib/components';
import { CheckCircleTickIcon as TickCircleIcon } from 'assets/svg';
import {
- Fragment,
- MutableRefObject,
- PropsWithChildren,
- useCallback,
- useEffect,
- useMemo,
- useRef,
- useState
+ Fragment,
+ MutableRefObject,
+ PropsWithChildren,
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState
} from 'react';
import { useAtomValue, useSetAtom } from 'jotai';
import { ActiveTaskIssuesDropdown, TaskIssuesDropdown } from './task-issue';
import { TaskItem } from './task-item';
import { TaskLabels } from './task-labels';
-import {
- ActiveTaskPropertiesDropdown,
- ActiveTaskSizesDropdown,
- ActiveTaskStatusDropdown
-} from './task-status';
+import { ActiveTaskPropertiesDropdown, ActiveTaskSizesDropdown, ActiveTaskStatusDropdown } from './task-status';
import { useTranslations } from 'next-intl';
import { useInfinityScrolling } from '@app/hooks/useInfinityFetch';
import { ObserverComponent } from '@components/shared/Observer';
@@ -63,32 +51,32 @@ import { LazyRender } from 'lib/components/lazy-render';
import { ProjectDropDown } from '@components/pages/task/details-section/blocks/task-secondary-info';
type Props = {
- task?: Nullable;
- tasks?: ITeamTask[];
- onTaskClick?: (task: ITeamTask) => void;
- initEditMode?: boolean;
- onCloseCombobox?: () => void;
- inputLoader?: boolean;
- onEnterKey?: (taskName: string, task: ITeamTask) => void;
- keepOpen?: boolean;
- loadingRef?: MutableRefObject;
- closeable_fc?: () => void;
- viewType?: 'input-trigger' | 'one-view';
- createOnEnterClick?: boolean;
- showTaskNumber?: boolean;
- showCombobox?: boolean;
- showEmoji?: boolean;
- autoAssignTaskAuth?: boolean;
- fullWidthCombobox?: boolean;
- fullHeightCombobox?: boolean;
- placeholder?: string;
- autoFocus?: boolean;
- autoInputSelectText?: boolean;
- usersTaskCreatedAssignTo?: { id: string }[];
- onTaskCreated?: (task: ITeamTask | undefined) => void;
- cardWithoutShadow?: boolean;
- assignTaskPopup?: boolean;
- forParentChildRelationship?: boolean;
+ task?: Nullable;
+ tasks?: ITeamTask[];
+ onTaskClick?: (task: ITeamTask) => void;
+ initEditMode?: boolean;
+ onCloseCombobox?: () => void;
+ inputLoader?: boolean;
+ onEnterKey?: (taskName: string, task: ITeamTask) => void;
+ keepOpen?: boolean;
+ loadingRef?: MutableRefObject;
+ closeable_fc?: () => void;
+ viewType?: 'input-trigger' | 'one-view';
+ createOnEnterClick?: boolean;
+ showTaskNumber?: boolean;
+ showCombobox?: boolean;
+ showEmoji?: boolean;
+ autoAssignTaskAuth?: boolean;
+ fullWidthCombobox?: boolean;
+ fullHeightCombobox?: boolean;
+ placeholder?: string;
+ autoFocus?: boolean;
+ autoInputSelectText?: boolean;
+ usersTaskCreatedAssignTo?: { id: string }[];
+ onTaskCreated?: (task: ITeamTask | undefined) => void;
+ cardWithoutShadow?: boolean;
+ assignTaskPopup?: boolean;
+ forParentChildRelationship?: boolean;
} & PropsWithChildren;
/**
@@ -99,357 +87,324 @@ type Props = {
*/
export function TaskInput(props: Props) {
- const t = useTranslations();
- const { issueTypes } = useIssueType();
- const defaultIssueType: IIssueTypesItemList | undefined = issueTypes.find(
- (issue) => issue.isDefault
- );
-
- const {
- viewType = 'input-trigger',
- showTaskNumber = false,
- showCombobox = true,
- } = props;
-
- const datas = useTaskInput({
- task: props.task,
- initEditMode: props.initEditMode,
- tasks: props.tasks
- });
-
- const { updateOrganizationTeamEmployee } = useOrganizationEmployeeTeams();
- const { activeTeam } = useOrganizationTeams();
- const { user } = useAuthenticateUser();
-
- const onCloseComboboxRef = useCallbackRef(props.onCloseCombobox);
- const closeable_fcRef = useCallbackRef(props.closeable_fc);
- const $onTaskClick = useCallbackRef(props.onTaskClick);
- const $onTaskCreated = useCallbackRef(props.onTaskCreated);
- const inputRef = useRef(null);
- const timerStatus = useAtomValue(timerStatusState);
- const timerRunningStatus = useMemo(() => {
- return Boolean(timerStatus?.running);
- }, [timerStatus]);
-
- const onTaskCreated = useCallback(
- (task: ITeamTask | undefined) =>
- $onTaskCreated.current && $onTaskCreated.current(task),
- [$onTaskCreated]
- );
-
- const onTaskClick = useCallback(
- (task: ITeamTask) => $onTaskClick.current && $onTaskClick.current(task),
- [$onTaskClick]
- );
-
- const {
- inputTask,
- setTaskIssue,
- editMode,
- setEditMode,
- setQuery,
- updateLoading,
- updateTaskTitleHandler,
- setFilter
- } = datas;
- const setActiveTask = useSetAtom(activeTeamTaskId);
-
- const inputTaskTitle = useMemo(() => inputTask?.title || '', [
- inputTask?.title
- ]);
-
- const [taskName, setTaskName] = useState('');
-
- const { targetEl, ignoreElementRef } = useOutsideClick(
- () => !props.keepOpen && setEditMode(false)
- );
-
- useEffect(() => {
- setQuery(taskName === inputTask?.title ? '' : taskName);
- }, [taskName, inputTask, setQuery]);
-
- useEffect(() => {
- setTaskName(inputTaskTitle);
- }, [editMode, inputTaskTitle]);
-
- useEffect(() => {
- /**
- * Call onCloseCombobox only when the menu has been closed
- */
- !editMode && onCloseComboboxRef.current && onCloseComboboxRef.current();
- }, [editMode, onCloseComboboxRef]);
-
- /**
- * set the active task for the authenticated user
- */
- const setAuthActiveTask = useCallback(
- (task: ITeamTask) => {
- if (datas.setActiveTask) {
- datas.setActiveTask(task);
-
- // Update Current user's active task to sync across multiple devices
- const currentEmployeeDetails = activeTeam?.members.find(
- (member) => member.employeeId === user?.employee?.id
- );
- if (currentEmployeeDetails && currentEmployeeDetails.id) {
- updateOrganizationTeamEmployee(currentEmployeeDetails.id, {
- organizationId: task.organizationId,
- activeTaskId: task.id,
- organizationTeamId: activeTeam?.id,
- tenantId: activeTeam?.tenantId
- });
- }
- }
- setEditMode(false);
- },
- [datas, setEditMode, activeTeam, user, updateOrganizationTeamEmployee]
- );
-
- /**
- * On update task name
- */
- const updateTaskNameHandler = useCallback(
- (task: ITeamTask, title: string) => {
- if (task.title !== title) {
- !updateLoading && updateTaskTitleHandler(task, title);
- }
- },
- [updateLoading, updateTaskTitleHandler]
- );
-
- /**
- * Signle parent about updating and close event (that can trigger close component e.g)
- */
- useEffect(() => {
- if (props.loadingRef?.current && !updateLoading) {
- closeable_fcRef.current && closeable_fcRef.current();
- }
-
- if (props.loadingRef) {
- props.loadingRef.current = updateLoading;
- }
- }, [updateLoading, props.loadingRef, closeable_fcRef]);
-
- /* Setting the filter to open when the edit mode is true. */
- useEffect(() => {
- editMode && setFilter('open');
- }, [editMode, setFilter]);
-
- /*
+ const t = useTranslations();
+ const { issueTypes } = useIssueType();
+ const defaultIssueType: IIssueTypesItemList | undefined = issueTypes.find((issue) => issue.isDefault);
+
+ const { viewType = 'input-trigger', showTaskNumber = false, showCombobox = true } = props;
+
+ const datas = useTaskInput({
+ task: props.task,
+ initEditMode: props.initEditMode,
+ tasks: props.tasks
+ });
+
+ const { updateOrganizationTeamEmployee } = useOrganizationEmployeeTeams();
+ const { activeTeam } = useOrganizationTeams();
+ const { user } = useAuthenticateUser();
+
+ const onCloseComboboxRef = useCallbackRef(props.onCloseCombobox);
+ const closeable_fcRef = useCallbackRef(props.closeable_fc);
+ const $onTaskClick = useCallbackRef(props.onTaskClick);
+ const $onTaskCreated = useCallbackRef(props.onTaskCreated);
+ const inputRef = useRef(null);
+ const timerStatus = useAtomValue(timerStatusState);
+ const timerRunningStatus = useMemo(() => {
+ return Boolean(timerStatus?.running);
+ }, [timerStatus]);
+
+ const onTaskCreated = useCallback(
+ (task: ITeamTask | undefined) => $onTaskCreated.current && $onTaskCreated.current(task),
+ [$onTaskCreated]
+ );
+
+ const onTaskClick = useCallback(
+ (task: ITeamTask) => $onTaskClick.current && $onTaskClick.current(task),
+ [$onTaskClick]
+ );
+
+ const {
+ inputTask,
+ setTaskIssue,
+ editMode,
+ setEditMode,
+ setQuery,
+ updateLoading,
+ updateTaskTitleHandler,
+ setFilter
+ } = datas;
+ const setActiveTask = useSetAtom(activeTeamTaskId);
+
+ const inputTaskTitle = useMemo(() => inputTask?.title || '', [inputTask?.title]);
+
+ const [taskName, setTaskName] = useState('');
+
+ const { targetEl, ignoreElementRef } = useOutsideClick(
+ () => !props.keepOpen && setEditMode(false)
+ );
+
+ useEffect(() => {
+ setQuery(taskName === inputTask?.title ? '' : taskName);
+ }, [taskName, inputTask, setQuery]);
+
+ useEffect(() => {
+ setTaskName(inputTaskTitle);
+ }, [editMode, inputTaskTitle]);
+
+ useEffect(() => {
+ /**
+ * Call onCloseCombobox only when the menu has been closed
+ */
+ !editMode && onCloseComboboxRef.current && onCloseComboboxRef.current();
+ }, [editMode, onCloseComboboxRef]);
+
+ /**
+ * set the active task for the authenticated user
+ */
+ const setAuthActiveTask = useCallback(
+ (task: ITeamTask) => {
+ if (datas.setActiveTask) {
+ datas.setActiveTask(task);
+
+ // Update Current user's active task to sync across multiple devices
+ const currentEmployeeDetails = activeTeam?.members.find(
+ (member) => member.employeeId === user?.employee?.id
+ );
+ if (currentEmployeeDetails && currentEmployeeDetails.id) {
+ updateOrganizationTeamEmployee(currentEmployeeDetails.id, {
+ organizationId: task.organizationId,
+ activeTaskId: task.id,
+ organizationTeamId: activeTeam?.id,
+ tenantId: activeTeam?.tenantId
+ });
+ }
+ }
+ setEditMode(false);
+ },
+ [datas, setEditMode, activeTeam, user, updateOrganizationTeamEmployee]
+ );
+
+ /**
+ * On update task name
+ */
+ const updateTaskNameHandler = useCallback(
+ (task: ITeamTask, title: string) => {
+ if (task.title !== title) {
+ !updateLoading && updateTaskTitleHandler(task, title);
+ }
+ },
+ [updateLoading, updateTaskTitleHandler]
+ );
+
+ /**
+ * Signle parent about updating and close event (that can trigger close component e.g)
+ */
+ useEffect(() => {
+ if (props.loadingRef?.current && !updateLoading) {
+ closeable_fcRef.current && closeable_fcRef.current();
+ }
+
+ if (props.loadingRef) {
+ props.loadingRef.current = updateLoading;
+ }
+ }, [updateLoading, props.loadingRef, closeable_fcRef]);
+
+ /* Setting the filter to open when the edit mode is true. */
+ useEffect(() => {
+ editMode && setFilter('open');
+ }, [editMode, setFilter]);
+
+ /*
If task is passed then we don't want to set the active task for the authenticated user.
after task creation
*/
- const autoActiveTask: boolean = props.task === undefined;
- const handleTaskCreation = useCallback(() => {
- /* Checking if the `handleTaskCreation` is available and if the `hasCreateForm` is true. */
- datas &&
- datas.handleTaskCreation &&
- datas.hasCreateForm &&
- datas
- .handleTaskCreation({
- autoActiveTask,
- autoAssignTaskAuth: props.autoAssignTaskAuth,
- assignToUsers: props.usersTaskCreatedAssignTo || []
- })
- ?.then(onTaskCreated)
- .finally(() => {
- viewType === 'one-view' && setTaskName('');
- });
- }, [datas, props, autoActiveTask, onTaskCreated, viewType]);
-
- const updatedTaskList = useMemo(() => {
- let updatedTaskList: ITeamTask[] = [];
- if (props.forParentChildRelationship) {
- if (
- // Story can have ParentId set to Epic ID
- props.task?.issueType === 'Story'
- ) {
- updatedTaskList = datas.filteredTasks.filter(
- (item) => item.issueType === 'Epic'
- );
- } else if (
- // TASK|BUG can have ParentId to be set either to Story ID or Epic ID
- props.task?.issueType === 'Task' ||
- props.task?.issueType === 'Bug' ||
- !props.task?.issueType
- ) {
- updatedTaskList = datas.filteredTasks.filter(
- (item) => item.issueType === 'Epic' || item.issueType === 'Story'
- );
- } else {
- updatedTaskList = datas.filteredTasks;
- }
-
- if (props.task?.children && props.task?.children?.length) {
- const childrenTaskIds = props.task?.children?.map((item) => item.id);
- updatedTaskList = updatedTaskList.filter(
- (item) => !childrenTaskIds.includes(item.id)
- );
- }
- }
-
- return updatedTaskList;
- }, [props.task, datas.filteredTasks]);
-
- useEffect(() => {
- const handleClickOutside = (event: MouseEvent) => {
- if (
- inputRef.current &&
- !inputRef.current.contains(event.target as Node) &&
- editMode
- ) {
- // inputTask && updateTaskNameHandler(inputTask, taskName);
- if (taskName == inputTaskTitle) {
- setEditMode(false);
- setActiveTask({
- id: ''
- });
- }
- }
- };
-
- // Attach the event listener
- document.addEventListener('mousedown', handleClickOutside);
-
- // Clean up the event listener on component unmount
- return () => {
- document.removeEventListener('mousedown', handleClickOutside);
- };
- }, [
- inputTask,
- taskName,
- setActiveTask,
- updateTaskNameHandler,
- editMode,
- inputTaskTitle,
- setEditMode
- ]);
- const [openPopoverId, setOpenPopoverId] = useState(null);
- const handlePopoverToggle = useCallback((popoverId: string) => {
- if (openPopoverId === popoverId) {
- setOpenPopoverId(null);
- } else {
- setOpenPopoverId(popoverId);
- }
- }, [openPopoverId]);
-
- // Handling Hotkeys
- const handleCommandKeySequence = useCallback(() => {
- if (!editMode) {
- setEditMode(true);
- if (targetEl.current) {
- targetEl.current.focus();
- }
- } else {
- setEditMode(false);
- }
- }, [setEditMode, editMode, targetEl]);
-
- useHotkeys(HostKeys.CREATE_TASK, handleCommandKeySequence);
-
- useEffect(() => {
- if (props.autoFocus && targetEl.current) {
- targetEl.current.focus();
- }
- }, [props.autoFocus, targetEl]);
-
- // const savedIssueType : string | null = localStorage.getItem('savedIssueType') as string && null;
-
- const inputField = (
-
- }
- autoFocus={props.autoFocus}
- wrapperClassName={`rounded-lg dark:bg-[#1B1D22]`}
- placeholder={props.placeholder || t('form.TASK_INPUT_PLACEHOLDER')}
- onFocus={(e) => {
- setEditMode(true);
- props.autoInputSelectText && setTimeout(() => e?.target?.select(), 10);
- }}
- onChange={(event) => {
- setTaskName(event.target.value);
- }}
- onKeyUp={(e) => {
- if (e.key === 'Enter' && inputTask) {
- /* If createOnEnterClick is false then updateTaskNameHandler is called. */
- !props.createOnEnterClick &&
- updateTaskNameHandler(inputTask, taskName);
-
- props.onEnterKey && props.onEnterKey(taskName, inputTask);
- }
- /* Creating a new task when the enter key is pressed. */
- if (e.key === 'Enter') {
- props.createOnEnterClick && handleTaskCreation();
- }
- }}
- trailingNode={
- /* Showing the spinner when the task is being updated. */
-
- {props.task ? (
- (updateLoading || props.inputLoader) &&
- ) : (
- <>{updateLoading && }>
- )}
-
- }
- className={clsxm(
- showTaskNumber && inputTask && ['pl-2'],
- 'dark:bg-[#1B1D22]',
- props.initEditMode && 'h-10'
- )}
- /* Showing the task number and issue type */
- leadingNode={
- // showTaskNumber &&
- // inputTask &&
-
- {!datas.hasCreateForm ? (
-
- ) : (
-
setTaskIssue(v)}
- defaultValue={
- defaultIssueType
- ? defaultIssueType.name
- : (localStorage.getItem('lastTaskIssue') as ITaskIssue) ||
- null
- }
- />
- )}
-
- {!datas.hasCreateForm && (
-
- #{(inputTask && inputTask.taskNumber) || ''}
-
- )}
-
- }
- />
- );
-
- const taskCard = (
+ const autoActiveTask: boolean = props.task === undefined;
+ const handleTaskCreation = useCallback(() => {
+ /* Checking if the `handleTaskCreation` is available and if the `hasCreateForm` is true. */
+ datas &&
+ datas.handleTaskCreation &&
+ datas.hasCreateForm &&
+ datas
+ .handleTaskCreation({
+ autoActiveTask,
+ autoAssignTaskAuth: props.autoAssignTaskAuth,
+ assignToUsers: props.usersTaskCreatedAssignTo || []
+ })
+ ?.then(onTaskCreated)
+ .finally(() => {
+ viewType === 'one-view' && setTaskName('');
+ });
+ }, [datas, props, autoActiveTask, onTaskCreated, viewType]);
+
+ const updatedTaskList = useMemo(() => {
+ let updatedTaskList: ITeamTask[] = [];
+ if (props.forParentChildRelationship) {
+ if (
+ // Story can have ParentId set to Epic ID
+ props.task?.issueType === 'Story'
+ ) {
+ updatedTaskList = datas.filteredTasks.filter((item) => item.issueType === 'Epic');
+ } else if (
+ // TASK|BUG can have ParentId to be set either to Story ID or Epic ID
+ props.task?.issueType === 'Task' ||
+ props.task?.issueType === 'Bug' ||
+ !props.task?.issueType
+ ) {
+ updatedTaskList = datas.filteredTasks.filter(
+ (item) => item.issueType === 'Epic' || item.issueType === 'Story'
+ );
+ } else {
+ updatedTaskList = datas.filteredTasks;
+ }
+
+ if (props.task?.children && props.task?.children?.length) {
+ const childrenTaskIds = props.task?.children?.map((item) => item.id);
+ updatedTaskList = updatedTaskList.filter((item) => !childrenTaskIds.includes(item.id));
+ }
+ }
+
+ return updatedTaskList;
+ }, [props.task, datas.filteredTasks]);
+
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (inputRef.current && !inputRef.current.contains(event.target as Node) && editMode) {
+ // inputTask && updateTaskNameHandler(inputTask, taskName);
+ if (taskName == inputTaskTitle) {
+ setEditMode(false);
+ setActiveTask({
+ id: ''
+ });
+ }
+ }
+ };
+
+ // Attach the event listener
+ document.addEventListener('mousedown', handleClickOutside);
+
+ // Clean up the event listener on component unmount
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, [inputTask, taskName, setActiveTask, updateTaskNameHandler, editMode, inputTaskTitle, setEditMode]);
+ const [openPopoverId, setOpenPopoverId] = useState(null);
+ const handlePopoverToggle = useCallback(
+ (popoverId: string) => {
+ if (openPopoverId === popoverId) {
+ setOpenPopoverId(null);
+ } else {
+ setOpenPopoverId(popoverId);
+ }
+ },
+ [openPopoverId]
+ );
+
+ // Handling Hotkeys
+ const handleCommandKeySequence = useCallback(() => {
+ if (!editMode) {
+ setEditMode(true);
+ if (targetEl.current) {
+ targetEl.current.focus();
+ }
+ } else {
+ setEditMode(false);
+ }
+ }, [setEditMode, editMode, targetEl]);
+
+ useHotkeys(HostKeys.CREATE_TASK, handleCommandKeySequence);
+
+ useEffect(() => {
+ if (props.autoFocus && targetEl.current) {
+ targetEl.current.focus();
+ }
+ }, [props.autoFocus, targetEl]);
+
+ // const savedIssueType : string | null = localStorage.getItem('savedIssueType') as string && null;
+
+ const inputField = (
+ }
+ autoFocus={props.autoFocus}
+ wrapperClassName={`rounded-lg dark:bg-[#1B1D22]`}
+ placeholder={props.placeholder || t('form.TASK_INPUT_PLACEHOLDER')}
+ onFocus={(e) => {
+ setEditMode(true);
+ props.autoInputSelectText && setTimeout(() => e?.target?.select(), 10);
+ }}
+ onChange={(event) => {
+ setTaskName(event.target.value);
+ }}
+ onKeyUp={(e) => {
+ if (e.key === 'Enter' && inputTask) {
+ /* If createOnEnterClick is false then updateTaskNameHandler is called. */
+ !props.createOnEnterClick && updateTaskNameHandler(inputTask, taskName);
+
+ props.onEnterKey && props.onEnterKey(taskName, inputTask);
+ }
+ /* Creating a new task when the enter key is pressed. */
+ if (e.key === 'Enter') {
+ props.createOnEnterClick && handleTaskCreation();
+ }
+ }}
+ trailingNode={
+ /* Showing the spinner when the task is being updated. */
+
+ {props.task ? (
+ (updateLoading || props.inputLoader) &&
+ ) : (
+ <>{updateLoading && }>
+ )}
+
+ }
+ className={clsxm(
+ showTaskNumber && inputTask && ['pl-2'],
+ 'dark:bg-[#1B1D22]',
+ props.initEditMode && 'h-10'
+ )}
+ /* Showing the task number and issue type */
+ leadingNode={
+ // showTaskNumber &&
+ // inputTask &&
+
+ {!datas.hasCreateForm ? (
+
+ ) : (
+
setTaskIssue(v)}
+ defaultValue={
+ defaultIssueType
+ ? defaultIssueType.name
+ : (localStorage.getItem('lastTaskIssue') as ITaskIssue) || null
+ }
+ />
+ )}
+
+ {!datas.hasCreateForm && (
+ #{(inputTask && inputTask.taskNumber) || ''}
+ )}
+
+ }
+ />
+ );
+
+ const taskCard = (
- );
-
- return viewType === 'one-view' ? (
- taskCard
- ) : (
- handlePopoverToggle('popover1')}
- className="relative z-20 w-full" ref={inputRef}>
-
- {inputField}
-
- {props.children}
-
-
-
- {taskCard}
-
-
-
- );
+ );
+
+ return viewType === 'one-view' ? (
+ taskCard
+ ) : (
+ handlePopoverToggle('popover1')} className="relative z-20 w-full" ref={inputRef}>
+
+ {inputField}
+
+ {props.children}
+
+
+
+ {taskCard}
+
+
+
+ );
}
/**
* A component that is used to render the task list.
*/
function TaskCard({
- datas,
- onItemClick,
- inputField,
- fullWidth,
- fullHeight,
- handleTaskCreation,
- cardWithoutShadow,
- forParentChildRelationship,
- updatedTaskList,
- assignTaskPopup,
+ datas,
+ onItemClick,
+ inputField,
+ fullWidth,
+ fullHeight,
+ handleTaskCreation,
+ cardWithoutShadow,
+ forParentChildRelationship,
+ updatedTaskList,
+ assignTaskPopup
}: {
- datas: Partial;
- onItemClick?: (task: ITeamTask) => void;
- inputField?: JSX.Element;
- fullWidth?: boolean;
- fullHeight?: boolean;
- handleTaskCreation: () => void;
- cardWithoutShadow?: boolean;
- forParentChildRelationship?: boolean;
- updatedTaskList?: ITeamTask[];
- assignTaskPopup?: boolean;
+ datas: Partial;
+ onItemClick?: (task: ITeamTask) => void;
+ inputField?: JSX.Element;
+ fullWidth?: boolean;
+ fullHeight?: boolean;
+ handleTaskCreation: () => void;
+ cardWithoutShadow?: boolean;
+ forParentChildRelationship?: boolean;
+ updatedTaskList?: ITeamTask[];
+ assignTaskPopup?: boolean;
}) {
- const [, setCount] = useState(0);
- const t = useTranslations();
- const activeTaskEl = useRef(null);
- const { taskLabels: taskLabelsData } = useTaskLabels();
- const { activeTeam } = useOrganizationTeams();
-
- const {
- taskStatus,
- taskPriority,
- taskSize,
- taskLabels,
- taskDescription,
- taskProject,
- taskAssignees
- } = datas;
- const { nextOffset, data } = useInfinityScrolling(updatedTaskList ?? [], 5);
-
- useEffect(() => {
- if (datas.editMode) {
- window.setTimeout(() => {
- activeTaskEl?.current?.scrollIntoView({
- block: 'nearest',
- inline: 'start'
- });
- }, 10);
- }
- }, [datas.editMode]);
-
- return (
- <>
-
- {inputField}
-
- {/* Create team button */}
-
- {datas.hasCreateForm && (
-
-
{
- if (taskDescription) {
- taskDescription.current = e.target.value;
- }
- }}
- className={'dark:bg-[#1B1D22]'}
- />
-
-
-
{
- if (v && taskStatus) {
- taskStatus.current = v;
- }
- setCount((c) => c + 1);
- }}
- defaultValue={taskStatus?.current as ITaskStatus}
- task={null}
- />
-
- {
- if (v && taskPriority) {
- taskPriority.current = v;
- }
- setCount((c) => c + 1);
- }}
- defaultValue={taskPriority?.current as ITaskPriority}
- task={null}
- />
-
- {
- if (v && taskSize) {
- taskSize.current = v;
- }
- setCount((c) => c + 1);
- }}
- defaultValue={taskSize?.current as ITaskSize}
- task={null}
- />
-
- {
- taskLabelsData.filter((tag) =>
- tag.name ? values?.includes(tag.name) : false
- );
-
- if (taskLabels && values?.length) {
- taskLabels.current = taskLabelsData.filter((tag) =>
- tag.name ? values?.includes(tag.name) : false
- );
- }
- }}
- task={datas.inputTask}
- />
-
- {taskAssignees !== undefined && }
-
-
- {
- if (taskProject) {
- taskProject.current = project.id
- }
- }}
- />
-
-
-
- )}
-
-
-
-
-
-
- {/* Task filter buttons */}
-
-
datas.setFilter && datas.setFilter('open')}
- >
-
-
- {datas.openTaskCount || 0} {t('common.OPEN')}
-
-
-
-
datas.setFilter && datas.setFilter('closed')}
- >
-
-
- {datas.closedTaskCount || 0} {t('common.CLOSED')}
-
-
-
-
-
-
- {/* Task list */}
-
- {forParentChildRelationship && (
-
- {(task, i) => {
- const last = (datas.filteredTasks?.length || 0) - 1 === i;
- const active = datas.inputTask === task;
-
- return (
- -
-
-
- {!last && }
-
- );
- }}
-
- )}
-
- {!forParentChildRelationship && (
-
- {(task, i) => {
- const last = (datas.filteredTasks?.length || 0) - 1 === i;
- const active = datas.inputTask === task;
-
- return (
- -
-
-
- {!last && }
-
- );
- }}
-
- )}
-
- {(forParentChildRelationship &&
- updatedTaskList &&
- updatedTaskList.length === 0) ||
- (!forParentChildRelationship &&
- datas.filteredTasks &&
- datas.filteredTasks.length === 0 && (
- {t('common.NO_TASKS')}
- ))}
-
-
-
- {/* Just some spaces at the end */}
- {'|'}
- >
- );
-}
+ const [, setCount] = useState(0);
+ const t = useTranslations();
+ const activeTaskEl = useRef(null);
+ const { taskLabels: taskLabelsData } = useTaskLabels();
+ const { activeTeam } = useOrganizationTeams();
+
+ const { taskStatus, taskPriority, taskSize, taskLabels, taskDescription, taskProject, taskAssignees } = datas;
+ const { nextOffset, data } = useInfinityScrolling(updatedTaskList ?? [], 5);
+
+ useEffect(() => {
+ if (datas.editMode) {
+ window.setTimeout(() => {
+ activeTaskEl?.current?.scrollIntoView({
+ block: 'nearest',
+ inline: 'start'
+ });
+ }, 10);
+ }
+ }, [datas.editMode]);
+
+ return (
+ <>
+
+ {inputField}
+
+ {/* Create team button */}
+
+ {datas.hasCreateForm && (
+
+
{
+ if (taskDescription) {
+ taskDescription.current = e.target.value;
+ }
+ }}
+ className={'dark:bg-[#1B1D22]'}
+ />
+
+
+
{
+ if (v && taskStatus) {
+ taskStatus.current = v;
+ }
+ setCount((c) => c + 1);
+ }}
+ defaultValue={taskStatus?.current as ITaskStatus}
+ task={null}
+ />
+
+ {
+ if (v && taskPriority) {
+ taskPriority.current = v;
+ }
+ setCount((c) => c + 1);
+ }}
+ defaultValue={taskPriority?.current as ITaskPriority}
+ task={null}
+ />
+
+ {
+ if (v && taskSize) {
+ taskSize.current = v;
+ }
+ setCount((c) => c + 1);
+ }}
+ defaultValue={taskSize?.current as ITaskSize}
+ task={null}
+ />
+
+ {
+ taskLabelsData.filter((tag) =>
+ tag.name ? values?.includes(tag.name) : false
+ );
+
+ if (taskLabels && values?.length) {
+ taskLabels.current = taskLabelsData.filter((tag) =>
+ tag.name ? values?.includes(tag.name) : false
+ );
+ }
+ }}
+ task={datas.inputTask}
+ />
+
+ {taskAssignees !== undefined && (
+
+ )}
+
+ {
+ if (taskProject) {
+ taskProject.current = project.id;
+ }
+ }}
+ />
+
+
+ )}
+
+
+
+
+
+ {/* Task filter buttons */}
+
+
datas.setFilter && datas.setFilter('open')}
+ >
+
+
+ {datas.openTaskCount || 0} {t('common.OPEN')}
+
+
+
+
datas.setFilter && datas.setFilter('closed')}
+ >
+
+
+ {datas.closedTaskCount || 0} {t('common.CLOSED')}
+
+
+
+
+
+
+ {/* Task list */}
+
+ {forParentChildRelationship && (
+
+ {(task, i) => {
+ const last = (datas.filteredTasks?.length || 0) - 1 === i;
+ const active = datas.inputTask === task;
+
+ return (
+ -
+
+
+ {!last && }
+
+ );
+ }}
+
+ )}
+
+ {!forParentChildRelationship && (
+
+ {(task, i) => {
+ const last = (datas.filteredTasks?.length || 0) - 1 === i;
+ const active = datas.inputTask === task;
+
+ return (
+ -
+
+
+ {!last && }
+
+ );
+ }}
+
+ )}
+
+ {(forParentChildRelationship && updatedTaskList && updatedTaskList.length === 0) ||
+ (!forParentChildRelationship && datas.filteredTasks && datas.filteredTasks.length === 0 && (
+ {t('common.NO_TASKS')}
+ ))}
+
+
+
+ {/* Just some spaces at the end */}
+ {'|'}
+ >
+ );
+}
/**
* ----------------------------------------------
@@ -809,9 +726,11 @@ function TaskCard({
interface ITeamMemberSelectProps {
teamMembers: OT_Member[];
- assignees : MutableRefObject<{
- id: string;
- }[]>
+ assignees: MutableRefObject<
+ {
+ id: string;
+ }[]
+ >;
}
/**
* A multi select component for assignees
@@ -822,11 +741,14 @@ interface ITeamMemberSelectProps {
*
* @return {JSX.Element} The multi select component
*/
- function AssigneesSelect(props: ITeamMemberSelectProps): JSX.Element {
- const { teamMembers , assignees} = props;
+function AssigneesSelect(props: ITeamMemberSelectProps): JSX.Element {
+ const { teamMembers, assignees } = props;
const t = useTranslations();
- const {user} = useAuthenticateUser()
- const authMember = useMemo(() => teamMembers.find(member => member.employee.user?.id == user?.id), [teamMembers, user?.id])
+ const { user } = useAuthenticateUser();
+ const authMember = useMemo(
+ () => teamMembers.find((member) => member.employee.user?.id == user?.id),
+ [teamMembers, user?.id]
+ );
return (
@@ -873,18 +795,21 @@ interface ITeamMemberSelectProps {
}`
}
onClick={() => {
- const isAssigned = assignees.current.map(el => el.id).includes(member.employee.id);
-
+ const isAssigned = assignees.current
+ .map((el) => el.id)
+ .includes(member.employee.id);
if (isAssigned) {
- assignees.current = assignees.current.filter((el) => el.id != member.employee.id)
+ assignees.current = assignees.current.filter(
+ (el) => el.id != member.employee.id
+ );
} else {
- assignees.current = [...assignees.current, {id:member.employee.id}];
+ assignees.current = [...assignees.current, { id: member.employee.id }];
}
}}
value={member}
>
- {assignees.current.map(el => el.id).includes(member.employee.id) && (
+ {assignees.current.map((el) => el.id).includes(member.employee.id) && (
diff --git a/apps/web/lib/features/user-nav-menu.tsx b/apps/web/lib/features/user-nav-menu.tsx
index 7762a76ae..f2ce88a50 100644
--- a/apps/web/lib/features/user-nav-menu.tsx
+++ b/apps/web/lib/features/user-nav-menu.tsx
@@ -46,7 +46,7 @@ export function UserNavAvatar() {
}, [timerStatus, currentMember, publicTeam]);
return (
-
+
-
+
diff --git a/apps/web/lib/layout/main-layout.tsx b/apps/web/lib/layout/main-layout.tsx
index 84923e728..9a4a284b1 100644
--- a/apps/web/lib/layout/main-layout.tsx
+++ b/apps/web/lib/layout/main-layout.tsx
@@ -5,11 +5,11 @@ import { useAtomValue } from 'jotai';
import { fullWidthState } from '@app/stores/fullWidth';
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@components/ui/resizable';
import { SidebarProvider, SidebarInset } from '@/components/ui/sidebar';
-import { AppSidebar } from '@components/app-sidebar';
import MainSidebarTrigger from './MainSidebarTrigger';
import AppContainer from './AppContainer';
import GlobalHeader from './GlobalHeader';
import GlobalFooter from './GlobalFooter';
+import { AppSidebar } from '@components/app-sidebar';
/**
* Props interface for the MainLayout component
diff --git a/apps/web/next.config.js b/apps/web/next.config.js
index 8ea592699..74c755633 100644
--- a/apps/web/next.config.js
+++ b/apps/web/next.config.js
@@ -44,6 +44,7 @@ const eslintBuildConfig = process.env.NEXT_IGNORE_ESLINT_ERROR_ON_BUILD
const nextConfig = {
output: ['standalone', 'export'].includes(BUILD_OUTPUT_MODE) ? BUILD_OUTPUT_MODE : undefined,
reactStrictMode: false,
+ transpilePackages: ['geist'],
...eslintBuildConfig,
swcMinify: true,
webpack: (config, { isServer }) => {
diff --git a/apps/web/package.json b/apps/web/package.json
index 2f70e08b0..608b20c86 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -37,6 +37,7 @@
"@fullcalendar/timegrid": "^6.1.15",
"@headlessui/react": "^1.7.7",
"@heroicons/react": "^2.0.12",
+ "@hookform/resolvers": "^3.9.1",
"@jitsi/react-sdk": "^1.3.0",
"@jitsu/jitsu-react": "^1.3.0",
"@livekit/components-react": "^2.4.1",
@@ -60,6 +61,7 @@
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-hover-card": "^1.0.6",
"@radix-ui/react-icons": "^1.3.0",
+ "@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.0.6",
"@radix-ui/react-scroll-area": "^1.2.1",
"@radix-ui/react-select": "^2.1.1",
@@ -86,6 +88,7 @@
"emoji-mart": "^5.5.2",
"eslint-config-next": "^14.0.4",
"firebase": "8.3.3",
+ "geist": "^1.3.1",
"hotkeys-js": "^3.12.0",
"i18next": "^23.6.0",
"jotai": "^2.9.3",
@@ -115,7 +118,7 @@
"react-day-picker": "^8.8.0",
"react-dom": "^18.2.0",
"react-google-recaptcha": "^2.1.0",
- "react-hook-form": "^7.42.1",
+ "react-hook-form": "^7.53.2",
"react-icons": "^5.2.0",
"react-loading-skeleton": "^3.1.1",
"react-paginate": "^8.2.0",
@@ -130,7 +133,8 @@
"slate-serializers": "^0.4.0",
"sonner": "^1.5.0",
"string-to-color": "^2.2.2",
- "tailwind-merge": "^1.14.0"
+ "tailwind-merge": "^1.14.0",
+ "zod": "^3.23.8"
},
"devDependencies": {
"@svgr/webpack": "^8.1.0",
diff --git a/apps/web/styles/globals.css b/apps/web/styles/globals.css
index 91ea4af66..2b15ec5a8 100644
--- a/apps/web/styles/globals.css
+++ b/apps/web/styles/globals.css
@@ -148,9 +148,8 @@ html.dark {
body {
font-style: normal;
font-weight: 300;
- font-size: 16px;
line-height: 160%;
- @apply text-default dark:text-white bg-[rgb(242,242,242)] dark:bg-background text-foreground;
+ @apply text-default dark:text-white bg-[rgb(242,242,242)] dark:bg-background text-foreground text-sm;
}
.x-container {
diff --git a/apps/web/tailwind.config.js b/apps/web/tailwind.config.js
index cc39fabe1..b66c6486c 100644
--- a/apps/web/tailwind.config.js
+++ b/apps/web/tailwind.config.js
@@ -12,177 +12,177 @@ module.exports = {
...createGlobPatternsForDependencies(__dirname)
],
theme: {
- screens: {
- xs: '414px',
- sm: '576px',
- md: '768px',
- lg: '992px',
- xl: '1200px',
- '2xl': '1400px',
- '3xl': '1600px'
- },
- container: {
- center: 'true',
- padding: '2rem',
- screens: {
- '2xl': '1400px'
- }
- },
- extend: {
- colors: {
- transparent: 'transparent',
- current: 'currentColor',
- neutral: '#7E7991',
- chetwodeBlue: '#8C7AE4',
- indianRed: '#D95F5F',
- grey: '#868296',
- transparentWhite: 'rgba(255, 255, 255, 0.30)',
- default: {
- DEFAULT: '#282048'
- },
- 'light--theme': {
- light: '#fff',
- DEFAULT: '#f7f7f8',
- dark: '#E7E7EA'
- },
- 'dark--theme': {
- light: '#1E2025',
- DEFAULT: 'var(--tw-color-dark--theme)'
- },
- primary: {
- DEFAULT: '#3826A6',
- light: '#6755C9',
- xlight: '#8E76FA',
- mid: '#483A95',
- foreground: 'hsl(var(--primary-foreground))'
- },
- dark: {
- high: '#16171B',
- lighter: '#1E2430',
- DEFAULT: '#1A1C1E'
- },
- 'regal-blue': '#6A71E7',
- 'regal-rose': '#E93CB9',
- border: 'hsl(var(--border))',
- ring: 'hsl(var(--ring))',
- background: 'hsl(var(--background))',
- foreground: 'hsl(var(--foreground))',
- secondary: {
- DEFAULT: 'hsl(var(--secondary))',
- foreground: 'hsl(var(--secondary-foreground))'
- },
- destructive: {
- DEFAULT: 'hsl(var(--destructive))',
- foreground: 'hsl(var(--destructive-foreground))'
- },
- muted: {
- DEFAULT: 'hsl(var(--muted))',
- foreground: 'hsl(var(--muted-foreground))'
- },
- accent: {
- DEFAULT: 'hsl(var(--accent))',
- foreground: 'hsl(var(--accent-foreground))'
- },
- popover: {
- DEFAULT: 'hsl(var(--popover))',
- foreground: 'hsl(var(--popover-foreground))'
- },
- card: {
- DEFAULT: 'hsl(var(--card))',
- foreground: 'hsl(var(--card-foreground))'
- },
- sidebar: {
- DEFAULT: 'hsl(var(--sidebar-background))',
- foreground: 'hsl(var(--sidebar-foreground))',
- primary: 'hsl(var(--sidebar-primary))',
- 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
- accent: 'hsl(var(--sidebar-accent))',
- 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
- border: 'hsl(var(--sidebar-border))',
- ring: 'hsl(var(--sidebar-ring))'
- }
- },
- fontFamily: {
- PlusJakartaSans: ['Plus-Jakarta-Sans-VariableFont_wght'],
- PlusJakartaSansRegular: ['Plus-Jakarta-Sans-Regular'],
- PlusJakartaSansBold: ['Plus-Jakarta-Sans-Bold'],
- PlusJakartaSansLight: ['Plus-Jakarta-Sans-Light'],
- PlusJakartaSansMedium: ['Plus-Jakarta-Sans-Medium'],
- PlusJakartaSansSemiBold: ['Plus-Jakarta-Sans-SemiBold']
- },
- boxShadow: {
- lgcard: '0px 50px 200px rgba(0, 0, 0, 0.1)',
- xlcard: '0px 16px 79px rgba(0, 0, 0, 0.12)',
- 'lgcard-white': '0px 50px 200px rgba(255, 255, 255, 0.1)',
- 'xlcard-white': '0px 16px 79px rgba(255, 255, 255, 0.12)',
- darker: '-8px -9px 14px rgba(255, 255, 255, 0.05), 10px 14px 34px rgba(0, 0, 0, 0.6), 0px 4px 24px rgba(0, 0, 0, 0.25)'
- },
- borderRadius: {
- lg: 'var(--radius)',
- md: 'calc(var(--radius) - 2px)',
- sm: 'calc(var(--radius) - 4px)'
- },
- keyframes: {
- 'accordion-down': {
- from: {
- height: '0'
- },
- to: {
- height: 'var(--radix-accordion-content-height)'
- }
- },
- 'accordion-up': {
- from: {
- height: 'var(--radix-accordion-content-height)'
- },
- to: {
- height: '0'
- }
- }
- },
- animation: {
- 'accordion-down': 'accordion-down 0.2s ease-out',
- 'accordion-up': 'accordion-up 0.2s ease-out',
- spine: 'spin 10s linear infinite'
- },
- typography: {
- DEFAULT: {
- css: {
- maxWidth: '100ch',
- 'h3, h4, h5, h6, p, span, em, ul, ol, dl, blockquote, code, figure, pre': {
- marginTop: '0.5rem',
- marginBottom: '0.5rem',
- lineHeight: '1.25rem',
- fontSize: '14px',
- wordSpacing: '-1px',
- fontWeight: '400'
- },
- h1: {
- fontSize: '1.3rem',
- marginTop: '0.65rem',
- marginBottom: '0.65rem',
- lineHeight: '40px'
- },
- h2: {
- fontSize: '1.1rem',
- marginTop: '0.35rem',
- marginBottom: '0.35rem',
- lineHeight: '30px'
- },
- 'h1 span': {
- fontSize: '1.3rem'
- },
- 'h2 span': {
- fontSize: '1.1rem',
- fontWeight: 'bold'
- },
- 'strong span': {
- fontWeight: '600'
- }
- }
- }
- }
- }
- },
+ screens: {
+ xs: '414px',
+ sm: '576px',
+ md: '768px',
+ lg: '992px',
+ xl: '1200px',
+ '2xl': '1400px',
+ '3xl': '1600px'
+ },
+ container: {
+ center: 'true',
+ padding: '2rem',
+ screens: {
+ '2xl': '1400px'
+ }
+ },
+ extend: {
+ colors: {
+ transparent: 'transparent',
+ current: 'currentColor',
+ neutral: '#7E7991',
+ chetwodeBlue: '#8C7AE4',
+ indianRed: '#D95F5F',
+ grey: '#868296',
+ transparentWhite: 'rgba(255, 255, 255, 0.30)',
+ default: {
+ DEFAULT: '#282048'
+ },
+ 'light--theme': {
+ light: '#fff',
+ DEFAULT: '#f7f7f8',
+ dark: '#E7E7EA'
+ },
+ 'dark--theme': {
+ light: '#1E2025',
+ DEFAULT: 'var(--tw-color-dark--theme)'
+ },
+ primary: {
+ DEFAULT: '#3826A6',
+ light: '#6755C9',
+ xlight: '#8E76FA',
+ mid: '#483A95',
+ foreground: 'hsl(var(--primary-foreground))'
+ },
+ dark: {
+ high: '#16171B',
+ lighter: '#1E2430',
+ DEFAULT: '#1A1C1E'
+ },
+ 'regal-blue': '#6A71E7',
+ 'regal-rose': '#E93CB9',
+ border: 'hsl(var(--border))',
+ ring: 'hsl(var(--ring))',
+ background: 'hsl(var(--background))',
+ foreground: 'hsl(var(--foreground))',
+ secondary: {
+ DEFAULT: 'hsl(var(--secondary))',
+ foreground: 'hsl(var(--secondary-foreground))'
+ },
+ destructive: {
+ DEFAULT: 'hsl(var(--destructive))',
+ foreground: 'hsl(var(--destructive-foreground))'
+ },
+ muted: {
+ DEFAULT: 'hsl(var(--muted))',
+ foreground: 'hsl(var(--muted-foreground))'
+ },
+ accent: {
+ DEFAULT: 'hsl(var(--accent))',
+ foreground: 'hsl(var(--accent-foreground))'
+ },
+ popover: {
+ DEFAULT: 'hsl(var(--popover))',
+ foreground: 'hsl(var(--popover-foreground))'
+ },
+ card: {
+ DEFAULT: 'hsl(var(--card))',
+ foreground: 'hsl(var(--card-foreground))'
+ },
+ sidebar: {
+ DEFAULT: 'hsl(var(--sidebar-background))',
+ foreground: 'hsl(var(--sidebar-foreground))',
+ primary: 'hsl(var(--sidebar-primary))',
+ 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
+ accent: 'hsl(var(--sidebar-accent))',
+ 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
+ border: 'hsl(var(--sidebar-border))',
+ ring: 'hsl(var(--sidebar-ring))'
+ }
+ },
+ fontFamily: {
+ PlusJakartaSans: ['Plus-Jakarta-Sans-VariableFont_wght'],
+ PlusJakartaSansRegular: ['Plus-Jakarta-Sans-Regular'],
+ PlusJakartaSansBold: ['Plus-Jakarta-Sans-Bold'],
+ PlusJakartaSansLight: ['Plus-Jakarta-Sans-Light'],
+ PlusJakartaSansMedium: ['Plus-Jakarta-Sans-Medium'],
+ PlusJakartaSansSemiBold: ['Plus-Jakarta-Sans-SemiBold']
+ },
+ boxShadow: {
+ lgcard: '0px 50px 200px rgba(0, 0, 0, 0.1)',
+ xlcard: '0px 16px 79px rgba(0, 0, 0, 0.12)',
+ 'lgcard-white': '0px 50px 200px rgba(255, 255, 255, 0.1)',
+ 'xlcard-white': '0px 16px 79px rgba(255, 255, 255, 0.12)',
+ darker: '-8px -9px 14px rgba(255, 255, 255, 0.05), 10px 14px 34px rgba(0, 0, 0, 0.6), 0px 4px 24px rgba(0, 0, 0, 0.25)'
+ },
+ borderRadius: {
+ lg: 'var(--radius)',
+ md: 'calc(var(--radius) - 2px)',
+ sm: 'calc(var(--radius) - 4px)'
+ },
+ keyframes: {
+ 'accordion-down': {
+ from: {
+ height: '0'
+ },
+ to: {
+ height: 'var(--radix-accordion-content-height)'
+ }
+ },
+ 'accordion-up': {
+ from: {
+ height: 'var(--radix-accordion-content-height)'
+ },
+ to: {
+ height: '0'
+ }
+ }
+ },
+ animation: {
+ 'accordion-down': 'accordion-down 0.2s ease-out',
+ 'accordion-up': 'accordion-up 0.2s ease-out',
+ spine: 'spin 10s linear infinite'
+ },
+ typography: {
+ DEFAULT: {
+ css: {
+ maxWidth: '100ch',
+ 'h3, h4, h5, h6, p, span, em, ul, ol, dl, blockquote, code, figure, pre': {
+ marginTop: '0.5rem',
+ marginBottom: '0.5rem',
+ lineHeight: '1.25rem',
+ fontSize: '14px',
+ wordSpacing: '-1px',
+ fontWeight: '400'
+ },
+ h1: {
+ fontSize: '1.3rem',
+ marginTop: '0.65rem',
+ marginBottom: '0.65rem',
+ lineHeight: '40px'
+ },
+ h2: {
+ fontSize: '1.1rem',
+ marginTop: '0.35rem',
+ marginBottom: '0.35rem',
+ lineHeight: '30px'
+ },
+ 'h1 span': {
+ fontSize: '1.3rem'
+ },
+ 'h2 span': {
+ fontSize: '1.1rem',
+ fontWeight: 'bold'
+ },
+ 'strong span': {
+ fontWeight: '600'
+ }
+ }
+ }
+ }
+ }
+ },
plugins: [require('tailwindcss-animate'), require('@tailwindcss/typography')]
};
diff --git a/yarn.lock b/yarn.lock
index e64a251b5..5bbcaf487 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2772,6 +2772,11 @@
resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-2.0.18.tgz#f80301907c243df03c7e9fd76c0286e95361f7c1"
integrity sha512-7TyMjRrZZMBPa+/5Y8lN0iyvUU/01PeMGX2+RE7cQWpEUIcb4QotzUObFkJDejj/HUH4qjP/eQ0gzzKs2f+6Yw==
+"@hookform/resolvers@^3.9.1":
+ version "3.9.1"
+ resolved "https://registry.yarnpkg.com/@hookform/resolvers/-/resolvers-3.9.1.tgz#a23883c40bfd449cb6c6ab5a0fa0729184c950ff"
+ integrity sha512-ud2HqmGBM0P0IABqoskKWI6PEf6ZDDBZkFqe2Vnl+mTHCEHzr3ISjjZyCwTjC/qpL25JC9aIDkloQejvMeq0ug==
+
"@humanwhocodes/config-array@^0.11.10":
version "0.11.11"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.11.tgz#88a04c570dbbc7dd943e4712429c3df09bc32844"
@@ -6396,6 +6401,13 @@
dependencies:
"@radix-ui/react-use-layout-effect" "1.1.0"
+"@radix-ui/react-label@^2.1.0":
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-label/-/react-label-2.1.0.tgz#3aa2418d70bb242be37c51ff5e51a2adcbc372e3"
+ integrity sha512-peLblDlFw/ngk3UWq0VnYaOLy6agTZZ+MUO/WhVfm14vJGML+xH4FAl2XQGLqdefjNb7ApRg6Yn7U42ZhmYXdw==
+ dependencies:
+ "@radix-ui/react-primitive" "2.0.0"
+
"@radix-ui/react-menu@2.1.2":
version "2.1.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-2.1.2.tgz#91f6815845a4298dde775563ed2d80b7ad667899"
@@ -15367,6 +15379,11 @@ gcp-metadata@^6.0.0:
gaxios "^6.0.0"
json-bigint "^1.0.0"
+geist@^1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/geist/-/geist-1.3.1.tgz#bbd95db23b2a00baf6020e3b1b63a5752f4787d2"
+ integrity sha512-Q4gC1pBVPN+D579pBaz0TRRnGA4p9UK6elDY/xizXdFk/g4EKR5g0I+4p/Kj6gM0SajDBZ/0FvDV9ey9ud7BWw==
+
gensequence@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/gensequence/-/gensequence-6.0.0.tgz#ae46a0f89ebd7cc334e45cfb8f1c99a65248694e"
@@ -22770,10 +22787,10 @@ react-google-recaptcha@^2.1.0:
prop-types "^15.5.0"
react-async-script "^1.1.1"
-react-hook-form@^7.42.1:
- version "7.46.0"
- resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.46.0.tgz#dba3491e4bc0968346e5615d9d669af780b47cd4"
- integrity sha512-sc22pXwuKgbWBR5/EYWOVoFw4i/w893tDRUgQY2/Xb7wlpajJBrqAMFhb4z1CDhZ0TSFFfX62+iKx3gCXnCHHw==
+react-hook-form@^7.53.2:
+ version "7.53.2"
+ resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.53.2.tgz#6fa37ae27330af81089baadd7f322cc987b8e2ac"
+ integrity sha512-YVel6fW5sOeedd1524pltpHX+jgU2u3DSDtXEaBORNdqiNrsX/nUI/iGXONegttg0mJVnfrIkiV0cmTU6Oo2xw==
react-i18next@^14.1.0:
version "14.1.2"
@@ -27172,3 +27189,8 @@ yup@^0.32.11:
nanoclone "^0.2.1"
property-expr "^2.0.4"
toposort "^2.0.2"
+
+zod@^3.23.8:
+ version "3.23.8"
+ resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d"
+ integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==