Compare commits

...

11 Commits
v0.0.1 ... main

22 changed files with 244 additions and 117 deletions

36
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@ -0,0 +1,36 @@
name: "CodeQL"
on:
push:
branches: [ main ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
schedule:
- cron: '19 9 * * 2'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@ -1,7 +1,7 @@
# Simple TODO application
<p align="center">
<img src="https://github.com/dm1sh/toodo/raw/main/logo.svg" alt="TooDo logo" width="150px">
<img src="./logo.png" alt="TooDo logo" width="150px">
</p>
## Overview
@ -38,7 +38,6 @@ npm run dev
## TODO
- Add task saving on Enter key press, remapping new line to Shift+Enter
- Convert to monorepo and add backend for tasks syncing
- Add ServiceWorker
- Switch to IndexedDB

View File

@ -3,7 +3,12 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>React application</title>
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="/pwa-192x192.png" />
<link rel="mask-icon" href="/favicon.svg" color="#FFFFFF" />
<meta name="msapplication-TileColor" content="#FFFFFF" />
<meta name="theme-color" content="#ffffff" />
<title>TooDo</title>
</head>
<body>

BIN
logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -1,93 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="512"
height="351"
viewBox="0 0 135.46666 92.868752"
version="1.1"
id="svg5"
inkscape:version="1.1 (c68e22c387, 2021-05-23)"
sodipodi:docname="logo.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#999999"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:document-units="px"
showgrid="false"
units="px"
width="512px"
inkscape:zoom="1.4142136"
inkscape:cx="181.01934"
inkscape:cy="141.42136"
inkscape:window-width="1920"
inkscape:window-height="1000"
inkscape:window-x="-11"
inkscape:window-y="-11"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
showguides="true" />
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:8.16147;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:0;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
id="rect3999"
width="135.46666"
height="92.868752"
x="0"
y="0" />
<path
id="rect846"
style="color:#000000;fill:#000000;stroke-width:2.25708;-inkscape-stroke:none"
d="M 7.937444,4.545348 V 7.9309728 H 28.815465 V 80.157642 h 3.385626 V 7.9309728 H 53.079112 V 4.545348 Z" />
<path
style="color:#000000;fill:#000000;stroke-width:0.85307;stroke-miterlimit:0;-inkscape-stroke:none"
d="m 51.950372,11.315964 c -9.017447,0 -16.363299,7.347496 -16.363299,16.364915 0,9.017419 7.345852,16.36325 16.363299,16.36325 9.017448,0 16.364966,-7.345831 16.364966,-16.36325 0,-9.017419 -7.347518,-16.364915 -16.364966,-16.364915 z m 0,3.385615 c 7.187722,0 12.979341,5.791601 12.979341,12.9793 0,7.1877 -5.791619,12.977635 -12.979341,12.977635 -7.187722,0 -12.977674,-5.789935 -12.977674,-12.977635 0,-7.187699 5.789952,-12.9793 12.977674,-12.9793 z"
id="path1247" />
<path
style="color:#000000;fill:#000000;stroke-width:0.853071;stroke-miterlimit:0;-inkscape-stroke:none"
d="m 51.950372,47.429933 c -9.017447,0 -16.363299,7.345852 -16.363299,16.363299 -3e-6,9.017445 7.34585,16.364962 16.363299,16.364962 9.017449,0 16.364969,-7.347517 16.364966,-16.364962 0,-9.017447 -7.347518,-16.363299 -16.364966,-16.363299 z m 0,3.385625 c 7.187722,0 12.979341,5.789952 12.979341,12.977674 2e-6,7.187723 -5.791618,12.979337 -12.979341,12.979337 -7.187722,0 -12.977676,-5.791614 -12.977674,-12.979337 0,-7.187722 5.789952,-12.977674 12.977674,-12.977674 z"
id="path1247-9" />
<path
id="path1247-8"
style="color:#000000;fill:#000000;stroke-width:0.853071;stroke-miterlimit:0;-inkscape-stroke:none"
d="m 75.05173,11.316158 v 4.4e-4 h -3.35168 v 68.841044 h 3.35168 v 4.52e-4 c 0.01135,0 0.0226,-4.52e-4 0.03394,-4.52e-4 8.888602,-0.01824 16.143403,-7.174814 16.324619,-16.025292 h 0.0049 V 27.567599 h -0.0018 C 91.35203,18.602098 84.031302,11.316158 75.05173,11.316158 Z m 0.03394,3.386066 c 7.171954,0.01824 12.943405,5.802297 12.943405,12.978669 h 4.51e-4 v 36.112454 h -4.51e-4 c 0,7.176372 -5.771451,12.960421 -12.943405,12.97867 z" />
<path
style="color:#000000;fill:#000000;stroke-width:0.853071;stroke-miterlimit:0;-inkscape-stroke:none"
d="m 111.16382,48.821171 c -9.01744,-2e-6 -16.363297,7.345851 -16.363297,16.3633 -2e-6,9.017454 7.345857,16.363298 16.363297,16.363298 9.01746,0 16.36497,-7.345844 16.36496,-16.363298 1e-5,-9.017449 -7.34751,-16.363302 -16.36496,-16.3633 z m 0,3.385625 c 7.18773,-2e-6 12.97935,5.789952 12.97934,12.977675 1e-5,7.187728 -5.79161,12.977673 -12.97934,12.977673 -7.18772,0 -12.977672,-5.789945 -12.977672,-12.977673 0,-7.187723 5.789952,-12.977677 12.977672,-12.977675 z"
id="path1247-3" />
<rect
style="fill:#000000;fill-opacity:1;stroke-width:1.29937"
id="rect2043"
width="12.943417"
height="3.3856249"
x="71.699966"
y="27.116087" />
<rect
style="fill:#000000;fill-opacity:1;stroke-width:1.29937"
id="rect2043-7"
width="12.943417"
height="3.3856082"
x="71.699966"
y="40.65852" />
<rect
style="fill:#000000;fill-opacity:1;stroke-width:1.29937"
id="rect2043-9"
width="12.943417"
height="3.3856249"
x="71.699966"
y="33.887302" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -11,7 +11,8 @@
"@types/react-dom": "^17.0.9",
"serve": "^12.0.0",
"typescript": "^4.4.2",
"vite": "^2.5.3"
"vite": "^2.5.3",
"vite-plugin-pwa": "^0.11.3"
},
"dependencies": {
"@mui/icons-material": "^5.0.3",
@ -19,7 +20,8 @@
"@reduxjs/toolkit": "^1.6.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-redux": "^7.2.5"
"react-redux": "^7.2.5",
"react-transition-group": "^4.4.2"
},
"private": "true"
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

