mirror of
https://github.com/bendtherules/demo-trello-app.git
synced 2026-08-18 13:42:40 +00:00
CreateOrEditTaskCard - use better combined crate/edit task card component
This commit is contained in:
@@ -0,0 +1,98 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
import { createTask, editTask } from "../../storeSlices/boardSlice";
|
||||||
|
import useAutoHeightTextArea from "../../utils/useAutoHeightTextArea";
|
||||||
|
|
||||||
|
import styles from "./TaskCard.module.css";
|
||||||
|
|
||||||
|
export function CreateOrEditTaskCard({
|
||||||
|
mode = "create" /* "create" | "edit" */,
|
||||||
|
listID,
|
||||||
|
cardID,
|
||||||
|
defaultCardText = "",
|
||||||
|
onSubmit: parentOnSubmit = () => {},
|
||||||
|
onCancel: parentOnCancel = () => {}
|
||||||
|
}) {
|
||||||
|
/*
|
||||||
|
---------
|
||||||
|
Init here
|
||||||
|
----------
|
||||||
|
*/
|
||||||
|
const textAreaRef = useAutoHeightTextArea();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const isCreateMode = mode === "create";
|
||||||
|
|
||||||
|
const [newCardText, setNewCardText] = useState(defaultCardText);
|
||||||
|
const [isFormOpen, setIsFormOpen] = useState(isCreateMode ? false : true);
|
||||||
|
|
||||||
|
const isTextNonEmpty = newCardText.trim().length > 0;
|
||||||
|
|
||||||
|
/*
|
||||||
|
---------
|
||||||
|
Create evt handlers here
|
||||||
|
----------
|
||||||
|
*/
|
||||||
|
const onComplete = () => {
|
||||||
|
setNewCardText(defaultCardText);
|
||||||
|
setIsFormOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = e => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (isTextNonEmpty > 0) {
|
||||||
|
if (isCreateMode) {
|
||||||
|
dispatch(createTask({ text: newCardText, listID }));
|
||||||
|
} else {
|
||||||
|
const payload = { text: newCardText, cardID: cardID, listID };
|
||||||
|
|
||||||
|
dispatch(editTask(payload));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onComplete();
|
||||||
|
|
||||||
|
parentOnSubmit();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCancel = e => {
|
||||||
|
onComplete();
|
||||||
|
|
||||||
|
parentOnCancel();
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
---------
|
||||||
|
Render here
|
||||||
|
----------
|
||||||
|
*/
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{!isFormOpen ? (
|
||||||
|
<button
|
||||||
|
className={styles.createInitBtn}
|
||||||
|
type="button"
|
||||||
|
onClick={e => setIsFormOpen(v => !v)}
|
||||||
|
>
|
||||||
|
{isCreateMode ? "+ Create Task" : "Edit Task"}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<form className={styles.createCardForm} onSubmit={onSubmit}>
|
||||||
|
<textarea
|
||||||
|
ref={textAreaRef}
|
||||||
|
className={`${styles.cardContainer} ${styles.createCardContainer}`}
|
||||||
|
placeholder="Create task"
|
||||||
|
autoFocus={true}
|
||||||
|
value={newCardText}
|
||||||
|
onChange={e => setNewCardText(e.target.value)}
|
||||||
|
></textarea>
|
||||||
|
<div className={styles.createActionBtnContainer}>
|
||||||
|
<input type="submit" value="Save" disabled={!isTextNonEmpty} />
|
||||||
|
<input type="button" value="Cancel" onClick={onCancel} />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import React, { useState } from "react";
|
|
||||||
import { useDispatch } from "react-redux";
|
|
||||||
import { createTask } from "../../storeSlices/boardSlice";
|
|
||||||
|
|
||||||
import styles from "./TaskCard.module.css";
|
|
||||||
|
|
||||||
export function NewTaskCardPrompt({ listID }) {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
const [newCardText, setNewCardText] = useState("");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.listContainer}>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Create task"
|
|
||||||
value={newCardText}
|
|
||||||
onChange={e => setNewCardText(e.target.value)}
|
|
||||||
onKeyPress={e => {
|
|
||||||
if (newCardText.trim().length > 0 && e.key === "Enter") {
|
|
||||||
dispatch(createTask({ text: newCardText, listID }));
|
|
||||||
|
|
||||||
setNewCardText("");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
></input>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,30 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useSelector, useDispatch } from "react-redux";
|
|
||||||
import { createList } from "../../storeSlices/boardSlice";
|
import { CreateOrEditTaskCard } from "./CreateOrEditTaskCard";
|
||||||
|
|
||||||
import styles from "./TaskCard.module.css";
|
import styles from "./TaskCard.module.css";
|
||||||
|
|
||||||
export function TaskCard({ cardData }) {
|
export function TaskCard({ cardData, listID }) {
|
||||||
const dispatch = useDispatch();
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
|
||||||
return (
|
const stopEditing = () => setIsEditing(false);
|
||||||
<div className={styles.cardContainer} tabIndex={1} contentEditable="true">
|
|
||||||
|
return !isEditing ? (
|
||||||
|
<div
|
||||||
|
className={styles.cardContainer}
|
||||||
|
tabIndex={1}
|
||||||
|
onDoubleClick={e => setIsEditing(true)}
|
||||||
|
>
|
||||||
{cardData.text}
|
{cardData.text}
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<CreateOrEditTaskCard
|
||||||
|
cardID={cardData.id}
|
||||||
|
listID={listID}
|
||||||
|
defaultCardText={cardData.text}
|
||||||
|
mode="edit"
|
||||||
|
onSubmit={stopEditing}
|
||||||
|
onCancel={stopEditing}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,3 +5,41 @@
|
|||||||
padding: 8px;
|
padding: 8px;
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.createCardContainer {
|
||||||
|
width: 100%;
|
||||||
|
resize: vertical;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.createCardForm {
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.createInitBtn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 0;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.createActionBtnContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
}
|
||||||
|
|
||||||
|
.createActionBtnContainer > input {
|
||||||
|
flex: 1 0 50px;
|
||||||
|
margin: 0 8px;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.createActionBtnContainer > input:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.createActionBtnContainer > input:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useSelector, useDispatch } from "react-redux";
|
|||||||
import { createList } from "../../storeSlices/boardSlice";
|
import { createList } from "../../storeSlices/boardSlice";
|
||||||
|
|
||||||
import { TaskCard } from "../TaskCard/TaskCard";
|
import { TaskCard } from "../TaskCard/TaskCard";
|
||||||
import { NewTaskCardPrompt } from "../TaskCard/NewTaskCardPrompt";
|
import { CreateOrEditTaskCard } from "../TaskCard/CreateOrEditTaskCard";
|
||||||
|
|
||||||
import styles from "./TaskList.module.css";
|
import styles from "./TaskList.module.css";
|
||||||
|
|
||||||
@@ -16,9 +16,9 @@ export function TaskList({ listData }) {
|
|||||||
<h3 className={styles.listName}>{listData.name}</h3>
|
<h3 className={styles.listName}>{listData.name}</h3>
|
||||||
<div className={styles.cardsContainer}>
|
<div className={styles.cardsContainer}>
|
||||||
{listData.cards.map(cardData => (
|
{listData.cards.map(cardData => (
|
||||||
<TaskCard key={cardData.id} cardData={cardData} />
|
<TaskCard key={cardData.id} listID={listData.id} cardData={cardData} />
|
||||||
))}
|
))}
|
||||||
<NewTaskCardPrompt listID={listData.id} />
|
<CreateOrEditTaskCard listID={listData.id} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export const slice = createSlice({
|
|||||||
const { text: cardText, cardID, listID: parentListID } = action.payload;
|
const { text: cardText, cardID, listID: parentListID } = action.payload;
|
||||||
|
|
||||||
const parentList = state.lists.find(({ id }) => id === parentListID);
|
const parentList = state.lists.find(({ id }) => id === parentListID);
|
||||||
const existingCard = parentList.find(({ id }) => id === cardID);
|
const existingCard = parentList.cards.find(({ id }) => id === cardID);
|
||||||
|
|
||||||
existingCard.text = cardText;
|
existingCard.text = cardText;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { createRef, useEffect, useCallback } from "react";
|
||||||
|
|
||||||
|
// Souce - http://bdadam.com/blog/automatically-adapting-the-height-textarea.html
|
||||||
|
|
||||||
|
function adjustHeight(el, minHeight) {
|
||||||
|
// compute the height difference which is caused by border and outline
|
||||||
|
var outerHeight = parseInt(window.getComputedStyle(el).height, 10);
|
||||||
|
var diff = outerHeight - el.clientHeight;
|
||||||
|
|
||||||
|
// set the height to 0 in case of it has to be shrinked
|
||||||
|
el.style.height = 0;
|
||||||
|
|
||||||
|
// set the correct height
|
||||||
|
// el.scrollHeight is the full height of the content, not just the visible part
|
||||||
|
el.style.height = Math.max(minHeight, el.scrollHeight + diff) + "px";
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useAutoHeightTextArea(minHeight = 54) {
|
||||||
|
const eleRef = createRef(null);
|
||||||
|
const onChange = useCallback(() => {
|
||||||
|
if (eleRef.current !== null) {
|
||||||
|
adjustHeight(eleRef.current, minHeight);
|
||||||
|
}
|
||||||
|
}, [minHeight, eleRef]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (eleRef.current !== null) {
|
||||||
|
const el = eleRef.current;
|
||||||
|
|
||||||
|
el.addEventListener("input", onChange);
|
||||||
|
|
||||||
|
// we adjust height to the initial content
|
||||||
|
adjustHeight(el, minHeight);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
el.removeEventListener("input", onChange);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return eleRef;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user