Browse Source

Feature: Actual dates instead of Unix Timestamps

master
parent
commit
2fdc010dc2
  1. 89
      pages/category.html
  2. 110
      pages/index.html
  3. 6
      scripts/api.js
  4. 14
      src/db/group.go
  5. 30
      src/db/todo.go
  6. 47
      src/server/endpoints.go
  7. 1
      src/server/server.go

89
pages/category.html

@ -18,7 +18,7 @@
<a href="/group/{{.ID}}" class="list-group-item list-group-item-action active py-3 lh-sm" aria-current="true">
<div class="d-flex w-100 align-items-center justify-content-between">
<strong class="mb-1">{{ .Name }}</strong>
<small>{{ .TimeCreatedUnix }}</small>
<small>{{ .TimeCreated }}</small>
</div>
<div class="col-10 mb-1 small">Is removable: {{ .Removable }}</div>
</a>
@ -46,29 +46,76 @@
</form>
<div class="container text-center">
<table class="table table-hover">
<thead>
<th>ToDo</th>
<th>Created</th>
<th>Due</th>
<th>Group Id</th>
</thead>
<tbody id="todos" class="text-break">
{{ range .Todos }}
<tr>
<td>{{ .Text }}</td>
<td>{{ .TimeCreatedUnix }}</td>
<td>{{ .DueUnix }}</td>
<td>{{ .GroupID }}</td>
</tr>
{{ end }}
</tbody>
</table>
<!-- Due -->
<table class="table table-hover" id="due-todos">
<thead>
<th>ToDo</th>
<th>Created</th>
<th>Due</th>
</thead>
<tbody class="text-break">
{{ range .Todos }}
{{ if not .IsDone }}
<tr>
<td>{{ .Text }}</td>
<td>{{ .TimeCreated }}</td>
<td>{{ .Due }}</td>
<td>
<button class="btn btn-success" onclick="markAsDoneRefresh({{.ID}});"><img src='/static/images/check.svg'></button>
<button class="btn btn-danger" onclick="deleteTodoRefresh({{.ID}});"><img src='/static/images/trash3-fill.svg'></button>
</td>
</tr>
{{ end }}
{{ end }}
</tbody>
</table>
<!-- Completed -->
<table class="table table-hover" style="display: none;" id="completed-todos">
<thead>
<th>ToDo</th>
<th>Created</th>
<th>Completed</th>
</thead>
<tbody class="text-break">
{{ range .Todos }}
{{ if .IsDone }}
<tr>
<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>
</td>
</tr>
{{ end }}
{{ end }}
</tbody>
</table>
</div>
</div>
</main>
<script>
async function markAsDoneRefresh(id) {
await markAsDone(id);
window.location.reload();
}
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";
}
document.addEventListener('DOMContentLoaded', async function() {
document.getElementById("new-todo-text").focus();
@ -77,7 +124,9 @@ document.addEventListener('DOMContentLoaded', async function() {
// Rename the button
showDoneButton.innerText = "Show To Do";
showDoneButton.className = "btn btn-success";
// Show done
showDone();
// Make it "reset to default"
showDoneButton.addEventListener("click", (event) => {
location.reload();

110
pages/index.html

@ -16,7 +16,7 @@
<a href="/group/{{.ID}}" class="list-group-item list-group-item-action active py-3 lh-sm" aria-current="true">
<div class="d-flex w-100 align-items-center justify-content-between">
<strong class="mb-1">{{ .Name }}</strong>
<small>{{ .TimeCreatedUnix }}</small>
<small>{{ .TimeCreated }}</small>
</div>
<div class="col-10 mb-1 small">Is removable: {{ .Removable }}</div>
</a>
@ -39,94 +39,34 @@
</a>
{{ end }}
</div>
<div>
<label for="newCategory">New category</label>
<input type="text" name="newCategory" id="new-category-input" placeholder="Category Name">
<input type="submit" value="Create" onclick="createNewCategory();">
</div>
</div>
</main>
<script>
// function todoBlock(todo, editable) {
// let todoCompleteBtnID = "btn-complete-" + String(todo.id);
// let todoDeleteBtnID = "btn-delete-" + String(todo.id);
// let todoEditBtnID = "btn-edit-" + String(todo.id);
// // Display
// let timeCreated = new Date(todo.timeCreatedUnix * 1000);
// return "<tr><td>" + todo.text + "</td>" +
// "<td>" + " " +
// timeCreated.getDate() + "/" +
// (timeCreated.getMonth() + 1) + "/" +
// timeCreated.getFullYear() + "</td>" +
// "<td><button class='btn btn-success' id='" + todoCompleteBtnID + "'>" +
// "<img src='/static/images/check.svg'></button><button class='btn btn-danger' id='" +
// todoDeleteBtnID + "'><img src='/static/images/trash3-fill.svg'></button></td></tr>";
// }
// async function displayTodos(showDone) {
// // Fetch and display TODOs
// let response = await getTodos();
// if (!response.ok) {
// // window.location.replace("/error")
// return;
// }
// let todosJson = await response.json();
// if (todosJson == null) {
// return;
// }
// let todosDisplayed = [];
// let todosDiv = document.getElementById("todos");
// // Clear what we've had before
// todosDiv.innerHTML = "";
// todosJson.forEach((item) => {
// let todoBlk = todoBlock(item, item.isDone);
// todosDiv.innerHTML += todoBlk;
// });
// }
// document.addEventListener('DOMContentLoaded', async function() {
// document.getElementById("new-todo-text").focus();
// let showDoneButton = document.getElementById("show-done");
// showDoneButton.addEventListener("click", (event) => {
// displayTodos(true); // Re-display without reloading
// // Rename the button
// showDoneButton.innerText = "Show To Do";
// showDoneButton.className = "btn btn-success";
// // Make it "reset to default"
// showDoneButton.addEventListener("click", (event) => {
// location.reload();
// });
// });
// // "Add" button
// document.getElementById("new-todo-submit").addEventListener("click", async (event) => {
// let newTodoTextInput = document.getElementById("new-todo-text");
// let newTodoText = newTodoTextInput.value;
// if (newTodoText.length < 1) {
// newTodoTextInput.setCustomValidity("At least one character is needed!");
// return;
// } else {
// newTodoTextInput.setCustomValidity("");
// }
// newTodoTextInput.value = "";
// // Make a request
// let response = await postNewTodo({text: newTodoText, groupId: groupId});
// if (response.ok) {
// location.reload();
// }
// });
// // Fetch and display TODOs
// await displayTodos(false);
// }, false)
async function createNewCategory() {
let categoryInput = document.getElementById("new-category-input");
let newCategoryName = categoryInput.value;
if (newCategoryName.length < 1) {
categoryInput.setCustomValidity("At least one character is needed!");
return;
} else {
categoryInput.setCustomValidity("");
}
categoryInput.value = "";
// Post new category and refresh
await postNewGroup({
Name: newCategoryName
});
window.location.reload();
}
</script>
{{ end }}

6
scripts/api.js

@ -54,7 +54,7 @@ async function getAllGroups() {
async function del(url) {
return fetch(url, {
method: "DELETE",
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
@ -74,6 +74,10 @@ async function updateTodo(id, updatedTodo) {
return update("/api/todo/update/"+id, updatedTodo);
}
async function markAsDone(id) {
return update("/api/todo/markdone/"+id);
}
async function updateGroup(id, updatedGroup) {
return update("/api/group/update/"+id, updatedGroup);
}

14
src/db/group.go

@ -18,7 +18,10 @@
package db
import "database/sql"
import (
"database/sql"
"time"
)
// Todo group structure
type TodoGroup struct {
@ -27,6 +30,7 @@ type TodoGroup struct {
TimeCreatedUnix uint64 `json:"timeCreatedUnix"`
OwnerLogin string `json:"ownerLogin"`
Removable bool `json:"removable"`
TimeCreated string
}
func NewTodoGroup(name string, timeCreatedUnix uint64, ownerLogin string, removable bool) TodoGroup {
@ -64,6 +68,14 @@ func scanTodoGroup(rows *sql.Rows) (*TodoGroup, error) {
return nil, err
}
// Convert to Basic time
timeCreated := time.Unix(int64(newTodoGroup.TimeCreatedUnix), 0)
if timeCreated.Year() == 1970 {
newTodoGroup.TimeCreated = "None"
} else {
newTodoGroup.TimeCreated = timeCreated.Format(time.DateOnly)
}
return &newTodoGroup, nil
}

30
src/db/todo.go

@ -18,7 +18,10 @@
package db
import "database/sql"
import (
"database/sql"
"time"
)
// Todo structure
type Todo struct {
@ -30,6 +33,9 @@ type Todo struct {
OwnerLogin string `json:"ownerLogin"`
IsDone bool `json:"isDone"`
CompletionTimeUnix uint64 `json:"completionTimeUnix"`
TimeCreated string
CompletionTime string
Due string
}
func scanTodo(rows *sql.Rows) (*Todo, error) {
@ -48,6 +54,28 @@ func scanTodo(rows *sql.Rows) (*Todo, error) {
return nil, err
}
// Convert to Basic time
timeCreated := time.Unix(int64(newTodo.TimeCreatedUnix), 0)
if timeCreated.Year() == 1970 {
newTodo.TimeCreated = "None"
} else {
newTodo.TimeCreated = timeCreated.Format(time.DateOnly)
}
due := time.Unix(int64(newTodo.DueUnix), 0)
if due.Year() == 1970 {
newTodo.Due = "None"
} else {
newTodo.Due = due.Format(time.DateOnly)
}
completionTime := time.Unix(int64(newTodo.CompletionTimeUnix), 0)
if completionTime.Year() == 1970 {
newTodo.CompletionTime = "None"
} else {
newTodo.CompletionTime = completionTime.Format(time.DateOnly)
}
return &newTodo, nil
}

47
src/server/endpoints.go

@ -265,6 +265,53 @@ func (s *Server) EndpointTodoUpdate(w http.ResponseWriter, req *http.Request) {
logger.Info("[Server] Updated TODO with ID %d", todoID)
}
func (s *Server) EndpointTodoMarkDone(w http.ResponseWriter, req *http.Request) {
defer req.Body.Close()
if req.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Check authentication information
if !IsUserAuthorizedReq(req, s.db) {
http.Error(w, "Invalid user auth data", http.StatusForbidden)
return
}
// Obtain TODO ID
todoIDStr := path.Base(req.URL.Path)
todoID, err := strconv.ParseUint(todoIDStr, 10, 64)
if err != nil {
http.Error(w, "Invalid TODO ID", http.StatusBadRequest)
return
}
// Check if the user owns this TODO
if !s.db.DoesUserOwnTodo(todoID, GetLoginFromReq(req)) {
http.Error(w, "You don't own this TODO", http.StatusForbidden)
return
}
todo, err := s.db.GetTodo(todoID)
if err != nil {
http.Error(w, "Can't access this TODO", http.StatusInternalServerError)
return
}
// Update
todo.IsDone = true
todo.CompletionTimeUnix = uint64(time.Now().Unix())
err = s.db.UpdateTodo(todoID, *todo)
if err != nil {
logger.Warning("[Server] Failed to update TODO: %s", err)
http.Error(w, "Failed to update", http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
logger.Info("[Server] Marked TODO as done %d", todoID)
}
func (s *Server) EndpointTodoDelete(w http.ResponseWriter, req *http.Request) {
defer req.Body.Close()

1
src/server/server.go

@ -196,6 +196,7 @@ func New(config conf.Conf) (*Server, error) {
mux.HandleFunc("/api/todo/get", server.EndpointUserTodosGet) // Non specific
mux.HandleFunc("/api/todo/delete/", server.EndpointTodoDelete) // Specific
mux.HandleFunc("/api/todo/update/", server.EndpointTodoUpdate) // Specific
mux.HandleFunc("/api/todo/markdone/", server.EndpointTodoMarkDone) // Specific
mux.HandleFunc("/api/group/create", server.EndpointTodoGroupCreate) // Non specific
mux.HandleFunc("/api/group/get/", server.EndpointTodoGroupGet) // Specific
mux.HandleFunc("/api/group/update/", server.EndpointTodoGroupUpdate) // Specific

Loading…
Cancel
Save