feat: Version 2

Closes #627, closes #1047
This commit is contained in:
Maksim Eltyshev
2025-05-10 02:09:06 +02:00
parent ad7fb51cfa
commit 2ee1166747
1557 changed files with 76832 additions and 47042 deletions

View 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;

View File

@@ -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;
}
}
}

View 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;