functional

This commit is contained in:
ak 2023-06-16 22:43:24 -07:00
parent d25e8506ab
commit 5ecdd2febe
14 changed files with 2140 additions and 0 deletions

1
.gitignore vendored Executable file
View file

@ -0,0 +1 @@
node_modules/

33
dist/index.html vendored Executable file
View file

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="Description" content="Enter your description here"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.1.0/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
<link rel="stylesheet" href="../src/style.css">
<title>To-Do List</title>
</head>
<body class="d-flex h-100 text-center bg-dark">
<div class="cover-container d-flex w-100 h-100 px-3 flex-column">
<header class="d-flex flex-wrap justify-content-between py-3 border-bottom align-items-center">
<div class="dropdown">
<button class="btn btn-primary btn-lg dropdown-toggle" type="button" id="dropdownMenuButton1" data-bs-toggle="dropdown" aria-expanded="false">
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton1">
</ul>
</div>
<div>
<ul class="nav nav-pills">
</ul>
</div>
</header>
<main class="col-sm-6 py-3"></main>
</div>
<script type="module" src="main.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.9.2/umd/popper.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.1.0/js/bootstrap.min.js"></script>
</body>
</html>

146
dist/main.js vendored Executable file

File diff suppressed because one or more lines are too long

1331
package-lock.json generated Executable file

File diff suppressed because it is too large Load diff

23
package.json Executable file
View file

@ -0,0 +1,23 @@
{
"name": "js-todolist",
"version": "1.0.0",
"description": "to-do list implemented in ES6",
"main": "js/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack"
},
"keywords": [
"js",
"javascript",
"odin",
"todo",
"list"
],
"author": "ak",
"license": "AGPL-3.0-or-later",
"devDependencies": {
"webpack": "^5.77.0",
"webpack-cli": "^5.0.1"
}
}

429
src/domifier.js Executable file
View file

