summaryrefslogtreecommitdiff
path: root/pages/docs.inc
diff options
context:
space:
mode:
authorMinteck <contact@minteck.org>2022-10-10 20:51:39 +0200
committerMinteck <contact@minteck.org>2022-10-10 20:51:39 +0200
commit108525534c28013cfe1897c30e4565f9893f3766 (patch)
treedd3e5132971f96ab5f05e7f3f8f6dbbf379a19bd /pages/docs.inc
parent2162eaa06f7e4764eb3dcfe130ec2c711d0c62ab (diff)
downloadpluralconnect-108525534c28013cfe1897c30e4565f9893f3766.tar.gz
pluralconnect-108525534c28013cfe1897c30e4565f9893f3766.tar.bz2
pluralconnect-108525534c28013cfe1897c30e4565f9893f3766.zip
Update
Diffstat (limited to 'pages/docs.inc')
-rw-r--r--pages/docs.inc361
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"] ?>&nbsp;
+ <?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"] ?>&nbsp;
+ <?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"] ?>&nbsp;
+ <?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'; ?>