Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: reusable popover component & participants tags unified into roles #96

Merged
merged 10 commits into from
Apr 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,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",
Expand Down
31 changes: 31 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/common/constants/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './avatar';
export * from './button';
export * from './carousel';
export * from './popover';
export * from './spinner';
export * from './tag';
19 changes: 19 additions & 0 deletions src/common/constants/components/popover.ts
Original file line number Diff line number Diff line change
@@ -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'
}
1 change: 1 addition & 0 deletions src/common/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './cn';
export * from './utils';
export * from './setIconByPreferenceScheme';
export * from './jsVanilla';
export * from './parser/groupParticipantsByRole';
44 changes: 44 additions & 0 deletions src/common/utils/parser/groupParticipantsByRole.ts
Original file line number Diff line number Diff line change
@@ -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<string, User[]>
);
return grouped;
}
13 changes: 13 additions & 0 deletions src/common/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
65 changes: 43 additions & 22 deletions src/components/Cards/ProjectCard/ProjectCard.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
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, Tag } from '@components';

interface ProjectCardProps extends Omit<Project, 'id' | 'createdAt' | 'repositoryUrl'>, HTMLAttributes<HTMLDivElement> {
/**
Expand All @@ -9,41 +9,66 @@ interface ProjectCardProps extends Omit<Project, 'id' | 'createdAt' | 'repositor
className?: string;

/**
* Specify if the project is active
* Specifies if the card is active
*/
isActive: boolean;
isActive?: boolean;
}

export const ProjectCard = ({
isActive,
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'),
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'),
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 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 (
<li key={member.name}>
<Tag className="capitalize">{member.role}</Tag>
<li className="text-cWhite capitalize px-1 py-0.5 flex gap-1" key={participant.name + idx}>
{participant.name}
{isAdmin && <span className="text-secondary-600">{'(Adm)'}</span>}
</li>
);
});
};

return (
<li key={role + idx}>
{/** TODO: change key later 😒 Why later?*/}
<Tag>
<div className="flex items-center gap-2">
{role}
<Popover content={<ul>{participantList}</ul>}>
<span className={classes.popoverTrigger}>{groupLength}</span>
</Popover>
</div>
</Tag>
</li>
);
});

/* TODO: Filtrar si está buscando antes del texto */
const renderRequiredRolesTag = () => {
Expand All @@ -61,22 +86,18 @@ export const ProjectCard = ({
{/* Header */}
<header className="grid gap-4">
<h3 className="font-bold text-8">{name}</h3>
<p className="text-4 h-24 text-balance ">{handleDescription()}</p>
<p className="text-4 h-24 text-balance truncate">{handleDescription()}</p>
</header>

{/* Participants Section */}
<section aria-labelledby="participants-title" className="py-4">
<section aria-labelledby="participants-title">
<h4 id="participants-title" className={classes.subTitle}>
Participantes
</h4>
<ul className={classes.list}>
<li>
<Tag className="capitalize">{administrator.role}</Tag>
</li>
{renderParticipantsTag()}
</ul>
<ul className={classes.list}>{renderParticipantsTag}</ul>
</section>

{/* Roles Section */}
{isActive && (
<>
<section aria-labelledby="roles-title">
Expand Down
Loading
Loading