mirror of
https://github.com/plankanban/planka.git
synced 2025-12-27 09:14:59 +03:00
188
client/src/components/projects/ProjectCard/ProjectCard.jsx
Normal file
188
client/src/components/projects/ProjectCard/ProjectCard.jsx
Normal file
@@ -0,0 +1,188 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import upperFirst from 'lodash/upperFirst';
|
||||
import camelCase from 'lodash/camelCase';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Button, Icon } from 'semantic-ui-react';
|
||||
|
||||
import selectors from '../../../selectors';
|
||||
import entryActions from '../../../entry-actions';
|
||||
import Paths from '../../../constants/Paths';
|
||||
import { ProjectBackgroundTypes } from '../../../constants/Enums';
|
||||
import UserAvatar from '../../users/UserAvatar';
|
||||
|
||||
import styles from './ProjectCard.module.scss';
|
||||
import globalStyles from '../../../styles.module.scss';
|
||||
|
||||
const Sizes = {
|
||||
SMALL: 'small',
|
||||
LARGE: 'large',
|
||||
};
|
||||
|
||||
const ProjectCard = React.memo(
|
||||
({ id, size, isActive, withDescription, withTypeIndicator, withFavoriteButton, className }) => {
|
||||
const selectProjectById = useMemo(() => selectors.makeSelectProjectById(), []);
|
||||
|
||||
const selectFirstBoardIdByProjectId = useMemo(
|
||||
() => selectors.makeSelectFirstBoardIdByProjectId(),
|
||||
[],
|
||||
);
|
||||
|
||||
const selectNotificationsTotalByProjectId = useMemo(
|
||||
() => selectors.makeSelectNotificationsTotalByProjectId(),
|
||||
[],
|
||||
);
|
||||
|
||||
const selectProjectManagerById = useMemo(() => selectors.makeSelectProjectManagerById(), []);
|
||||
const selectBackgroundImageById = useMemo(() => selectors.makeSelectBackgroundImageById(), []);
|
||||
|
||||
const project = useSelector((state) => selectProjectById(state, id));
|
||||
const firstBoardId = useSelector((state) => selectFirstBoardIdByProjectId(state, id));
|
||||
|
||||
const notificationsTotal = useSelector((state) =>
|
||||
selectNotificationsTotalByProjectId(state, id),
|
||||
);
|
||||
|
||||
const ownerProjectManager = useSelector(
|
||||
(state) =>
|
||||
project.ownerProjectManagerId &&
|
||||
selectProjectManagerById(state, project.ownerProjectManagerId),
|
||||
);
|
||||
|
||||
const backgroundImageUrl = useSelector((state) => {
|
||||
if (!project.backgroundType || project.backgroundType !== ProjectBackgroundTypes.IMAGE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const backgroundImage = selectBackgroundImageById(state, project.backgroundImageId);
|
||||
|
||||
if (!backgroundImage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return backgroundImage.thumbnailUrls.outside360;
|
||||
});
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleToggleFavoriteClick = useCallback(() => {
|
||||
dispatch(
|
||||
entryActions.updateProject(project.id, {
|
||||
isFavorite: !project.isFavorite,
|
||||
}),
|
||||
);
|
||||
}, [project, dispatch]);
|
||||
|
||||
const withSidebar = withTypeIndicator || (withFavoriteButton && !project.isHidden);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
className,
|
||||
styles.wrapper,
|
||||
styles[`wrapper${upperFirst(size)}`],
|
||||
project.isHidden && styles.wrapperHidden,
|
||||
)}
|
||||
>
|
||||
<Link
|
||||
to={
|
||||
firstBoardId
|
||||
? Paths.BOARDS.replace(':id', firstBoardId)
|
||||
: Paths.PROJECTS.replace(':id', id)
|
||||
}
|
||||
className={styles.content}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
styles.cover,
|
||||
project.backgroundType === ProjectBackgroundTypes.GRADIENT &&
|
||||
globalStyles[`background${upperFirst(camelCase(project.backgroundGradient))}`],
|
||||
)}
|
||||
style={{
|
||||
background: backgroundImageUrl && `url("${backgroundImageUrl}") center / cover`,
|
||||
}}
|
||||
/>
|
||||
{notificationsTotal > 0 && (
|
||||
<span className={styles.notifications}>{notificationsTotal}</span>
|
||||
)}
|
||||
<div
|
||||
className={classNames(styles.information, withSidebar && styles.informationWithSidebar)}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
styles.title,
|
||||
isActive !== undefined && styles.titleActivatable,
|
||||
isActive && styles.titleActive,
|
||||
)}
|
||||
>
|
||||
{project.name}
|
||||
</div>
|
||||
{withDescription && project.description && (
|
||||
<div className={styles.description}>{project.description}</div>
|
||||
)}
|
||||
</div>
|
||||
{withTypeIndicator && (
|
||||
<div
|
||||
className={classNames(
|
||||
styles.typeIndicator,
|
||||
ownerProjectManager && styles.typeIndicatorWithUser,
|
||||
)}
|
||||
>
|
||||
{ownerProjectManager ? (
|
||||
<UserAvatar id={ownerProjectManager.userId} size="small" />
|
||||
) : (
|
||||
<Icon
|
||||
fitted
|
||||
name="group"
|
||||
className={classNames(styles.icon, styles.typeIndicatorIcon)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
{withFavoriteButton && !project.isHidden && (
|
||||
<Button
|
||||
className={classNames(
|
||||
styles.favoriteButton,
|
||||
!project.isFavorite && styles.favoriteButtonAppearable,
|
||||
)}
|
||||
onClick={handleToggleFavoriteClick}
|
||||
>
|
||||
<Icon
|
||||
fitted
|
||||
name={project.isFavorite ? 'star' : 'star outline'}
|
||||
className={classNames(styles.icon, styles.favoriteButtonIcon)}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
ProjectCard.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
size: PropTypes.oneOf(Object.values(Sizes)),
|
||||
isActive: PropTypes.bool,
|
||||
withDescription: PropTypes.bool,
|
||||
withTypeIndicator: PropTypes.bool,
|
||||
withFavoriteButton: PropTypes.bool,
|
||||
className: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
ProjectCard.defaultProps = {
|
||||
size: Sizes.LARGE,
|
||||
isActive: undefined,
|
||||
withDescription: false,
|
||||
withTypeIndicator: false,
|
||||
withFavoriteButton: false,
|
||||
};
|
||||
|
||||
export default ProjectCard;
|
||||
@@ -0,0 +1,196 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
:global(#app) {
|
||||
.content {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.cover {
|
||||
background: #555;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
transition: filter 0.2s ease;
|
||||
|
||||
&::after {
|
||||
background: #000;
|
||||
bottom: 0;
|
||||
content: "";
|
||||
left: 0;
|
||||
opacity: 0.3;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
border-left: 1px solid;
|
||||
display: -webkit-box;
|
||||
font-weight: lighter;
|
||||
hyphens: auto;
|
||||
left: 0;
|
||||
line-height: 1.2;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
transition: opacity 0.3s, transform 0.3s;
|
||||
transform: translate(-40px, 0);
|
||||
white-space: pre-line;
|
||||
word-break: break-word;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.favoriteButton {
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
height: 36px;
|
||||
line-height: 1;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: 4px;
|
||||
width: 36px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
.favoriteButtonAppearable {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.information {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.notifications {
|
||||
background: #eb5a46;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
padding: 0px 6px;
|
||||
position: absolute;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.title {
|
||||
bottom: 0;
|
||||
display: -webkit-box;
|
||||
hyphens: auto;
|
||||
left: 0;
|
||||
line-height: 1.1;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
word-break: break-word;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.titleActivatable {
|
||||
color: rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
|
||||
.titleActive {
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.typeIndicator {
|
||||
bottom: 12px;
|
||||
margin-top: auto;
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
.typeIndicatorIcon {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.typeIndicatorWithUser {
|
||||
bottom: 12px;
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
.cover {
|
||||
filter: brightness(0.8);
|
||||
}
|
||||
|
||||
.description {
|
||||
opacity: 1;
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
|
||||
.favoriteButtonAppearable {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.titleActivatable {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wrapperHidden {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Sizes */
|
||||
|
||||
.wrapperSmall {
|
||||
.notifications {
|
||||
font-size: 10px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.title {
|
||||
line-clamp: 2;
|
||||
-webkit-line-clamp: 2;
|
||||
margin: 0 12px 10px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapperLarge {
|
||||
.description {
|
||||
line-clamp: 3;
|
||||
-webkit-line-clamp: 3;
|
||||
margin: 24px 20px 0 20px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.informationWithSidebar {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.notifications {
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 22px;
|
||||
line-clamp: 2;
|
||||
-webkit-line-clamp: 2;
|
||||
margin: 0 20px 14px 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
8
client/src/components/projects/ProjectCard/index.js
Normal file
8
client/src/components/projects/ProjectCard/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import ProjectCard from './ProjectCard';
|
||||
|
||||
export default ProjectCard;
|
||||
Reference in New Issue
Block a user