Skip to content

Commit

Permalink
Merge pull request #1 from okplanbo/feature/S1/edit_mode
Browse files Browse the repository at this point in the history
Edit mode added
  • Loading branch information
okplanbo authored Dec 14, 2023
2 parents 9662582 + dcfc62c commit 63e8723
Show file tree
Hide file tree
Showing 8 changed files with 240 additions and 43 deletions.
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap" rel="stylesheet">

<title>Vite + React + TS</title>
<title>Sia | Minimalistic ToDo list</title>
</head>
<body>
<div id="root"></div>
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "sia",
"private": true,
"version": "0.1.0",
"version": "0.2.0",
"type": "module",
"scripts": {
"dev": "vite",
Expand All @@ -24,12 +24,14 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-infinite-scroll-component": "^6.1.0",
"react-router-dom": "^6.12.1"
"react-router-dom": "^6.12.1",
"uuid": "^9.0.1"
},
"devDependencies": {
"@types/node": "^20.2.5",
"@types/react": "^18.0.37",
"@types/react-dom": "^18.0.11",
"@types/uuid": "^9.0.7",
"@typescript-eslint/eslint-plugin": "^5.59.0",
"@typescript-eslint/parser": "^5.59.0",
"@vitejs/plugin-react": "^4.0.0",
Expand Down
8 changes: 8 additions & 0 deletions src/App.scss
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ a {

.item {
padding: 0.2rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

.item span:last-child {
overflow: hidden;
text-overflow: ellipsis;
}

#root .progress {
Expand Down
223 changes: 186 additions & 37 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,33 @@
import { useCallback, useState } from "react";
import { useCallback, useState, useRef } from "react";
import { v4 as uuidv4 } from 'uuid';

import { Paper, Tooltip, FormControlLabel, Checkbox, Dialog, DialogActions,
DialogContent, DialogContentText, DialogTitle, Typography, LinearProgress,
Button, useMediaQuery, Box } from "@mui/material";
Button, IconButton, useMediaQuery, Box, Input } from "@mui/material";

import AddIcon from '@mui/icons-material/Add';
import DeleteIcon from '@mui/icons-material/Delete';
import { useTheme } from '@mui/material/styles';

import { basicList, storage_key, tooptip_offset, total_percent } from ":src/constants";
import { debounce } from ":src/helpers";
import { basicList, debounce_delay, storage_key,
task_input_limit, tooptip_offset, total_percent } from ":src/constants";

import "./App.scss";

type Task = {
description: string;
checked: boolean;
key: string;
};

const initialTasks: Task[] = basicList.map(description => {
return { description, checked: false }
});
initialTasks[0].checked = true;
function getInitialTasks() {
const initialTasks: Task[] = basicList.map(description => {
return { description, checked: false, key: uuidv4() }
});
initialTasks[0].checked = true;
return initialTasks;
}

const saved_tasks = localStorage.getItem(storage_key);

Expand All @@ -33,11 +42,14 @@ const calcProgress = (tasks: Task[]) => {
}

export default function App(): JSX.Element {
const [tasks, setTasks] = useState<Task[]>(saved_tasks ? JSON.parse(saved_tasks) : initialTasks);
const [tasks, setTasks] = useState<Task[]>(saved_tasks ? JSON.parse(saved_tasks) : getInitialTasks());
const [editedTasks, setEditedTasks] = useState<Task[]>([]);
const [progress, setProgress] = useState(calcProgress(tasks));
const [open, setOpen] = useState(false);
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
const theme = useTheme();
const fullScreen = useMediaQuery(theme.breakpoints.down('md'));
const addButtonRef = useRef<HTMLButtonElement>(null);

const updateTasks = (newTasks: Task[]) => {
localStorage.setItem(storage_key, JSON.stringify(newTasks));
Expand All @@ -52,22 +64,72 @@ export default function App(): JSX.Element {
}, [tasks]);

const handleRestart = useCallback(() => {
const newTasks = initialTasks;
const newTasks = tasks.slice();
newTasks.forEach(task => task.checked = false);
updateTasks(newTasks);
setIsConfirmOpen(false);
}, [tasks]);

const handleConfirmOpen = () => {
setIsConfirmOpen(true);
};

const handleConfirmClose = () => {
setIsConfirmOpen(false);
};

const handleEdit = () => {
setEditedTasks(tasks.slice());
setIsEditDialogOpen(true);
};

const handleCancelEdit = () => {
setIsEditDialogOpen(false);
};

const handleAddNewTask = () => {
const newTasks = editedTasks.slice();
newTasks.push({
key: uuidv4(),
description: '',
checked: false
});
setEditedTasks(newTasks);
addButtonRef.current?.scrollIntoView({behavior: 'smooth', inline: 'end' });
};

const handleSaveChanges = () => {
setIsEditDialogOpen(false);
const newTasks = editedTasks.filter(task => task.description);
updateTasks(newTasks);
setOpen(false);
}, []);
};

const debouncedUpdate = debounce((newDescription: string, key: string) => {
const tasks = editedTasks.map(task => {
if (task.key === key) {
task.description = newDescription;
}
return task;
});
setEditedTasks(tasks);
}, debounce_delay);

const handleDialogOpen = () => {
setOpen(true);
const handleTaskEdit = (value: string, key: string) => {
debouncedUpdate(value, key);
};

const handleDialogClose = () => {
setOpen(false);

const handleDeleteTask = (key: string) => {
const tasks = editedTasks.filter(task => task.key !== key);
setEditedTasks(tasks);
};

return (
<>
<Typography variant="h2" component="h1" align="center">
<Typography
variant="h2"
component="h1"
align="center"
>
<Tooltip
title="is Georgian for 'List'"
placement="top"
Expand All @@ -88,29 +150,116 @@ export default function App(): JSX.Element {
</Box>
</Tooltip>
</Typography>
<Paper variant="outlined" component="main">
{tasks.map((task, index) => (
<FormControlLabel
className="item"
key={index}
label={task.description}
control={
<Checkbox
checked={task.checked}
onChange={() => toggleTask(index)}
/>
}
/>
))}
</Paper>

{ tasks.length === 0 ? (
<Typography
variant="h4"
component="h4"
align="center"
marginTop="2rem"
>
It is so empty here... What&apos;s the plan? :)
</Typography>
) : (
<Paper variant="outlined" component="main">
{tasks.map((task, index) => (
<FormControlLabel
className="item"
key={task.key}
label={task.description}
control={
<Checkbox
checked={task.checked}
onChange={() => toggleTask(index)}
/>
}
/>
))}
</Paper>
)}

<Box display="flex" justifyContent="center" marginTop="2rem">
<Button variant="outlined" onClick={handleDialogOpen}>Restart</Button>
<Box display="flex" marginRight="2rem">
<Button variant="outlined" onClick={handleEdit}>
Edit
</Button>
</Box>
<Button variant="outlined" onClick={handleConfirmOpen}>
Restart
</Button>
</Box>

<LinearProgress variant="determinate" value={progress} className="progress" />

{/* Editing */}

<Dialog
fullScreen={fullScreen}
open={isEditDialogOpen}
onClose={handleCancelEdit}
aria-labelledby="responsive-dialog-title"
>
<DialogTitle id="responsive-dialog-title">
Edit tasks
</DialogTitle>
<DialogContent sx={ fullScreen ? undefined : { minWidth: '500px'}}>
<Box
display="flex"
flexDirection="column"
marginBottom="1rem"
>
{editedTasks.map(item =>
<Box key={item.key} display="flex">
<Input
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
handleTaskEdit(event.target.value, item.key)
}}
defaultValue={item.description}
fullWidth
sx={{ marginBottom: "0.5rem" }}
inputProps={{
'aria-label': 'Task description',
maxLength: task_input_limit
}}
/>
<IconButton
aria-label="delete"
size="medium"
onClick={() => {
handleDeleteTask(item.key)
}}
>
<DeleteIcon fontSize="medium" />
</IconButton>
</Box>
)}
<Button
variant="outlined"
startIcon={<AddIcon />}
sx={{ marginTop: "0.5rem", marginBottom: "0.5rem" }}
ref={addButtonRef}
onClick={handleAddNewTask}
>
New task
</Button>
</Box>
</DialogContent>
<DialogActions>
<Button autoFocus onClick={handleCancelEdit}>
Cancel
</Button>
<Button onClick={handleSaveChanges} variant="contained" autoFocus>
Save
</Button>
</DialogActions>
</Dialog>

{/* Restart Confirmation */}

<Dialog
fullScreen={fullScreen}
open={open}
onClose={handleDialogClose}
open={isConfirmOpen}
onClose={handleConfirmClose}
aria-labelledby="responsive-dialog-title"
>
<DialogTitle id="responsive-dialog-title">
Expand All @@ -122,7 +271,7 @@ export default function App(): JSX.Element {
</DialogContentText>
</DialogContent>
<DialogActions>
<Button autoFocus onClick={handleDialogClose}>
<Button autoFocus onClick={handleConfirmClose}>
False alarm
</Button>
<Button onClick={handleRestart} variant="contained" autoFocus>
Expand Down
6 changes: 4 additions & 2 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export const basicList = [
"Put phone to charge",
"23:00 - go to sleep",
];
export const storage_key = "SIA_TASKS_STATE";
export const storage_key = "SIA_TASKS_STATE_V2";
export const total_percent = 100;
export const tooptip_offset = [45, -10];
export const tooptip_offset = [45, -10];
export const task_input_limit = 45;
export const debounce_delay = 300;
13 changes: 13 additions & 0 deletions src/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export function debounce<T extends (...args: string[]) => void>(fn: T, time: number) {
let timeoutId: NodeJS.Timeout | null;
return wrapper
function wrapper (...args: string[]) {
if (timeoutId) {
clearTimeout(timeoutId)
}
timeoutId = setTimeout(() => {
timeoutId = null
fn(...args)
}, time)
}
}
15 changes: 14 additions & 1 deletion src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { createTheme, ThemeProvider } from '@mui/material';

import App from "./App.tsx";

import "@fontsource/roboto/latin-300.css";

const theme = createTheme({
components: {
MuiButtonBase: {
defaultProps: {
disableRipple: true,
},
},
},
});

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<App />
<ThemeProvider theme={theme}>
<App />
</ThemeProvider>
</React.StrictMode>
);
Loading

0 comments on commit 63e8723

Please sign in to comment.