@ -0,0 +1,429 @@
import { todo } from "./todo.js";
import { project } from "./project.js";
import { getArray, setArray } from "./projects.js"
const CURRENTPROJECT = document.getElementById("dropdownMenuButton1");
// shim for setting proper prototype when pulling from JSON
const PROJECT = project("shim");
// initializes certain dom objects
export const initDOM = () => {
// creates New Todo button
const newTodo = document.createElement("button");
newTodo.className = "btn btn-warning";
newTodo.textContent = "+ New Todo";
// event handlers
newTodo.onclick = () => createTodo();
// appends
document.querySelector(".nav-pills").appendChild(newTodo)
// creates New Project button
const newProject = document.createElement("button");
newProject.className = "btn btn-success";
newProject.textContent = "+ New Project";
// event handlers
newProject.onclick = () => {
// asks for a project name
const projectName = prompt("What will your new project be called?", "Default Project");
const dummyProjects = getArray();
// checks for blanks or default values
if (projectName === null || projectName === "" || projectName === "Default Project") {
alert("Please enter a valid project name");
}
// checks if any projects are named the same way
else if (dummyProjects.find (item => item.name === projectName)) {
alert("Please enter an unused project name");
}
// if valid, create new project
else {
createProject(projectName);
}
}
// appends
document.querySelector(".nav-pills").appendChild(newProject)
// creates Delete Project button
const delProject = document.createElement("button");
delProject.className = "btn btn-danger";
delProject.textContent = "x Delete Project";
// event handlers
delProject.onclick = () => deleteProject();
// appends
document.querySelector(".nav-pills").appendChild(delProject);
displayProjects();
}
export const changeCurrentProject = projectName => {
// changes dropdown button to main project
CURRENTPROJECT.textContent = projectName;
// clears the board
document.querySelector("main").innerHTML = "";
// finds all todos in the project and displays them (if any)
const dummyProjects = getArray();
const projectInArray = dummyProjects.find (item => item.name === CURRENTPROJECT.textContent);
if (projectInArray.todos.length > -1) {
projectInArray.todos.forEach(todo => displayTodo(todo));
}
}
export const displayTodo = todo => {
// sets up the necessary containers
const card = document.createElement('div');
card.className = "card";
const cardBody = document.createElement('div');
cardBody.className = "card-body";
// appends title of current todo to DOM
const title = document.createElement('h5');
title.className = "card-title mb-2";
title.textContent = todo.title;
const dueDate = document.createElement('h6');
dueDate.className = "card-subtitle text-muted mb-1";
// bit of a rigamarole due to localStorage
const date = new Date(Date.parse(todo.dueDate));
dueDate.textContent = "Due: " + date.toDateString();
//
const priority = document.createElement('h6');
priority.className = "mb-3 btn prioritybtn";
const determinePriority = () => {
if (todo.priority === 0) {
priority.className = priority.className + " btn-success";
priority.textContent = "Low";
}
else if (todo.priority == 1) {
priority.className = priority.className + " btn-warning";
priority.textContent = "Medium";
}
else if (todo.priority == 2) {
priority.className = priority.className + " btn-danger";
priority.textContent = "High";
}
return priority;
}
priority.textContent = "Priority: " + determinePriority().textContent;
const comment = document.createElement('p');
comment.className = "card-text text-muted";
comment.textContent = todo.description;
const editButton = document.createElement('button');
editButton.className = "btn btn-primary me-1";
const editButtonText = document.createElement('div');
editButtonText.textContent = "Edit";
editButton.onclick = () => editTodo(cardBody, todo);
const deleteButton = document.createElement('button');
deleteButton.className = "btn btn-danger ms-1";
const deleteButtonText = document.createElement('div');
deleteButtonText.textContent = "Delete";
deleteButton.onclick = () => deleteTodo(todo, card);
// append to DOM
document.querySelector("main").appendChild(card);
card.appendChild(cardBody);
cardBody.appendChild(title);
cardBody.appendChild(dueDate);
cardBody.appendChild(priority);
cardBody.appendChild(comment);
cardBody.appendChild(editButton);
editButton.appendChild(editButtonText);
cardBody.appendChild(deleteButton);
deleteButton.appendChild(deleteButtonText);
}
const deleteProject = () => {
// removes the todos from the project
const dummyProjects = getArray();
const projectInArray = dummyProjects.find (item => item.name === CURRENTPROJECT.textContent);
projectInArray.todos = [];
// finds the current project in the dropdown menu
// gets array of dropdown objects in dom and casts it to a "real" array;
const dropdownArray = Array.prototype.slice.call(document.getElementsByClassName("dropdown-item"));
// finds the project which matches current project
const currentProjectDropdown = dropdownArray.find(element => element.textContent === CURRENTPROJECT.textContent);
// finds the next project
const nextProjectDropdown = dropdownArray[dropdownArray.indexOf(currentProjectDropdown) + 1];
const prevProjectDropdown = dropdownArray[dropdownArray.indexOf(currentProjectDropdown) - 1];
// removes the current project from dropdowns
const li = currentProjectDropdown.parentElement;
li.parentElement.removeChild(li);
// removes from projects array
const projectNumber = dummyProjects.indexOf(projectInArray);
dummyProjects.splice(projectNumber, 1);
setArray(dummyProjects);
// changes current project
if (nextProjectDropdown) {
changeCurrentProject(nextProjectDropdown.textContent);
}
else if (prevProjectDropdown) {
changeCurrentProject(prevProjectDropdown.textContent);
}
else {
CURRENTPROJECT.textContent = "";
}
}
export const createProjectDOM = projectName => {
// adds to dropdown
const li = document.createElement("li");
document.querySelector(".dropdown-menu").appendChild(li);
const dropdownOption = document.createElement("a");
dropdownOption.className = "dropdown-item";
dropdownOption.textContent = projectName;
li.appendChild(dropdownOption);
// event handler for when user clicks the project in dropdowns
li.onclick = () => changeCurrentProject(projectName);
// changes to new project
changeCurrentProject(projectName);
}
export const createTodo = () => {
// sets up the necessary containers
const card = document.createElement('div');
card.className = "card";
const cardBody = document.createElement('div');
cardBody.className = "card-body";
// creates input fields to get user input for todo info
const title = document.createElement('input');
title.className = "card-title mb-2 form-control";
title.type = "text";
title.placeholder = "Title";
const dueDate = document.createElement('input');
dueDate.className = "card-subtitle text-muted mb-2 form-control";
dueDate.type = "date";
dueDate.oninput = () => {
const dueDateSplit = dueDate.value.split('-');
if (dueDateSplit[0] && dueDateSplit[0].length > 4) {
dueDateSplit[0]=dueDateSplit[0].slice(0,4);
dueDate.value = dueDateSplit.join('-');
}
}
const priority = document.createElement('h6');
const oldClassName = "mb-3 btn btn-primary";
priority.className = oldClassName;
priority.textContent = "Priority"
let priorityLevel;
let counter = 0;
priority.onclick = () => {
if (counter === 0) {
priority.className = oldClassName + " btn-success";
priority.textContent = "Priority: Low";
priorityLevel = counter;
counter++;
}
else if (counter === 1) {
priority.className = oldClassName + " btn-warning";
priority.textContent = "Priority: Medium";
priorityLevel = counter;
counter++;
}
else {
priority.className = oldClassName + " btn-danger";
priority.textContent = "Priority: High";
priorityLevel = counter;
counter = 0;
}
}
const comment = document.createElement('input');
comment.className = "card-text text-muted form-control mb-3";
comment.type = "text";
comment.placeholder = "Description"
const createButton = document.createElement('button');
createButton.className = "btn btn-success me-1";
const createButtonText = document.createElement('div');
createButtonText.textContent = "Create";
createButton.onclick = () => finalizeTodo();
const deleteButton = document.createElement('button');
deleteButton.className = "btn btn-danger ms-1";
const deleteButtonText = document.createElement('div');
deleteButtonText.textContent = "Cancel";
deleteButton.onclick = () => document.querySelector("main").removeChild(card);
document.querySelector('main').appendChild(card);
card.appendChild(cardBody);
cardBody.appendChild(title);
cardBody.appendChild(dueDate);
cardBody.appendChild(priority);
cardBody.appendChild(comment);
cardBody.appendChild(createButton);
createButton.appendChild(createButtonText);
cardBody.appendChild(deleteButton);
deleteButton.appendChild(deleteButtonText);
function finalizeTodo() {
// some validation
if (title.value === "Title" || title.value === "") {
alert("Please enter a name for your to-do");
}
else {
if (priorityLevel || priorityLevel === 0) {
// adds todo to project
const finalizedTodo = todo(title.value, comment.value, dueDate.valueAsDate, priorityLevel);
const dummyProjects = getArray();
let foundProject = dummyProjects.find (item => item.name === CURRENTPROJECT.textContent);
foundProject.todos[foundProject.todos.length] = finalizedTodo;
setArray(dummyProjects);
// wipes prompt window
document.querySelector("main").removeChild(card);
displayTodo(finalizedTodo);
}
else {
alert("Please select a priority for your to-do by clicking the Priority button");
}
}
}
}
export const editTodo = (cardBody, todo) => {
// creates title field and fills with existing title
const title = document.createElement('input');
title.className = "card-title mb-2 form-control";
title.type = "text";
// gets title out of existing title (title is the first child of cardBody)
title.value = cardBody.firstChild.textContent;
const dueDate = document.createElement('input');
dueDate.maxLength = 8;
dueDate.className = "card-subtitle text-muted mb-2 form-control";
dueDate.type = "date";
if (typeof todo.dueDate === "object") {
dueDate.value = todo.dueDate.toISOString().slice(0,10);
}
else {
dueDate.value = todo.dueDate.slice(0,10);
}
dueDate.oninput = () => {
const dueDateSplit = dueDate.value.split('-');
if (dueDateSplit[0] && dueDateSplit[0].length > 4) {
dueDateSplit[0]=dueDateSplit[0].slice(0,4);
dueDate.value = dueDateSplit.join('-');
}
}
const priority = document.createElement('h6');
const oldClassName = "mb-3 btn btn-primary";
priority.className = oldClassName;
priority.textContent = "Priority";
let counter;
let priorityLevel = todo.priority;
if (todo.priority === 0) {
priority.className = oldClassName + " btn-success";
priority.textContent = "Priority: Low";
counter = 1;
}
else if (todo.priority === 1) {
priority.className = oldClassName + " btn-warning";
priority.textContent = "Priority: Medium";
counter = 2;
}
else {
priority.className = oldClassName + " btn-danger";
priority.textContent = "Priority: High";
counter = 0;
}
priority.onclick = () => {
if (counter === 0) {
priority.className = oldClassName + " btn-success";
priority.textContent = "Priority: Low";
priorityLevel = counter;
counter++;
}
else if (counter === 1) {
priority.className = oldClassName + " btn-warning";
priority.textContent = "Priority: Medium";
priorityLevel = counter;
counter++;
}
else {
priority.className = oldClassName + " btn-danger";
priority.textContent = "Priority: High";
priorityLevel = counter;
counter = 0;
}
}
const comment = document.createElement('input');
comment.className = "card-text text-muted form-control mb-3";
comment.type = "text";
comment.value = cardBody.children[3].textContent;
for (let i = 0; i < 4; i++) {
cardBody.removeChild(cardBody.firstChild);
}
cardBody.firstChild.textContent = "Finish";
cardBody.firstChild.className = "btn btn-success me-1";
cardBody.firstChild.onclick = () => finalizeTodo();
cardBody.prepend(comment);
cardBody.prepend(priority);
cardBody.prepend(dueDate);
cardBody.prepend(title);
function finalizeTodo() {
// some validation
if (title.value === "") {
alert("Please enter a name for your to-do");
}
else {
if (priorityLevel || priorityLevel === 0) {
todo.title = title.value;
todo.description = comment.value;
todo.dueDate = dueDate.valueAsDate;
todo.priority = priorityLevel;
// proper handling
const dummyProjects = getArray();
const foundProject = dummyProjects.find (item => item.name === CURRENTPROJECT.textContent);
setArray(dummyProjects);
// wipes prompt window
document.querySelector("main").removeChild(cardBody.parentElement);
displayTodo(todo);
}
else {
alert("Please select a priority for your to-do by clicking the Priority button")
}
}
}
}
const deleteTodo = (todo, card) => {
document.querySelector("main").removeChild(card);
const dummyProjects = getArray();
let foundProject = dummyProjects.find (item => item.name === CURRENTPROJECT.textContent);
const todoIndex = foundProject.todos.indexOf(todo);
if (todoIndex) {
foundProject.todos.splice(todoIndex, 1);
}
setArray(dummyProjects);
}
const createProject = (projectName) => {
const dummyProjects = getArray();
dummyProjects[dummyProjects.length] = project(projectName);
setArray(dummyProjects);
createProjectDOM(projectName);
}
const displayProjects = () => {
const dummyProjects = getArray();
dummyProjects.forEach(item => createProjectDOM(item.name));
}

