1
0
Fork 0
mirror of https://github.com/plankanban/planka.git synced 2025-07-27 09:09:46 +02:00

Add project backgrounds

This commit is contained in:
Maksim Eltyshev 2020-05-26 00:46:04 +05:00
parent af4297ac62
commit 3bb68b0d4f
67 changed files with 774 additions and 210 deletions

View file

@ -23,7 +23,7 @@
}
.wrapper {
background-color: #e2e4e6;
background: #e2e4e6;
border-radius: 3px;
padding: 4px;
transition: opacity 40ms ease-in;

View file

@ -110,6 +110,14 @@ const Board = React.memo(
prevPosition.current = null;
}, [prevPosition]);
useEffect(() => {
document.body.style.overflowX = 'auto';
return () => {
document.body.style.overflowX = null;
};
}, []);
useEffect(() => {
if (isAddListOpened) {
window.scroll(document.body.scrollWidth, 0);

View file

@ -1,11 +1,11 @@
.addListButton {
background: hsla(0, 0%, 0%, 0.24);
background: rgba(0, 0, 0, 0.24);
border: none;
border-radius: 3px;
color: hsla(0, 0%, 100%, 0.72);
color: rgba(255, 255, 255, 0.72);
cursor: pointer;
display: block;
fill: hsla(0, 0%, 100%, 0.72);
fill: rgba(255, 255, 255, 0.72);
font-weight: normal;
height: 42px;
padding: 11px;
@ -20,7 +20,7 @@
}
.addListButton:hover {
background: hsla(0, 0%, 0%, 0.32);
background: rgba(0, 0, 0, 0.32);
}
.addListButtonIcon {

View file

@ -23,7 +23,7 @@
}
.filterLabel {
background: hsla(0, 0%, 0%, 0.24);
background: rgba(0, 0, 0, 0.24);
border-radius: 3px;
color: #fff;
display: inline-block;
@ -33,7 +33,7 @@
}
.filterLabel:hover {
background: hsla(0, 0%, 0%, 0.32);
background: rgba(0, 0, 0, 0.32);
}
.filterTitle {

View file

@ -24,7 +24,7 @@
}
.editButton:hover {
background: hsla(0, 0%, 100%, 0.08) !important;
background: rgba(255, 255, 255, 0.08) !important;
}
.link {
@ -45,7 +45,7 @@
}
.tab:hover {
background: hsla(0, 0%, 0%, 0.24) !important;
background: rgba(0, 0, 0, 0.24) !important;
}
.tab:hover .target {
@ -53,11 +53,11 @@
}
.tabActive {
background: hsla(0, 0%, 0%, 0.24) !important;
background: rgba(0, 0, 0, 0.24) !important;
}
.tabActive:hover {
background: hsla(0, 0%, 0%, 0.32) !important;
background: rgba(0, 0, 0, 0.32) !important;
}
.tabWrapper {
@ -66,7 +66,7 @@
}
.tabs {
border-bottom: 2px solid hsla(0, 0%, 0%, 0.24);
border-bottom: 2px solid rgba(0, 0, 0, 0.24);;
display: flex;
height: 38px;
flex: 0 0 auto;

View file

@ -49,7 +49,7 @@
}
.card {
background-color: #fff;
background: #fff;
border-radius: 3px;
box-shadow: 0 1px 0 #ccc;
position: relative;
@ -57,7 +57,7 @@
}
.card:hover {
background-color: #f5f6f7;
background: #f5f6f7;
border-bottom-color: rgba(9, 30, 66, 0.25);
}

View file

@ -10,7 +10,7 @@
}
.fieldWrapper {
background-color: #fff !important;
background: #fff !important;
border-radius: 3px !important;
box-shadow: 0 1px 0 #ccc !important;
margin-bottom: 8px !important;

View file

@ -22,7 +22,7 @@
}
.text {
background-color: #fff;
background: #fff;
border-radius: 0px 8px 8px;
box-shadow: 0 1px 2px -1px rgba(9, 30, 66, 0.25),
0 0 0 1px rgba(9, 30, 66, 0.08);

View file

@ -1,5 +1,5 @@
.divider {
background-color: #eee;
background: #eee;
border: 0;
height: 1px;
margin-bottom: 8px;

View file

@ -60,7 +60,7 @@ const Item = React.memo(
<div
className={styles.thumbnail}
style={{
backgroundImage: coverUrl && `url(${coverUrl}`,
background: coverUrl && `url("${coverUrl}") center / cover`,
}}
>
{coverUrl ? (

View file

@ -13,7 +13,7 @@
}
.button:hover {
background-color: rgba(9, 30, 66, 0.08) !important;
background: rgba(9, 30, 66, 0.08) !important;
}
.date {
@ -84,10 +84,7 @@
.thumbnail {
border-radius: 3px;
background-color: rgba(9, 30, 66, 0.04);
background-position: center;
background-repeat: no-repeat;
background-size: cover;
background: rgba(9, 30, 66, 0.04);
border-radius: 3px;
height: 80px;
left: 0;
@ -112,7 +109,7 @@
}
.wrapper:hover .details {
background-color: rgba(9, 30, 66, 0.04);
background: rgba(9, 30, 66, 0.04);
}
.wrapper:hover .target {
@ -120,5 +117,5 @@
}
.wrapperSubmitting {
background-color: rgba(9, 30, 66, 0.04);
background: rgba(9, 30, 66, 0.04);
}

View file

@ -100,7 +100,7 @@
}
.descriptionButton:hover {
background-color: rgba(9, 30, 66, 0.08);
background: rgba(9, 30, 66, 0.08);
color: #092d42;
}

View file

@ -13,7 +13,7 @@
}
.button:hover {
background-color: rgba(9, 30, 66, 0.08) !important;
background: rgba(9, 30, 66, 0.08) !important;
}
.checkboxWrapper {
@ -30,7 +30,7 @@
}
.content:hover {
background-color: rgba(9, 30, 66, 0.04);
background: rgba(9, 30, 66, 0.04);
}
.content:hover .target {

View file

@ -20,7 +20,7 @@
}
.taskButton:hover {
background-color: rgba(9, 30, 66, 0.04);
background: rgba(9, 30, 66, 0.04);
color: #092d42;
}

View file

@ -3,7 +3,7 @@
}
.item:hover {
background: hsla(0, 0%, 0%, 0.32) !important;
background: rgba(0, 0, 0, 0.32) !important;
}
.logo {
@ -52,7 +52,7 @@
}
.wrapper {
background: hsla(0, 0%, 0%, 0.24);
background: rgba(0, 0, 0, 0.24);
display: flex;
flex: 0 0 auto;
}

View file

@ -9,7 +9,7 @@
}
.fieldWrapper {
background-color: #fff !important;
background: #fff !important;
border-radius: 3px !important;
box-shadow: 0 1px 0 #ccc !important;
margin-bottom: 8px !important;

View file

@ -16,7 +16,7 @@
}
.addCardButton:hover {
background-color: #c3cbd0 !important;
background: #c3cbd0 !important;
color: #17394d !important;
fill: #17394d !important;
}
@ -70,7 +70,7 @@
}
.headerButton:hover {
background-color: rgba(9, 30, 66, 0.13) !important;
background: rgba(9, 30, 66, 0.13) !important;
color: #516b7a !important;
}

View file

@ -1,6 +1,5 @@
.cover {
background: url("../../assets/images/cover.jpg") no-repeat center center;
background-size: cover;
background: url("../../assets/images/cover.jpg") center / cover !important;
}
.descriptionSubtitle {
@ -20,7 +19,7 @@
}
.descriptionWrapperOverlay {
background-color: rgba(33, 33, 33, 0.5);
background: rgba(33, 33, 33, 0.5);
bottom: 0;
height: 100%;
left: 0;
@ -37,7 +36,7 @@
}
.fullHeight {
background-color: #fff;
background: #fff;
height: 100%;
}

View file

@ -0,0 +1,142 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Menu } from 'semantic-ui-react';
import { withPopup } from '../../../lib/popup';
import { Popup } from '../../../lib/custom-ui';
import { useSteps } from '../../../hooks';
import EditNameStep from './EditNameStep';
import EditBackgroundStep from './EditBackgroundStep';
import DeleteStep from '../../DeleteStep';
import styles from './ActionsPopup.module.css';
const StepTypes = {
EDIT_NAME: 'EDIT_NAME',
EDIT_BACKGROUND: 'EDIT_BACKGROUND',
DELETE: 'DELETE',
};
const ActionsStep = React.memo(
({ project, onUpdate, onBackgroundImageUpdate, onDelete, onClose }) => {
const [t] = useTranslation();
const [step, openStep, handleBack] = useSteps();
const handleEditNameClick = useCallback(() => {
openStep(StepTypes.EDIT_NAME);
}, [openStep]);
const handleEditBackgroundClick = useCallback(() => {
openStep(StepTypes.EDIT_BACKGROUND);
}, [openStep]);
const handleDeleteClick = useCallback(() => {
openStep(StepTypes.DELETE);
}, [openStep]);
const handleNameUpdate = useCallback(
(newName) => {
onUpdate({
name: newName,
});
},
[onUpdate],
);
const handleBackgroundUpdate = useCallback(
(newBackground) => {
onUpdate({
background: newBackground,
});
},
[onUpdate],
);
const handleBackgroundImageDelete = useCallback(() => {
onUpdate({
background: null,
backgroundImage: null,
});
}, [onUpdate]);
if (step) {
if (step) {
switch (step.type) {
case StepTypes.EDIT_NAME:
return (
<EditNameStep
defaultValue={project.name}
onUpdate={handleNameUpdate}
onBack={handleBack}
onClose={onClose}
/>
);
case StepTypes.EDIT_BACKGROUND:
return (
<EditBackgroundStep
defaultValue={project.background}
isImageUpdating={project.isBackgroundImageUpdating}
onUpdate={handleBackgroundUpdate}
onImageUpdate={onBackgroundImageUpdate}
onImageDelete={handleBackgroundImageDelete}
onBack={handleBack}
/>
);
case StepTypes.DELETE:
return (
<DeleteStep
title={t('common.deleteProject', {
context: 'title',
})}
content={t('common.areYouSureYouWantToDeleteThisProject')}
buttonContent={t('action.deleteProject')}
onConfirm={onDelete}
onBack={handleBack}
/>
);
default:
}
}
}
return (
<>
<Popup.Header>
{t('common.projectActions', {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<Menu secondary vertical className={styles.menu}>
<Menu.Item className={styles.menuItem} onClick={handleEditNameClick}>
{t('action.editTitle', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleEditBackgroundClick}>
{t('action.editBackground', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleDeleteClick}>
{t('action.deleteProject', {
context: 'title',
})}
</Menu.Item>
</Menu>
</Popup.Content>
</>
);
},
);
ActionsStep.propTypes = {
project: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
onUpdate: PropTypes.func.isRequired,
onBackgroundImageUpdate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
export default withPopup(ActionsStep);

View file

@ -0,0 +1,9 @@
.menu {
margin: -7px -12px -5px !important;
width: calc(100% + 24px) !important;
}
.menuItem {
margin: 0 !important;
padding-left: 14px !important;
}

View file

@ -0,0 +1,74 @@
import React, { useCallback, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button } from 'semantic-ui-react';
import { FilePicker, Popup } from '../../../lib/custom-ui';
import styles from './EditBackgroundStep.module.css';
const EditBackgroundStep = React.memo(
({ defaultValue, isImageUpdating, onImageUpdate, onImageDelete, onBack }) => {
const [t] = useTranslation();
const field = useRef(null);
const handleFileSelect = useCallback(
(file) => {
onImageUpdate({
file,
});
},
[onImageUpdate],
);
useEffect(() => {
field.current.focus();
}, []);
return (
<>
<Popup.Header onBack={onBack}>
{t('common.editBackground', {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<div className={styles.action}>
<FilePicker accept="image/*" onSelect={handleFileSelect}>
<Button
ref={field}
content={t('action.uploadNewBackground')}
loading={isImageUpdating}
disabled={isImageUpdating}
className={styles.actionButton}
/>
</FilePicker>
</div>
{defaultValue && (
<Button
negative
content={t('action.deleteBackground')}
disabled={isImageUpdating}
onClick={onImageDelete}
/>
)}
</Popup.Content>
</>
);
},
);
EditBackgroundStep.propTypes = {
defaultValue: PropTypes.object, // eslint-disable-line react/forbid-prop-types
isImageUpdating: PropTypes.bool.isRequired,
// onUpdate: PropTypes.func.isRequired,
onImageUpdate: PropTypes.func.isRequired,
onImageDelete: PropTypes.func.isRequired,
onBack: PropTypes.func.isRequired,
};
EditBackgroundStep.defaultProps = {
defaultValue: undefined,
};
export default EditBackgroundStep;

View file

@ -0,0 +1,25 @@
.action {
border: none;
display: inline-block;
height: 36px;
overflow: hidden;
position: relative;
transition: background 0.3s ease;
width: 100%;
}
.action:hover {
background: #e9e9e9 !important;
}
.actionButton {
background: transparent !important;
color: #6b808c !important;
font-weight: normal !important;
height: 36px;
line-height: 24px !important;
padding: 6px 12px !important;
text-align: left !important;
text-decoration: underline !important;
width: 100%;
}

View file

@ -0,0 +1,67 @@
import React, { useCallback, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, Form } from 'semantic-ui-react';
import { Input, Popup } from '../../../lib/custom-ui';
import { useField } from '../../../hooks';
import styles from './EditNameStep.module.css';
const EditNameStep = React.memo(({ defaultValue, onUpdate, onBack, onClose }) => {
const [t] = useTranslation();
const [value, handleFieldChange] = useField(defaultValue);
const field = useRef(null);
const handleSubmit = useCallback(() => {
const cleanValue = value.trim();
if (!cleanValue) {
field.current.select();
return;
}
if (cleanValue !== defaultValue) {
onUpdate(cleanValue);
}
onClose();
}, [defaultValue, onUpdate, onClose, value]);
useEffect(() => {
field.current.select();
}, []);
return (
<>
<Popup.Header onBack={onBack}>
{t('common.editTitle', {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<Form onSubmit={handleSubmit}>
<div className={styles.text}>{t('common.title')}</div>
<Input
fluid
ref={field}
value={value}
className={styles.field}
onChange={handleFieldChange}
/>
<Button positive content={t('action.save')} />
</Form>
</Popup.Content>
</>
);
});
EditNameStep.propTypes = {
defaultValue: PropTypes.string.isRequired,
onUpdate: PropTypes.func.isRequired,
onBack: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
export default EditNameStep;

View file

@ -1,10 +1,3 @@
.deleteButton {
bottom: 12px;
box-shadow: 0 1px 0 #cbcccc;
position: absolute;
right: 9px;
}
.field {
margin-bottom: 8px;
}

View file

@ -0,0 +1,3 @@
import ActionsPopup from './ActionsPopup';
export default ActionsPopup;

View file

@ -1,107 +0,0 @@
import dequal from 'dequal';
import React, { useCallback, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, Form } from 'semantic-ui-react';
import { withPopup } from '../../lib/popup';
import { Input, Popup } from '../../lib/custom-ui';
import { useForm, useSteps } from '../../hooks';
import DeleteStep from '../DeleteStep';
import styles from './EditPopup.module.css';
const StepTypes = {
DELETE: 'DELETE',
};
const EditStep = React.memo(({ defaultData, onUpdate, onDelete, onClose }) => {
const [t] = useTranslation();
const [data, handleFieldChange] = useForm(() => ({
name: '',
...defaultData,
}));
const [step, openStep, handleBack] = useSteps();
const nameField = useRef(null);
const handleSubmit = useCallback(() => {
const cleanData = {
...data,
name: data.name.trim(),
};
if (!cleanData.name) {
nameField.current.select();
return;
}
if (!dequal(cleanData, defaultData)) {
onUpdate(cleanData);
}
onClose();
}, [defaultData, onUpdate, onClose, data]);
const handleDeleteClick = useCallback(() => {
openStep(StepTypes.DELETE);
}, [openStep]);
useEffect(() => {
nameField.current.select();
}, []);
if (step && step.type === StepTypes.DELETE) {
return (
<DeleteStep
title={t('common.deleteProject', {
context: 'title',
})}
content={t('common.areYouSureYouWantToDeleteThisProject')}
buttonContent={t('action.deleteProject')}
onConfirm={onDelete}
onBack={handleBack}
/>
);
}
return (
<>
<Popup.Header>
{t('common.editProject', {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<Form onSubmit={handleSubmit}>
<div className={styles.text}>{t('common.title')}</div>
<Input
fluid
ref={nameField}
name="name"
value={data.name}
className={styles.field}
onChange={handleFieldChange}
/>
<Button positive content={t('action.save')} />
</Form>
<Button
content={t('action.delete')}
className={styles.deleteButton}
onClick={handleDeleteClick}
/>
</Popup.Content>
</>
);
});
EditStep.propTypes = {
defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
onUpdate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
export default withPopup(EditStep);

View file

@ -1,9 +1,9 @@
import React, { useCallback } from 'react';
import React, { useCallback, useEffect } from 'react';
import PropTypes from 'prop-types';
import { Button, Grid } from 'semantic-ui-react';
import BoardsContainer from '../../containers/BoardsContainer';
import EditPopup from './EditPopup';
import ActionsPopup from './ActionsPopup';
import AddMembershipPopup from './AddMembershipPopup';
import EditMembershipPopup from './EditMembershipPopup';
import User from '../User';
@ -13,10 +13,14 @@ import styles from './Project.module.css';
const Project = React.memo(
({
name,
background,
backgroundImage,
isBackgroundImageUpdating,
memberships,
allUsers,
isEditable,
onUpdate,
onBackgroundImageUpdate,
onDelete,
onMembershipCreate,
onMembershipDelete,
@ -28,21 +32,41 @@ const Project = React.memo(
[onMembershipDelete],
);
useEffect(() => {
return () => {
document.body.style.background = null;
};
}, []);
useEffect(() => {
if (background) {
if (background.type === 'image') {
document.body.style.background = `url(${backgroundImage.url}) center / cover fixed #22252a`;
}
} else {
document.body.style.background = null;
}
}, [background, backgroundImage]);
return (
<div className={styles.wrapper}>
<Grid className={styles.header}>
<Grid.Row>
<Grid.Column>
{isEditable ? (
<EditPopup
defaultData={{
<ActionsPopup
project={{
name,
background,
backgroundImage,
isBackgroundImageUpdating,
}}
onUpdate={onUpdate}
onBackgroundImageUpdate={onBackgroundImageUpdate}
onDelete={onDelete}
>
<Button content={name} disabled={!isEditable} className={styles.name} />
</EditPopup>
</ActionsPopup>
) : (
<span className={styles.name}>{name}</span>
)}
@ -85,14 +109,25 @@ const Project = React.memo(
Project.propTypes = {
name: PropTypes.string.isRequired,
/* eslint-disable react/forbid-prop-types */
background: PropTypes.object,
backgroundImage: PropTypes.object,
/* eslint-enable react/forbid-prop-types */
isBackgroundImageUpdating: PropTypes.bool.isRequired,
/* eslint-disable react/forbid-prop-types */
memberships: PropTypes.array.isRequired,
allUsers: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */
isEditable: PropTypes.bool.isRequired,
onUpdate: PropTypes.func.isRequired,
onBackgroundImageUpdate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onMembershipCreate: PropTypes.func.isRequired,
onMembershipDelete: PropTypes.func.isRequired,
};
Project.defaultProps = {
background: undefined,
backgroundImage: undefined,
};
export default Project;

View file

@ -1,5 +1,5 @@
.addUser {
background: hsla(0, 0%, 0%, 0.24) !important;
background: rgba(0, 0, 0, 0.24) !important;
border-radius: 50% !important;
box-shadow: none !important;
color: #fff !important;
@ -12,7 +12,7 @@
}
.addUser:hover {
background: hsla(0, 0%, 0%, 0.32) !important;
background: rgba(0, 0, 0, 0.32) !important;
}
.header {
@ -46,7 +46,7 @@
}
.wrapper {
background: hsla(0, 0%, 0%, 0.08);
background: rgba(0, 0, 0, 0.08);
display: flex;
flex-direction: column;
height: 100%;

View file

@ -25,7 +25,15 @@ const Projects = React.memo(({ items, isEditable, onAdd }) => {
: Paths.PROJECTS.replace(':id', item.id)
}
>
<div className={classNames(styles.card, styles.open)}>
<div
className={classNames(styles.card, styles.open)}
style={{
background:
item.background &&
item.background.type === 'image' &&
`url("${item.backgroundImage.coverUrl}") center / cover`,
}}
>
{item.notificationsTotal > 0 && (
<span className={styles.notification}>{item.notificationsTotal}</span>
)}

View file

@ -75,7 +75,6 @@
.open {
background: #555;
background-size: cover;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.1), 0 6px 12px rgba(0, 0, 0, 0.1);
color: #fff;
position: relative;

View file

@ -1,4 +1,4 @@
import React, { useEffect } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useTranslation, Trans } from 'react-i18next';
@ -12,10 +12,6 @@ import styles from './StaticWrapper.module.css';
const StaticWrapper = ({ cardId, boardId, projectId }) => {
const [t] = useTranslation();
useEffect(() => {
document.body.style.overflowX = 'auto'; // TODO: only for board
}, []);
if (projectId === undefined) {
return (
<div className={styles.root}>

View file

@ -75,7 +75,7 @@ const getColor = (name) => {
const User = React.memo(({ name, avatarUrl, size, isDisabled, onClick }) => {
const style = {
...STYLES[size],
background: avatarUrl ? `url("${avatarUrl}")` : getColor(name),
background: avatarUrl ? `url("${avatarUrl}") center / cover` : getColor(name),
};
const contentNode = (

View file

@ -12,9 +12,6 @@
}
.wrapper {
background-position: center center !important;
background-repeat: no-repeat !important;
background-size: cover !important;
border: none;
border-radius: 50%;
color: #fff;