Compare commits

...

10 Commits

Author SHA1 Message Date
thayol 8e8259ab42 Added null safety to pinning. 2021-10-17 01:32:39 +02:00
thayol 58f9080137 GitHub link moved to a separate file 2021-07-19 20:34:46 +02:00
thayol c004bb6bdd Added GitHub links 2021-07-19 20:03:53 +02:00
thayol cd07c1fce7 Added new section comment 2021-07-10 01:47:46 +02:00
thayol 8acc8bc44e Added category pinning 2021-07-07 23:04:25 +02:00
Thayol a2a5c55d80 Updated README 2021-07-06 04:45:13 +02:00
thayol 71048409dc Updated README 2021-07-06 04:43:29 +02:00
thayol edf9244382 Fix crash from empty category 2021-07-06 04:23:06 +02:00
thayol 4f596d0d07 Added supercategories above dates. 2021-07-06 02:05:16 +02:00
thayol 30a5f72a35 Add more timestamps and details. 2021-07-05 16:55:48 +02:00
12 changed files with 359 additions and 95 deletions
+38 -2
View File
@@ -1,2 +1,38 @@
# todo-php # To-Do List PHP
A PHP app that offers todo notes. Not safe, not recommended, but it exists.
A to-do list app that uses **unencrypted** files to store data. Not safe for anything else than self-hosted personal use.
It requires no external libraries and can run off of PHP's built-in server if needed.
Pull requests are welcome.
## Description
This is a to-do list that supports deadlines.
It is a great way of organizing your thoughts and plans.
The dates are "translated" to plain English,
which means that the deadlines are written in relative names like Tomorrow, Yesterday, Next Tuesday, etc.
This is not a scrum board like approach, more like a simplified calendar.
A calendar that has no other function than to store your upcoming events and reminders.
Unlike other calendars, all features are removed that would obscure the at-a-glance overview aspect of all notes.
It is not meant for archiving or very long term (or recurring) events, but for planning the week.
While it is not secure enough to use it in public, it could be used by multiple people simultaneously.
The app fully supports user separation. Each user can have unlimited backups of their lists that can be easily restored.
As a possible daily-driver to-do app that could serve as your home page,
it features categorization so that certain events can be viewed as distinct notes and will never get mixed up.
Editing notes is possible, but just like the "snooze" button on alarms, there are quick-access buttons for delaying reminders by a day or a week.
## Features
- Users
- Backups
- Categorization
- Note editing
- Quick delaying without manually editing
- Friendly dates (Tomorrow, Yesterday, Next Tuesday, etc.)
## Requirements
- PHP 7.4.10 or higher
+232 -66
View File
@@ -256,7 +256,7 @@ else if (!empty($_POST["action"]))
$modified = false; $modified = false;
if ($action === "add" || $action == "edit") if ($action === "add" || $action == "edit")
{ {
if (!empty($_POST["todo_item_id"])) if (isset($_POST["todo_item_id"]))
{ {
$id_to_edit = $_POST["todo_item_id"]; $id_to_edit = $_POST["todo_item_id"];
} }
@@ -285,6 +285,12 @@ else if (!empty($_POST["action"]))
} }
$now = time(); $now = time();
$created = $now;
if (isset($id_to_edit) && !empty($model["list"][$id_to_edit]) && !empty($model["list"][$id_to_edit]["created"]))
{
$created = $model["list"][$id_to_edit]["created"];
}
if (empty($model["list"])) if (empty($model["list"]))
{ {
@@ -296,11 +302,11 @@ else if (!empty($_POST["action"]))
"description" => $description, "description" => $description,
"deadline" => $deadline, "deadline" => $deadline,
"category" => $category, "category" => $category,
"created" => $now, "created" => $created,
"modified" => $now, "modified" => $now,
); );
if (!empty($id_to_edit) && !empty($model["list"][$id_to_edit])) if (isset($id_to_edit) && !empty($model["list"][$id_to_edit]))
{ {
$model["list"][$id_to_edit] = $new_item; $model["list"][$id_to_edit] = $new_item;
} }
@@ -346,6 +352,31 @@ else if (!empty($_POST["action"]))
} }
} }
} }
else if ($action === "pin" && !empty($_POST["category_name"]))
{
if (empty($model["pinned"]))
{
$model["pinned"] = array();
}
$category_name = $_POST["category_name"];
if (in_array($category_name, $model["pinned"]))
{
foreach ($model["pinned"] as $pin_key => $pin_name)
{
if ($pin_name == $category_name)
{
unset($model["pinned"][$pin_key]);
}
}
}
else
{
$model["pinned"][] = $category_name;
}
$modified = true;
}
if ($modified) if ($modified)
{ {
@@ -373,6 +404,55 @@ else
} }
// functions used by renderers
function get_fancy_date($date) : string
{
$now = time();
$now_dayth = intval(date("z", $now));
$ymd = date("y-m-d", $date);
$day = date("l", $date);
$dayth = intval(date("z", $date));
$fancy_date = $ymd;
if ($dayth - $now_dayth == 0)
{
$fancy_date = "Today ({$day})";
}
else if ($dayth - $now_dayth == 1)
{
$fancy_date = "Tomorrow ({$day})";
}
else if ($dayth - $now_dayth == -1)
{
$fancy_date = "Yesterday ({$day})";
}
else if ($dayth - $now_dayth < -7)
{
$fancy_date = "{$day} ({$ymd})";
}
else if ($dayth - $now_dayth < 0)
{
$fancy_date = "Last " . $day;
}
else if ($dayth - $now_dayth < 7)
{
$fancy_date = $day;
}
else if ($dayth - $now_dayth < 14)
{
$fancy_date = "Next " . $day;
}
else
{
$fancy_date = "{$day} ({$ymd})";
}
return $fancy_date;
}
// renderers // renderers
function todo_list(array $model = []) : string function todo_list(array $model = []) : string
{ {
@@ -388,112 +468,165 @@ function todo_list(array $model = []) : string
$te->append_block_template("CONTENT", "ADD_FORM"); $te->append_block_template("CONTENT", "ADD_FORM");
$te->append_block_template("CONTENT", "NAVBAR"); $te->append_block_template("CONTENT", "NAVBAR");
$categories = array(); $pinned_categories = array();
if (!empty($model["pinned"]))
{
$pinned_categories = array_merge($model["pinned"]);
}
$default_category = "Uncategorized";
$default_category_id = "uncategorized";
$categories = array( $default_category_id => [] );
$now = time(); $now = time();
$now_dayth = intval(date("z", $now)); $now_dayth = intval(date("z", $now));
$category_autofills = array();
if (!empty($list)) if (!empty($list))
{ {
foreach ($list as $key => $item) foreach ($list as $key => $item)
{ {
$item["id"] = $key;
$date = "Unscheduled";
if (!empty($item["deadline"]))
{
$date = get_fancy_date($item["deadline"]);
$date_int = $item["deadline"];
}
else
{
$date_int = "-1";
}
if (!empty($item["category"])) if (!empty($item["category"]))
{ {
$category = strtolower($item["category"]);
$category_name = $item["category"]; $category_name = $item["category"];
$category_basis = "custom";
}
else if (!empty($item["deadline"]))
{
$deadline = $item["deadline"];
$ymd = date("y-m-d", $deadline); $category_autofills[] = $item["category"];
$day = date("l", $deadline);
$dayth = intval(date("z", $deadline));
$category = $ymd;
if ($dayth - $now_dayth == 0)
{
$category_name = "Today ({$day})";
}
else if ($dayth - $now_dayth == 1)
{
$category_name = "Tomorrow ({$day})";
}
else if ($dayth - $now_dayth == -1)
{
$category_name = "Yesterday ({$day})";
}
else if ($dayth - $now_dayth < -7)
{
$category_name = "{$day} ({$ymd})";
}
else if ($dayth - $now_dayth < 0)
{
$category_name = "Last " . $day;
}
else if ($dayth - $now_dayth < 7)
{
$category_name = $day;
}
else if ($dayth - $now_dayth < 14)
{
$category_name = "Next " . $day;
} }
else else
{ {
$category_name = "{$day} ({$ymd})"; $category_name = $default_category;
} }
$category_basis = "date"; $category = strtolower($category_name);
} $category = str_replace(" ", "-", $category);
else $category = str_replace([ "\t", "\n", "\r", "\0", "\v" ], "--", $category);
$is_pinned = false;
if (in_array($category, $pinned_categories))
{ {
$category = "uncategorized"; $is_pinned = true;
$category_name = "Uncategorized";
$category_basis = "default";
} }
if (empty($categories[$category])) if (empty($categories[$category]))
{ {
$categories[$category] = array( $categories[$category] = array(
"title" => $category_name, "title" => $category_name,
"basis" => $category_basis, "id" => $category,
"list" => array(), "list" => array(),
"pinned" => $is_pinned,
); );
} }
$categories[$category]["list"][$key] = $item; if (empty($categories[$category]["list"][$date_int]))
{
$categories[$category]["list"][$date_int] = array(
"title" => $date,
"id" => $date_int,
"list" => array(),
);
}
$categories[$category]["list"][$date_int]["list"][$key] = $item;
} }
} }
if (!empty($categories)) if (!empty($categories))
{ {
$te->set_block("MAIN_ITEMS", ""); $te->set_block("MAIN_ITEMS", "");
} }
$te->set_block("DATALIST_AUTOFILLS", ""); $te->set_block("DATALIST_AUTOFILLS", "");
ksort($categories); foreach ($categories as $category_key => $category)
{
if (!empty($categories[$category_key]["list"]))
{
ksort($categories[$category_key]["list"]);
}
}
uasort($categories, function($a, $b)
{
global $pinned_categories;
$is_a_pinned = isset($a["pinned"]) ? $a["pinned"] : false;
$is_b_pinned = isset($b["pinned"]) ? $b["pinned"] : false;
$a_id = isset($a["id"]) ? $a["id"] : 1;
if ($is_a_pinned && $is_b_pinned)
{
return 0;
}
else if ($a_id == "uncategorized")
{
if ($is_b_pinned)
{
return 1;
}
else
{
return -1;
}
}
else if ($is_a_pinned)
{
return -1;
}
return 1;
});
$category_autofills = array_unique($category_autofills);
foreach ($category_autofills as $autofill)
{
$te->append_argumented_block("DATALIST_AUTOFILLS", "DATALIST_AUTOFILL", [
"DATALIST_AUTOFILL_DATA" => $autofill,
]);
}
if (!empty($json_requested) || isset($_GET["json"]))
{
header("Content-Type: application/json");
echo json_encode($categories);
exit(0);
}
$titles = array(); $titles = array();
foreach ($categories as $category_key => $category) foreach ($categories as $category_key => $category)
{ {
$category_name = $category["title"]; if (empty($category["list"]))
$category_basis = $category["basis"];
$te->set_block("MAIN_CATEGORY_ITEMS", "");
if ($category_basis == "custom")
{ {
$te->append_argumented_block("DATALIST_AUTOFILLS", "DATALIST_AUTOFILL", [ continue;
"DATALIST_AUTOFILL_DATA" => $category_name,
]);
} }
foreach ($category["list"] as $key => $item) $te->set_block("MAIN_CATEGORY_ITEMS", "");
foreach ($category["list"] as $date_key => $date)
{
$te->set_block("MAIN_DATE_ITEMS", "");
foreach ($date["list"] as $key => $item)
{ {
$titles[] = $item["title"]; $titles[] = $item["title"];
$deadline = "N/A";
$created = "N/A";
$modified = "N/A";
$displayed_category = "Uncategorized";
$category_raw = $item["category"];
if (empty($item["description"])) if (empty($item["description"]))
{ {
$te->set_block_template("MAIN_ITEM_SUMMARY", "MAIN_ITEM_SUMMARY_NODESC"); $te->set_block_template("MAIN_ITEM_SUMMARY", "MAIN_ITEM_SUMMARY_NODESC");
@@ -503,16 +636,49 @@ function todo_list(array $model = []) : string
$te->set_block_template("MAIN_ITEM_SUMMARY", "MAIN_ITEM_SUMMARY_DESC"); $te->set_block_template("MAIN_ITEM_SUMMARY", "MAIN_ITEM_SUMMARY_DESC");
} }
$te->append_argumented_block("MAIN_CATEGORY_ITEMS", "MAIN_ITEM", [ if (isset($item["deadline"]))
{
$deadline = date("Y-m-d", intval($item["deadline"]));
}
if (isset($item["created"]))
{
$created = date("Y-m-d", intval($item["created"]));
}
if (isset($item["modified"]))
{
$modified = date("Y-m-d", intval($item["modified"]));
}
if (!empty($item["category"]))
{
$displayed_category = $item["category"];
}
$te->append_argumented_block("MAIN_DATE_ITEMS", "MAIN_ITEM", [
"MAIN_ITEM_ID" => $key, "MAIN_ITEM_ID" => $key,
"MAIN_ITEM_TITLE" => $item["title"], "MAIN_ITEM_TITLE" => str_replace([ "\"", "'" ], [ "&quot;", "&apos;" ], $item["title"]),
"MAIN_ITEM_DESCRIPTION" => $item["description"], "MAIN_ITEM_DESCRIPTION" => str_replace([ "\"", "'" ], [ "&quot;", "&apos;" ], $item["description"]),
"MAIN_ITEM_CATEGORY" => str_replace([ "\"", "'" ], [ "&quot;", "&apos;" ], $displayed_category),
"MAIN_ITEM_CATEGORY_RAW" => $category_raw,
"MAIN_ITEM_DEADLINE" => $deadline,
"MAIN_ITEM_CREATED" => $created,
"MAIN_ITEM_MODIFIED" => $modified,
]);
}
$te->append_argumented_block("MAIN_CATEGORY_ITEMS", "MAIN_DATE", [
"MAIN_DATE_ID" => $date_key,
"MAIN_DATE_TITLE" => $date["title"],
]); ]);
} }
$te->append_argumented_block("MAIN_ITEMS", "MAIN_CATEGORY", [ $te->append_argumented_block("MAIN_ITEMS", "MAIN_CATEGORY", [
"MAIN_CATEGORY_ID" => $category_key, "MAIN_CATEGORY_ID" => $category_key,
"MAIN_CATEGORY_TITLE" => $category_name, "MAIN_CATEGORY_TITLE" => $category["title"],
"MAIN_CATEGORY_PIN_OR_UNPIN" => $category["pinned"] ? "Unpin" : "Pin",
"MAIN_CATEGORY_EXTRA_PIN_BUTTON_CLASS" => $category["id"] == $default_category_id ? "hidden" : "",
]); ]);
} }
$te->append_argumented_block("DATALISTS", "DATALIST", [ $te->append_argumented_block("DATALISTS", "DATALIST", [
+5 -3
View File
@@ -1,8 +1,9 @@
<div class="todo-add-container"> <div class="todo-add-container">
<details> <details>
<summary>New</summary> <summary id="addFormSummary">New</summary>
<form action="./" method="POST" autocomplete="off"> <form action="./" method="POST" autocomplete="off">
<input type="hidden" name="action" value="add" /> <input id="add_form_action" type="hidden" name="action" value="add" />
<input id="todo_item_id" type="hidden" name="todo_item_id" value="" />
<div class="todo-add-div"> <div class="todo-add-div">
<p>{{ ADD_NOTICE }}</p> <p>{{ ADD_NOTICE }}</p>
</div> </div>
@@ -15,6 +16,7 @@
<div class="todo-add-div"> <div class="todo-add-div">
<label class="label" for="todo_description">Description</label> <label class="label" for="todo_description">Description</label>
<input id="todo_description" type="text" name="todo_description" /> <input id="todo_description" type="text" name="todo_description" />
<i>(optional)</i>
</div> </div>
<div class="todo-add-div"> <div class="todo-add-div">
@@ -30,7 +32,7 @@
</div> </div>
<div class="todo-add-div"> <div class="todo-add-div">
<input class="todo-add-button" type="submit" value="Add" /> <input id="add_form_button" class="todo-add-button" type="submit" value="Add" />
</div> </div>
</form> </form>
</details> </details>
+1
View File
@@ -0,0 +1 @@
<p><a href="https://github.com/Thayol/todo-php"><small>GitHub</small></p>
+1
View File
@@ -17,4 +17,5 @@
</div> </div>
</form> </form>
<p>Don't have an account? <a href="./?reg">Sign up!</a></p> <p>Don't have an account? <a href="./?reg">Sign up!</a></p>
{{ GITHUB_LINK }}
</div> </div>
+9 -1
View File
@@ -1,4 +1,12 @@
<div class="todo-category" id="todo-category-{{ MAIN_CATEGORY_ID }}"> <div class="todo-category" id="todo-category-{{ MAIN_CATEGORY_ID }}">
<h3>{{ MAIN_CATEGORY_TITLE }}</h3> <h3>
{{ MAIN_CATEGORY_TITLE }}
<form style="float: right;" class="{{ MAIN_CATEGORY_EXTRA_PIN_BUTTON_CLASS }}" action="./" method="POST">
<input type="hidden" name="action" value="pin" />
<input type="hidden" name="category_name" value="{{ MAIN_CATEGORY_ID }}" />
<input type="submit" value="{{ MAIN_CATEGORY_PIN_OR_UNPIN }}" class="todo-button" />
</form>
</h3>
{{ MAIN_CATEGORY_ITEMS }} {{ MAIN_CATEGORY_ITEMS }}
</div> </div>
+4
View File
@@ -0,0 +1,4 @@
<div class="todo-category" id="todo-category-{{ MAIN_DATE_ID }}">
<h3>{{ MAIN_DATE_TITLE }}</h3>
{{ MAIN_DATE_ITEMS }}
</div>
+11
View File
@@ -2,6 +2,13 @@
<details> <details>
<summary>{{ MAIN_ITEM_SUMMARY }}</summary> <summary>{{ MAIN_ITEM_SUMMARY }}</summary>
<small><i>ID: {{ MAIN_ITEM_ID }}</i></small><br>
<small><i>Created: {{ MAIN_ITEM_CREATED }}</i></small><br>
<small><i>Modified: {{ MAIN_ITEM_MODIFIED }}</i></small><br>
Deadline: {{ MAIN_ITEM_DEADLINE }}<br>
Category: {{ MAIN_ITEM_CATEGORY }}<br>
<br>
<form action="./" method="POST"> <form action="./" method="POST">
<input type="hidden" name="action" value="delay" /> <input type="hidden" name="action" value="delay" />
<input type="hidden" name="delay_by" value="-1 week" /> <input type="hidden" name="delay_by" value="-1 week" />
@@ -35,6 +42,10 @@
<div style="display:inline-block;width:1em;"></div> <div style="display:inline-block;width:1em;"></div>
<div style="display:inline-block;width:1em;"></div> <div style="display:inline-block;width:1em;"></div>
<form onsubmit="return false">
<input type="submit" value="Edit" onclick="editNote('{{ MAIN_ITEM_ID }}', '{{ MAIN_ITEM_TITLE }}', '{{ MAIN_ITEM_DESCRIPTION }}', '{{ MAIN_ITEM_DEADLINE }}', '{{ MAIN_ITEM_CATEGORY_RAW }}')" />
</form>
<form action="./" method="POST"> <form action="./" method="POST">
<input type="hidden" name="action" value="remove" /> <input type="hidden" name="action" value="remove" />
<input type="hidden" name="todo_item_id" value="{{ MAIN_ITEM_ID }}" /> <input type="hidden" name="todo_item_id" value="{{ MAIN_ITEM_ID }}" />
+1
View File
@@ -13,4 +13,5 @@
<input type="hidden" name="logout" value="1"> <input type="hidden" name="logout" value="1">
<input type="submit" value="Log out"> <input type="submit" value="Log out">
</form> </form>
{{ GITHUB_LINK }}
</div> </div>
+1
View File
@@ -21,4 +21,5 @@
</div> </div>
</form> </form>
<p>Have an account? <a href="./?">Log in!</a></p> <p>Have an account? <a href="./?">Log in!</a></p>
{{ GITHUB_LINK }}
</div> </div>
+23
View File
@@ -1,3 +1,5 @@
var autoClose = true;
document.addEventListener("DOMContentLoaded", function(){ document.addEventListener("DOMContentLoaded", function(){
// Fetch all the details element. // Fetch all the details element.
const details = document.querySelectorAll("details"); const details = document.querySelectorAll("details");
@@ -6,11 +8,32 @@ document.addEventListener("DOMContentLoaded", function(){
details.forEach((targetDetail) => { details.forEach((targetDetail) => {
targetDetail.addEventListener("click", () => { targetDetail.addEventListener("click", () => {
// Close all the details that are not targetDetail. // Close all the details that are not targetDetail.
if (autoClose) {
details.forEach((detail) => { details.forEach((detail) => {
if (detail !== targetDetail) { if (detail !== targetDetail) {
detail.removeAttribute("open"); detail.removeAttribute("open");
} }
}); });
}
}); });
}); });
}); });
function editNote(id, title, description, deadline, category) {
document.getElementById("add_form_action").value = "edit";
document.getElementById("add_form_button").value = "Edit";
document.getElementById("todo_item_id").value = id.toString();
document.getElementById("todo_title").value = title;
document.getElementById("todo_description").value = description;
document.getElementById("todo_deadline").value = deadline;
document.getElementById("todo_category").value = category;
setTimeout(
function() {
document.getElementById("addFormSummary").click();
},
5);
// document.getElementById("addFormSummary").click();
}
+13 -3
View File
@@ -1,3 +1,7 @@
* {
box-sizing: border-box;
}
html { html {
font-family: 'Roboto', sans-serif; font-family: 'Roboto', sans-serif;
background-color: #111111; background-color: #111111;
@@ -43,7 +47,7 @@ h3 {
padding: 0; padding: 0;
display: inline-block; display: inline-block;
} }
.todo-item form input, .navbar-form input { .todo-item form input, .navbar-form input, .todo-button {
border: 2px solid #333333; border: 2px solid #333333;
padding: 5px; padding: 5px;
margin: 0; margin: 0;
@@ -51,12 +55,15 @@ h3 {
color: white; color: white;
border-radius: 4px; border-radius: 4px;
} }
.todo-item form input:focus, .navbar-form input:focus { .todo-item form input:focus, .navbar-form input:focus, .todo-button:focus {
outline: none; outline: none;
} }
.todo-item form input:active, .navbar-form input:active { .todo-item form input:active, .navbar-form input:active, .todo-button:active {
background-color: #222222; background-color: #222222;
} }
.todo-button {
margin: -5px;
}
summary, details[open] { summary, details[open] {
padding: 0.5em; padding: 0.5em;
@@ -117,6 +124,9 @@ summary:focus {
.navbar { .navbar {
text-align: right; text-align: right;
} }
.hidden {
display: none;
}