diff --git a/src/components/Home/AddItem.jsx b/src/components/Home/AddItem.jsx new file mode 100644 index 0000000..652326b --- /dev/null +++ b/src/components/Home/AddItem.jsx @@ -0,0 +1,38 @@ +import AddIcon from '@mui/icons-material/Add'; +import { IconButton, InputAdornment, TextField } from '@mui/material'; +import { useState } from 'react'; + +const AddItem = ({ label, onAdd }) => { + const [inputValue, setInputValue] = useState(''); + + const handleAdd = async () => { + if (inputValue.trim()) { + await onAdd(inputValue); + setInputValue(''); + } + }; + + return ( + setInputValue(e.target.value)} + fullWidth + onKeyDown={(e) => e.key === 'Enter' && handleAdd()} + slotProps={{ + input: { + endAdornment: ( + + + + + + ), + }, + }} + /> + ); +}; + +export default AddItem; diff --git a/src/components/Home/GoalTracker.jsx b/src/components/Home/GoalTracker.jsx index 55403a5..431dc49 100644 --- a/src/components/Home/GoalTracker.jsx +++ b/src/components/Home/GoalTracker.jsx @@ -1,177 +1,20 @@ +import AddItem from '@components/Home/AddItem'; +import MacroGoal from '@components/Home/MacroGoal'; import { useUser } from '@contexts/UserContext'; -import ExpandLessIcon from '@mui/icons-material/ExpandLess'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import { - Box, - Checkbox, - CircularProgress, - Divider, - IconButton, - List, - ListItem, - ListItemText, - Paper, - Typography, -} from '@mui/material'; -import { useState } from 'react'; - -const ProgressIndicator = ({ value, size = 40, thickness = 4 }) => ( - - - - - {`${Math.round(value)}%`} - - - -); - -const Task = ({ task, onToggle }) => ( - - - - -); - -const MicroGoal = ({ microGoal, macroGoalIndex, microGoalIndex, onToggleTask, onToggleExpand }) => { - const progress = - (microGoal.tasks.filter((t) => t.completed).length / microGoal.tasks.length) * 100; - - return ( - - - - {microGoal.name} - onToggleExpand(macroGoalIndex, microGoalIndex)} - size="small" - sx={{ ml: 'auto' }} - > - {microGoal.expanded ? : } - - - {microGoal.expanded && ( - - {microGoal.tasks.map((task, taskIndex) => ( - onToggleTask(macroGoalIndex, microGoalIndex, taskIndex)} - /> - ))} - - )} - - ); -}; - -const MacroGoal = ({ macroGoal, macroGoalIndex, onToggleTask, onToggleExpand }) => { - const progress = - (macroGoal.microgoals.reduce((acc, mg) => acc + mg.tasks.filter((t) => t.completed).length, 0) / - macroGoal.microgoals.reduce((acc, mg) => acc + mg.tasks.length, 0)) * - 100; - - return ( - - - - - {macroGoal.name} - - onToggleExpand(macroGoalIndex)} size="small" sx={{ ml: 'auto' }}> - {macroGoal.expanded ? : } - - - - {macroGoal.expanded && ( - - {macroGoal.microgoals.map((microGoal, microGoalIndex) => ( - - ))} - - )} - - ); -}; +import useGoalsUpdater from '@hooks/useGoalsUpdater'; +import { Box } from '@mui/material'; export default function GoalTracker() { - const { user, loading } = useUser(); - const [goals, setGoals] = useState(user.goals || []); - - const handleToggleTask = (macroGoalIndex, microGoalIndex, taskIndex) => { - setGoals((prevGoals) => - prevGoals.map((macroGoal, mgi) => - mgi === macroGoalIndex - ? { - ...macroGoal, - microgoals: macroGoal.microgoals.map((microGoal, mgi) => - mgi === microGoalIndex - ? { - ...microGoal, - tasks: microGoal.tasks.map((task, ti) => - ti === taskIndex ? { ...task, completed: !task.completed } : task, - ), - } - : microGoal, - ), - } - : macroGoal, - ), - ); - }; - - const handleToggleExpand = (macroGoalIndex, microGoalIndex) => { - setGoals((prevGoals) => - prevGoals.map((macroGoal, mgi) => - mgi === macroGoalIndex - ? microGoalIndex !== undefined - ? { - ...macroGoal, - microgoals: macroGoal.microgoals.map((microGoal, mgi) => - mgi === microGoalIndex - ? { ...microGoal, expanded: !microGoal.expanded } - : microGoal, - ), - } - : { ...macroGoal, expanded: !macroGoal.expanded } - : macroGoal, - ), - ); - }; + const { user } = useUser(); + const { addGoal } = useGoalsUpdater(); return ( - - {loading ? ( - - ) : ( - goals.map((macroGoal, macroGoalIndex) => ( - - )) - )} + + + + {user.goals.map((macroGoal, index) => ( + + ))} ); } diff --git a/src/components/Home/MacroGoal.jsx b/src/components/Home/MacroGoal.jsx new file mode 100644 index 0000000..779201c --- /dev/null +++ b/src/components/Home/MacroGoal.jsx @@ -0,0 +1,45 @@ +import AddItem from '@components/Home/AddItem'; +import MicroGoal from '@components/Home/MicroGoal'; +import ProgressIndicator from '@components/Home/ProgressIndicator'; +import useGoalsUpdater from '@hooks/useGoalsUpdater'; +import { ExpandLess, ExpandMore } from '@mui/icons-material'; +import { Box, Collapse, Divider, IconButton, List, Paper, Typography } from '@mui/material'; +import { calculateProgress } from '@utils/calculateProgress'; + +const MacroGoal = ({ macroGoal, macroGoalIndex }) => { + const { addMicrogoal, toggleGoalExpansion } = useGoalsUpdater(); + const progress = calculateProgress(macroGoal.microgoals); + + return ( + + + + + {macroGoal.name} + + toggleGoalExpansion(macroGoalIndex)} size="small"> + {macroGoal.expanded ? : } + + + + + addMicrogoal(macroGoalIndex, microGoalName)} + /> + + {macroGoal.microgoals.map((microGoal, microGoalIndex) => ( + + ))} + + + + ); +}; + +export default MacroGoal; diff --git a/src/components/Home/MicroGoal.jsx b/src/components/Home/MicroGoal.jsx new file mode 100644 index 0000000..fa03335 --- /dev/null +++ b/src/components/Home/MicroGoal.jsx @@ -0,0 +1,46 @@ +import AddItem from '@components/Home/AddItem'; +import ProgressIndicator from '@components/Home/ProgressIndicator'; +import Task from '@components/Home/Task'; +import useGoalsUpdater from '@hooks/useGoalsUpdater'; +import { ExpandLess, ExpandMore } from '@mui/icons-material'; +import { Box, Collapse, IconButton, List, Paper, Typography } from '@mui/material'; +import { calculateProgress } from '@utils/calculateProgress'; + +const MicroGoal = ({ microGoal, macroGoalIndex, microGoalIndex }) => { + const { addTask, toggleMicroGoalExpansion, toggleTaskCompletion } = useGoalsUpdater(); + const progress = calculateProgress([microGoal]); + + return ( + + + + + {microGoal.name} + + toggleMicroGoalExpansion(macroGoalIndex, microGoalIndex)} + size="small" + > + {microGoal.expanded ? : } + + + + + {microGoal.tasks.map((task, taskIndex) => ( + toggleTaskCompletion(macroGoalIndex, microGoalIndex, taskIndex)} + /> + ))} + + addTask(macroGoalIndex, microGoalIndex, taskName)} + /> + + + ); +}; + +export default MicroGoal; diff --git a/src/components/Home/ProgressIndicator.jsx b/src/components/Home/ProgressIndicator.jsx new file mode 100644 index 0000000..834dafb --- /dev/null +++ b/src/components/Home/ProgressIndicator.jsx @@ -0,0 +1,45 @@ +import { Box, CircularProgress, Typography } from '@mui/material'; + +const ProgressIndicator = ({ value, size = 40, thickness = 4 }) => ( + + + value >= 100 + ? theme.palette.success.main + : `linear-gradient(to right, ${theme.palette.info.main}, ${theme.palette.warning.main})`, + transition: 'all 0.4s ease-in-out', // Smooth transition effect + }} + /> + + = 100 ? 'success.main' : 'text.secondary'} + sx={{ + fontWeight: value >= 100 ? 'bold' : 'normal', + fontSize: value >= 100 ? '0.75rem' : 'inherit', + }} + > + {`${Math.round(value)}%`} + + + +); + +export default ProgressIndicator; diff --git a/src/components/Home/Task.jsx b/src/components/Home/Task.jsx new file mode 100644 index 0000000..aafbe9b --- /dev/null +++ b/src/components/Home/Task.jsx @@ -0,0 +1,35 @@ +import { Checkbox, ListItem, ListItemText } from '@mui/material'; + +const Task = ({ task, onToggle }) => ( + + + + +); + +export default Task; diff --git a/src/hooks/useGoalsUpdater.js b/src/hooks/useGoalsUpdater.js index 4c03841..a491eee 100644 --- a/src/hooks/useGoalsUpdater.js +++ b/src/hooks/useGoalsUpdater.js @@ -1,117 +1,87 @@ import { useUser } from '@contexts/UserContext'; -/** - * Custom hook to manage user goals, microgoals, and tasks. - */ -const useGoalsManager = () => { +const useGoalsUpdater = () => { const { user, updateProfile } = useUser(); - // Add a new goal - const addGoal = async (goalName) => { + // Update the goals in the user profile + const updateGoals = async (updatedGoals, message) => { try { - const newGoal = { - name: goalName, - expanded: false, - microgoals: [], - }; - - // Add the new goal to the goals array - const updatedGoals = [...(user.goals || []), newGoal]; - - // Update the user profile in Firestore await updateProfile({ goals: updatedGoals }); - console.log('Goal added successfully.'); - } catch (err) { - console.error('Error adding goal:', err); + console.log(message); + } catch (error) { + console.error(`Error updating goals: ${message}`, error); } }; - // Add a new microgoal to a specific goal - const addMicrogoal = async (goalIndex, microgoalName) => { - try { - const updatedGoals = [...(user.goals || [])]; - if (!updatedGoals[goalIndex]) { - console.error('Specified goal does not exist'); - return; - } - - // Define the new microgoal structure - const newMicrogoal = { - name: microgoalName, - expanded: false, - tasks: [], - }; - - // Add the new microgoal to the specified goal's microgoals array - const targetGoal = { ...updatedGoals[goalIndex] }; - targetGoal.microgoals = [...(targetGoal.microgoals || []), newMicrogoal]; - updatedGoals[goalIndex] = targetGoal; - - // Update the user profile in Firestore - await updateProfile({ goals: updatedGoals }); - console.log('Microgoal added successfully.'); - } catch (err) { - console.error('Error adding microgoal:', err); + // Add a new goal, microgoal, or task + const addItem = async (goalIndex, microGoalIndex, newItem, itemType) => { + const updatedGoals = [...user.goals]; + let target = updatedGoals[goalIndex]; + if (microGoalIndex !== undefined) { + target = target?.microgoals[microGoalIndex]; } - }; - - // Add a new task to a specific microgoal - const addTask = async (goalIndex, microgoalIndex, taskName) => { - try { - const updatedGoals = [...(user.goals || [])]; - if (!updatedGoals[goalIndex] || !updatedGoals[goalIndex].microgoals[microgoalIndex]) { - console.error('Specified goal or microgoal does not exist'); - return; - } - - const newTask = { - name: taskName, - completed: false, - }; - // Add the new task to the specified microgoal's tasks array - const targetMicrogoal = { ...updatedGoals[goalIndex].microgoals[microgoalIndex] }; - targetMicrogoal.tasks = [...(targetMicrogoal.tasks || []), newTask]; - updatedGoals[goalIndex].microgoals[microgoalIndex] = targetMicrogoal; + if (!target) { + console.error(`${itemType} does not exist`); + return; + } - // Update the user profile in Firestore - await updateProfile({ goals: updatedGoals }); - console.log('Task added successfully.'); - } catch (err) { - console.error('Error adding task:', err); + if (itemType === 'task') { + target.tasks.push(newItem); + } else if (itemType === 'microgoal') { + target.microgoals.push(newItem); + } else if (itemType === 'goal') { + updatedGoals.push(newItem); } - }; - // Update the completion status of a specific task - const updateTaskStatus = async (goalIndex, microgoalIndex, taskIndex, completed) => { - try { - const updatedGoals = [...(user.goals || [])]; - if ( - !updatedGoals[goalIndex] || - !updatedGoals[goalIndex].microgoals[microgoalIndex] || - !updatedGoals[goalIndex].microgoals[microgoalIndex].tasks[taskIndex] - ) { - console.error('Specified goal, microgoal, or task does not exist'); - return; - } + await updateGoals( + updatedGoals, + `${itemType.charAt(0).toUpperCase() + itemType.slice(1)} added successfully.`, + ); + }; - // Update the task's completed status - updatedGoals[goalIndex].microgoals[microgoalIndex].tasks[taskIndex].completed = completed; + // Toggle the expansion status of a goal or microgoal + const toggleExpansion = async (goalIndex, microGoalIndex) => { + const updatedGoals = [...user.goals]; + const target = + microGoalIndex !== undefined + ? updatedGoals[goalIndex].microgoals[microGoalIndex] + : updatedGoals[goalIndex]; + target.expanded = !target.expanded; + await updateGoals(updatedGoals, 'Expansion toggled successfully.'); + }; - // Update the user profile in Firestore - await updateProfile({ goals: updatedGoals }); - console.log('Task status updated successfully.'); - } catch (err) { - console.error('Error updating task status:', err); + // Toggle the completion status of a task + const toggleTaskCompletion = async (goalIndex, microgoalIndex, taskIndex) => { + const updatedGoals = [...user.goals]; + const task = updatedGoals[goalIndex]?.microgoals[microgoalIndex]?.tasks[taskIndex]; + if (!task) { + console.error('Specified goal, microgoal, or task does not exist'); + return; } + task.completed = !task.completed; + await updateGoals(updatedGoals, 'Task completion status toggled successfully.'); }; return { - addGoal, - addMicrogoal, - addTask, - updateTaskStatus, + addGoal: (goalName) => + addItem(undefined, undefined, { name: goalName, expanded: false, microgoals: [] }, 'goal'), + addMicrogoal: (goalIndex, microGoalName) => + addItem( + goalIndex, + undefined, + { name: microGoalName, expanded: false, tasks: [] }, + 'microgoal', + ), + addTask: (goalIndex, microGoalIndex, taskName) => + addItem(goalIndex, microGoalIndex, { name: taskName, completed: false }, 'task'), + + toggleTaskCompletion, + + toggleGoalExpansion: (goalIndex) => toggleExpansion(goalIndex), + toggleMicroGoalExpansion: (goalIndex, microGoalIndex) => + toggleExpansion(goalIndex, microGoalIndex), }; }; -export default useGoalsManager; +export default useGoalsUpdater; diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx index a6949e8..d4f8543 100644 --- a/src/pages/Home.jsx +++ b/src/pages/Home.jsx @@ -1,77 +1,9 @@ import GoalTracker from '@components/Home/GoalTracker'; import { Box } from '@mui/material'; -import { useState } from 'react'; const Home = () => { - const [goals, setGoals] = useState([ - { - id: '1', - title: 'Improve Coding Skills', - expanded: true, - microGoals: [ - { - id: '1-1', - title: 'Learn React', - expanded: true, - tasks: [ - { id: '1-1-1', title: 'Complete React tutorial', completed: true }, - { id: '1-1-2', title: 'Build a small project', completed: false }, - ], - }, - { - id: '1-2', - title: 'Master JavaScript', - expanded: false, - tasks: [ - { id: '1-2-1', title: 'Study ES6+ features', completed: false }, - { id: '1-2-2', title: 'Practice coding challenges', completed: false }, - ], - }, - ], - }, - ]); - - const handleToggleTask = (macroGoalId, microGoalId, taskId) => { - setGoals((prevGoals) => - prevGoals.map((macroGoal) => - macroGoal.id === macroGoalId - ? { - ...macroGoal, - microGoals: macroGoal.microGoals.map((microGoal) => - microGoal.id === microGoalId - ? { - ...microGoal, - tasks: microGoal.tasks.map((task) => - task.id === taskId ? { ...task, completed: !task.completed } : task, - ), - } - : microGoal, - ), - } - : macroGoal, - ), - ); - }; - - const handleToggleExpand = (goalId, goalType) => { - setGoals((prevGoals) => - prevGoals.map((macroGoal) => - goalType === 'macro' && macroGoal.id === goalId - ? { ...macroGoal, expanded: !macroGoal.expanded } - : { - ...macroGoal, - microGoals: macroGoal.microGoals.map((microGoal) => - goalType === 'micro' && microGoal.id === goalId - ? { ...microGoal, expanded: !microGoal.expanded } - : microGoal, - ), - }, - ), - ); - }; - return ( - + ); diff --git a/src/utils/calculateProgress.js b/src/utils/calculateProgress.js new file mode 100644 index 0000000..1bb3608 --- /dev/null +++ b/src/utils/calculateProgress.js @@ -0,0 +1,9 @@ +export const calculateProgress = (items) => { + // Calculate the progress of the goal tracker + const completed = items.reduce( + (acc, item) => acc + item.tasks.filter((t) => t.completed).length, + 0, + ); + const total = items.reduce((acc, item) => acc + item.tasks.length, 0); + return total > 0 ? (completed / total) * 100 : 0; +};