5
src/incrementer.js Executable file
View file

@ -0,0 +1,5 @@
// increments date based from today's date on number of days specified
export const incrementer = (date, days) => {
date.setDate(date.getDate() + days);
return date;
}

25
src/index.js Executable file
View file

@ -0,0 +1,25 @@
import { todo } from "./todo.js"
import { incrementer } from "./incrementer.js";
import { changeCurrentProject, createProjectDOM } from "./domifier.js";
import { initDOM } from "./domifier.js";
import { getArray, setArray } from "./projects.js";
import { project } from "./project.js";
const TODAY = new Date();
let dummyProjects = getArray();
dummyProjects[0] = project("Default Project");
const defaultTodo = todo(
"Default Todo",
"This is the default to-do for the to-do list. Feel free to add some more!",
incrementer(TODAY, 7),
0
);
dummyProjects[0].todos[dummyProjects[0].todos.length] = defaultTodo;
setArray(dummyProjects);
initDOM();
changeCurrentProject(dummyProjects[0].name);

9
src/project.js Executable file
View file

@ -0,0 +1,9 @@
export const project = (name) => {
// sets up array of todo objects
let todos = []
return {
name,
todos
}
}

52
src/projects.js Executable file
View file

@ -0,0 +1,52 @@
// logic for the projects array itself
import { storageAvailable } from "./storageAvailable.js";
// get and set arrays - get from local storage if available, if not then get from a global var
// set it here too, as well as extend the regular array methods
// everything will be here regards getting and setting this stuff, without a need to expose the actual array out to other modules
// master projects array
let projectsArray = [];
// tests if a "projects" item exists in local storage
const projectsExists = localStorage.getItem("projects") ? true : false;
// returns the proper array depending on where it is
export const getArray = () => {
// localStorage exists
if (storageAvailable) {
// "projects" exists
if (projectsExists) {
// set projects array to local storage and return
projectsArray = JSON.parse(localStorage.projects);
return projectsArray;
}
else {
return projectsArray;
}
}
// otherwise return the projects array here
else {
return projectsArray;
}
}
// easiest workaround to getting too in-depth - passed array is set as the main, depending on if storage exists, etc.
export const setArray = (array) => {
// set the master array to passed array - regardless of localStorage
projectsArray = array;
// check for localStorage
if (storageAvailable) {
// and existing "projects"
if (projectsExists) {
// remove existing array from localStorage
localStorage.removeItem("projects");
// add the modified array
localStorage.projects = JSON.stringify(projectsArray);
}
// if "projects" doesn't exist, add it
else {
localStorage.projects = JSON.stringify(projectsArray);
}
}
}

