From 3ab29eaefdd161d3caedbd3a6f8fc6409cadda0e Mon Sep 17 00:00:00 2001 From: Joseph Date: Sun, 7 Apr 2024 23:24:15 +0100 Subject: [PATCH 01/10] feat: reusable popover component & participants tags unified into roles --- package.json | 1 + pnpm-lock.yaml | 48 ++++++ src/common/utils/index.ts | 1 + .../utils/parser/groupParticipantsByRole.ts | 44 +++++ .../Cards/ProjectCard/ProjectCard.tsx | 110 +++++++----- src/components/Popover/Popover.tsx | 162 ++++++++++++++++++ src/components/Tag/Tag.tsx | 6 +- src/components/index.ts | 1 + 8 files changed, 324 insertions(+), 49 deletions(-) create mode 100644 src/common/utils/parser/groupParticipantsByRole.ts create mode 100644 src/components/Popover/Popover.tsx diff --git a/package.json b/package.json index edc5123..6bb476b 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "preview": "vite preview" }, "dependencies": { + "@floating-ui/react": "^0.26.11", "@hookform/resolvers": "3.3.4", "@supabase/auth-ui-react": "^0.4.7", "@supabase/auth-ui-shared": "^0.1.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9fd17dc..bdfe084 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,9 @@ settings: excludeLinksFromLockfile: false dependencies: + '@floating-ui/react': + specifier: ^0.26.11 + version: 0.26.11(react-dom@18.2.0)(react@18.2.0) '@hookform/resolvers': specifier: 3.3.4 version: 3.3.4(react-hook-form@7.51.2) @@ -1125,6 +1128,47 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true + /@floating-ui/core@1.6.0: + resolution: {integrity: sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==} + dependencies: + '@floating-ui/utils': 0.2.1 + dev: false + + /@floating-ui/dom@1.6.3: + resolution: {integrity: sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==} + dependencies: + '@floating-ui/core': 1.6.0 + '@floating-ui/utils': 0.2.1 + dev: false + + /@floating-ui/react-dom@2.0.8(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + dependencies: + '@floating-ui/dom': 1.6.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /@floating-ui/react@0.26.11(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-fo01Cu+jzLDVG/AYAV2OtV6flhXvxP5rDaR1Fk8WWhtsFqwk478Dr2HGtB8s0HqQCsFWVbdHYpPjMiQiR/A9VA==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + dependencies: + '@floating-ui/react-dom': 2.0.8(react-dom@18.2.0)(react@18.2.0) + '@floating-ui/utils': 0.2.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + tabbable: 6.2.0 + dev: false + + /@floating-ui/utils@0.2.1: + resolution: {integrity: sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==} + dev: false + /@hookform/resolvers@3.3.4(react-hook-form@7.51.2): resolution: {integrity: sha512-o5cgpGOuJYrd+iMKvkttOclgwRW86EsWJZZRC23prf0uU2i48Htq4PuT73AVb9ionFyZrwYEITuOFGF+BydEtQ==} peerDependencies: @@ -4004,6 +4048,10 @@ packages: has-flag: 4.0.0 dev: true + /tabbable@6.2.0: + resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} + dev: false + /tailwind-merge@2.2.2: resolution: {integrity: sha512-tWANXsnmJzgw6mQ07nE3aCDkCK4QdT3ThPMCzawoYA2Pws7vSTCvz3Vrjg61jVUGfFZPJzxEP+NimbcW+EdaDw==} dependencies: diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index 6997210..fa6cacf 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -2,3 +2,4 @@ export * from './cn'; export * from './utils'; export * from './setIconByPreferenceScheme'; export * from './jsVanilla'; +export * from './parser/groupParticipantsByRole'; diff --git a/src/common/utils/parser/groupParticipantsByRole.ts b/src/common/utils/parser/groupParticipantsByRole.ts new file mode 100644 index 0000000..8e40f3c --- /dev/null +++ b/src/common/utils/parser/groupParticipantsByRole.ts @@ -0,0 +1,44 @@ +import { User } from '../../types'; + +/** + * Concat the "administrador" object + * Create grouped roles with the "roles" as unique "keys" + * each grouped role has an array of users (User[]) + * example of result: + * from this: + * [ + { name: "jp", role: "back-end" }, + { name: "marcos", role: "back-end" }, + { name: "aforcita", role: "front-end" }, + { name: "unai", role: "front-end" }, + { name: "nico", role: "front-end" }, + { name: "ana", role: "front-end" }, + ] + to this: + * { + 'front-end': [ + { name: 'aforcita', role: 'front-end' }, + { name: 'unai', role: 'front-end' }, + { name: "nico", role: "front-end" }, + { name: "ana", role: "front-end" }, + ], + 'back-end': [ + { name: 'jp', role: 'back-end' }, + { name: 'marcos', role: 'back-end' }, + ] + } + */ + +export function groupParticipantsByRole(membersObj: User[], administrator: User) { + const grouped = membersObj.concat(administrator).reduce( + (acc, obj) => { + if (!acc[obj.role]) { + acc[obj.role] = []; + } + acc[obj.role].push(obj); + return acc; + }, + {} as Record + ); + return grouped; +} diff --git a/src/components/Cards/ProjectCard/ProjectCard.tsx b/src/components/Cards/ProjectCard/ProjectCard.tsx index 6905a7c..57992bc 100644 --- a/src/components/Cards/ProjectCard/ProjectCard.tsx +++ b/src/components/Cards/ProjectCard/ProjectCard.tsx @@ -1,49 +1,73 @@ import { HTMLAttributes } from 'react'; -import { cn, Project } from '@common'; -import { Button, CardWrapper, Tag } from '@components'; +import { cn, groupParticipantsByRole, Project } from '@common'; +import { Button, CardWrapper, Popover, PopoverContent, PopoverTrigger, Tag } from '@components'; interface ProjectCardProps extends Omit, HTMLAttributes { /** * Specify an optional className to be added to the component */ className?: string; - - /** - * Specify if the project is active - */ - isActive: boolean; } - -export const ProjectCard = ({ - isActive, - className, - name, - description, - administrator, - members, - requiredRoles, - ...restOfProps -}: ProjectCardProps) => { +export const ProjectCard = ({ className, name, description, administrator, members, requiredRoles, ...restOfProps }: ProjectCardProps) => { const classes = { container: cn('grid gap-8 max-w-md w-full max-xl:mx-auto', className), subTitle: cn('text-4 font-bold'), - list: cn('flex flex-wrap gap-x-4 gap-y-2 mt-2 text-3.5') + list: cn('flex flex-wrap gap-4 mt-4 text-3.5') }; + const popoverclasses = { + trigger: cn('bg-secondary-600 w-5 h-5 rounded-full text-xs flex items-center justify-center cursor-pointer'), + ulContainer: cn('border border-pBorder p-2 rounded-md text-sm bg-secondary-950/50 backdrop-blur-md'), + liElement: cn('text-cWhite capitalize px-1 py-0.5 flex gap-1'), + admin: cn('text-secondary-600') + }; const handleDescription = () => { - if (description.length > 210) return `${description.slice(0, 210)}...`; + if (description.length > 175) return `${description.slice(0, 130)}...`; return description; }; - const renderParticipantsTag = () => { - return members.map((member) => { - if (member.role === undefined) return null; + + /** + * Concat the admin and parse the Array of members + */ + const participantsByRole = groupParticipantsByRole(members, administrator); + + const renderParticipantsTag = Object.keys(participantsByRole).map((role, idx) => { + const groupLength = participantsByRole[role].length; + + const participantList = participantsByRole[role].map((participant, idx) => { + const isAdmin = Object.values(administrator).includes(participant.name); return ( -
  • - {member.role} +
  • + {participant.name} {isAdmin && '(Admin)'}
  • ); }); - }; + + const popOver = () => { + return ( + + + {groupLength} + + +
      {participantList}
    +
    +
    + ); + }; + + return ( +
  • + {/** TODO: change key later */} + +
    + {role} + {popOver()} +
    +
    +
  • + ); + }); /* TODO: Filtrar si está buscando antes del texto */ const renderRequiredRolesTag = () => { @@ -61,36 +85,28 @@ export const ProjectCard = ({ {/* Header */}

    {name}

    -

    {handleDescription()}

    +

    {handleDescription()}

    {/* Participants Section */} -
    +

    Participantes

    -
      -
    • - {administrator.role} -
    • - {renderParticipantsTag()} -
    +
      {renderParticipantsTag}
    - {isActive && ( - <> -
    -

    - Estamos buscando -

    -
      {renderRequiredRolesTag()}
    -
    + {/* Roles Section */} +
    +

    + Estamos buscando +

    +
      {renderRequiredRolesTag()}
    +
    -
    - -
    - - )} +
    + +
    ); }; diff --git a/src/components/Popover/Popover.tsx b/src/components/Popover/Popover.tsx new file mode 100644 index 0000000..61a70ca --- /dev/null +++ b/src/components/Popover/Popover.tsx @@ -0,0 +1,162 @@ +import React, { forwardRef, HTMLProps, ReactNode, useContext, useMemo } from 'react'; +import { + autoUpdate, + flip, + FloatingPortal, + offset, + Placement, + shift, + useClick, + useDismiss, + useFloating, + useInteractions, + useMergeRefs, + useRole +} from '@floating-ui/react'; + +interface PopoverProps { + initialOpen?: boolean; + className?: string; + placement?: Placement; + modal?: boolean; + open?: boolean; + onOpenChange?: (open: boolean) => void; +} + +interface PopoverTriggerProps { + children: React.ReactNode; + asChild?: boolean; +} + +// eslint-disable-next-line react-refresh/only-export-components +export function usePopover({ + initialOpen, + placement = 'bottom', + modal, + open: controlledOpen, + onOpenChange: setControlledOpen +}: PopoverProps = {}) { + const [uncontrolledOpen, setUncontrolledOpen] = React.useState(initialOpen); + const [labelId, setLabelId] = React.useState(); + const [descriptionId, setDescriptionId] = React.useState(); + + const open = controlledOpen ?? uncontrolledOpen; + const setOpen = setControlledOpen ?? setUncontrolledOpen; + + const floatingData = useFloating({ + whileElementsMounted: autoUpdate, + open, + onOpenChange: setOpen, + placement, + middleware: [ + offset(10), + flip({ + fallbackAxisSideDirection: 'end', + padding: 5 + }), + shift({ padding: 5 }) + ] + }); + + const floatingContext = floatingData.context; + + const click = useClick(floatingContext); + const dismiss = useDismiss(floatingContext); + const role = useRole(floatingContext); + const interactions = useInteractions([click, dismiss, role]); + + return useMemo( + () => ({ + open, + setOpen, + ...interactions, + ...floatingData, + modal, + labelId, + descriptionId, + setLabelId, + setDescriptionId + }), + [open, setOpen, interactions, floatingData, modal, labelId, descriptionId] + ); +} + +type ContextType = + | (ReturnType & { + setLabelId: React.Dispatch>; + setDescriptionId: React.Dispatch>; + }) + | null; + +const PopoverContext = React.createContext(null); + +// eslint-disable-next-line react-refresh/only-export-components +export const usePopOverContext = () => { + const context = useContext(PopoverContext); + + if (!context) { + throw new Error('Compontes must be wrapped in '); + } + + return context; +}; + +export const Popover = ({ + children, + modal = false, + ...restOptions +}: { + children: ReactNode; +} & PopoverProps) => { + const popover = usePopover({ modal, ...restOptions }); + return {children}; +}; + +export const PopoverTrigger = React.forwardRef & PopoverTriggerProps>(function PopoverTrigger( + { children, asChild, ...props }, + propRef +) { + const context = usePopOverContext(); + const childrenRef = (children as any).ref; + const ref = useMergeRefs([context.refs.setReference, propRef, childrenRef]); + + // `asChild` allows passing any element as the anchor + if (asChild && React.isValidElement(children)) { + return React.cloneElement( + children, + context.getReferenceProps({ + ref, + ...props, + ...children.props, + 'data-state': context.open ? 'open' : 'closed' + }) + ); + } + + return ( + + ); +}); + +export const PopoverContent = forwardRef>(function PopoverContent({ style, ...props }, propRef) { + const { context: fltContext, ...context } = usePopOverContext(); + const ref = useMergeRefs([context.refs.setFloating, propRef]); + + if (!fltContext.open) return null; + + return ( + +
    + {props.children} +
    +
    + ); +}); diff --git a/src/components/Tag/Tag.tsx b/src/components/Tag/Tag.tsx index bc76337..7d561e8 100644 --- a/src/components/Tag/Tag.tsx +++ b/src/components/Tag/Tag.tsx @@ -1,3 +1,4 @@ +import React from 'react'; import { cn, TagSize, TagVariant } from '@common'; const TagVariants: Record = { @@ -30,7 +31,7 @@ interface TagProps { /** * Set the Tag content */ - children: string; + children: string | React.ReactNode; /** * The shape of the component. @@ -54,9 +55,10 @@ interface TagProps { } export const Tag = ({ children, variant = TagVariant.primary, size = TagSize.sm, className, borderSize = TagSize.xs }: TagProps) => { + const isAnElement = React.isValidElement(children); const classes = { container: cn('flex items-center justify-center rounded-full w-fit', TagContainerVariants[variant], BorderSizes[borderSize], className), - tag: cn('rounded-full py-px px-2.5', TagVariants[variant], Sizes[size]) + tag: cn('rounded-full py-px px-2 capitalize', TagVariants[variant], Sizes[size], { 'pl-2 pr-1': isAnElement }) }; return ( diff --git a/src/components/index.ts b/src/components/index.ts index d12d1db..53df226 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -10,4 +10,5 @@ export * from './Navigation/Nav'; export * from './Spinner/Spinner'; export * from './Tag/Tag'; export * from './Countdown/Countdown'; +export * from './Popover/Popover'; export * from './Avatar/Avatar'; From e77b798ea77ab9cd24e514232abaf30cdfa2f6d6 Mon Sep 17 00:00:00 2001 From: Samuel Llibre Santos Date: Mon, 8 Apr 2024 13:46:56 -0300 Subject: [PATCH 02/10] =?UTF-8?q?feat(constants):=20=E2=9C=A8=20add=20Popo?= =?UTF-8?q?verPlacement=20and=20PopoverVariant=20enums?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduced PopoverPlacement and PopoverVariant enumerations in src/common/constants/components to standardize popover configurations across the application. This addition enhances code readability and maintains consistency in popover component usage. --- src/common/constants/components/index.ts | 1 + src/common/constants/components/popover.ts | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 src/common/constants/components/popover.ts diff --git a/src/common/constants/components/index.ts b/src/common/constants/components/index.ts index 3e62444..b60809f 100644 --- a/src/common/constants/components/index.ts +++ b/src/common/constants/components/index.ts @@ -1,5 +1,6 @@ export * from './avatar'; export * from './button'; export * from './carousel'; +export * from './popover'; export * from './spinner'; export * from './tag'; diff --git a/src/common/constants/components/popover.ts b/src/common/constants/components/popover.ts new file mode 100644 index 0000000..73c1234 --- /dev/null +++ b/src/common/constants/components/popover.ts @@ -0,0 +1,19 @@ +export enum PopoverPlacement { + topStart = 'top-start', + top = 'top', + topEnd = 'top-end', + leftStart = 'left-start', + left = 'left', + leftEnd = 'left-end', + rightStart = 'right-start', + right = 'right', + rightEnd = 'right-end', + bottomStart = 'bottom-start', + bottom = 'bottom', + bottomEnd = 'bottom-end' +} + +export enum PopoverVariant { + primary = 'primary', + ghost = 'ghost' +} From b286478a22376d5c899d11053da2e97610361da3 Mon Sep 17 00:00:00 2001 From: Samuel Llibre Santos Date: Mon, 8 Apr 2024 13:47:32 -0300 Subject: [PATCH 03/10] =?UTF-8?q?feat(utils):=20=E2=9C=A8=20add=20hasProp?= =?UTF-8?q?=20function=20for=20property=20validation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added a utility function `hasProp` in the utilities section to facilitate the validation of property existence within objects. This enhancement improves code robustness and aids in object property checks across different parts of the application. --- src/common/utils/utils.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/common/utils/utils.ts b/src/common/utils/utils.ts index c7b5a4a..376b469 100644 --- a/src/common/utils/utils.ts +++ b/src/common/utils/utils.ts @@ -8,3 +8,16 @@ export const getDeviceSize = (width: number): Breakpoint => { if (width >= 1280 && width < 1536) return Breakpoint.xl; return Breakpoint['2xl']; }; + +/** + * hasProp + * @description - validate if an object has the prop passed arg + * @function + * @param {any} obj - Object to validate + * @param {string} prop - prop's key to check if it belongs to the obj + * @return {boolean} The obj does has the prop. + */ +export const hasProp = (obj = {}, prop: string): boolean => { + if (obj === null || typeof obj !== 'object') return false; + return prop in obj; +}; From 4a5abcc23c9452a5f5dc04eaca0c885097d49929 Mon Sep 17 00:00:00 2001 From: Samuel Llibre Santos Date: Mon, 8 Apr 2024 13:48:50 -0300 Subject: [PATCH 04/10] =?UTF-8?q?feat(components):=20=E2=9C=A8=20introduce?= =?UTF-8?q?=20Portal=20component=20for=20external=20DOM=20rendering?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implemented a Portal component to enable rendering of children components outside the regular DOM hierarchy. This feature facilitates the creation of modals, tooltips, and other floating UI elements that require an unconstrained rendering environment, enhancing the flexibility and usability of the UI components across the application. --- src/components/Portal/Portal.tsx | 29 +++++++++++++++++++++++++++++ src/components/index.ts | 1 + 2 files changed, 30 insertions(+) create mode 100644 src/components/Portal/Portal.tsx diff --git a/src/components/Portal/Portal.tsx b/src/components/Portal/Portal.tsx new file mode 100644 index 0000000..17135f2 --- /dev/null +++ b/src/components/Portal/Portal.tsx @@ -0,0 +1,29 @@ +import { useEffect, useState } from 'react'; +import ReactDOM from 'react-dom'; + +const appendChild = (containerEl: HTMLDivElement): HTMLDivElement => document.body.appendChild(containerEl); +const removeChild = (containerEl: HTMLDivElement): HTMLDivElement => document.body.removeChild(containerEl); + +export interface PortalProps { + /** + * The content displayed inside the portal. + */ + children: JSX.Element; +} + +/** + * Portal component is used to render content in a different DOM node. + */ +export const Portal = ({ children }: PortalProps) => { + const [containerEl] = useState(document.createElement('div')); + + useEffect(() => { + appendChild(containerEl); + + return (): void => { + removeChild(containerEl); + }; + }, [containerEl]); + + return ReactDOM.createPortal(children, containerEl); +}; diff --git a/src/components/index.ts b/src/components/index.ts index 53df226..9c8520e 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -7,6 +7,7 @@ export * from './Countdown/Countdown'; export * from './Footer/Footer'; export * from './Logo/Logo'; export * from './Navigation/Nav'; +export * from './Portal/Portal'; export * from './Spinner/Spinner'; export * from './Tag/Tag'; export * from './Countdown/Countdown'; From bfa7449ac4905baaa71dcb0317dc49fecc1663bf Mon Sep 17 00:00:00 2001 From: Samuel Llibre Santos Date: Mon, 8 Apr 2024 13:51:00 -0300 Subject: [PATCH 05/10] =?UTF-8?q?refactor(popover):=20=E2=99=BB=EF=B8=8F?= =?UTF-8?q?=20switch=20to=20react-popper=20&=20Portal=20integration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Migrated positioning logic from @floating-ui/react to react-popper, optimizing performance and compatibility. - Implemented Portal for out-of-DOM rendering, enhancing UI flexibility. - Added `variant` and `menuFullWidth` props for more style/layout options. - Introduced useOnClickOutside for better dismissal behavior, improving user interaction. --- src/components/Popover/Popover.tsx | 268 +++++++++++++---------------- src/components/index.ts | 4 +- 2 files changed, 124 insertions(+), 148 deletions(-) diff --git a/src/components/Popover/Popover.tsx b/src/components/Popover/Popover.tsx index 61a70ca..751e85f 100644 --- a/src/components/Popover/Popover.tsx +++ b/src/components/Popover/Popover.tsx @@ -1,162 +1,140 @@ -import React, { forwardRef, HTMLProps, ReactNode, useContext, useMemo } from 'react'; -import { - autoUpdate, - flip, - FloatingPortal, - offset, - Placement, - shift, - useClick, - useDismiss, - useFloating, - useInteractions, - useMergeRefs, - useRole -} from '@floating-ui/react'; - -interface PopoverProps { - initialOpen?: boolean; +import { Children, cloneElement, ReactElement, ReactNode, RefObject, useEffect, useRef, useState } from 'react'; +import { cn, hasProp, PopoverPlacement, PopoverVariant, useOnClickOutside } from '@common'; +import { Portal } from '@components'; +import { usePopper } from 'react-popper'; + +const PopoverVariants: Record = { + [PopoverVariant.primary]: 'border border-primary-900 bg-primary-950/50 backdrop-blur-md', + [PopoverVariant.ghost]: 'border border-neutral-700 bg-neutral-950/50 backdrop-blur-md' +}; + +export interface PopoverProps { + /** + * The content displayed inside the popover. + */ + content: ReactNode; + + /** + * When true, the popover is manually shown. + */ + isOpen?: boolean; + + /** + * The position (relative to the target) at which the popover should appear. + */ + placement?: PopoverPlacement; + + /** + * Specify an optional className to be added to the menu component + */ className?: string; - placement?: Placement; - modal?: boolean; - open?: boolean; - onOpenChange?: (open: boolean) => void; -} -interface PopoverTriggerProps { - children: React.ReactNode; - asChild?: boolean; + /** + * Whether the float menu has the same trigger's width + */ + menuFullWidth?: boolean; + + /** + * Specify the role of the popover in order to improve accessibility + */ + role?: string; + + /** + * Elements to display inside the Navbar. + */ + children?: ReactNode; + + /** + * Style Variant (e.g., 'primary', 'secondary'), defines appearance. + */ + variant?: PopoverVariant; } -// eslint-disable-next-line react-refresh/only-export-components -export function usePopover({ - initialOpen, - placement = 'bottom', - modal, - open: controlledOpen, - onOpenChange: setControlledOpen -}: PopoverProps = {}) { - const [uncontrolledOpen, setUncontrolledOpen] = React.useState(initialOpen); - const [labelId, setLabelId] = React.useState(); - const [descriptionId, setDescriptionId] = React.useState(); - - const open = controlledOpen ?? uncontrolledOpen; - const setOpen = setControlledOpen ?? setUncontrolledOpen; - - const floatingData = useFloating({ - whileElementsMounted: autoUpdate, - open, - onOpenChange: setOpen, +export const Popover = ({ + content, + isOpen = false, + placement = PopoverPlacement.bottom, + role = 'menu', + className, + menuFullWidth = false, + children, + variant = PopoverVariant.ghost +}: PopoverProps) => { + const [popoverElement, setPopoverElement] = useState | HTMLElement | null>(null); + const refTriggerNode = useRef(null); + const [open, setOpen] = useState(isOpen); + const popoverMenuRef = useRef(null); + useOnClickOutside(popoverMenuRef, () => setOpen(false)); + + const classes = { + menu: cn( + 'shadow-lg z-50', + 'rounded-md', + 'p-2 text-sm', + 'transition-all', + { + 'opacity-0 invisible': !open, + 'opacity-100 animate-fade-in animate-duration-200': open + }, + PopoverVariants[variant], + className + ) + }; + + /* Popper config */ + const { styles, attributes, forceUpdate } = usePopper(refTriggerNode.current, popoverElement as HTMLElement, { placement, - middleware: [ - offset(10), - flip({ - fallbackAxisSideDirection: 'end', - padding: 5 - }), - shift({ padding: 5 }) + modifiers: [ + { name: 'offset', options: { offset: [0, 8] } }, + { + name: 'flip', + options: { + fallbackPlacements: ['top', 'right'] + } + } ] }); - const floatingContext = floatingData.context; - - const click = useClick(floatingContext); - const dismiss = useDismiss(floatingContext); - const role = useRole(floatingContext); - const interactions = useInteractions([click, dismiss, role]); - - return useMemo( - () => ({ - open, - setOpen, - ...interactions, - ...floatingData, - modal, - labelId, - descriptionId, - setLabelId, - setDescriptionId - }), - [open, setOpen, interactions, floatingData, modal, labelId, descriptionId] - ); -} - -type ContextType = - | (ReturnType & { - setLabelId: React.Dispatch>; - setDescriptionId: React.Dispatch>; - }) - | null; - -const PopoverContext = React.createContext(null); + const menuStyles = menuFullWidth + ? { + ...styles.popper, + minWidth: refTriggerNode.current?.scrollWidth, + maxWidth: refTriggerNode.current?.scrollWidth + } + : { ...styles.popper }; -// eslint-disable-next-line react-refresh/only-export-components -export const usePopOverContext = () => { - const context = useContext(PopoverContext); + const handleForceUpdate = () => { + let timeout: ReturnType; + if (forceUpdate) timeout = setTimeout(() => forceUpdate()); + return () => clearTimeout(timeout); + }; - if (!context) { - throw new Error('Compontes must be wrapped in '); - } + useEffect(() => { + setOpen(isOpen); + handleForceUpdate(); + }, [isOpen]); - return context; -}; + useEffect(() => { + handleForceUpdate(); + }, [open]); -export const Popover = ({ - children, - modal = false, - ...restOptions -}: { - children: ReactNode; -} & PopoverProps) => { - const popover = usePopover({ modal, ...restOptions }); - return {children}; -}; + const handleTriggerClick = (): void => setOpen(!open); -export const PopoverTrigger = React.forwardRef & PopoverTriggerProps>(function PopoverTrigger( - { children, asChild, ...props }, - propRef -) { - const context = usePopOverContext(); - const childrenRef = (children as any).ref; - const ref = useMergeRefs([context.refs.setReference, propRef, childrenRef]); - - // `asChild` allows passing any element as the anchor - if (asChild && React.isValidElement(children)) { - return React.cloneElement( - children, - context.getReferenceProps({ - ref, - ...props, - ...children.props, - 'data-state': context.open ? 'open' : 'closed' - }) - ); - } + const child = Children.only(children) as ReactElement; - return ( - - ); -}); - -export const PopoverContent = forwardRef>(function PopoverContent({ style, ...props }, propRef) { - const { context: fltContext, ...context } = usePopOverContext(); - const ref = useMergeRefs([context.refs.setFloating, propRef]); - - if (!fltContext.open) return null; + /* Append handle to the trigger component */ + const element = hasProp(child.props, 'onClick') + ? cloneElement(child, { ref: refTriggerNode }) + : cloneElement(child, { ref: refTriggerNode, onClick: handleTriggerClick }); return ( - -
    - {props.children} -
    -
    + <> + {element} + +
    +
    {content}
    +
    +
    + ); -}); +}; diff --git a/src/components/index.ts b/src/components/index.ts index 9c8520e..48e82ff 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -7,9 +7,7 @@ export * from './Countdown/Countdown'; export * from './Footer/Footer'; export * from './Logo/Logo'; export * from './Navigation/Nav'; +export * from './Popover/Popover'; export * from './Portal/Portal'; export * from './Spinner/Spinner'; export * from './Tag/Tag'; -export * from './Countdown/Countdown'; -export * from './Popover/Popover'; -export * from './Avatar/Avatar'; From 1fe8179b8424bb6531abaf28c7b36f42f72f4e04 Mon Sep 17 00:00:00 2001 From: Samuel Llibre Santos Date: Mon, 8 Apr 2024 13:51:50 -0300 Subject: [PATCH 06/10] =?UTF-8?q?refactor(ProjectCard):=20=E2=99=BB?= =?UTF-8?q?=EF=B8=8F=20enhance=20Popover=20trigger=20style=20&=20participa?= =?UTF-8?q?nt=20list?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Updated Popover trigger styling for a gradient appearance, improving the visual distinction and interactive feedback. - Simplified participant listing implementation, ensuring a cleaner and more efficient rendering of project participants. - Adjusted admin label to 'Adm' for brevity and consistency across the UI. - Streamlined code by integrating content directly into the Popover component, enhancing readability and maintainability. --- .../Cards/ProjectCard/ProjectCard.tsx | 37 ++++++------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/src/components/Cards/ProjectCard/ProjectCard.tsx b/src/components/Cards/ProjectCard/ProjectCard.tsx index 57992bc..cc66def 100644 --- a/src/components/Cards/ProjectCard/ProjectCard.tsx +++ b/src/components/Cards/ProjectCard/ProjectCard.tsx @@ -1,6 +1,6 @@ import { HTMLAttributes } from 'react'; import { cn, groupParticipantsByRole, Project } from '@common'; -import { Button, CardWrapper, Popover, PopoverContent, PopoverTrigger, Tag } from '@components'; +import { Button, CardWrapper, Popover, Tag } from '@components'; interface ProjectCardProps extends Omit, HTMLAttributes { /** @@ -12,15 +12,12 @@ export const ProjectCard = ({ className, name, description, administrator, membe const classes = { container: cn('grid gap-8 max-w-md w-full max-xl:mx-auto', className), subTitle: cn('text-4 font-bold'), - list: cn('flex flex-wrap gap-4 mt-4 text-3.5') + list: cn('flex flex-wrap gap-4 mt-4 text-3.5'), + popoverTrigger: cn( + 'bg-gradient-to-rb from-primary-600 to-secondary-500 w-5 h-5 rounded-full text-xs flex items-center justify-center cursor-pointer select-none' + ) }; - const popoverclasses = { - trigger: cn('bg-secondary-600 w-5 h-5 rounded-full text-xs flex items-center justify-center cursor-pointer'), - ulContainer: cn('border border-pBorder p-2 rounded-md text-sm bg-secondary-950/50 backdrop-blur-md'), - liElement: cn('text-cWhite capitalize px-1 py-0.5 flex gap-1'), - admin: cn('text-secondary-600') - }; const handleDescription = () => { if (description.length > 175) return `${description.slice(0, 130)}...`; return description; @@ -37,32 +34,22 @@ export const ProjectCard = ({ className, name, description, administrator, membe const participantList = participantsByRole[role].map((participant, idx) => { const isAdmin = Object.values(administrator).includes(participant.name); return ( -
  • - {participant.name} {isAdmin && '(Admin)'} +
  • + {participant.name} + {isAdmin && {'(Adm)'}}
  • ); }); - const popOver = () => { - return ( - - - {groupLength} - - -
      {participantList}
    -
    -
    - ); - }; - return (
  • - {/** TODO: change key later */} + {/** TODO: change key later 😒 Why later?*/}
    {role} - {popOver()} + {participantList}}> + {groupLength} +
  • From 0a1f135558518ce8dec100a0550c2f09d8465abd Mon Sep 17 00:00:00 2001 From: Samuel Llibre Santos Date: Mon, 8 Apr 2024 13:52:39 -0300 Subject: [PATCH 07/10] =?UTF-8?q?refactor(deps):=20=E2=99=BB=EF=B8=8F=20sw?= =?UTF-8?q?itch=20from=20@floating-ui/react=20to=20react-popper?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Removed @floating-ui/react due to alignment with project requirements or to leverage react-popper's API and features. - Added react-popper as a dependency to support floating UI components such as tooltips and popovers. --- package.json | 1 + pnpm-lock.yaml | 84 +++++++++++++++++++------------------------------- 2 files changed, 32 insertions(+), 53 deletions(-) diff --git a/package.json b/package.json index 6bb476b..abc8a99 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-hook-form": "^7.51.2", + "react-popper": "^2.3.0", "react-router-dom": "^6.22.3", "tailwind-merge": "^2.2.2", "three": "0.134.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bdfe084..0df5cf4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,9 @@ dependencies: react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) + react-popper: + specifier: ^2.3.0 + version: 2.3.0(@popperjs/core@2.11.8)(react-dom@18.2.0)(react@18.2.0) react-hook-form: specifier: ^7.51.2 version: 7.51.2(react@18.2.0) @@ -1128,55 +1131,6 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /@floating-ui/core@1.6.0: - resolution: {integrity: sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==} - dependencies: - '@floating-ui/utils': 0.2.1 - dev: false - - /@floating-ui/dom@1.6.3: - resolution: {integrity: sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==} - dependencies: - '@floating-ui/core': 1.6.0 - '@floating-ui/utils': 0.2.1 - dev: false - - /@floating-ui/react-dom@2.0.8(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==} - peerDependencies: - react: '>=16.8.0' - react-dom: '>=16.8.0' - dependencies: - '@floating-ui/dom': 1.6.3 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - dev: false - - /@floating-ui/react@0.26.11(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-fo01Cu+jzLDVG/AYAV2OtV6flhXvxP5rDaR1Fk8WWhtsFqwk478Dr2HGtB8s0HqQCsFWVbdHYpPjMiQiR/A9VA==} - peerDependencies: - react: '>=16.8.0' - react-dom: '>=16.8.0' - dependencies: - '@floating-ui/react-dom': 2.0.8(react-dom@18.2.0)(react@18.2.0) - '@floating-ui/utils': 0.2.1 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - tabbable: 6.2.0 - dev: false - - /@floating-ui/utils@0.2.1: - resolution: {integrity: sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==} - dev: false - - /@hookform/resolvers@3.3.4(react-hook-form@7.51.2): - resolution: {integrity: sha512-o5cgpGOuJYrd+iMKvkttOclgwRW86EsWJZZRC23prf0uU2i48Htq4PuT73AVb9ionFyZrwYEITuOFGF+BydEtQ==} - peerDependencies: - react-hook-form: ^7.0.0 - dependencies: - react-hook-form: 7.51.2(react@18.2.0) - dev: false - /@humanwhocodes/config-array@0.11.11: resolution: {integrity: sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==} engines: {node: '>=10.10.0'} @@ -1324,6 +1278,10 @@ packages: /@polka/url@1.0.0-next.23: resolution: {integrity: sha512-C16M+IYz0rgRhWZdCmK+h58JMv8vijAA61gmz2rspCSwKwzBebpdcsiUmwrtJRdphuY30i6BSLEOP8ppbNLyLg==} + /@popperjs/core@2.11.8: + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + dev: false + /@remix-run/router@1.15.3: resolution: {integrity: sha512-Oy8rmScVrVxWZVOpEF57ovlnhpZ8CCPlnIIumVcV9nFdiSIrus99+Lw78ekXyGvVDlIsFJbSfmSovJUhCWYV3w==} engines: {node: '>=14.0.0'} @@ -3706,6 +3664,10 @@ packages: scheduler: 0.23.0 dev: false + /react-fast-compare@3.2.2: + resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} + dev: false + /react-hook-form@7.51.2(react@18.2.0): resolution: {integrity: sha512-y++lwaWjtzDt/XNnyGDQy6goHskFualmDlf+jzEZvjvz6KWDf7EboL7pUvRCzPTJd0EOPpdekYaQLEvvG6m6HA==} engines: {node: '>=12.22.0'} @@ -3719,6 +3681,20 @@ packages: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} dev: false + /react-popper@2.3.0(@popperjs/core@2.11.8)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==} + peerDependencies: + '@popperjs/core': ^2.0.0 + react: ^16.8.0 || ^17 || ^18 + react-dom: ^16.8.0 || ^17 || ^18 + dependencies: + '@popperjs/core': 2.11.8 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-fast-compare: 3.2.2 + warning: 4.0.3 + dev: false + /react-refresh@0.14.0: resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==} engines: {node: '>=0.10.0'} @@ -4048,10 +4024,6 @@ packages: has-flag: 4.0.0 dev: true - /tabbable@6.2.0: - resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} - dev: false - /tailwind-merge@2.2.2: resolution: {integrity: sha512-tWANXsnmJzgw6mQ07nE3aCDkCK4QdT3ThPMCzawoYA2Pws7vSTCvz3Vrjg61jVUGfFZPJzxEP+NimbcW+EdaDw==} dependencies: @@ -4310,6 +4282,12 @@ packages: optionalDependencies: fsevents: 2.3.3 + /warning@4.0.3: + resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} + dependencies: + loose-envify: 1.4.0 + dev: false + /webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} dev: false From f10f9e927800c2a5b77717bd583ffb84d110ac4e Mon Sep 17 00:00:00 2001 From: Samuel Llibre Santos Date: Mon, 8 Apr 2024 13:58:51 -0300 Subject: [PATCH 08/10] chore(components/tag): :rewind: rollback tag to previous implementation --- src/components/Tag/Tag.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/Tag/Tag.tsx b/src/components/Tag/Tag.tsx index 7d561e8..10b8e87 100644 --- a/src/components/Tag/Tag.tsx +++ b/src/components/Tag/Tag.tsx @@ -55,10 +55,9 @@ interface TagProps { } export const Tag = ({ children, variant = TagVariant.primary, size = TagSize.sm, className, borderSize = TagSize.xs }: TagProps) => { - const isAnElement = React.isValidElement(children); const classes = { container: cn('flex items-center justify-center rounded-full w-fit', TagContainerVariants[variant], BorderSizes[borderSize], className), - tag: cn('rounded-full py-px px-2 capitalize', TagVariants[variant], Sizes[size], { 'pl-2 pr-1': isAnElement }) + tag: cn('rounded-full py-px px-2', TagVariants[variant], Sizes[size]) }; return ( From 310379741ef38483887e4a0d33dd14230f606c62 Mon Sep 17 00:00:00 2001 From: Samuel Llibre Santos Date: Mon, 8 Apr 2024 14:03:54 -0300 Subject: [PATCH 09/10] =?UTF-8?q?refactor(components/cards):=20=E2=99=BB?= =?UTF-8?q?=EF=B8=8F=20add=20isActive=20prop,=20implement=20it=20for=20the?= =?UTF-8?q?=20footer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Cards/ProjectCard/ProjectCard.tsx | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/src/components/Cards/ProjectCard/ProjectCard.tsx b/src/components/Cards/ProjectCard/ProjectCard.tsx index cc66def..b4d4f98 100644 --- a/src/components/Cards/ProjectCard/ProjectCard.tsx +++ b/src/components/Cards/ProjectCard/ProjectCard.tsx @@ -7,8 +7,22 @@ interface ProjectCardProps extends Omit { +export const ProjectCard = ({ + className, + name, + description, + administrator, + members, + requiredRoles, + isActive, + ...restOfProps +}: ProjectCardProps) => { const classes = { container: cn('grid gap-8 max-w-md w-full max-xl:mx-auto', className), subTitle: cn('text-4 font-bold'), @@ -84,16 +98,20 @@ export const ProjectCard = ({ className, name, description, administrator, membe
    {/* Roles Section */} -
    -

    - Estamos buscando -

    -
      {renderRequiredRolesTag()}
    -
    + {isActive && ( + <> +
    +

    + Estamos buscando +

    +
      {renderRequiredRolesTag()}
    +
    -
    - -
    +
    + +
    + + )} ); }; From 3071d80798aeb0a59b164897e2ec8d98a93057e5 Mon Sep 17 00:00:00 2001 From: Samuel Llibre Santos Date: Tue, 9 Apr 2024 13:04:00 -0300 Subject: [PATCH 10/10] chore: resolve changes from conflicts --- package.json | 1 - pnpm-lock.yaml | 17 +++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index abc8a99..bcb783e 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,6 @@ "preview": "vite preview" }, "dependencies": { - "@floating-ui/react": "^0.26.11", "@hookform/resolvers": "3.3.4", "@supabase/auth-ui-react": "^0.4.7", "@supabase/auth-ui-shared": "^0.1.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0df5cf4..2c9b049 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,9 +5,6 @@ settings: excludeLinksFromLockfile: false dependencies: - '@floating-ui/react': - specifier: ^0.26.11 - version: 0.26.11(react-dom@18.2.0)(react@18.2.0) '@hookform/resolvers': specifier: 3.3.4 version: 3.3.4(react-hook-form@7.51.2) @@ -47,12 +44,12 @@ dependencies: react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) - react-popper: - specifier: ^2.3.0 - version: 2.3.0(@popperjs/core@2.11.8)(react-dom@18.2.0)(react@18.2.0) react-hook-form: specifier: ^7.51.2 version: 7.51.2(react@18.2.0) + react-popper: + specifier: ^2.3.0 + version: 2.3.0(@popperjs/core@2.11.8)(react-dom@18.2.0)(react@18.2.0) react-router-dom: specifier: ^6.22.3 version: 6.22.3(react-dom@18.2.0)(react@18.2.0) @@ -1131,6 +1128,14 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true + /@hookform/resolvers@3.3.4(react-hook-form@7.51.2): + resolution: {integrity: sha512-o5cgpGOuJYrd+iMKvkttOclgwRW86EsWJZZRC23prf0uU2i48Htq4PuT73AVb9ionFyZrwYEITuOFGF+BydEtQ==} + peerDependencies: + react-hook-form: ^7.0.0 + dependencies: + react-hook-form: 7.51.2(react@18.2.0) + dev: false + /@humanwhocodes/config-array@0.11.11: resolution: {integrity: sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==} engines: {node: '>=10.10.0'}