A web TODO list application
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

400 lines
16 KiB

{{ template "base" . }}
{{ define "content" }}
<h1 style="display: none;" id="categoryId">{{.Data.CurrentGroupId}}</h1>
<!-- Main -->
<main class="d-flex flex-wrap">
<!-- MODALS -->
<!-- Delete Confirmation Modal -->
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteModalLabel">{{index .Translation "category modal confirm deletion"}}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
{{index .Translation "category modal deletion confirmation"}}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" id="confirmDeleteButton">{{index .Translation "category modal delete button"}}</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{index .Translation "category modal cancel button"}}</button>
</div>
</div>
</div>
</div>
<!-- End delete confirmation modal -->
<!-- Paint Canvas Modal -->
<div class="modal fade" id="paintModal" tabindex="-1" aria-labelledby="paintModal" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="paintModal">{{index .Translation "category modal draw note"}}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" onclick="clearCanvas();"></button>
</div>
<div class="modal-body w-100 d-flex flex-column justify-content-center align-items-center">
{{ template "paint" . }}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" id="saveCanvasButton" data-bs-dismiss="modal">{{index .Translation "category modal save button"}}</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" onclick="clearCanvas();">{{index .Translation "category modal cancel button"}}</button>
</div>
</div>
</div>
</div>
<!-- End Paint Canvas Modal -->
<!-- ToDo display Modal -->
<div class="modal fade" id="todoModal" tabindex="-1" aria-labelledby="todoModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="todoModalLabel">{{index .Translation "category modal todo details"}}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div>
<strong>{{index .Translation "category modal todo text"}}</strong>
<span id="modalTodoTextDisplay"></span>
<input type="text" id="modalTodoTextInput" class="form-control" style="display: none;">
</div>
<div>
<strong>{{index .Translation "category modal todo created"}}</strong> <span id="modalTodoCreated"></span>
</div>
<div>
<strong>{{index .Translation "category modal todo due"}}</strong>
<span id="modalTodoDueDisplay"></span>
<input type="date" id="modalTodoDueInput" class="form-control" style="display: none;">
</div>
<div>
<strong>{{index .Translation "category modal todo completion"}}</strong> <span id="modalTodoCompletionTime"></span>
</div>
<img id="modalTodoImage" class="img-fluid" style="display: none;">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{index .Translation "category modal close button"}}</button>
<button type="button" class="btn btn-primary" id="editButton" style="display: none;" onclick="toggleEditMode(true)">{{index .Translation "category modal edit button"}}</button>
<button type="button" class="btn btn-success" id="saveButton" style="display: none;" onclick="saveEditedTodo()">{{index .Translation "category modal save button"}}</button>
</div>
</div>
</div>
</div>
<!-- Sidebar -->
<div id="sidebar" class="col border-right shadow-lg flex-shrink-1 p-2 d-flex flex-column align-items-stretch bg-body-tertiary" style="width: 380px;">
<a href="/" class="d-flex align-items-center flex-shrink-0 p-3 link-body-emphasis text-decoration-none border-bottom">
<img class="bi pe-none me-2" width="30" height="24" src="/static/images/arrows-fullscreen.svg">
<span class="fs-5 fw-semibold">{{index .Translation "category categories"}}</span>
</a>
<div class="list-group list-group-flush border-bottom scrollarea">
{{ range .Data.Groups }}
<a id="group-{{.ID}}" href="/group/{{.ID}}" class="list-group-item list-group-item-action py-3 lh-sm {{if eq .ID $.Data.CurrentGroupId}} active {{end}}" aria-current="true" ondragover="allowDrop(event);" ondrop="drop(event);">
<div id="group-{{.ID}}" class="d-flex w-100 align-items-center justify-content-between">
<strong id="group-{{.ID}}" class="mb-1">{{ .Name }}</strong>
<small id="group-{{.ID}}">{{ .TimeCreated }}</small>
</div>
{{ if not .Removable }}
<div id="group-{{.ID}}" class="col-10 mb-1 small">{{index $.Translation "category not removable"}}</div>
{{ end }}
</a>
{{ end }}
</div>
</div>
<!-- Main ToDos section -->
<div class="p-2 flex-grow-1">
<form action="javascript:void(0);" id="todoForm">
<div class="row g-3 align-items-center">
<div class="col-md">
<label for="newTodoText" class="form-label">{{index .Translation "category todo text"}}</label>
<input type="text" class="form-control" id="newTodoText" placeholder='{{index .Translation "category enter todo text"}}' required>
</div>
<div class="col-md">
<label for="newTodoDue" class="form-label">{{index .Translation "category due date"}}</label>
<input type="date" class="form-control" name="newTodoDue" id="newTodoDue" required>
</div>
<div class="col-auto">
<button type="button" class="btn btn-primary" id="newTodoPaint" onclick="openPaintModal();"><img src="/static/images/paint-bucket.svg"></button>
</div>
<div class="col-auto">
<button type="submit" id="newTodoSubmit" class="btn btn-primary">{{index .Translation "category add"}}</button>
<button type="button" id="show-done" class="btn btn-secondary">{{index .Translation "category show done"}}</button>
</div>
</div>
</form>
<div class="container text-center">
<!-- Due -->
<table class="table table-hover" id="due-todos">
<thead>
<th>{{index .Translation "category image"}}</th>
<th>{{index .Translation "category todo"}}</th>
<th>{{index .Translation "category created"}}</th>
<th>{{index .Translation "category due"}}</th>
</thead>
<tbody class="text-break">
{{ range .Data.Todos }}
{{ if not .IsDone }}
<tr draggable="true" id="todo-{{.ID}}" ondragstart="dragStart(event);">
{{ if not .Image }}
<!-- Display transparent white pixel -->
<td><img class="todo-image" src='data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' width="64px" height="64px"></td>
{{ else }}
<td><img class="todo-image" src='{{ printf "%s" .Image }}' width="64px" height="64px"></td>
{{ end }}
<td class="todo-text text-wrap text-break">{{ .Text }}</td>
<td class="todo-created">{{ .TimeCreated }}</td>
<td class="todo-due">{{ .Due }}</td>
<td class="todo-due-unix" style="display: none;">{{ .DueUnix }}</td>
<td>
<button class="btn btn-success" onclick="markAsDoneRefresh('{{.ID}}');">
<img src='/static/images/check.svg'>
</button>
<button class="btn btn-danger" onclick="openDeleteModal('{{.ID}}');">
<img src='/static/images/trash3-fill.svg'>
</button>
<button class="btn btn-secondary" onclick="openTodoModal('{{.ID}}', '{{.Text}}', '{{.TimeCreated}}', '{{.Due}}', null, '{{ printf "%s" .Image }}', true);">
<img src="/static/images/journal-arrow-up.svg">
</button>
</td>
</tr>
{{ end }}
{{ end }}
</tbody>
</table>
<!-- Completed -->
<table class="table table-hover" style="display: none;" id="completed-todos">
<thead>
<th>{{index .Translation "category image"}}</th>
<th>{{index .Translation "category todo"}}</th>
<th>{{index .Translation "category created"}}</th>
<th>{{index .Translation "category completed"}}</th>
</thead>
<tbody class="text-break">
{{ range .Data.Todos }}
{{ if .IsDone }}
<tr>
{{ if not .Image }}
<!-- Display transparent white pixel -->
<td><img src='data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' width="64px" height="64px"></td>
{{ else }}
<td><img src='{{ printf "%s" .Image }}' width="64px" height="64px"></td>
{{ end }}
<td>{{ .Text }}</td>
<td>{{ .TimeCreated }}</td>
<td>{{ .CompletionTime }}</td>
<td>
<button class="btn btn-danger" onclick="deleteTodoRefresh('{{.ID}}');">
<img src='/static/images/trash3-fill.svg'>
</button>
<button class="btn btn-secondary" onclick="openTodoModal('{{.ID}}', '{{.Text}}', '{{.TimeCreated}}', '{{.Due}}', '{{.CompletionTime}}', '{{ printf "%s" .Image }}', false);">
<img src="/static/images/journal-arrow-up.svg">
</button>
</td>
</tr>
{{ end }}
{{ end }}
</tbody>
</table>
</div>
</div>
</main>
<script>
let todoToDeleteId;
// TODO deletion modal functions
function openDeleteModal(id) {
todoToDeleteId = id; // Save ID for handleTodoDelete to work properly
const deleteModal = new bootstrap.Modal(document.getElementById('deleteModal'), {});
// Remove any existing event listener to prevent multiple bindings
const confirmDeleteButton = document.getElementById('confirmDeleteButton');
confirmDeleteButton.removeEventListener('click', handleTodoDelete);
// Add the event listener for the delete action
confirmDeleteButton.addEventListener('click', handleTodoDelete);
deleteModal.show();
}
async function handleTodoDelete() {
await deleteTodoRefresh(todoToDeleteId); // Call the delete function
const deleteModal = bootstrap.Modal.getInstance(document.getElementById('deleteModal'));
deleteModal.hide(); // Hide the modal
}
function openPaintModal() {
const paintModal = new bootstrap.Modal(document.getElementById('paintModal'), {});;
paintModal.show();
}
// Mark TODO as done
async function markAsDoneRefresh(id) {
await markAsDone(id);
window.location.reload();
}
// Delete TODO with ID
async function deleteTodoRefresh(id) {
await deleteTodo(id);
window.location.reload();
}
async function showDone() {
// Hide not done, show done
let completedTodos = document.getElementById("completed-todos");
completedTodos.style.display = "table";
let dueTodos = document.getElementById("due-todos");
dueTodos.style.display = "none";
}
function allowDrop(event) {
event.preventDefault();
}
function dragStart(event) {
event.dataTransfer.setData("text", event.target.id);
event.dataTransfer.effectAllowed = "move";
}
async function drop(event) {
event.preventDefault();
var todoPageId = event.dataTransfer.getData("text");
let draggedTodo = document.getElementById(todoPageId);
let todoId = todoPageId.split("-")[1];
let targetGroupId = event.target.id.split("-")[1];
if (targetGroupId == document.getElementById("categoryId").innerText) {
// Do nothing
return;
}
// Update todo's group ID
let result = await updateTodo(todoId, {
groupId: Number(targetGroupId),
});
window.location.reload();
}
let viewedTodoID;
function openTodoModal(id, text, created, due, completionTime, image, editable) {
viewedTodoID = id;
document.getElementById('modalTodoTextDisplay').innerText = text;
document.getElementById('modalTodoTextInput').value = text;
document.getElementById('modalTodoCreated').innerText = created;
document.getElementById('modalTodoDueDisplay').innerText = due;
document.getElementById('modalTodoDueInput').value = due;
document.getElementById('modalTodoCompletionTime').innerText = completionTime;
let img = document.getElementById('modalTodoImage');
if (img) {
img.src = image;
img.style.display = 'block';
} else {
img.style.display = 'none';
}
let editButton = document.getElementById("editButton");
if (editable) {
// Show "Edit" button
editButton.style.display = "inline";
} else {
editButton.style.display = "none";
}
const todoModal = new bootstrap.Modal(document.getElementById('todoModal'));
todoModal.show();
}
async function saveEditedTodo() {
const updatedText = document.getElementById('modalTodoTextInput').value;
const updatedDue = document.getElementById('modalTodoDueInput').value;
document.getElementById('modalTodoTextDisplay').innerText = updatedText;
document.getElementById('modalTodoDueDisplay').innerText = updatedDue;
const updatedDueUnix = Date.parse(updatedDue) / 1000;
toggleEditMode(false);
await updateTodo(viewedTodoID, {"text":updatedText, "dueUnix":updatedDueUnix, "isDone":false});
window.location.reload();
}
function toggleEditMode(isEditing) {
document.getElementById('modalTodoTextDisplay').style.display = isEditing ? 'none' : 'inline';
document.getElementById('modalTodoTextInput').style.display = isEditing ? 'inline' : 'none';
document.getElementById('modalTodoDueDisplay').style.display = isEditing ? 'none' : 'inline';
document.getElementById('modalTodoDueInput').style.display = isEditing ? 'inline' : 'none';
document.getElementById('editButton').style.display = isEditing ? 'none' : 'inline';
document.getElementById('saveButton').style.display = isEditing ? 'inline' : 'none';
}
document.addEventListener('DOMContentLoaded', async function() {
document.getElementById("newTodoText").focus();
let showDoneButton = document.getElementById("show-done");
showDoneButton.addEventListener("click", (event) => {
// Rename the button
showDoneButton.innerText = '{{index .Translation "category js button show todo"}}';
showDoneButton.className = "btn btn-success";
// Show done
showDone();
// Make it "reset to default"
showDoneButton.addEventListener("click", (event) => {
location.reload();
});
});
// "Add" button
document.getElementById("newTodoSubmit").addEventListener("click", async (event) => {
let newTodoTextInput = document.getElementById("newTodoText");
let newTodoText = newTodoTextInput.value;
if (newTodoText.length < 1) {
newTodoTextInput.setCustomValidity("At least one character is needed!");
return;
} else {
newTodoTextInput.setCustomValidity("");
}
newTodoTextInput.value = "";
let newTodoDueInput = document.getElementById("newTodoDue");
let dueTimeStamp = Date.parse(newTodoDueInput.value) / 1000;
let groupId = document.getElementById("categoryId").innerText;
let canvasImage = getCanvasImage();
if (canvasImage) {
canvasImage = Array.from(canvasImage, char => char.charCodeAt(0));
}
// Make a request
let response = await postNewTodo(
{"text": newTodoText, "groupId": Number(groupId), "dueUnix": Number(dueTimeStamp), "image": canvasImage}
);
if (response.ok) {
location.reload();
}
});
});
</script>
{{ end }}