Browse Source

FEATURE: ToDo canvas image

master
parent
commit
5bc18be76f
  1. 69
      pages/category.html
  2. 99
      pages/paint.html
  3. 1
      src/db/db.go
  4. 42
      src/db/todo.go
  5. 15
      src/server/page.go
  6. 20
      src/server/server.go

69
pages/category.html

@ -20,14 +20,34 @@
Are you sure you want to delete this ToDo?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" id="confirmDeleteButton">Delete</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</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">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">Save</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" onclick="clearCanvas();">Cancel</button>
</div>
</div>
</div>
</div>
<!-- End Paint Canvas Modal -->
<!-- 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;">
@ -52,20 +72,24 @@
</div>
<!-- Main ToDos section -->
<div class="p-2 flex-grow-1">
<form action="javascript:void(0);">
<div class="container">
<div class="row">
<div class="col">
<input type="text" class="form-control" id="new-todo-text" placeholder="TODO text">
<input type="text" class="form-control" id="newTodoText" placeholder="TODO text">
</div>
<div class="col">
<label for="new-todo-due">Due</label>
<input aria-placeholder="Due" type="date" name="new-todo-due" id="new-todo-due" placeholder="Due">
<label for="newTodoDue">Due</label>
<input aria-placeholder="Due" type="date" name="newTodoDue" id="newTodoDue" placeholder="Due">
</div>
<div class="col">
<button id="new-todo-submit" class="btn btn-primary">Add</button>
<button class="btn btn-primary" id="newTodoPaint" onclick="openPaintModal();">Paint</button>
</div>
<div class="col">
<button id="newTodoSubmit" class="btn btn-primary">Add</button>
<button class="btn btn-secondary" id="show-done">Show Done</button>
</div>
</div>
@ -84,7 +108,7 @@
{{ range .Todos }}
{{ if not .IsDone }}
<tr draggable="true" id="todo-{{.ID}}" ondragstart="dragStart(event);">
<td class="todo-text">{{ .Text }}</td>
<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>
@ -128,32 +152,38 @@
</div>
</main>
<script>
<script>
let todoToDeleteId;
// TODO deletion modal functions
function openDeleteModal(id) {
todoToDeleteId = id; // Save ID for handleDelete to work properly
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', handleDelete); // Optional: Prevent multiple bindings
confirmDeleteButton.removeEventListener('click', handleTodoDelete);
// Add the event listener for the delete action
confirmDeleteButton.addEventListener('click', handleDelete);
confirmDeleteButton.addEventListener('click', handleTodoDelete);
deleteModal.show();
}
async function handleDelete() {
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);
@ -208,7 +238,7 @@ async function drop(event) {
}
document.addEventListener('DOMContentLoaded', async function() {
document.getElementById("new-todo-text").focus();
document.getElementById("newTodoText").focus();
let showDoneButton = document.getElementById("show-done");
showDoneButton.addEventListener("click", (event) => {
@ -226,8 +256,8 @@ document.addEventListener('DOMContentLoaded', async function() {
// "Add" button
document.getElementById("new-todo-submit").addEventListener("click", async (event) => {
let newTodoTextInput = document.getElementById("new-todo-text");
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!");
@ -237,14 +267,19 @@ document.addEventListener('DOMContentLoaded', async function() {
}
newTodoTextInput.value = "";
let newTodoDueInput = document.getElementById("new-todo-due");
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)}
{text: newTodoText, groupId: Number(groupId), dueUnix: Number(dueTimeStamp), image: canvasImage}
);
if (response.ok) {
location.reload();

99
pages/paint.html

@ -0,0 +1,99 @@
{{ define "paint" }}
<canvas class="row border border-dark" id="drawingCanvas" width="256" height="256"></canvas>
<input class="row border border-dark" type="color" id="colorPicker" value="#000000" aria-label="Drawing color">
<script>
const canvas = document.getElementById('drawingCanvas');
const ctx = canvas.getContext('2d');
const colorPicker = document.getElementById('colorPicker');
let drawing = false;
function startDrawing(x, y) {
drawing = true;
ctx.beginPath();
ctx.moveTo(x, y);
};
function draw(x, y){
if (drawing) {
ctx.strokeStyle = colorPicker.value;
ctx.lineWidth = 5;
ctx.lineTo(x, y);
ctx.stroke();
}
};
function stopDrawing() {
drawing = false;
ctx.closePath();
};
function getMousePos(event) {
const rect = canvas.getBoundingClientRect();
return {
x: event.clientX - rect.left,
y: event.clientY - rect.top
};
};
function getTouchPos(event) {
const rect = canvas.getBoundingClientRect();
return {
x: event.touches[0].clientX - rect.left,
y: event.touches[0].clientY - rect.top
};
};
// Mouse events
canvas.addEventListener('mousedown', (e) => {
const pos = getMousePos(e);
startDrawing(pos.x, pos.y);
});
canvas.addEventListener('mousemove', (e) => {
const pos = getMousePos(e);
draw(pos.x, pos.y);
});
canvas.addEventListener('mouseup', stopDrawing);
canvas.addEventListener('mouseleave', stopDrawing);
// Touch events
canvas.addEventListener('touchstart', (e) => {
e.preventDefault(); // Prevent scrolling
const pos = getTouchPos(e);
startDrawing(pos.x, pos.y);
});
canvas.addEventListener('touchmove', (e) => {
e.preventDefault(); // Prevent scrolling
const pos = getTouchPos(e);
draw(pos.x, pos.y);
});
canvas.addEventListener('touchend', stopDrawing);
// Fills with white
function clearCanvas() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
function isCanvasEmpty() {
const pixels = new Uint32Array(
ctx.getImageData(0, 0, canvas.width, canvas.height).data.buffer
);
return !pixels.some(color => color !== 0);
}
function getCanvasImage() {
if (!isCanvasEmpty()) {
return canvas.toDataURL("image/png");
}
return null;
}
</script>
{{ end }}

1
src/db/db.go

@ -65,6 +65,7 @@ func setUpTables(db *DB) error {
owner_login TEXT NOT NULL,
is_done INTEGER,
completion_time_unix INTEGER,
image BLOB,
FOREIGN KEY(group_id) REFERENCES todo_groups(id),
FOREIGN KEY(owner_login) REFERENCES users(login))`,
)

42
src/db/todo.go

@ -33,11 +33,21 @@ type Todo struct {
OwnerLogin string `json:"ownerLogin"`
IsDone bool `json:"isDone"`
CompletionTimeUnix uint64 `json:"completionTimeUnix"`
Image []byte `json:"image"`
TimeCreated string
CompletionTime string
Due string
}
func unixToTimeStr(unixTimeSec uint64) string {
timeUnix := time.Unix(int64(unixTimeSec), 0)
if timeUnix.Year() == 1970 {
return "None"
} else {
return timeUnix.Format(time.DateOnly)
}
}
func scanTodo(rows *sql.Rows) (*Todo, error) {
var newTodo Todo
err := rows.Scan(
@ -49,32 +59,16 @@ func scanTodo(rows *sql.Rows) (*Todo, error) {
&newTodo.OwnerLogin,
&newTodo.IsDone,
&newTodo.CompletionTimeUnix,
&newTodo.Image,
)
if err != nil {
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)
}
newTodo.TimeCreated = unixToTimeStr(newTodo.TimeCreatedUnix)
newTodo.Due = unixToTimeStr(newTodo.DueUnix)
newTodo.CompletionTime = unixToTimeStr(newTodo.CompletionTimeUnix)
return &newTodo, nil
}
@ -123,7 +117,7 @@ func (db *DB) GetTodos() ([]*Todo, error) {
// Creates a new TODO in the database
func (db *DB) CreateTodo(todo Todo) error {
_, err := db.Exec(
"INSERT INTO todos(group_id, text, time_created_unix, due_unix, owner_login, is_done, completion_time_unix) VALUES(?, ?, ?, ?, ?, ?, ?)",
"INSERT INTO todos(group_id, text, time_created_unix, due_unix, owner_login, is_done, completion_time_unix, image) VALUES(?, ?, ?, ?, ?, ?, ?, ?)",
todo.GroupID,
todo.Text,
todo.TimeCreatedUnix,
@ -131,6 +125,7 @@ func (db *DB) CreateTodo(todo Todo) error {
todo.OwnerLogin,
todo.IsDone,
todo.CompletionTimeUnix,
todo.Image,
)
return err
@ -146,15 +141,16 @@ func (db *DB) DeleteTodo(id uint64) error {
return err
}
// Updates TODO's due date, text, done state, completion time and group id
// Updates TODO's due date, text, done state, completion time and group id with image
func (db *DB) UpdateTodo(todoID uint64, updatedTodo Todo) error {
_, err := db.Exec(
"UPDATE todos SET group_id=?, due_unix=?, text=?, is_done=?, completion_time_unix=? WHERE id=?",
"UPDATE todos SET group_id=?, due_unix=?, text=?, is_done=?, completion_time_unix=?, image=? WHERE id=?",
updatedTodo.GroupID,
updatedTodo.DueUnix,
updatedTodo.Text,
updatedTodo.IsDone,
updatedTodo.CompletionTimeUnix,
updatedTodo.Image,
todoID,
)

15
src/server/page.go

@ -20,23 +20,8 @@ package server
import (
"Unbewohnte/dela/db"
"html/template"
"path/filepath"
)
// Constructs a pageName template via inserting basePageName in pagesDir
func getPage(pagesDir string, basePageName string, pageName string) (*template.Template, error) {
page, err := template.ParseFiles(
filepath.Join(pagesDir, basePageName),
filepath.Join(pagesDir, pageName),
)
if err != nil {
return nil, err
}
return page, nil
}
type IndexPageData struct {
Groups []*db.TodoGroup `json:"groups"`
}

20
src/server/server.go

@ -30,6 +30,7 @@ import (
"path"
"path/filepath"
"strconv"
"text/template"
"time"
)
@ -105,6 +106,7 @@ func New(config conf.Conf) (*Server, error) {
)
// handle page requests
pagesDirPath := filepath.Join(server.config.BaseContentDir, PagesDirName)
mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
if req.Method != "GET" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
@ -118,8 +120,9 @@ func New(config conf.Conf) (*Server, error) {
return
}
requestedPage, err := getPage(
filepath.Join(server.config.BaseContentDir, PagesDirName), "base.html", "index.html",
requestedPage, err := template.ParseFiles(
filepath.Join(pagesDirPath, "base.html"),
filepath.Join(pagesDirPath, "index.html"),
)
if err != nil {
http.Redirect(w, req, "/error", http.StatusTemporaryRedirect)
@ -166,8 +169,10 @@ func New(config conf.Conf) (*Server, error) {
return
}
requestedPage, err := getPage(
filepath.Join(server.config.BaseContentDir, PagesDirName), "base.html", "category.html",
requestedPage, err := template.ParseFiles(
filepath.Join(pagesDirPath, "base.html"),
filepath.Join(pagesDirPath, "paint.html"),
filepath.Join(pagesDirPath, "category.html"),
)
if err != nil {
http.Redirect(w, req, "/error", http.StatusTemporaryRedirect)
@ -192,10 +197,9 @@ func New(config conf.Conf) (*Server, error) {
} else {
// default
requestedPage, err := getPage(
filepath.Join(server.config.BaseContentDir, PagesDirName),
"base.html",
req.URL.Path[1:]+".html",
requestedPage, err := template.ParseFiles(
filepath.Join(pagesDirPath, "base.html"),
filepath.Join(pagesDirPath, req.URL.Path[1:]+".html"),
)
if err == nil {
err = requestedPage.ExecuteTemplate(w, req.URL.Path[1:]+".html", nil)

Loading…
Cancel
Save