diff options
Diffstat (limited to 'pages/docs.inc')
-rw-r--r-- | pages/docs.inc | 361 |
1 files changed, 361 insertions, 0 deletions
diff --git a/pages/docs.inc b/pages/docs.inc new file mode 100644 index 0000000..f893e95 --- /dev/null +++ b/pages/docs.inc @@ -0,0 +1,361 @@ +<?php + +require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/init.inc"; global $title; global $isLoggedIn; global $_PROFILE; +require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/random.inc"; + +$parts = explode("/", $_GET['_']); +$select = $parts[2] ?? null; + +if ($select === "add") { + $id = random(); + file_put_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/docs/" . $id . ".json", json_encode([ + "name" => "Untitled document ($id)", + "category" => null, + "contents" => "<div class='alert alert-primary'>This is a new document you just created.</div>", + "last" => [ + "author" => $_PROFILE["login"], + "date" => time() + ] + ])); + + header("Location: /-/docs/$id"); + die(); +} elseif (isset($select)) { + if (ctype_alnum($select) && file_exists($_SERVER['DOCUMENT_ROOT'] . "/includes/data/docs/" . $select . ".json")) { + $id = $select; + $data = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/docs/" . $select . ".json"), true); + $titleBase = " · " . $title . " · Cold Haze"; + $title = $data["name"] . " · " . $title; + } else { + header("Location: /-/docs"); + die(); + } +} + +require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/header.inc'; + +?> + +<br> +<div class="container"> + <div> + <?php if (isset($data)): ?><div id="page-content"> + <h2> + <span contenteditable="true" id="document-name" style="outline: none;"><?= $data["name"] ?></span> + <a href="/-/docs" class="small btn btn-outline-light" style="float:right;margin-top:5px;vertical-align:middle;opacity:1 !important; color:white;">Back</a> + </h2> + <p><b>Category:</b> <span id="category" contenteditable="true" style="outline: none;"><?= $data["category"] ?? "Unsorted" ?></span></p> + <p> + <?php if ($data["last"]["date"] === 0): ?> + Last modified <span id="last-edit-time" class="relative-time" data-relative-timestamp="">never</span> + <?php else: ?> + Last modified <span id="last-edit-time" class="relative-time" data-relative-timestamp="<?= $data["last"]["date"] ?>"><?= timeAgo($data["last"]["date"]) ?></span> + <?php if ($data["last"]["author"] !== $_PROFILE["login"]): ?> + <span id="last-edit-author">by <?= $data["last"]["author"] === "raindrops" ? "the Raindrops System" : "the Cloudburst System" ?></span> + <?php endif; ?> + <?php endif; ?> + · <span id="editor-save-status" class="text-muted">Saved</span> + </p> + + <textarea id="editor"><?= $data["contents"] ?></textarea> + + <script src="/assets/editor/editor.js"></script> + <script> + let editor; + ClassicEditor + .create( document.querySelector( '#editor' ), { + toolbar: [ + 'undo', 'redo', '|', 'removeFormat', '|', 'heading', '|', 'fontSize', 'fontColor', 'fontBackgroundColor', 'alignment', '|', 'bold', 'italic', 'underline', 'strikethrough', '|', 'subscript', 'superscript', '|', 'code', '|', 'outdent', 'indent', '|', 'bulletedList', 'numberedList', '|', 'link', 'imageUpload', 'mediaEmbed', 'blockQuote', 'insertTable', 'codeBlock', '|', 'horizontalLine' + ] + } ) + + .then( newEditor => { + editor = newEditor; + } ) + .catch( error => { + console.error( error ); + } ); + </script> + <!--suppress CssUnresolvedCustomProperty --> + <style> + :root { + --ck-color-base-background: transparent; + } + + .ck-toolbar { + filter: invert(1); + border-bottom-left-radius: var(--ck-border-radius) !important; + border-bottom-right-radius: var(--ck-border-radius) !important; + border-bottom-width: 1px !important; + } + + .ck-tooltip__text { + color: white !important; + } + + .ck-dropdown__panel { + background: #ddd !important; + } + + .ck-color-grid__tile { + filter: invert(1); + } + + .ck-balloon-rotator { + background-color: #ccc !important; + } + + .ck-balloon-panel { + filter: invert(1); + } + + .ck-editor__editable { + border-color: transparent !important; + } + </style> + <script> + + let titleBase = "<?= $titleBase ?>"; + let lastSavedData = editor.getData(); + let lastFetchedData = editor.getData(); + let timeSinceLastModified = 0; + let saving = false; + + async function save() { + let data = editor.getData(); + document.getElementById("editor-save-status").innerHTML = "Saving..."; + document.getElementById("editor-save-status").classList.remove("text-danger"); + document.getElementById("editor-save-status").classList.remove("text-muted"); + document.getElementById("editor-save-status").classList.remove("text-warning"); + document.getElementById("editor-save-status").classList.add("text-primary"); + saving = true; + + try { + await window.fetch("/api/docs?id=<?= $id ?>", { + method: "POST", + body: JSON.stringify({ content: data, name: document.getElementById("document-name").innerText, category: document.getElementById("category").innerText }) + }); + document.getElementById("last-edit-time").setAttribute("data-relative-timestamp", (new Date().getTime() / 1000).toFixed(0)); + if (document.getElementById("last-edit-author")) document.getElementById("last-edit-author").outerHTML = ""; + document.title = document.getElementById("document-name").innerText + titleBase; + document.getElementById("editor-save-status").innerHTML = "Saved"; + document.getElementById("editor-save-status").classList.remove("text-danger"); + document.getElementById("editor-save-status").classList.add("text-muted"); + document.getElementById("editor-save-status").classList.remove("text-warning"); + document.getElementById("editor-save-status").classList.remove("text-primary"); + lastSavedData = data; + saving = false; + } catch (e) { + console.error(e); + document.getElementById("editor-save-status").innerHTML = "Failed to save"; + document.getElementById("editor-save-status").classList.add("text-danger"); + document.getElementById("editor-save-status").classList.remove("text-muted"); + document.getElementById("editor-save-status").classList.remove("text-warning"); + document.getElementById("editor-save-status").classList.remove("text-primary"); + } + } + + document.onclick = async () => { + if (saving) return; + + if (editor.getData() !== lastSavedData) { + await save(); + } + } + + setInterval(async () => { + if (saving) return; + + if (editor.getData() !== lastSavedData) { + document.getElementById("editor-save-status").innerHTML = "Modified"; + document.getElementById("editor-save-status").classList.remove("text-danger"); + document.getElementById("editor-save-status").classList.remove("text-muted"); + document.getElementById("editor-save-status").classList.add("text-warning"); + document.getElementById("editor-save-status").classList.remove("text-primary"); + + if (editor.getData() !== lastFetchedData) { + lastFetchedData = editor.getData(); + timeSinceLastModified = 0; + } else { + timeSinceLastModified++; + } + + if (timeSinceLastModified > 20 || !document.hasFocus()) { + await save(); + } + } else { + timeSinceLastModified = 0; + } + }, 100) + + </script> + </div><?php else: ?> + <h2>Documents</h2> + <div id="list"> + <?php + + $documents = array_map(function ($i) { + return [ + "id" => substr($i, 0, -5), + ...(json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/docs/" . $i), true) ?? []) + ]; + }, array_filter(scandir($_SERVER['DOCUMENT_ROOT'] . "/includes/data/docs"), function ($i) { + return !str_starts_with($i, ".") && str_ends_with($i, ".json"); + })); + + $deletable = array_values(array_filter($documents, function ($i) { + return strip_tags($i["contents"]) === "/delete"; + })); + $unsorted_pre = array_values(array_filter($documents, function ($i) { + return strip_tags($i["contents"]) !== "/delete"; + })); + + $unsorted = []; + $categories = []; + foreach ($unsorted_pre as $item) { + if (isset($item["category"])) { + $existing_categories = array_keys($categories); + $matched_category = null; + + foreach ($existing_categories as $existing_category) { + if (levenshtein($item["category"], $existing_category) < 3) { + $matched_category = $existing_category; + } + } + + $selected_category = $matched_category ?? $item["category"]; + if (!isset($categories[$selected_category])) $categories[$selected_category] = []; + $categories[$selected_category][] = $item; + } else { + $unsorted[] = $item; + } + } + + uasort($unsorted, function ($a, $b) { + return $b["last"]["date"] - $a["last"]["date"]; + }); + + uasort($deletable, function ($a, $b) { + return $b["last"]["date"] - $a["last"]["date"]; + }); + + ?> + + <?php foreach ($categories as $category => $items): ?> + <h4><?= $category ?></h4><div class="list-group"> + <?php foreach ($items as $item): ?> + <a href="/-/docs/<?= $item["id"] ?>" id="document-<?= $item["id"] ?>" class="list-group-item list-group-item-action document-listing <?php if (strip_tags($item["contents"]) === "/delete"): ?>opacity-75<?php endif; ?>"> + <?= $item["name"] ?> + <?php if (strip_tags($item["contents"]) === "/delete"): ?> + <span class="badge bg-warning rounded-pill text-black">Deleting in <?= round((($item["last"]["date"] + 86400) - time()) / 3600) ?> hours</span> + <?php else: ?> + <span class="relative-time text-muted" data-relative-timestamp="<?= $item["last"]["date"] ?>"><?= timeAgo($item["last"]["date"]) ?></span> + <?php endif; ?> + </a> + <?php endforeach; ?></div> + <hr> + <?php endforeach; ?> + + <h4>Unsorted</h4><div class="list-group"> + <?php foreach ($unsorted as $item): ?> + <a href="/-/docs/<?= $item["id"] ?>" id="document-<?= $item["id"] ?>" class="list-group-item list-group-item-action document-listing <?php if (strip_tags($item["contents"]) === "/delete"): ?>opacity-75<?php endif; ?>"> + <?= $item["name"] ?> + <?php if (strip_tags($item["contents"]) === "/delete"): ?> + <span class="badge bg-warning rounded-pill text-black">Deleting in <?= round((($item["last"]["date"] + 86400) - time()) / 3600) ?> hours</span> + <?php else: ?> + <span class="relative-time text-muted" data-relative-timestamp="<?= $item["last"]["date"] ?>"><?= timeAgo($item["last"]["date"]) ?></span> + <?php endif; ?> + </a> + <?php endforeach; ?></div> + + <hr> + <h4>Marked for deletion</h4><div class="list-group"> + <?php foreach ($deletable as $item): ?> + <a href="/-/docs/<?= $item["id"] ?>" id="document-<?= $item["id"] ?>" class="list-group-item list-group-item-action document-listing <?php if (strip_tags($item["contents"]) === "/delete"): ?>opacity-75<?php endif; ?>"> + <?= $item["name"] ?> + <?php if (strip_tags($item["contents"]) === "/delete"): ?> + <span class="badge bg-warning rounded-pill text-black">Deleting in <?= round((($item["last"]["date"] + 86400) - time()) / 3600) ?> hours</span> + <?php else: ?> + <span class="relative-time text-muted" data-relative-timestamp="<?= $item["last"]["date"] ?>"><?= timeAgo($item["last"]["date"]) ?></span> + <?php endif; ?> + </a> + <?php endforeach; ?></div> + </div> + + <hr> + <div id="page-content"> + <a href="/-/docs/add">Create a new document</a> + </div> + <?php endif; ?> + </div> +</div> + +<script> + setInterval(async () => { + Array.from(document.getElementsByClassName("relative-time")).forEach((el) => { + el.innerText = timeAgo(parseInt(el.getAttribute("data-relative-timestamp")) * 1000); + }) + }, 1000) + + function timeAgo(time) { + if (!isNaN(parseInt(time))) { + time = new Date(time).getTime(); + } + + let periods = ["sec.", "mn.", "hr.", "d.", "wk.", "mo.", "y.", "ages"]; + + let lengths = ["60", "60", "24", "7", "4.35", "12", "100"]; + + let now = new Date().getTime(); + + let difference = Math.round((now - time) / 1000); + let tense; + let period; + + if (difference <= 10 && difference >= 0) { + return "now"; + } else if (difference > 0) { + tense = "ago"; + } else { + tense = "later"; + } + + let j; + + for (j = 0; difference >= lengths[j] && j < lengths.length - 1; j++) { + difference /= lengths[j]; + } + + difference = Math.round(difference); + + period = periods[j]; + + return `${difference} ${period} ${tense}`; + } +</script> +<style> + .list-group-item { + color: #fff; + background-color: #222; + border: 1px solid rgba(255, 255, 255, .125); + } + + .list-group-item.disabled { + color: #fff; + background-color: #222; + border-color: rgba(255, 255, 255, .125); + opacity: .75; + } + + .list-group-item:hover { + background-color: #252525; + color: #ddd; + } + + .list-group-item:active, .list-group-item:focus { + background-color: #272727; + color: #bbb; + } +</style> + +<?php require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/footer.inc'; ?> |