Skip to content

Commit

Permalink
feature: Adds deletion check on a goal with multiple tasks, adds the …
Browse files Browse the repository at this point in the history
…date picker, refactors the Goal component.
  • Loading branch information
JonnyDeates committed Oct 8, 2024
1 parent b8f3408 commit aba1c61
Show file tree
Hide file tree
Showing 10 changed files with 234 additions and 114 deletions.
2 changes: 1 addition & 1 deletion apps/private/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"@paralleldrive/cuid2": "^2.2.2",
"axios": "^1.7.7",
"dayjs": "^1.11.13",
"koi-pool": "^0.0.33",
"koi-pool": "^0.0.34",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.26.2",
Expand Down
2 changes: 1 addition & 1 deletion apps/private/src/Pages/Home/Home.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
min-height: 100vh;
}

.Home h1 {
.Home > h1 {
color: #fff;
text-shadow: 2px 2px 3px rgba(0,0,0,.26666666666666666);
cursor: pointer;
Expand Down
9 changes: 3 additions & 6 deletions apps/private/src/components/GoalList/actions/GoalActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,12 @@ const GoalActions = {

return newState;
},
updateDueDate: (goalId: string, dueDate: DUE_DATE) => (prevState: GoalListType): GoalListType => {
updateDueDate: (goalId: string, dueDate: DUE_DATE | Date) => (prevState: GoalListType): GoalListType => {
const goalBeingModified = prevState[goalId];
if (goalBeingModified) {
const date = getDateFromDueDate(dueDate);
goalBeingModified.completionDate = typeof dueDate !== 'object' ? getDateFromDueDate(dueDate) : dueDate;

if (date !== null) {
goalBeingModified.completionDate = date;
return {...prevState};
}
return {...prevState};
}

return prevState;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,6 @@
height: 32px;

}
#Star{
fill: cadetblue;
}
.Goal .Subheader{
justify-content: space-around;
padding-top: .25rem;
Expand All @@ -61,7 +58,7 @@ padding-top: .25rem;
background-color: white;
box-shadow: unset;
}
.Goal .GoalIndicator {
.Goal .BottomIndicator {
height: 6px;
position: absolute;
bottom: 0;
Expand Down
85 changes: 15 additions & 70 deletions apps/private/src/components/GoalList/components/Goal/Goal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,55 +14,35 @@ import {
allDueDates,
ColorSelection,
type DUE_DATE,
findNextElementInListToFocusOn,
getDueDateFromDate
} from "../../../../utils/utils";
import Tasks from "../Task/Task";
import {handleSubmitEnter} from "@repo/shared";
import dayjs from "dayjs";
import copy from './assets/copy.svg'
import star from './assets/star.svg'
import starOutline from './assets/star_outline.svg'
import uncheckAll from './assets/remove_check.svg'
import checkAll from './assets/confirm_check.svg'
import edit from './assets/pencil.svg'
import trash from './assets/archive.ico'
import './Goal.css';
import GoalHeader from "./components/GoalHeader";
import GoalActionGroup from "./components/GoalActionGroup";
import GoalDeleteButton from "./components/GoalDeleteModal";

type GoalProps = GoalType & { id: string }

function Goal({id, completionDate, name, isEditing, isFavorite, tasks, tasksCompleted}: GoalProps) {
function Goal(currentGoal: GoalProps) {
const {id, completionDate, isFavorite, tasks, name} = currentGoal;
const {applyActionToGoalList} = useGoalListContext();

const tasksListOfIds = Object.keys(tasks);
const taskListOfIds = Object.keys(tasks);

const handleAddTask = () => applyActionToGoalList(TaskActions.create(id));
const handleDuplicateGoal = () => applyActionToGoalList(GoalActions.duplicate(id));
const handleUpdateDueDate = (value: DUE_DATE) => applyActionToGoalList(GoalActions.updateDueDate(id, value))
const handleUpdateGoalName = (value: ChangeEvent<HTMLInputElement>) => applyActionToGoalList(GoalActions.updateName(id, value.target.value))
const handleDeleteGoal = () => applyActionToGoalList(GoalActions.remove(id));
const handleToggleGoalEditing = () => applyActionToGoalList(GoalActions.toggleEditing(id, 'isEditing'));
const handleToggleGoalFavorite = () => applyActionToGoalList(GoalActions.toggleEditing(id, 'isFavorite'));
const handleToggleAllTasks = () => applyActionToGoalList(TaskActions.toggleAllTasks(id));

const formattedDate = dayjs(completionDate).format('M/D/YY')

const isEveryTaskComplete = tasksListOfIds.every((taskId) => {
const taskBeingChecked = tasks[taskId] as TaskType;
const handleAddTask = () => applyActionToGoalList(TaskActions.create(id));

return taskBeingChecked.isCompleted
})

const selectedOption = getDueDateFromDate(completionDate);

const handleGoalNameEnterPress = (event: KeyboardEvent) => handleSubmitEnter(event, ()=> {
handleToggleGoalEditing()
findNextElementInListToFocusOn(tasksListOfIds)
})

return <div className='Goal'>
<div className={'TopIndicator'} style={ColorSelection['Default'][selectedOption]}/>

<Select<DUE_DATE>
containerAttributes={{className: 'Select'}}
selectedOptionAttributes={{
Expand All @@ -73,59 +53,24 @@ function Goal({id, completionDate, name, isEditing, isFavorite, tasks, tasksComp
{style: ((option) => ({...ColorSelection['Default'][option]}))}
}
selectedOption={selectedOption} onClick={handleUpdateDueDate}/>
<CloseButton onClick={handleDeleteGoal}/>
<GoalDeleteButton id={id} name={name} taskListOfIds={taskListOfIds}/>
<IconButton className={'FavoriteButton'} src={isFavorite ? star : starOutline}
alt={'Star'} variant={'standard'} isActive={isFavorite} title={'Favorite Goal'}
style={{backgroundColor: isFavorite ? ColorSelection['Default'][selectedOption].backgroundColor : ''}}
onClick={handleToggleGoalFavorite}/>
<div className='Header'>
{
isEditing
?
<FloatingLabelInputWithButton label='' placeholder={'Goal Name'} value={name}
onClick={handleToggleGoalEditing}
onKeyDown={handleGoalNameEnterPress}
width={300} onChange={handleUpdateGoalName}
/>
:
<h2 onDoubleClick={handleToggleGoalEditing}>
{name}
</h2>
}
<div>
<p>Due: {formattedDate}</p>
<p>Completed: <b>{Math.round((tasksCompleted / tasksListOfIds.length) * 100)}%</b></p>
</div>

</div>
<div className='Subheader'>

<IconButton className={'IconButton'}
src={isEveryTaskComplete ? checkAll : uncheckAll}
alt={'Reset Objectives'}
isActive={isEveryTaskComplete}
variant={'accept'} title={'Toggle goal tasks'}
onClick={handleToggleAllTasks}/>
<IconButton className={'IconButton'} src={edit} alt={'Duplicate'}
variant={'accept'} title={'Edit goal'}
onClick={handleToggleGoalEditing}/>
<IconButton className={'IconButton'} src={copy} alt={'Duplicate'}
variant={'accept'} title={'Duplicate goal'}
onClick={handleDuplicateGoal}/>

<IconButton className={'IconButton'} src={trash} alt={'Archive'}
variant={'caution'} onClick={handleDuplicateGoal}/>
</div>
<GoalHeader {...currentGoal} handleToggleGoalEditing={handleToggleGoalEditing} tasksListOfIds={taskListOfIds}/>
<GoalActionGroup {...currentGoal} taskListOfIds={taskListOfIds}
handleToggleGoalEditing={handleToggleGoalEditing}/>
<div>
{tasksListOfIds.map((taskId, index) =>
{taskListOfIds.map((taskId, index) =>
<React.Fragment key={taskId}>
<Tasks id={taskId} tasksListOfIds={tasksListOfIds} index={index}
goalId={id} isLast={index === tasksListOfIds.length}
<Tasks id={taskId} tasksListOfIds={taskListOfIds} index={index}
goalId={id} isLast={index === taskListOfIds.length}
{...(tasks[taskId]) as TaskType} />
</React.Fragment>
)}
</div>
<div className={'GoalIndicator'} style={ColorSelection['Default'][selectedOption]}/>
<div className={'BottomIndicator'} style={ColorSelection['Default'][selectedOption]}/>
<Button className="AddTask" onClick={handleAddTask}>Add Task</Button>
</div>;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from 'react';
import {IconButton} from "koi-pool";
import checkAll from "../assets/confirm_check.svg";
import uncheckAll from "../assets/remove_check.svg";
import edit from "../assets/pencil.svg";
import copy from "../assets/copy.svg";
import trash from "../assets/archive.ico";
import {GoalType, TaskType} from "@repo/types";
import TaskActions from "../../../actions/TaskActions";
import {useGoalListContext} from "../../../../../contexts/GoalListProvider/GoalListProvider";
import GoalActions from "../../../actions/GoalActions";

type GoalActionGroupProps = GoalType & {id: string, taskListOfIds: string[], handleToggleGoalEditing: ()=> void}

const GoalActionGroup = ({tasks, id, taskListOfIds, handleToggleGoalEditing}: GoalActionGroupProps) => {
const {applyActionToGoalList} = useGoalListContext();

const handleToggleAllTasks = () => applyActionToGoalList(TaskActions.toggleAllTasks(id));
const handleDuplicateGoal = () => applyActionToGoalList(GoalActions.duplicate(id));


const isEveryTaskComplete = taskListOfIds.every((taskId) => {
const taskBeingChecked = tasks[taskId] as TaskType;

return taskBeingChecked.isCompleted
})

return (
<div className='Subheader'>

<IconButton className={'IconButton'}
src={isEveryTaskComplete ? checkAll : uncheckAll}
alt={'Reset Objectives'}
isActive={isEveryTaskComplete}
variant={'accept'} title={'Toggle goal tasks'}
onClick={handleToggleAllTasks}/>
<IconButton className={'IconButton'} src={edit} alt={'Edit'}
variant={'accept'} title={'Edit goal'}
onClick={handleToggleGoalEditing}/>

<IconButton className={'IconButton'} src={copy} alt={'Duplicate'}
variant={'accept'} title={'Duplicate goal'}
onClick={handleDuplicateGoal}/>

<IconButton className={'IconButton'} src={trash} alt={'Archive'}
variant={'caution'} onClick={handleDuplicateGoal}/>
</div>
);
};

export default GoalActionGroup;
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React, {useState} from 'react';
import {CloseButton, GenericAcceptanceModal} from "koi-pool";
import GoalActions from "../../../actions/GoalActions.js";
import {useGoalListContext} from "../../../../../contexts/GoalListProvider/GoalListProvider.js";
import {TaskType} from "@repo/types";

type GoalDeleteButtonProps = { id: string, name: string, taskListOfIds: string[] };

const GoalDeleteButton = ({id, name, taskListOfIds}: GoalDeleteButtonProps) => {
const {applyActionToGoalList} = useGoalListContext();
const [showConfirmDeleteModal, setShowConfirmDeleteModal] = useState<boolean>(false)

const handleClose = () => {
setShowConfirmDeleteModal(false);
}

const handleDeleteGoal = () => applyActionToGoalList(GoalActions.remove(id));

const showDeleteModal = () => {
if (taskListOfIds.length <= 1) {
handleDeleteGoal();
} else {
setShowConfirmDeleteModal(true)
}
}

return (
<>
<CloseButton onClick={showDeleteModal}/>

<GenericAcceptanceModal handleClose={handleClose} handleSubmit={handleDeleteGoal}
isOpen={showConfirmDeleteModal}
modalAttributes={{
style: {
height: 'fit-content',
fontSize: '1rem',
minHeight: "200px"
}
}}
isNegative
cancelButtonText={'Keep it'}
submitButtonText={'Delete it'}
actionGroupAttributes={{style: {marginTop: '3rem', border: 'unset'}}}
>
<h2>
Are you sure you want to delete the goal
{Boolean(name) ? <i> ({name})</i> : ''}?
</h2>
</GenericAcceptanceModal>
</>

);
};

export default GoalDeleteButton;
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React, {ChangeEvent, KeyboardEvent} from 'react';
import {FloatingLabelInputWithButton} from "koi-pool";
import {GoalType} from "@repo/types";
import dayjs from "dayjs";
import GoalActions from "../../../actions/GoalActions";
import {useGoalListContext} from "../../../../../contexts/GoalListProvider/GoalListProvider";
import {handleSubmitEnter} from "@repo/shared";
import {findNextElementInListToFocusOn} from "../../../../../utils/utils";

type GoalHeaderProps = GoalType & { id: string, handleToggleGoalEditing: () => void, tasksListOfIds: string[] }

const GoalHeader = ({
completionDate,
isEditing,
tasksCompleted,
name,
id,
handleToggleGoalEditing,
tasksListOfIds
}: GoalHeaderProps) => {
const {applyActionToGoalList} = useGoalListContext();


const handleGoalNameEnterPress = (event: KeyboardEvent) => handleSubmitEnter(event, () => {
handleToggleGoalEditing()
findNextElementInListToFocusOn(tasksListOfIds)
})

const handleUpdateGoalName = (value: ChangeEvent<HTMLInputElement>) => applyActionToGoalList(GoalActions.updateName(id, value.target.value))

const handleUpdateGoalDate = (event: ChangeEvent<HTMLInputElement>) => {
const userDate = event.target.value;
const parsedDate = dayjs(userDate);
if (parsedDate.isValid()) {
applyActionToGoalList(GoalActions.updateDueDate(id, parsedDate.toDate()))
} else {
console.error('Invalid date:', userDate);
}
}

const formattedDate = dayjs(completionDate).format('YYYY-MM-DD')

return (
<div className='Header'>
{
isEditing
?
<FloatingLabelInputWithButton label='' placeholder={'Goal Name'} value={name}
onClick={handleToggleGoalEditing}
onKeyDown={handleGoalNameEnterPress}
width={300} onChange={handleUpdateGoalName}
/>
:
<h2 onDoubleClick={handleToggleGoalEditing}>
{name}
</h2>
}
<div>

<p>Due: <input type='date' value={formattedDate} onChange={handleUpdateGoalDate}/></p>
<p>Completed: <b>{Math.round((tasksCompleted / tasksListOfIds.length) * 100)}%</b></p>
</div>

</div>
);
};

export default GoalHeader;
Loading

0 comments on commit aba1c61

Please sign in to comment.