29
src/storageAvailable.js Executable file
View file

@ -0,0 +1,29 @@
function storageAvailableFunc(type) {
let storage;
try {
storage = window[type];
const x = "__storage_test__";
storage.setItem(x, x);
storage.removeItem(x);
return true;
} catch (e) {
return (
e instanceof DOMException &&
// everything except Firefox
(e.code === 22 ||
// Firefox
e.code === 1014 ||
// test name field too, because code might not be present
// everything except Firefox
e.name === "QuotaExceededError" ||
// Firefox
e.name === "NS_ERROR_DOM_QUOTA_REACHED") &&
// acknowledge QuotaExceededError only if there's something already stored
storage &&
storage.length !== 0
);
}
}
// copyright MDN https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API
export const storageAvailable = storageAvailableFunc("localStorage");

45
src/style.css Executable file
View file

@ -0,0 +1,45 @@
.nav-pills {
gap: 1rem;
}
.dropdown-toggle::after {
margin-left: .3em;
vertical-align: .15em;
}
.card {
width: 18rem;
}
.col-sm-6 {
flex: 1;
width: auto;
}
.card {
box-shadow: 0.5px 0.5px 5px black;
margin: 0rem 1.5rem 1.5rem 0rem;
flex-grow: 0;
flex-shrink: 0;
}
.prioritybtn {
cursor: default;
}
main {
display: inline-flex;
flex-flow: row wrap;
overflow: scroll;
justify-content: flex-start
}
/* hides scrollbar*/
main::-webkit-scrollbar {
display: none;
}
main {
-ms-overflow-style: none;
scrollbar-width: none;
}

9
src/todo.js Executable file
View file

@ -0,0 +1,9 @@
// todo factory function
export const todo = (title, description, dueDate, priority) => {
return {
title,
description,
dueDate,
priority
}
}

3
webpack.config.js Executable file
View file

@ -0,0 +1,3 @@
module.exports = {
mode: 'development'
};