Sammy/t 392 aau i can drag and drop opportunities (#257)

* refactor: extract data from Board component

* feature: display board on opportunities page

* test: add strict mode in storybook

* chore: replace dnd to make it work with React 18 and strict mode

Atlassion has not fixed this issue in a year so we use the fork @hello-pangea/dnd
https://github.com/atlassian/react-beautiful-dnd/issues/2350

* refactor: move mocked-data in a file

* chore: use real column names in mock data

* feature: design columns

* feature: add New button at bottum of columns

* bugfix: move header out of dragable so the cards does not flicker on drop

* lint: remove useless imports

* refactor: rename board item key
This commit is contained in:
Sammy Teillet
2023-06-08 17:40:25 +02:00
committed by GitHub
parent b827716d1b
commit 49a99c8ae6
10 changed files with 224 additions and 105 deletions

View File

@ -1,12 +1,14 @@
import { useCallback, useState } from 'react';
import styled from '@emotion/styled';
import {
DragDropContext,
Draggable,
Droppable,
OnDragEndResponder,
} from 'react-beautiful-dnd';
import styled from '@emotion/styled';
} from '@hello-pangea/dnd';
// Atlassian dnd does not support StrictMode from RN 18, so we use a fork @hello-pangea/dnd
// https://github.com/atlassian/react-beautiful-dnd/issues/2350
import { BoardCard } from './BoardCard';
import { BoardColumn } from './BoardColumn';
@ -16,43 +18,27 @@ const StyledBoard = styled.div`
height: 100%;
`;
type ItemKey = `item-${number}`;
interface Item {
export type BoardItemKey = `item-${number}`;
export interface Item {
id: string;
content: string;
}
interface Items {
export interface Items {
[key: string]: Item;
}
interface Column {
export interface Column {
id: string;
title: string;
itemKeys: ItemKey[];
colorCode?: string;
itemKeys: BoardItemKey[];
}
const items: Items = {
'item-1': { id: 'item-1', content: 'Item 1' },
'item-2': { id: 'item-2', content: 'Item 2' },
'item-3': { id: 'item-3', content: 'Item 3' },
'item-4': { id: 'item-4', content: 'Item 4' },
'item-5': { id: 'item-5', content: 'Item 5' },
'item-6': { id: 'item-6', content: 'Item 6' },
} satisfies Record<ItemKey, { id: ItemKey; content: string }>;
type BoardProps = {
initialBoard: Column[];
items: Items;
};
const initialBoard = [
{
id: 'column-1',
title: 'Column 1',
itemKeys: ['item-1', 'item-2', 'item-3', 'item-4'],
},
{
id: 'column-2',
title: 'Column 2',
itemKeys: ['item-5', 'item-6'],
},
] satisfies Column[];
export const Board = () => {
export const Board = ({ initialBoard, items }: BoardProps) => {
const [board, setBoard] = useState<Column[]>(initialBoard);
const onDragEnd: OnDragEndResponder = useCallback(
@ -99,7 +85,11 @@ export const Board = () => {
<Droppable key={column.id} droppableId={column.id}>
{(provided) =>
provided && (
<BoardColumn title={column.title} droppableProvided={provided}>
<BoardColumn
title={column.title}
colorCode={column.colorCode}
droppableProvided={provided}
>
{column.itemKeys.map((itemKey, index) => (
<Draggable
key={itemKey}

View File

@ -1,11 +1,13 @@
import { DraggableProvided } from 'react-beautiful-dnd';
import styled from '@emotion/styled';
import { DraggableProvided } from '@hello-pangea/dnd';
const StyledCard = styled.div`
background-color: #ffffff;
background-color: ${({ theme }) => theme.secondaryBackground};
border: 1px solid ${({ theme }) => theme.quaternaryBackground};
border-radius: 4px;
padding: 16px;
margin-bottom: 16px;
padding: 8px;
margin-bottom: 8px;
box-shadow: ${({ theme }) => theme.boxShadow};
`;
type BoardCardProps = {

View File

@ -1,35 +1,53 @@
import { DroppableProvided } from 'react-beautiful-dnd';
import styled from '@emotion/styled';
import { DroppableProvided } from '@hello-pangea/dnd';
import { NewButton } from './BoardNewButton';
const StyledColumn = styled.div`
display: flex;
flex-direction: column;
width: 300px;
margin-right: 16px;
background-color: #f5f5f5;
border-radius: 4px;
padding: 16px;
background-color: ${({ theme }) => theme.primaryBackground};
padding: ${({ theme }) => theme.spacing(2)};
`;
const StyledColumnTitle = styled.h3`
font-family: 'Inter';
font-style: normal;
font-weight: ${({ theme }) => theme.fontWeightBold};
font-size: ${({ theme }) => theme.fontSizeMedium};
line-height: ${({ theme }) => theme.lineHeight};
color: ${({ color }) => color};
margin: 0;
margin-bottom: ${({ theme }) => theme.spacing(2)};
`;
const ItemContainer = styled.div``;
type BoardColumnProps = {
title: string;
colorCode?: string;
children: any[];
droppableProvided: DroppableProvided;
};
export const BoardColumn = ({
title,
colorCode,
children,
droppableProvided,
}: BoardColumnProps) => {
return (
<StyledColumn
ref={droppableProvided.innerRef}
{...droppableProvided.droppableProps}
>
<h3>{title}</h3>
{children}
{droppableProvided.placeholder}
<StyledColumn>
<StyledColumnTitle color={colorCode}> {title}</StyledColumnTitle>
<ItemContainer
ref={droppableProvided.innerRef}
{...droppableProvided.droppableProps}
>
{children}
{droppableProvided.placeholder}
<NewButton />
</ItemContainer>
</StyledColumn>
);
};

View File

@ -0,0 +1,29 @@
import styled from '@emotion/styled';
import { IconPlus } from '@tabler/icons-react';
const StyledButton = styled.button`
display: flex;
align-items: center;
justify-content: center;
background-color: ${({ theme }) => theme.primaryBackground};
color: ${({ theme }) => theme.text40};
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s ease-in-out;
align-self: baseline;
gap: ${({ theme }) => theme.spacing(1)};
&:hover {
background-color: ${({ theme }) => theme.secondaryBackground};
}
`;
export const NewButton = () => {
return (
<StyledButton>
<IconPlus size={16} />
New
</StyledButton>
);
};

View File

@ -1,7 +1,10 @@
import { StrictMode } from 'react';
import { Meta, StoryObj } from '@storybook/react';
import { Board } from '../Board';
import { initialBoard, items } from './mock-data';
const meta: Meta<typeof Board> = {
title: 'Components/Board',
component: Board,
@ -11,5 +14,9 @@ export default meta;
type Story = StoryObj<typeof Board>;
export const OneColumnBoard: Story = {
render: () => <Board />,
render: () => (
<StrictMode>
<Board initialBoard={initialBoard} items={items} />
</StrictMode>
),
};

View File

@ -0,0 +1,55 @@
import { Column, Items } from '../Board';
export const items: Items = {
'item-1': { id: 'item-1', content: 'Item 1' },
'item-2': { id: 'item-2', content: 'Item 2' },
'item-3': { id: 'item-3', content: 'Item 3' },
'item-4': { id: 'item-4', content: 'Item 4' },
'item-5': { id: 'item-5', content: 'Item 5' },
'item-6': { id: 'item-6', content: 'Item 6' },
};
for (let i = 7; i <= 20; i++) {
const key = `item-${i}`;
items[key] = { id: key, content: `Item ${i}` };
}
export const initialBoard = [
{
id: 'column-1',
title: 'New',
colorCode: '#B76796',
itemKeys: [
'item-1',
'item-2',
'item-3',
'item-4',
'item-7',
'item-8',
'item-9',
],
},
{
id: 'column-2',
title: 'Screening',
colorCode: '#CB912F',
itemKeys: ['item-5', 'item-6'],
},
{
id: 'column-3',
colorCode: '#9065B0',
title: 'Meeting',
itemKeys: [],
},
{
id: 'column-4',
title: 'Proposal',
colorCode: '#337EA9',
itemKeys: [],
},
{
id: 'column-5',
colorCode: '#079039',
title: 'Customer',
itemKeys: [],
},
] satisfies Column[];