70
public/favicon.svg Normal file
View File

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="512"
height="512"
viewBox="0 0 135.46666 135.46667"
version="1.1"
id="svg6347"
sodipodi:docname="favicon.svg"
inkscape:version="1.1 (c68e22c387, 2021-05-23)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview6349"
pagecolor="#ffffff"
bordercolor="#999999"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:document-units="px"
showgrid="false"
units="px"
inkscape:zoom="2"
inkscape:cx="195"
inkscape:cy="282.5"
inkscape:window-width="1920"
inkscape:window-height="1000"
inkscape:window-x="-11"
inkscape:window-y="-11"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs6344" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
style="color:#000000;fill:#1565c0;stroke-width:3.77953;-inkscape-stroke:none"
d="M 422,286.56641 V 302 H 336.68555 C 305.05466,322.31153 284,357.80423 284,397.99805 c 0,62.72345 51.27458,114 113.99805,114 62.72345,0 114,-51.27655 114,-114 0,-54.49514 -38.70808,-100.33896 -89.99805,-111.43164 z m -24.00195,37.42773 c 41.10595,0 73.99609,32.89793 73.99609,74.00391 0,41.10595 -32.89014,73.99609 -73.99609,73.99609 -41.10597,0 -74.00391,-32.89014 -74.00391,-73.99609 0,-41.10598 32.89794,-74.00391 74.00391,-74.00391 z"
id="circle11-0"
transform="scale(0.26458333)" />
<path
style="color:#000000;fill:#000000;stroke-width:10.5833;-inkscape-stroke:none"
d="M 39.687481,0 H 50.270814 V 135.46666 H 39.687481 Z"
id="rect6430" />
<path
id="path6644"
style="color:#000000;fill:#000000;stroke-width:3.77953;-inkscape-stroke:none"
d="M 300 0 L 300 340 L 340 340 C 433.41973 340 509.8112 264.91908 511.94727 171.99805 L 512 171.99805 C 512 171.30446 511.98091 170.61552 511.97266 169.92383 C 511.97974 169.28281 511.99805 168.6446 511.99805 168.00195 L 511.94922 168.00195 C 509.81315 75.080923 433.42168 1.8680609e-14 340.00195 0 L 340.00195 40.001953 C 412.44625 40.001953 470.87001 97.750087 471.97266 169.92188 C 470.95198 242.16664 412.4965 300.00587 340 300.00586 L 340 0 L 300 0 z "
transform="scale(0.26458333)" />
<path
style="color:#000000;fill:#1565c0;-inkscape-stroke:none"
d="M 44.978516,5.2910156 C 20.19992,5.2910156 0,25.492888 0,50.271484 0,75.05008 20.19992,95.25 44.978516,95.25 c 24.778596,0 44.980468,-20.19992 44.980468,-44.978516 0,-24.778596 -20.201872,-44.9804684 -44.980468,-44.9804684 z m 0,10.5839844 c 19.058962,0 34.396484,15.337522 34.396484,34.396484 0,19.058962 -15.337522,34.394532 -34.396484,34.394532 -19.058962,0 -34.394532,-15.33557 -34.394532,-34.394532 0,-19.058962 15.33557,-34.396484 34.394532,-34.396484 z"
id="path6644-1-2-5-6" />
<path
style="color:#000000;fill:#000000;stroke-width:8.10116;-inkscape-stroke:none"
d="M 0,0 H 79.375008 V 10.583333 H 0 Z"
id="rect6430-2" />
<path
style="color:#000000;fill:#000000;stroke-width:3.77953;-inkscape-stroke:none"
d="M 169.99805,342.00391 C 76.346656,342.00391 0,418.3486 0,512 h 40.001953 c 0,-72.0339 57.962218,-130.00195 129.996097,-130.00195 C 242.03192,381.99805 300,439.9661 300,512 h 40.00195 c 0,-5.57081 -0.36122,-11.0532 -0.89062,-16.48828 -32.97319,-20.03088 -55.10742,-56.29887 -55.10742,-97.50977 0,-3.84101 0.19294,-7.63875 0.56836,-11.38476 -30.25769,-27.67704 -70.4894,-44.61328 -114.57422,-44.61328 z"
id="path6644-1-2-5"
transform="scale(0.26458333)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
public/pwa-192x192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
public/pwa-512x512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -12,18 +12,19 @@ import {
close as closeAction,
} from "../store/slices/uiState";
import { add } from "../store/slices/todo";
import { enterHandler } from "../utils";
export const AddTask: React.FC = () => {
const open = useAppSelector((state) => state.uiState.addBarOpen);
const dispatch = useAppDispatch();
const { value, onChange, submit } = useInputValue("", (submitValue) =>
const { value, change, submit } = useInputValue("", (submitValue) =>
dispatch(add(submitValue))
);
const save = () => {
submit();
onChange("");
change("");
dispatch(closeAction());
};
@ -34,6 +35,8 @@ export const AddTask: React.FC = () => {
"& .MuiDrawer-paperAnchorBottom": {
overflowY: "visible",
padding: (theme) => theme.spacing(0, 2, 2, 2),
maxWidth: "100vh",
margin: "0 auto",
},
position: "relative",
}}
@ -56,6 +59,7 @@ export const AddTask: React.FC = () => {
}}
/>
<InputBase
sx={{ "& .MuiInputBase-inputMultiline": { whiteSpace: "pre-wrap" } }}
fullWidth
placeholder="New task"
autoFocus={open}
@ -63,7 +67,8 @@ export const AddTask: React.FC = () => {
onFocus={(e) => {
e.currentTarget.setSelectionRange(value.length, value.length);
}}
onChange={(e) => onChange(e.currentTarget.value)}
onChange={(e) => change(e.currentTarget.value)}
onKeyDown={enterHandler(save)}
multiline
/>
<Box sx={{ paddingTop: (theme) => theme.spacing(1) }}>

View File

@ -0,0 +1,19 @@
import React from "react";
import Typography from "@mui/material/Typography";
import Box from "@mui/material/Box";
import InboxIcon from "@mui/icons-material/Inbox";
export const EmptyList: React.FC = () => (
<Box
sx={{
paddingBottom: (theme) => theme.spacing(2),
textAlign: "center",
}}
>
<InboxIcon sx={{ fontSize: "10rem" }} color={"primary"} />
<Typography variant={"h6"}>Empty in tasks</Typography>
<Typography>Write first task to save it here</Typography>
<Typography>To insert new line in task, press Shift+Enter</Typography>
</Box>
);

View File

@ -29,7 +29,7 @@ export const Layout: React.FC<LayoutProps> = ({ appBar, content, title }) => {
<CssBaseline />
<Box sx={{ padding: (theme) => theme.spacing(2, 2, 10, 2) }}>
<Typography variant="h4" sx={{ paddingLeft: 2, marginBottom: 2 }}>
<Checkbox sx={{ visibility: "hidden" }} />
<Checkbox aria-hidden={true} sx={{ visibility: "hidden" }} />
{title}
</Typography>
{content}

View File

@ -11,6 +11,7 @@ import DeleteOutlined from "@mui/icons-material/DeleteOutlined";
import { TaskItemT } from "../types";
import { useAppDispatch, useInputValue } from "../hooks";
import { updateText, markDone, remove } from "../store/slices/todo";
import { enterHandler } from "../utils";
export type TodoItemProps = { task: TaskItemT; index: number };
@ -20,10 +21,15 @@ export const TodoItem: React.FC<TodoItemProps> = ({ task, index }) => {
const dispatch = useAppDispatch();
const [editing, setEditing] = useState(false);
const { value, onChange, submit } = useInputValue(text, (submitValue) =>
const { value, change, submit } = useInputValue(text, (submitValue) =>
dispatch(updateText({ index, text: submitValue }))
);
const save = () => {
setEditing(false);
submit();
};
return (
<ListItem>
<Checkbox
@ -39,10 +45,10 @@ export const TodoItem: React.FC<TodoItemProps> = ({ task, index }) => {
onFocus={(e) => {
e.currentTarget.setSelectionRange(value.length, value.length);
}}
onChange={(e) => onChange(e.currentTarget.value)}
onBlur={() => {
setEditing(false);
submit();
onChange={(e) => change(e.currentTarget.value)}
onBlur={save}
inputProps={{
onKeyDown: enterHandler(save),
}}
multiline
/>
@ -50,6 +56,8 @@ export const TodoItem: React.FC<TodoItemProps> = ({ task, index }) => {
<ListItemText
sx={{
textDecoration: done ? "line-through" : undefined,
whiteSpace: "pre-wrap",
overflow: "hidden",
}}
onClick={() => setEditing(true)}
primary={text}

View File

@ -1,20 +1,42 @@
import React from "react";
import React, { useEffect, useState } from "react";
import List from "@mui/material/List";
import Paper from "@mui/material/Paper";
import Collapse from "@mui/material/Collapse";
import { TransitionGroup } from "react-transition-group";
import { TodoItem } from "./TodoItem";
import { useAppSelector } from "../hooks";
import { EmptyList } from "./EmptyList";
import { isTaskItem, TaskItemT } from "../types";
export const TodoList: React.FC = () => {
const tasks = useAppSelector((state) => state.todo.tasks);
const [list, setList] = useState<TaskItemT[] | {}[]>([]);
useEffect(() => {
if (tasks.length) setList(tasks);
else setList([{}]);
}, [tasks]);
return (
<Paper variant="outlined">
<List>
{tasks.map((task, index) => (
<TodoItem key={task.id} task={task} index={index} />
))}
<TransitionGroup>
{list.map((task, index) =>
isTaskItem(task) ? (
<Collapse key={task.id}>
<TodoItem task={task} index={index} />
</Collapse>
) : (
<Collapse key={"empty"}>
<EmptyList />
</Collapse>
)
)}
</TransitionGroup>
</List>
</Paper>
);

View File

@ -4,7 +4,7 @@ import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import { AppDispatch, RootState } from "./store";
export type UseInputValueReturnT = {
onChange: (value: string) => void;
change: (value: string) => void;
submit: () => void;
value: string;
};
@ -16,7 +16,7 @@ export const useInputValue = (
const [value, setValue] = useState(initialValue);
return {
onChange: (value) => setValue(value),
change: (value) => setValue(value),
submit: () => onSubmit(value),
value,
};

View File

@ -10,7 +10,7 @@ export const todoSlice = createSlice({
name: "todo",
initialState: initialState,
reducers: {
hydrate: (state, { payload }) => {
hydrate: (state, { payload }: PayloadAction<TaskItemT[]>) => {
state.tasks = payload;
},
add: (state, { payload }: PayloadAction<string>) => {

View File

@ -3,3 +3,7 @@ export type TaskItemT = {
text: string;
done: boolean;
};
export const isTaskItem = (el: TaskItemT | {}): el is TaskItemT => {
return "id" in el && "text" in el && "done" in el;
};

19
src/utils.ts Normal file
View File

@ -0,0 +1,19 @@
import { KeyboardEventHandler } from "react";
export const enterHandler =
(
save: () => void
): KeyboardEventHandler<HTMLInputElement | HTMLTextAreaElement> =>
(e) => {
if (e.code === "Enter") {
e.preventDefault();
if (e.shiftKey) {
const value = e.currentTarget.value,
selEnd = e.currentTarget.selectionEnd ?? 0,
selStart = e.currentTarget.selectionStart ?? 0;
e.currentTarget.value =
value.slice(0, selStart) + "\n" + value.slice(selEnd);
} else save();
}
};

View File

@ -1,3 +0,0 @@
import { defineConfig } from "vite";
export default defineConfig({});

34
vite.config.ts Normal file
View File

@ -0,0 +1,34 @@
import { defineConfig } from "vite";
import { VitePWA } from "vite-plugin-pwa";
export default defineConfig({
plugins: [
VitePWA({
includeAssets: ["robots.txt", "favicon.ico", "favicon.svg"],
manifest: {
name: "TooDo",
short_name: "TooDo",
description: "Task management application",
theme_color: "#ffffff",
icons: [
{
src: "pwa-192x192.png",
sizes: "192x192",
type: "image/png",
},
{
src: "pwa-512x512.png",
sizes: "512x512",
type: "image/png",
},
{
src: "pwa-maskable-512x512.png",
sizes: "512x512",
type: "image/png",
purpose: "any maskable",
},
],
},
}),
],
});