├── src ├── components │ ├── ColorSwatches.tsx │ ├── Display.tsx │ ├── AddTodoBox.tsx │ ├── Topbar.tsx │ ├── Category.tsx │ ├── screens │ │ └── MainScreen.tsx │ └── Todo.tsx ├── styles │ ├── index.scss │ ├── _display.scss │ ├── _general.scss │ └── _components.scss ├── selectors │ └── index.ts ├── index.html ├── slices │ ├── index.ts │ ├── globals.ts │ └── todos.ts ├── index.tsx ├── types │ └── index.ts ├── electron.js └── utils │ └── index.ts ├── .gitignore ├── assets ├── icon.icns ├── icon.ico ├── logo.png ├── screenshot.png └── icon.svg ├── tsconfig.json ├── LICENSE ├── config └── webpack.config.js ├── package.json └── README.md /src/components/ColorSwatches.tsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules/ 3 | destined-todos.json 4 | destined*/ -------------------------------------------------------------------------------- /assets/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aditya-azad/destined/HEAD/assets/icon.icns -------------------------------------------------------------------------------- /assets/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aditya-azad/destined/HEAD/assets/icon.ico -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aditya-azad/destined/HEAD/assets/logo.png -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aditya-azad/destined/HEAD/assets/screenshot.png -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import 'general'; 2 | 3 | @import 'display'; 4 | @import 'components'; 5 | -------------------------------------------------------------------------------- /src/selectors/index.ts: -------------------------------------------------------------------------------- 1 | import { RootState } from "../types"; 2 | 3 | export const getGlobals = (state: RootState) => state.globalsState; 4 | export const getTodos = (state: RootState) => state.todosState; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "sourceMap": true, 5 | "noImplicitAny": true, 6 | "module": "commonjs", 7 | "target": "es6", 8 | "jsx": "react" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/styles/_display.scss: -------------------------------------------------------------------------------- 1 | .display-container { 2 | display: flex; 3 | flex-direction: column; 4 | height: 100vh; 5 | } 6 | 7 | .display-content { 8 | margin-top: 40px; 9 | width: 90%; 10 | align-self: center; 11 | justify-content: flex-start; 12 | } -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Destined 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /src/components/Display.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import MainScreen from "./screens/MainScreen"; 3 | 4 | const Display: React.FC = () => { 5 | return ( 6 |
7 | 8 |
9 | ) 10 | } 11 | 12 | export default Display; -------------------------------------------------------------------------------- /src/slices/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers, Reducer } from "redux"; 2 | 3 | import todosReducer from "./todos"; 4 | import globalsReducer from "./globals"; 5 | import { RootState } from "../types"; 6 | 7 | const rootReducer: Reducer = combineReducers({ 8 | todosState: todosReducer, 9 | globalsState: globalsReducer, 10 | }); 11 | 12 | export default rootReducer; -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactDOM from "react-dom"; 3 | import { Provider } from "react-redux"; 4 | import { configureStore } from "@reduxjs/toolkit"; 5 | 6 | import "./styles/index.scss"; 7 | import Display from "./components/Display"; 8 | import rootReducer from "./slices"; 9 | 10 | const store = configureStore({ 11 | reducer: rootReducer, 12 | devTools: process.env.NODE_ENV !== "production", 13 | }) 14 | 15 | ReactDOM.render( 16 | 17 | 18 | , 19 | document.getElementById("root") 20 | ); 21 | -------------------------------------------------------------------------------- /src/components/AddTodoBox.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { useSelector } from "react-redux"; 3 | 4 | import { getGlobals } from "../selectors"; 5 | import Todo from "./Todo"; 6 | 7 | const AddTodoBox: React.FC = () => { 8 | 9 | const { toggleTodoAddBox } = useSelector(getGlobals); 10 | 11 | const render = () => { 12 | return ( 13 |
14 | 15 |
16 | ) 17 | } 18 | 19 | return( 20 | <> 21 | {toggleTodoAddBox ? render() : null} 22 | 23 | ) 24 | } 25 | 26 | export default AddTodoBox; -------------------------------------------------------------------------------- /src/slices/globals.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit" 2 | 3 | import { GlobalsState } from "../types"; 4 | 5 | const initialState: GlobalsState = { 6 | toggleTodoAddBox: false, 7 | }; 8 | 9 | const globalsSlice = createSlice({ 10 | name: "globals", 11 | initialState, 12 | reducers: { 13 | toggleAddTodoBar: (state) => { 14 | return({ 15 | ...state, 16 | toggleTodoAddBox: !state.toggleTodoAddBox 17 | }); 18 | } 19 | } 20 | }); 21 | 22 | export const { 23 | toggleAddTodoBar 24 | } = globalsSlice.actions; 25 | 26 | export default globalsSlice.reducer; 27 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface Todo { 2 | body: string, 3 | date: string, 4 | time: string, 5 | repeat: string, 6 | color: string 7 | } 8 | 9 | export interface TodoWithIDPayload { 10 | todo: Todo, 11 | id: string 12 | } 13 | 14 | export interface TodoProps { 15 | shouldDisplayDate?: boolean 16 | shouldDisplayTime?: boolean 17 | todoAdder?: boolean 18 | id?: string 19 | todo?: Todo 20 | } 21 | 22 | export interface GlobalsState { 23 | toggleTodoAddBox: boolean 24 | } 25 | 26 | export interface RootState { 27 | todosState: { [todos: string]: Todo } 28 | globalsState: GlobalsState 29 | } 30 | 31 | export interface TodoStateInterface { 32 | [key: string]: Todo 33 | } 34 | 35 | export interface CategoryProps { 36 | todos: any[], 37 | title: string, 38 | sort: number 39 | } 40 | -------------------------------------------------------------------------------- /src/styles/_general.scss: -------------------------------------------------------------------------------- 1 | // fonts 2 | @import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap'); 3 | 4 | :root { 5 | // fonts 6 | --heading-font: "Roboto", "sans-serif"; 7 | --content-font: "Roboto", "serif"; 8 | 9 | //colors 10 | --background-color: #1c1c1c; 11 | --secondary-color: #404040; 12 | --tertiary-color: #8c8c8c; 13 | --text-color: #f2f2f2; 14 | --accent-color: #d99962; 15 | --warning-color: #d15d5d; 16 | } 17 | 18 | 19 | // basic reset 20 | html { 21 | box-sizing: border-box; 22 | } 23 | 24 | body { 25 | color: var(--text-color); 26 | font-family: var(--content-font); 27 | background-color: var(--background-color); 28 | } 29 | 30 | // hide scrollbars 31 | body::-webkit-scrollbar { 32 | display: none; 33 | } 34 | 35 | input { 36 | outline: none; 37 | background-color: transparent; 38 | border: none; 39 | border-bottom: 1px solid var(--tertiary-color); 40 | color: var(--text-color); 41 | } 42 | 43 | *, *::before, *::after { 44 | box-sizing: inherit; 45 | margin: 0; 46 | padding: 0; 47 | } 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020-present Aditya Azad 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/electron.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow, ipcMain } = require("electron"); 2 | const fs = require("fs"); 3 | const path = require("path"); 4 | const isDev = require("electron-is-dev"); 5 | 6 | const TODOS_FILE = "destined-todos.json"; 7 | 8 | function createWindow() { 9 | let win = new BrowserWindow({ 10 | width: 800, 11 | height: 1000, 12 | webPreferences: { 13 | nodeIntegration: true, 14 | contextIsolation: false 15 | }, 16 | }); 17 | isDev ? null : win.removeMenu(); 18 | win.loadURL(isDev ? "http://localhost:8080" : `file://${path.join(__dirname, "../build/index.html")}`); 19 | } 20 | 21 | app.whenReady().then(createWindow); 22 | 23 | app.on("window-all-closed", () => { 24 | if (process.platform !== "darwin") { 25 | app.quit(); 26 | } 27 | }); 28 | 29 | app.on("activate", () => { 30 | if (BrowserWindow.getAllWindows().length === 0) { 31 | createWindow(); 32 | } 33 | }); 34 | 35 | ipcMain.handle("save-todos", (event, todos) => { 36 | fs.writeFile(TODOS_FILE, todos, "utf8", function (err) { 37 | if (err) { 38 | throw "cannot save todos!"; 39 | } 40 | }); 41 | }) 42 | 43 | ipcMain.on("get-todos", (event) => { 44 | try { 45 | event.returnValue = JSON.parse(fs.readFileSync(TODOS_FILE, "utf8")); 46 | } catch { 47 | event.returnValue = {}; 48 | } 49 | }); 50 | -------------------------------------------------------------------------------- /src/components/Topbar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { useEffect, useState } from "react"; 3 | import { useDispatch } from "react-redux"; 4 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" 5 | import { faPlus } from '@fortawesome/free-solid-svg-icons' 6 | 7 | import { toggleAddTodoBar } from "../slices/globals"; 8 | 9 | const TopBar: React.FC = () => { 10 | 11 | const dispatch = useDispatch(); 12 | 13 | const getDateTime = () => { 14 | const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", 15 | "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; 16 | const days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; 17 | let currDateTime = new Date(); 18 | let formattedDateTime = currDateTime.getDate() + " " + 19 | months[currDateTime.getMonth()] + " " + currDateTime.getFullYear() + ", " + 20 | days[currDateTime.getDay()] + ", " + currDateTime.toLocaleTimeString(); 21 | return formattedDateTime; 22 | } 23 | 24 | useEffect(() => { 25 | setTimeout(() => { 26 | setDateTime(getDateTime()); 27 | }, 1000); 28 | }); 29 | 30 | const [dateTime, setDateTime] = useState(getDateTime()); 31 | 32 | return ( 33 |
34 |
35 | {dateTime} 36 |
37 |
38 |
dispatch(toggleAddTodoBar())}/>
39 |
40 |
41 | ) 42 | } 43 | 44 | export default TopBar; -------------------------------------------------------------------------------- /config/webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 2 | const webpack = require("webpack"); 3 | const path = require("path"); 4 | 5 | module.exports = { 6 | // Enable sourcemaps for debugging webpack's output. 7 | devtool: "source-map", 8 | 9 | resolve: { 10 | // Add '.ts' and '.tsx' as resolvable extensions. 11 | extensions: [".ts", ".tsx", ".js", ".jsx"], 12 | }, 13 | 14 | entry: "./src/index.tsx", 15 | 16 | output: { 17 | path: path.resolve(__dirname, "../build"), 18 | }, 19 | 20 | module: { 21 | rules: [ 22 | // Load css/scss 23 | { 24 | test: /\.(s?)css$/, 25 | use: [ 26 | "style-loader", 27 | { 28 | loader: "css-loader", 29 | options: { sourceMap: true, importLoaders: 1 }, 30 | }, 31 | { 32 | loader: "sass-loader", 33 | options: { sourceMap: true }, 34 | }, 35 | ], 36 | }, 37 | // Typescript 38 | { 39 | test: /\.ts(x?)$/, 40 | exclude: /node_modules/, 41 | use: [ 42 | { 43 | loader: "ts-loader", 44 | }, 45 | ], 46 | }, 47 | // All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'. 48 | { 49 | enforce: "pre", 50 | test: /\.js$/, 51 | loader: "source-map-loader", 52 | }, 53 | ], 54 | }, 55 | plugins: [ 56 | new HtmlWebpackPlugin({ 57 | template: "./src/index.html", 58 | }), 59 | new webpack.ExternalsPlugin('commonjs', [ 60 | 'electron' 61 | ]) 62 | ], 63 | }; 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "destined", 3 | "version": "2.0.0", 4 | "description": "A tasks/calendar management system", 5 | "main": "./src/electron.js", 6 | "scripts": { 7 | "client": "webpack-dev-server --config config/webpack.config.js", 8 | "electron": "electron .", 9 | "webpack": "webpack --config config/webpack.config.js", 10 | "dev": "concurrently \"npm run client\" \"npm run electron\"", 11 | "build": "npm run webpack && electron-packager . --icon=assets/icon.ico", 12 | "build-osx": "npm run webpack && electron-packager . --icon=assets/icon.icns --platform=darwin" 13 | }, 14 | "author": "Aditya Azad", 15 | "license": "MIT", 16 | "devDependencies": { 17 | "@types/react": "^18.0.14", 18 | "@types/react-dom": "^18.0.5", 19 | "@types/react-redux": "^7.1.24", 20 | "concurrently": "^7.2.2", 21 | "css-loader": "^6.7.1", 22 | "electron": "^19.0.7", 23 | "electron-packager": "^15.5.1", 24 | "html-webpack-plugin": "^5.5.0", 25 | "node-sass": "^7.0.1", 26 | "nodemon": "^2.0.18", 27 | "sass-loader": "^13.0.2", 28 | "source-map-loader": "^4.0.0", 29 | "style-loader": "^3.3.1", 30 | "ts-loader": "^9.3.1", 31 | "typescript": "^4.7.4", 32 | "webpack": "^5.73.0", 33 | "webpack-cli": "^4.10.0", 34 | "webpack-dev-server": "^4.9.3" 35 | }, 36 | "dependencies": { 37 | "@fortawesome/fontawesome-svg-core": "^6.1.1", 38 | "@fortawesome/free-solid-svg-icons": "^6.1.1", 39 | "@fortawesome/react-fontawesome": "^0.2.0", 40 | "@reduxjs/toolkit": "^1.8.3", 41 | "electron-is-dev": "^2.0.0", 42 | "react": "^18.2.0", 43 | "react-dom": "^18.2.0", 44 | "react-redux": "^8.0.2" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/components/Category.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { useState } from "react" 3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" 4 | import { faTint, faClock, faQuoteRight } from "@fortawesome/free-solid-svg-icons" 5 | 6 | import { CategoryProps } from "../types"; 7 | 8 | const Category: React.FC = ({ todos, title, sort }) => { 9 | 10 | const [sorterIndex, setSorterIndex] = useState(sort); 11 | 12 | const sorters = [ 13 | (a: any, b: any) => { // color sorter 14 | return (a.props.todo.color.localeCompare(b.props.todo.color)); 15 | }, 16 | (a: any, b: any) => { // date sorter 17 | let dateA = new Date(a.props.todo.date + " " + a.props.todo.time); 18 | let dateB = new Date(b.props.todo.date + " " + b.props.todo.time); 19 | return dateA.getTime() - dateB.getTime(); 20 | }, 21 | (a: any, b: any) => { // body sorter 22 | return (a.props.todo.body.localeCompare(b.props.todo.body)); 23 | }, 24 | ]; 25 | 26 | const cycleSorter = () => { 27 | setSorterIndex((sorterIndex + 1) % sorters.length); 28 | } 29 | 30 | const renderCycleButton = () => { 31 | const icons = [ 32 | , 33 | , 34 | 35 | ]; 36 | return ( 37 |
38 | {icons[sorterIndex]} 39 |
40 | ) 41 | } 42 | 43 | if (todos.length > 0) { 44 | return ( 45 |
46 |
47 |

{title}

48 | {renderCycleButton()} 49 |
50 | {[...todos].sort(sorters[sorterIndex])} 51 | 52 |
53 | ) 54 | } else { 55 | return null; 56 | } 57 | } 58 | 59 | export default Category; 60 | -------------------------------------------------------------------------------- /src/components/screens/MainScreen.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { useSelector } from "react-redux"; 3 | 4 | import Todo from "../Todo"; 5 | import TopBar from "../Topbar"; 6 | import AddTodoBox from "../AddTodoBox"; 7 | import Category from "../Category"; 8 | import { getTodos } from "../../selectors"; 9 | 10 | const MainScreen: React.FC = () => { 11 | 12 | const todos = useSelector(getTodos); 13 | 14 | let todayTodoList = []; 15 | let upcomingTodoList = []; 16 | let unscheduledTodoList = []; 17 | let overdueTodoList = []; 18 | 19 | const isToday = (dateTimeString: string) => { 20 | let currDate = new Date(new Date().toDateString()); 21 | let date = new Date(new Date(dateTimeString).toDateString()); 22 | return date.getTime() - currDate.getTime(); 23 | } 24 | 25 | // populate the lists 26 | for (let key in todos) { 27 | let dateTimeString = todos[key].date + " " + todos[key].time; 28 | if (todos[key].date == "") { 29 | unscheduledTodoList.push( 30 | 31 | ) 32 | } else if (isToday(dateTimeString) == 0) { 33 | todayTodoList.push( 34 | 35 | ) 36 | } else if (isToday(dateTimeString) < 0) { 37 | overdueTodoList.push( 38 | 39 | ) 40 | } else { 41 | upcomingTodoList.push( 42 | 43 | ) 44 | } 45 | } 46 | 47 | return ( 48 |
49 |
50 | 51 | 52 | 53 | 54 | 55 | 56 |
57 |
58 | ) 59 | } 60 | 61 | export default MainScreen; 62 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from "electron"; 2 | 3 | import { TodoStateInterface, Todo } from "../types"; 4 | 5 | export const randomIDGenerator = () => { 6 | let s4 = () => { 7 | return Math.floor((1 + Math.random()) * 0x10000) 8 | .toString(16) 9 | .substring(1); 10 | } 11 | return s4() + s4() + s4() + s4() + s4() + s4(); 12 | } 13 | 14 | export const saveTodos = (todos: TodoStateInterface) => { 15 | ipcRenderer.invoke("save-todos", JSON.stringify(todos)); 16 | } 17 | 18 | export const fetchTodos = () => { 19 | return ipcRenderer.sendSync("get-todos"); 20 | } 21 | 22 | export const todoParser = (todo: Todo): boolean => { 23 | // parse body 24 | if (todo.body != "") { 25 | // parse date 26 | if (todo.date != "") { 27 | let parsedDate = new Date(todo.date); 28 | // check for special case 29 | if (todo.date.match(/^tom.*$/i)) { 30 | parsedDate = new Date(); 31 | parsedDate.setDate(parsedDate.getDate() + 1); 32 | } 33 | else if (todo.date.match(/^t.*$/i)) parsedDate = new Date(); 34 | // final check 35 | if (isNaN(parsedDate.getTime())) return false; 36 | // fix year if not fixed 37 | if (parsedDate.getFullYear() < new Date().getFullYear()) parsedDate.setFullYear(new Date().getFullYear()); 38 | todo.date = parsedDate.toDateString(); 39 | // parse time 40 | if (todo.time != "") { 41 | var time = todo.time.match(/(\d+)(:(\d\d))?\s*(p?)/i); 42 | if (time == null) return false; 43 | var hours = parseInt(time[1],10); 44 | if (hours == 12 && !time[4]) { hours = 0; } 45 | else { hours += (hours < 12 && time[4])? 12 : 0; } 46 | var d = new Date(); 47 | d.setHours(hours); 48 | d.setMinutes(parseInt(time[3],10) || 0); 49 | d.setSeconds(0, 0); 50 | todo.time = d.toLocaleTimeString(); 51 | } 52 | } 53 | // parse repeat 54 | if (todo.repeat != "") { 55 | if (todo.date == "") return false; 56 | let repeat = todo.repeat.match(/[dwmy]/i); 57 | if (!repeat) return false; 58 | todo.repeat = repeat[0]; 59 | } 60 | return true; 61 | } 62 | return false; 63 | } 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Destined 2 | 3 | ![Screenshot](/assets/screenshot.png) 4 | 5 | ## Why even use this ? 6 | 7 | This project aims to combine calendar and tasks management systems into one. It is pretty similar to some other software/web applications available. 8 | 9 | This exists to sidestep the complexity that comes with most other scheduling software. It simply tells you what you have to do, by when you have to do it and you don't need to worry about organizing anything. 10 | 11 | There are no servers involved, everything is stored in a JSON file. 12 | 13 | ## How it works 14 | 15 | ### Today's tasks 16 | 17 | You can view all the tasks that are supposed to be done/started/due today in one place. Rest of the scheduled tasks are displayed in upcoming tasks section. This is the section which serves as the calendar. You can assign time to the task which can also serve as a reminder. 18 | 19 | ### Upcoming tasks 20 | 21 | Since, all the scheduled tasks are displayed together at one place, it is easier to know what it is that you have to do next at all times. 22 | 23 | ### Overdue tasks 24 | 25 | If you missed a deadline on a task, it is displayed in this section. 26 | 27 | ### Unscheduled tasks 28 | 29 | Tasks that do not have a deadline are put into this section. 30 | 31 | ## Tiny features 32 | 33 | - Optionally add date/time/repeat 34 | - Sort the categories according to date/time, color and todo body by clicking the button on the side of category headings 35 | - Date 36 | - Writing `t` in date section adds today's date to the task date 37 | - Writing `tom` in date section adds tomorrow's date to the task date 38 | - Repeat 39 | - Writing `d`, `w`, `m`, `y` in repeat section makes the task repeatable on daily, weekly, monthly, yearly basis 40 | - Once the task is done a new date is added to it depending on the repeat value 41 | - That's 42 | 43 | ## Disclaimer 44 | 45 | - Not fully tested on Linux (partial) and Mac OS (none) 46 | - The todos are stored in `destined-todos.json` file in the main directory 47 | - **`v1` destined todos file is not compatible with `v2` and so on** 48 | 49 | ## Installation 50 | 51 | 1. Clone the repository 52 | 2. Run these commands in the directory 53 | 54 | ``` 55 | npm install 56 | ``` 57 | 58 | Then for windows/linux: 59 | 60 | ``` 61 | npm run build 62 | ``` 63 | 64 | For OSX (not tested): 65 | 66 | ``` 67 | npm run build-osx 68 | ``` 69 | 70 | 3. A directory named `destined*` will be created (\* can be anything depending on your OS eg. on Windows 64bit it will be `destined-win32-x64`) 71 | 4. Copy it to any place on your system and run the destined executable 72 | -------------------------------------------------------------------------------- /src/slices/todos.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit" 2 | 3 | import { randomIDGenerator } from "../utils"; 4 | import { Todo, TodoStateInterface, TodoWithIDPayload } from "../types"; 5 | import { saveTodos, fetchTodos, todoParser } from "../utils"; 6 | 7 | let initialState:TodoStateInterface = fetchTodos(); 8 | 9 | const todosSlice = createSlice({ 10 | name: "todos", 11 | initialState, 12 | reducers: { 13 | 14 | addTodo: (state, { payload }: PayloadAction) => { 15 | let todo = payload; 16 | // parse Todo 17 | if (todoParser(todo)) { 18 | // generate id 19 | let id: string = randomIDGenerator(); 20 | while (id in state) { 21 | id = randomIDGenerator(); 22 | } 23 | // create new state 24 | let newState = { 25 | ...state, 26 | [id]: todo 27 | } 28 | // write state to file 29 | saveTodos(newState); 30 | return(newState); 31 | } else { 32 | return ({ ...state }); 33 | } 34 | }, 35 | 36 | modifyTodo: (state, { payload }: PayloadAction) => { 37 | if (payload.id in state) { 38 | let todo = payload.todo; 39 | // parse todo 40 | if (todoParser(todo)) { 41 | let id: string = payload.id; 42 | let newState = { 43 | ...state, 44 | [id]: payload.todo 45 | }; 46 | saveTodos(newState); 47 | return (newState); 48 | } 49 | } 50 | return ({ ...state }) 51 | }, 52 | 53 | deleteTodo: (state, { payload }: PayloadAction) => { 54 | let newState = { ...state }; 55 | delete newState[payload]; 56 | saveTodos(newState); 57 | return newState; 58 | }, 59 | 60 | doneTodo: (state, { payload }: PayloadAction) => { 61 | let newState = { ...state }; 62 | let todo = {...newState[payload]}; 63 | if (todo.repeat != "") { 64 | let currDate = new Date(todo.date); 65 | switch (todo.repeat) { 66 | case "d": 67 | currDate.setDate(currDate.getDate() + 1); 68 | break; 69 | case "w": 70 | currDate.setDate(currDate.getDate() + 7); 71 | break; 72 | case "m": 73 | currDate.setMonth(currDate.getMonth() + 1); 74 | break; 75 | case "y": 76 | currDate.setFullYear(currDate.getFullYear() + 1); 77 | break; 78 | } 79 | todo.date = currDate.toDateString(); 80 | newState[payload] = todo; 81 | } else { 82 | delete newState[payload]; 83 | } 84 | saveTodos(newState); 85 | return newState; 86 | } 87 | 88 | } 89 | }); 90 | 91 | export const { 92 | addTodo, 93 | modifyTodo, 94 | doneTodo, 95 | deleteTodo 96 | } = todosSlice.actions; 97 | 98 | export default todosSlice.reducer; -------------------------------------------------------------------------------- /src/styles/_components.scss: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////// category 2 | .category-container { 3 | margin-bottom: 25px; 4 | } 5 | 6 | .category-header { 7 | display: flex; 8 | flex-direction: row; 9 | align-items: center; 10 | margin-bottom: 10px; 11 | * { 12 | margin-right: 10px; 13 | } 14 | svg { 15 | font-size: 1.2em; 16 | color: var(--tertiary-color); 17 | } 18 | } 19 | 20 | .category-heading { 21 | font-size: 1.3em; 22 | font-family: var(--heading-font); 23 | color: var(--accent-color); 24 | } 25 | 26 | //////////////////////////////////////////////////////////////////////////// todo 27 | .todo-container { 28 | display: flex; 29 | margin-top: 5px; 30 | font-size: 1em; 31 | * { 32 | align-self: center; 33 | } 34 | } 35 | 36 | .todo-checkbox { 37 | background-color: var(--secondary-color); 38 | border-radius: 4px; 39 | margin: 2px; 40 | margin-right: 10px; 41 | width: 20px; 42 | height: 20px; 43 | } 44 | 45 | .todo-checkbox:hover { 46 | border: 2px solid var(--tertiary-color); 47 | } 48 | 49 | .todo-body { 50 | width: 100%; 51 | display: flex; 52 | justify-content: space-between; 53 | } 54 | 55 | .todo-body-left { 56 | display: flex; 57 | :not(:first-child) { 58 | color: var(--tertiary-color); 59 | font-size: 18px; 60 | padding-left: 5px; 61 | } 62 | } 63 | 64 | .todo-timestamp-container { 65 | display: flex; 66 | color: var(--tertiary-color); 67 | font-weight: 800; 68 | * { 69 | margin-right: 10px; 70 | } 71 | :last-child { 72 | margin-right: 0; 73 | } 74 | } 75 | 76 | //////////////////////////////////////////////////////////////////////////// topbar 77 | .topbar-container { 78 | display: flex; 79 | justify-content: space-between; 80 | margin-bottom: 20px; 81 | width: 100%; 82 | height: 40px; 83 | * { 84 | align-self: center; 85 | font-size: 1.3em; 86 | color: var(--tertiary-color); 87 | } 88 | svg { 89 | font-size: 1em; 90 | } 91 | } 92 | 93 | //////////////////////////////////////////////////////////////////////////// add todo 94 | .add-todo-container { 95 | display: flex; 96 | padding: 0 10px; 97 | border: 3px solid var(--secondary-color); 98 | border-radius: 5px; 99 | margin-bottom: 30px; 100 | } 101 | 102 | .add-todo-content { 103 | display: flex; 104 | width: 100%; 105 | padding: 5px 0; 106 | flex-direction: column; 107 | flex-wrap: wrap; 108 | } 109 | 110 | .add-todo-row { 111 | display: flex; 112 | justify-content: flex-start; 113 | align-items: center; 114 | margin: 5px 0; 115 | width: 100%; 116 | * { 117 | font-size: 1em; 118 | margin-right: 15px; 119 | } 120 | input { // all input fields other than the first one 121 | width: 80px; 122 | } 123 | :last-child { 124 | margin-right: 0; 125 | } 126 | svg { // icons 127 | margin-right: 0; 128 | } 129 | input[type="date"], input[type="time"] { 130 | appearance: none; 131 | font-family: var(--content-font); 132 | color: var(--tertiary-color); 133 | } 134 | } 135 | 136 | .grow { 137 | width: auto; 138 | flex-grow: 2; 139 | } 140 | 141 | .submit-button { 142 | display: none; 143 | } 144 | 145 | ///////////////////////////////////////////////////////////////////////////// pill 146 | 147 | .pill { 148 | margin-right: 10px; 149 | display: inline-block; 150 | border-radius: 100%; 151 | border: 2px solid var(--secondary-color); 152 | width: 25px; 153 | height: 25px; 154 | } 155 | 156 | .pill-active { 157 | border-color: var(--text-color); 158 | } 159 | 160 | .pill:hover { 161 | border: 2px solid var(--text-color); 162 | } 163 | -------------------------------------------------------------------------------- /assets/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 20 | 22 | 25 | 29 | 30 | 33 | 41 | 42 | 45 | 49 | 50 | 51 | 70 | 72 | 73 | 75 | image/svg+xml 76 | 78 | 79 | 80 | 81 | 82 | 86 | 93 | 96 | 104 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /src/components/Todo.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { useState, useEffect } from "react" 3 | import { useDispatch } from "react-redux"; 4 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" 5 | import { faTrash, faRedoAlt, faCheck } from "@fortawesome/free-solid-svg-icons" 6 | 7 | import { TodoProps } from "../types"; 8 | import { Todo as TodoInterface } from "../types"; 9 | import { deleteTodo, addTodo, modifyTodo, doneTodo } from "../slices/todos"; 10 | 11 | const Todo: React.FC = ({todo, id, shouldDisplayDate, shouldDisplayTime, todoAdder}) => { 12 | 13 | const dispatch = useDispatch(); 14 | 15 | const [modifying, setModifying] = useState(todoAdder ? true : false); 16 | const defaultColor = getComputedStyle(document.documentElement).getPropertyValue('--text-color').trim(); 17 | 18 | const [taskText, setTaskText] = useState(todoAdder ? "" : todo.body); 19 | const [dateText, setDateText] = useState(todoAdder ? "" : todo.date); 20 | const [timeText, setTimeText] = useState(todoAdder ? "" : todo.time); 21 | const [todoColor, setTodoColor] = useState(todoAdder ? defaultColor : todo.color); 22 | const [repeatText, setRepeatText] = useState(todoAdder ? "" : todo.repeat); 23 | 24 | const handleTaskTextChange = (e: React.FormEvent) => {setTaskText(e.currentTarget.value)}; 25 | const handleDateTextChange = (e: React.FormEvent) => {setDateText(e.currentTarget.value)}; 26 | const handleTimeTextChange = (e: React.FormEvent) => {setTimeText(e.currentTarget.value)}; 27 | const handleTodoColorChange = (color: string) => {setTodoColor(color)}; 28 | const handleRepeatTextChange = (e: React.FormEvent) => {setRepeatText(e.currentTarget.value)}; 29 | 30 | const openEditBox = () => { 31 | setTaskText(todoAdder ? "" : todo.body); 32 | setDateText(todoAdder ? "" : todo.date); 33 | setTimeText(todoAdder ? "" : todo.time); 34 | setTodoColor(todoAdder ? "" : todo.color); 35 | setRepeatText(todoAdder ? "" : todo.repeat); 36 | setModifying(true); 37 | } 38 | 39 | const handleSubmit = (e: React.FormEvent) => { 40 | e.preventDefault(); 41 | let newTodo: TodoInterface = { 42 | body: taskText, 43 | date: dateText, 44 | time: timeText, 45 | repeat: repeatText, 46 | color: todoColor 47 | } 48 | if (todoAdder) { 49 | dispatch(addTodo(newTodo)) 50 | } else { 51 | dispatch(modifyTodo({todo: newTodo, id})) 52 | setModifying(false); 53 | } 54 | } 55 | 56 | const renderDate = () => { 57 | let arr = todo.date.split(" "); 58 | let string = arr[0] + ", " + parseInt(arr[2]) + " " + arr[1]; 59 | if (parseInt(arr[3]) > new Date().getFullYear()) string += " " + arr[3]; 60 | return ( 61 |
62 | {string} 63 |
64 | ) 65 | } 66 | 67 | const renderTime = () => { 68 | let arr = todo.time.split(" "); 69 | let arr2 = arr[0].split(":"); 70 | let string = arr2[0]; 71 | if (parseInt(arr2[1]) != 0) string += ":" + arr2[1]; 72 | string += " " + arr[1]; 73 | return ( 74 |
75 | {string} 76 |
77 | ) 78 | } 79 | 80 | const renderColorPills = () => { 81 | // get the default text color from sass 82 | let colors = [ 83 | defaultColor, "#ddd566", "#c4750d", "#62bf54", 84 | "#6bd4d6", "#d06bd6", "#db484a" 85 | ]; 86 | let pills = []; 87 | for (let i = 0; i < colors.length; i++) { 88 | pills.push( 89 |
{ handleTodoColorChange(colors[i]) }} /> 93 | ); 94 | } 95 | return pills; 96 | } 97 | 98 | const renderModifier = () => { 99 | return ( 100 |
101 |
102 | 103 | 104 | 105 | 106 | {todoAdder ? null : dispatch(deleteTodo(id))} />} 107 |
108 |
109 |
110 | {renderColorPills()} 111 |
112 | 113 |
114 | 115 |
116 | ) 117 | } 118 | 119 | const renderNormal = () => { 120 | return ( 121 |
122 |
dispatch(doneTodo(id))}>
123 |
125 |
126 | {todo ?
{todo.body}
: null} 127 | {todo ? (todo.repeat ? : null) : null} 128 |
129 |
130 | {shouldDisplayDate && todo.date != "" ? renderDate() : null} 131 | {shouldDisplayTime && todo.time != "" ? renderTime() : null} 132 |
133 |
134 |
135 | ) 136 | } 137 | 138 | return ( 139 | <> 140 | {modifying ? renderModifier() : renderNormal()} 141 | 142 | ) 143 | } 144 | 145 | export default Todo; 146 | --------------------------------------------------------------------------------