From ae68bbc6bb5450e572978791e9e59fdefabf675b Mon Sep 17 00:00:00 2001 From: Arick Bulakali Date: Thu, 21 Nov 2024 20:49:23 +0200 Subject: [PATCH 1/4] refactor(web): [Sidebar] improve sidebar, styles, add form and workspace switter --- apps/web/app/[locale]/layout.tsx | 2 +- apps/web/components/app-sidebar.tsx | 130 +- apps/web/components/nav-main.tsx | 32 +- apps/web/components/nav-projects.tsx | 158 +- apps/web/components/nav-secondary.tsx | 6 +- .../editor-components/LinkElement.tsx | 2 +- apps/web/components/sidebar-opt-in-form.tsx | 63 + apps/web/components/ui/card.tsx | 43 + apps/web/components/ui/form.tsx | 135 ++ apps/web/components/ui/label.tsx | 19 + apps/web/components/ui/sidebar.tsx | 4 +- ...am-switcher.tsx => workspace-switcher.tsx} | 27 +- .../lib/features/task/task-input-kanban.tsx | 864 +++++----- apps/web/lib/features/task/task-input.tsx | 1437 ++++++++--------- apps/web/lib/layout/main-layout.tsx | 2 +- apps/web/next.config.js | 1 + apps/web/package.json | 8 +- apps/web/styles/globals.css | 3 +- apps/web/tailwind.config.js | 344 ++-- yarn.lock | 30 +- 20 files changed, 1744 insertions(+), 1566 deletions(-) create mode 100644 apps/web/components/sidebar-opt-in-form.tsx create mode 100644 apps/web/components/ui/card.tsx create mode 100644 apps/web/components/ui/form.tsx create mode 100644 apps/web/components/ui/label.tsx rename apps/web/components/{team-switcher.tsx => workspace-switcher.tsx} (76%) 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/components/app-sidebar.tsx b/apps/web/components/app-sidebar.tsx index eaf3048e7..19e980a99 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 ( @@ -249,26 +265,16 @@ 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..664fb66b4 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({ ) : ( @@ -148,17 +152,17 @@ export function NavMain({ - + {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) => ( - - + + { 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..7d23cecaf --- /dev/null +++ b/apps/web/components/sidebar-opt-in-form.tsx @@ -0,0 +1,63 @@ +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'; + +export function SidebarOptInForm() { + const { state } = useSidebar(); + const subscribeFormSchema = z + .object({ + email: z.string().email() + }) + .required(); + const form = useForm>({ + resolver: zodResolver(subscribeFormSchema) + }); + const onSubmit = (data: z.infer) => { + console.log(data); + }; + return state == 'expanded' ? ( +
+ + + + Subscribe to our newsletter + + Opt-in to receive updates and news about Ever Teams. + + + + ( + + + + + + + )} + /> + + + +
+ + ) : 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