-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Integrate goals, micro-goals, and tasks with Firebase (#12)
- Linked goal, micro-goal, and task addition directly to Firebase, replacing hardcoded placeholders. - Refined UI with `AddItem` for consistent input UX across goals, micro-goals, and tasks. - Added `Collapse` animations for smoother goal/micro-goal expand/collapse interactions. - Enhanced `useGoalsUpdater` hook to handle database updates and status toggling. - Extract `calculateProgress.js` into the utils folder. --------- Co-authored-by: ZL Asica <[email protected]>
- Loading branch information
1 parent
086bbe0
commit a137f80
Showing
9 changed files
with
295 additions
and
332 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<TextField | ||
variant="outlined" | ||
label={label} | ||
value={inputValue} | ||
onChange={(e) => setInputValue(e.target.value)} | ||
fullWidth | ||
onKeyDown={(e) => e.key === 'Enter' && handleAdd()} | ||
slotProps={{ | ||
input: { | ||
endAdornment: ( | ||
<InputAdornment position="end"> | ||
<IconButton color="primary" onClick={handleAdd} disabled={!inputValue.trim()}> | ||
<AddIcon /> | ||
</IconButton> | ||
</InputAdornment> | ||
), | ||
}, | ||
}} | ||
/> | ||
); | ||
}; | ||
|
||
export default AddItem; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 }) => ( | ||
<Box sx={{ position: 'relative', display: 'inline-flex', mr: 2 }}> | ||
<CircularProgress variant="determinate" value={value} size={size} thickness={thickness} /> | ||
<Box | ||
sx={{ | ||
top: 0, | ||
left: 0, | ||
bottom: 0, | ||
right: 0, | ||
position: 'absolute', | ||
display: 'flex', | ||
alignItems: 'center', | ||
justifyContent: 'center', | ||
}} | ||
> | ||
<Typography variant="caption" component="div" color="text.secondary"> | ||
{`${Math.round(value)}%`} | ||
</Typography> | ||
</Box> | ||
</Box> | ||
); | ||
|
||
const Task = ({ task, onToggle }) => ( | ||
<ListItem dense> | ||
<Checkbox edge="start" checked={task.completed} onChange={onToggle} /> | ||
<ListItemText primary={task.name} /> | ||
</ListItem> | ||
); | ||
|
||
const MicroGoal = ({ microGoal, macroGoalIndex, microGoalIndex, onToggleTask, onToggleExpand }) => { | ||
const progress = | ||
(microGoal.tasks.filter((t) => t.completed).length / microGoal.tasks.length) * 100; | ||
|
||
return ( | ||
<Paper elevation={2} sx={{ p: 2, mb: 2, bgcolor: 'background.default' }}> | ||
<Box display="flex" alignItems="center"> | ||
<ProgressIndicator value={progress} size={32} thickness={3} /> | ||
<Typography variant="subtitle1">{microGoal.name}</Typography> | ||
<IconButton | ||
onClick={() => onToggleExpand(macroGoalIndex, microGoalIndex)} | ||
size="small" | ||
sx={{ ml: 'auto' }} | ||
> | ||
{microGoal.expanded ? <ExpandLessIcon /> : <ExpandMoreIcon />} | ||
</IconButton> | ||
</Box> | ||
{microGoal.expanded && ( | ||
<List sx={{ mt: 1, pl: 4 }}> | ||
{microGoal.tasks.map((task, taskIndex) => ( | ||
<Task | ||
key={`${macroGoalIndex}-${microGoalIndex}-${taskIndex}`} // Unique key for tasks | ||
task={task} | ||
onToggle={() => onToggleTask(macroGoalIndex, microGoalIndex, taskIndex)} | ||
/> | ||
))} | ||
</List> | ||
)} | ||
</Paper> | ||
); | ||
}; | ||
|
||
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 ( | ||
<Paper elevation={3} sx={{ p: 3, mb: 4, bgcolor: 'background.paper' }}> | ||
<Box display="flex" alignItems="center" mb={2}> | ||
<ProgressIndicator value={progress} size={48} thickness={4} /> | ||
<Typography variant="h5" sx={{ fontWeight: 'bold' }}> | ||
{macroGoal.name} | ||
</Typography> | ||
<IconButton onClick={() => onToggleExpand(macroGoalIndex)} size="small" sx={{ ml: 'auto' }}> | ||
{macroGoal.expanded ? <ExpandLessIcon /> : <ExpandMoreIcon />} | ||
</IconButton> | ||
</Box> | ||
<Divider sx={{ mb: 2 }} /> | ||
{macroGoal.expanded && ( | ||
<List sx={{ pl: 2 }}> | ||
{macroGoal.microgoals.map((microGoal, microGoalIndex) => ( | ||
<MicroGoal | ||
key={`${macroGoalIndex}-${microGoalIndex}`} // Unique key for microgoals | ||
microGoal={microGoal} | ||
macroGoalIndex={macroGoalIndex} | ||
microGoalIndex={microGoalIndex} // Pass the micro goal index | ||
onToggleTask={onToggleTask} | ||
onToggleExpand={onToggleExpand} | ||
/> | ||
))} | ||
</List> | ||
)} | ||
</Paper> | ||
); | ||
}; | ||
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 ( | ||
<Box sx={{ maxWidth: 800, margin: 'auto', pt: 4 }}> | ||
{loading ? ( | ||
<CircularProgress /> | ||
) : ( | ||
goals.map((macroGoal, macroGoalIndex) => ( | ||
<MacroGoal | ||
key={macroGoalIndex} // Use the index as the key | ||
macroGoal={macroGoal} | ||
macroGoalIndex={macroGoalIndex} // Pass the index | ||
onToggleTask={handleToggleTask} | ||
onToggleExpand={handleToggleExpand} | ||
/> | ||
)) | ||
)} | ||
<Box sx={{ maxWidth: 800, margin: 'auto', padding: 2 }}> | ||
<AddItem label="New Goal" onAdd={addGoal} /> | ||
<Box sx={{ height: 16 }} /> | ||
{user.goals.map((macroGoal, index) => ( | ||
<MacroGoal key={index} macroGoal={macroGoal} macroGoalIndex={index} /> | ||
))} | ||
</Box> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<Paper elevation={2} sx={{ p: 2, mb: 3, borderRadius: 2 }}> | ||
<Box display="flex" alignItems="center"> | ||
<ProgressIndicator value={progress} size={40} thickness={3} /> | ||
<Typography variant="h6" sx={{ flexGrow: 1, ml: 2 }}> | ||
{macroGoal.name} | ||
</Typography> | ||
<IconButton onClick={() => toggleGoalExpansion(macroGoalIndex)} size="small"> | ||
{macroGoal.expanded ? <ExpandLess /> : <ExpandMore />} | ||
</IconButton> | ||
</Box> | ||
<Collapse in={macroGoal.expanded} timeout="auto" unmountOnExit> | ||
<Divider sx={{ my: 1 }} /> | ||
<AddItem | ||
label="New Microgoal" | ||
onAdd={(microGoalName) => addMicrogoal(macroGoalIndex, microGoalName)} | ||
/> | ||
<List> | ||
{macroGoal.microgoals.map((microGoal, microGoalIndex) => ( | ||
<MicroGoal | ||
key={microGoalIndex} | ||
microGoal={microGoal} | ||
macroGoalIndex={macroGoalIndex} | ||
microGoalIndex={microGoalIndex} | ||
/> | ||
))} | ||
</List> | ||
</Collapse> | ||
</Paper> | ||
); | ||
}; | ||
|
||
export default MacroGoal; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<Paper elevation={1} sx={{ p: 1, mb: 2, bgcolor: 'background.default', borderRadius: 1 }}> | ||
<Box display="flex" alignItems="center"> | ||
<ProgressIndicator value={progress} size={30} thickness={3} /> | ||
<Typography variant="subtitle1" sx={{ flexGrow: 1, ml: 1 }}> | ||
{microGoal.name} | ||
</Typography> | ||
<IconButton | ||
onClick={() => toggleMicroGoalExpansion(macroGoalIndex, microGoalIndex)} | ||
size="small" | ||
> | ||
{microGoal.expanded ? <ExpandLess /> : <ExpandMore />} | ||
</IconButton> | ||
</Box> | ||
<Collapse in={microGoal.expanded} timeout="auto" unmountOnExit> | ||
<List sx={{ pl: 2 }}> | ||
{microGoal.tasks.map((task, taskIndex) => ( | ||
<Task | ||
key={taskIndex} | ||
task={task} | ||
onToggle={() => toggleTaskCompletion(macroGoalIndex, microGoalIndex, taskIndex)} | ||
/> | ||
))} | ||
</List> | ||
<AddItem | ||
label="New Task" | ||
onAdd={(taskName) => addTask(macroGoalIndex, microGoalIndex, taskName)} | ||
/> | ||
</Collapse> | ||
</Paper> | ||
); | ||
}; | ||
|
||
export default MicroGoal; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { Box, CircularProgress, Typography } from '@mui/material'; | ||
|
||
const ProgressIndicator = ({ value, size = 40, thickness = 4 }) => ( | ||
<Box sx={{ position: 'relative', display: 'inline-flex', mr: 2 }}> | ||
<CircularProgress | ||
variant="determinate" | ||
value={value} | ||
size={size} | ||
thickness={thickness} | ||
sx={{ | ||
color: (theme) => | ||
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 | ||
}} | ||
/> | ||
<Box | ||
sx={{ | ||
position: 'absolute', | ||
top: 0, | ||
left: 0, | ||
bottom: 0, | ||
right: 0, | ||
display: 'flex', | ||
alignItems: 'center', | ||
justifyContent: 'center', | ||
}} | ||
> | ||
<Typography | ||
variant="caption" | ||
component="div" | ||
color={value >= 100 ? 'success.main' : 'text.secondary'} | ||
sx={{ | ||
fontWeight: value >= 100 ? 'bold' : 'normal', | ||
fontSize: value >= 100 ? '0.75rem' : 'inherit', | ||
}} | ||
> | ||
{`${Math.round(value)}%`} | ||
</Typography> | ||
</Box> | ||
</Box> | ||
); | ||
|
||
export default ProgressIndicator; |
Oops, something went wrong.