Added Routing
authorAdam Dullage <redacted>
Thu, 17 Feb 2022 12:36:40 +0000 (12:36 +0000)
committerAdam Dullage <redacted>
Thu, 17 Feb 2022 12:36:40 +0000 (12:36 +0000)
.gitignore
README.md
flatnotes/main.py
flatnotes/src/api.js
flatnotes/src/components/App.js
flatnotes/src/components/App.vue
flatnotes/src/components/classes.js
flatnotes/src/constants.js [new file with mode: 0644]
flatnotes/src/helpers.js [new file with mode: 0644]
flatnotes/src/main.scss

index 53dadd98607aac3ab3f5550d769507a34d7969c7..fb14dd3a37e3938afd9ee73227ccfcdcd4ed9474 100644 (file)
@@ -248,3 +248,4 @@ dist
 # Custom
 .vscode/
 data/
+notes/
index 8a82143bcb3004538122740eb6478bae2c51787c..2c1d3594447c29d9ff4a88f70b983627b852ed30 100644 (file)
--- a/README.md
+++ b/README.md
@@ -34,9 +34,15 @@ This is what flatnotes aims to achieve.
 * [x] Password Authentication
 * [x] Ability to Create a Note
 * [x] Ability to Rename a Note
+* [x] Routing
+* [ ] Loading & Not Found Indicators
+* [ ] Ability to Delete a Note
+* [ ] / to search
+* [ ] e to edit
+* [ ] CTRL-S to save
+* [ ] Drafts
+* [ ] Image Embedding
+* [ ] Index Page (alphabetically sorted note list)
 * [ ] Clean & Responsive UI
 * [ ] Public URL Sharing
-* [ ] Image Embedding
 * [ ] Attachment Upload
-* [ ] Ability to Delete a Note
-* [ ] Error Handling
index 4123faab13c74efdc7bb65caba9143c9900bfb5d..7d7ea21a70cbbcbf61edf4dd80b82a5a5a287a08 100644 (file)
@@ -45,7 +45,10 @@ async def token(data: LoginModel):
 
 
 @app.get("/")
-async def root():
+@app.get("/login")
+@app.get("/search")
+@app.get("/note/{filename}")
+async def root(filename: str = ""):
     with open("flatnotes/dist/index.html", "r", encoding="utf-8") as f:
         html = f.read()
     return HTMLResponse(content=html)
index b1912cf169949dec4731e0e2d8e2f4e0610f93b8..3b3c336c33c0ee6009403270edde8d49be84e38a 100644 (file)
@@ -1,5 +1,5 @@
 import axios from "axios";
-import EventBus from "./eventBus.js";
+import * as constants from "./constants";
 
 const api = axios.create();
 
@@ -25,7 +25,12 @@ api.interceptors.response.use(
       typeof error.response !== "undefined" &&
       error.response.status === 401
     ) {
-      EventBus.$emit("logout");
+      window.open(
+        `/${constants.basePaths.login}?${constants.params.redirect}=${encodeURI(
+          window.location.pathname + window.location.search
+        )}`,
+        "_self"
+      );
     }
     return Promise.reject(error);
   }
index 93264b455878bdd8c5d13dfda1d88fab1a37a677..393c7e1eef69d07f65c6e5cd593c9c3dca800958 100644 (file)
@@ -4,8 +4,10 @@ import { Editor } from "@toast-ui/vue-editor";
 import { Viewer } from "@toast-ui/vue-editor";
 
 import api from "../api";
+import * as constants from "../constants";
 import { Note, SearchResult } from "./classes";
 import EventBus from "../eventBus";
+import * as helpers from "../helpers";
 
 export default {
   components: {
@@ -15,7 +17,13 @@ export default {
 
   data: function() {
     return {
-      loggedIn: false,
+      views: {
+        login: 0,
+        home: 1,
+        note: 2,
+        search: 3,
+      },
+      currentView: 1,
       usernameInput: null,
       passwordInput: null,
       rememberMeInput: false,
@@ -30,29 +38,6 @@ export default {
   },
 
   computed: {
-    currentView: function() {
-      // 4 - Login
-      if (this.loggedIn == false) {
-        return 4;
-      }
-      // 3 - Edit Note
-      else if (this.currentNote && this.editMode) {
-        return 3;
-      }
-      // 2 - View Note
-      else if (this.currentNote) {
-        return 2;
-      }
-      // 1 - Search Results
-      else if (this.searchResults) {
-        return 1;
-      }
-      // 0 - Notes List
-      else {
-        return 0;
-      }
-    },
-
     notesByLastModifiedDesc: function() {
       return this.notes.sort(function(a, b) {
         return b.lastModified - a.lastModified;
@@ -60,18 +45,55 @@ export default {
     },
   },
 
-  watch: {
-    searchTerm: function() {
-      this.clearSearchTimeout();
-      if (this.searchTerm) {
-        this.startSearchTimeout();
-      } else {
-        this.searchResults = null;
+  methods: {
+    route: function() {
+      let path = window.location.pathname.split("/");
+      let basePath = path[1];
+
+      // Home Page
+      if (basePath == "") {
+        this.getNotes();
+        this.currentView = this.views.home;
+      }
+
+      // Search
+      else if (basePath == constants.basePaths.search) {
+        this.searchTerm = helpers.getSearchParam(constants.params.searchTerm);
+        this.getSearchResults();
+        this.currentView = this.views.search;
+      }
+
+      // Note
+      else if (basePath == constants.basePaths.note) {
+        let noteTitle = path[2];
+        this.loadNote(noteTitle);
+        this.currentView = this.views.note;
+      }
+
+      // Login
+      else if (basePath == constants.basePaths.login) {
+        this.currentView = this.views.login;
       }
+
+      this.updateDocumentTitle();
+    },
+
+    updateDocumentTitle: function() {
+      let pageTitleSuffix = null;
+      if (this.currentView == this.views.login) {
+        pageTitleSuffix = "Login";
+      } else if (this.currentView == this.views.search) {
+        pageTitleSuffix = "Search";
+      } else if (
+        this.currentView == this.views.note &&
+        this.currentNote != null
+      ) {
+        pageTitleSuffix = this.currentNote.title;
+      }
+      window.document.title =
+        (pageTitleSuffix ? `${pageTitleSuffix} - ` : "") + "flatnotes";
     },
-  },
 
-  methods: {
     login: function() {
       let parent = this;
       api
@@ -84,8 +106,8 @@ export default {
           if (parent.rememberMeInput == true) {
             localStorage.setItem("token", response.data.access_token);
           }
-          parent.loggedIn = true;
-          parent.getNotes();
+          let redirectPath = helpers.getSearchParam(constants.params.redirect);
+          window.open(redirectPath || "/", "_self");
         })
         .finally(function() {
           parent.usernameInput = null;
@@ -97,7 +119,7 @@ export default {
     logout: function() {
       sessionStorage.removeItem("token");
       localStorage.removeItem("token");
-      this.loggedIn = false;
+      window.open(`/${constants.basePaths.login}`, "_self");
     },
 
     getNotes: function() {
@@ -110,61 +132,60 @@ export default {
       });
     },
 
-    clearSearchTimeout: function() {
-      if (this.searchTimeout != null) {
-        clearTimeout(this.searchTimeout);
-      }
-    },
-
-    startSearchTimeout: function() {
-      this.clearSearchTimeout();
-      this.searchTimeout = setTimeout(this.search, 1000);
+    search: function() {
+      window.open(
+        `/${constants.basePaths.search}?${
+          constants.params.searchTerm
+        }=${encodeURI(this.searchTerm)}`,
+        "_self"
+      );
     },
 
-    search: function() {
-      let parent = this;
-      this.clearSearchTimeout();
-      if (this.searchTerm) {
-        api
-          .get("/api/search", { params: { term: this.searchTerm } })
-          .then(function(response) {
-            parent.searchResults = [];
-            response.data.forEach(function(result) {
-              parent.searchResults.push(
-                new SearchResult(
-                  result.filename,
-                  result.lastModified,
-                  result.titleHighlights,
-                  result.contentHighlights
-                )
-              );
-            });
+    getSearchResults: function() {
+      var parent = this;
+      api
+        .get("/api/search", { params: { term: this.searchTerm } })
+        .then(function(response) {
+          parent.searchResults = [];
+          response.data.forEach(function(result) {
+            parent.searchResults.push(
+              new SearchResult(
+                result.filename,
+                result.lastModified,
+                result.titleHighlights,
+                result.contentHighlights
+              )
+            );
           });
-      }
+        });
     },
 
     loadNote: function(filename) {
       let parent = this;
-      api.get(`/api/notes/${filename}`).then(function(response) {
-        parent.currentNote = response.data;
-        parent.newFilename = parent.currentNote.filename;
-      });
+      api
+        .get(`/api/notes/${filename}.${constants.markdownExt}`)
+        .then(function(response) {
+          parent.currentNote = new Note(
+            response.data.filename,
+            response.data.lastModified,
+            response.data.content
+          );
+          parent.newFilename = parent.currentNote.filename;
+          parent.updateDocumentTitle();
+        });
+    },
+
+    toggleEditMode: function() {
+      this.editMode = !this.editMode;
     },
 
     newNote: function() {
       this.currentNote = new Note();
       this.editMode = true;
-    },
-
-    unloadNote: function() {
-      this.currentNote = null;
-      this.newFilename = null;
-      this.editMode = false;
-      this.getNotes();
+      this.currentView = this.views.note;
     },
 
     saveNote: function() {
-      let parent = this;
       let newContent = this.$refs.toastUiEditor.invoke("getMarkdown");
 
       // New Note
@@ -174,11 +195,7 @@ export default {
             filename: this.newFilename,
             content: newContent,
           })
-          .then(function(response) {
-            parent.currentNote = response.data;
-            parent.newFilename = parent.currentNote.filename;
-            parent.editMode = false;
-          });
+          .then(this.saveNoteResponseHandler);
       }
 
       // Modified Note
@@ -191,18 +208,26 @@ export default {
             newFilename: this.newFilename,
             newContent: newContent,
           })
-          .then(function(response) {
-            parent.currentNote = response.data;
-            parent.newFilename = parent.currentNote.filename;
-            parent.editMode = false;
-          });
+          .then(this.saveNoteResponseHandler);
       }
 
       // No Change
       else {
-        this.editMode = false;
+        this.toggleEditMode();
       }
     },
+
+    saveNoteResponseHandler: function(response) {
+      this.currentNote = new Note(
+        response.data.filename,
+        response.data.lastModified,
+        response.data.content
+      );
+      this.newFilename = this.currentNote.filename;
+      this.updateDocumentTitle();
+      history.replaceState(null, "", this.currentNote.href);
+      this.toggleEditMode();
+    },
   },
 
   created: function() {
@@ -211,8 +236,8 @@ export default {
     let token = localStorage.getItem("token");
     if (token != null) {
       sessionStorage.setItem("token", token);
-      this.loggedIn = true;
-      this.getNotes();
     }
+
+    this.route();
   },
 };
index 58fce648cecef1992a0d10408f6e42973dadbe00..b960d40559fef916e7eb187cb1bec42d067a5700 100644 (file)
@@ -3,14 +3,53 @@
     <div>
       <!-- Header -->
       <div class="mt-4 mb-4">
-        <h1 class="text-center">flatnotes</h1>
+        <h1 class="h1 clickable-link text-center"><a href="/">flatnotes</a></h1>
+      </div>
+
+      <!-- Login -->
+      <div
+        v-if="currentView == views.login"
+        class="d-flex justify-content-center"
+      >
+        <form v-on:submit.prevent="login">
+          <div class="mb-3">
+            <label for="username" class="form-label">Username</label>
+            <input
+              type="text"
+              class="form-control"
+              id="username"
+              autocomplete="username"
+              v-model="usernameInput"
+            />
+          </div>
+          <div class="mb-3">
+            <label for="password" class="form-label">Password</label>
+            <input
+              type="password"
+              class="form-control"
+              id="password"
+              autocomplete="current-password"
+              v-model="passwordInput"
+            />
+          </div>
+          <div class="mb-3 form-check">
+            <input
+              type="checkbox"
+              class="form-check-input"
+              id="rememberMe"
+              v-model="rememberMeInput"
+            />
+            <label class="form-check-label" for="rememberMe">Remember Me</label>
+          </div>
+          <button type="submit" class="btn btn-primary">Log In</button>
+        </form>
       </div>
 
       <!-- Buttons -->
       <div v-if="currentView != 4" class="d-flex justify-content-center mb-4">
         <!-- Logout -->
         <button
-          v-if="currentView == 0"
+          v-if="currentView == views.home"
           type="button"
           class="btn btn-light mx-1"
           @click="logout"
@@ -20,7 +59,7 @@
 
         <!-- New -->
         <button
-          v-if="currentView == 0"
+          v-if="currentView == views.home"
           type="button"
           class="btn btn-primary mx-1"
           @click="newNote"
         </button>
 
         <!-- Close -->
-        <button
-          v-if="currentView == 2"
-          type="button"
-          class="btn btn-secondary mx-1"
-          @click="unloadNote"
-        >
-          Close
-        </button>
+        <a href="/">
+          <button
+            v-if="currentView == 2 && editMode == false"
+            type="button"
+            class="btn btn-secondary mx-1"
+          >
+            Close
+          </button>
+        </a>
 
         <!-- Edit -->
         <button
-          v-if="currentView == 2"
+          v-if="currentView == views.note && editMode == false"
           type="button"
           class="btn btn-warning mx-1"
-          @click="editMode = true"
+          @click="toggleEditMode"
         >
           Edit
         </button>
 
         <!-- Cancel -->
         <button
-          v-if="currentView == 3"
+          v-if="currentView == views.note && editMode == true"
           type="button"
           class="btn btn-secondary mx-1"
-          @click="editMode = false"
+          @click="toggleEditMode"
         >
           Cancel
         </button>
 
         <!-- Save -->
         <button
-          v-if="currentView == 3"
+          v-if="currentView == views.note && editMode == true"
           type="button"
           class="btn btn-success mx-1"
           @click="saveNote"
         </button>
       </div>
 
-      <!-- Login -->
-      <div v-if="currentView == 4" class="d-flex justify-content-center">
-        <form v-on:submit.prevent="login">
-          <div class="mb-3">
-            <label for="username" class="form-label">Username</label>
-            <input
-              type="text"
-              class="form-control"
-              id="username"
-              autocomplete="username"
-              v-model="usernameInput"
-            />
-          </div>
-          <div class="mb-3">
-            <label for="password" class="form-label">Password</label>
-            <input
-              type="password"
-              class="form-control"
-              id="password"
-              autocomplete="current-password"
-              v-model="passwordInput"
-            />
-          </div>
-          <div class="mb-3 form-check">
-            <input
-              type="checkbox"
-              class="form-check-input"
-              id="rememberMe"
-              v-model="rememberMeInput"
-            />
-            <label class="form-check-label" for="rememberMe">Remember Me</label>
-          </div>
-          <button type="submit" class="btn btn-primary">Log In</button>
-        </form>
-      </div>
+      <!-- Search Input -->
+      <form
+        v-if="[views.search, views.home].includes(currentView)"
+        v-on:submit.prevent="search"
+      >
+        <div class="form-group mb-4 d-flex justify-content-center">
+          <input
+            type="text"
+            class="form-control"
+            placeholder="Search"
+            v-model="searchTerm"
+            style="max-width: 500px"
+          />
+        </div>
+      </form>
 
-      <!-- Viewer -->
-      <div v-else-if="currentView == 2">
-        <viewer :initialValue="currentNote.content" height="600px" />
-      </div>
+      <!-- Note -->
+      <div v-if="currentView == views.note && currentNote != null">
+        <!-- Viewer -->
+        <div v-if="editMode == false">
+          <viewer :initialValue="currentNote.content" height="600px" />
+        </div>
 
-      <!-- Editor -->
-      <div v-else-if="currentView == 3">
-        <input type="text" class="form-control" v-model="newFilename" />
-        <editor
-          :initialValue="currentNote.content"
-          previewStyle="tab"
-          height="calc(100vh - 180px)"
-          ref="toastUiEditor"
-        />
+        <!-- Editor -->
+        <div v-else>
+          <input type="text" class="form-control" v-model="newFilename" />
+          <editor
+            :initialValue="currentNote.content"
+            previewStyle="tab"
+            height="calc(100vh - 180px)"
+            ref="toastUiEditor"
+          />
+        </div>
       </div>
 
-      <!-- Front Page -->
-      <div v-else>
-        <!-- Search Input -->
-        <form v-on:submit.prevent="search">
-          <div class="form-group mb-4 d-flex justify-content-center">
-            <input
-              type="text"
-              class="form-control"
-              placeholder="Search"
-              v-model="searchTerm"
-              style="max-width: 500px"
-            />
-          </div>
-        </form>
-
+      <!-- Search -->
+      <div v-if="currentView == views.search">
         <!-- Search Results -->
-        <div v-if="currentView == 1">
+        <div v-if="searchResults">
           <div
             v-for="result in searchResults"
             :key="result.filename"
             class="mb-5"
           >
-            <p
-              class="h5 text-center clickable-link"
-              v-html="result.titleHighlightsOrTitle"
-              @click="loadNote(result.filename)"
-            ></p>
+            <p class="h5 text-center clickable-link">
+              <a v-html="result.titleHighlightsOrTitle" :href="result.href"></a>
+            </p>
             <p
               class="text-center text-muted"
               v-html="result.contentHighlights"
             ></p>
           </div>
         </div>
+      </div>
 
-        <!-- Notes -->
-        <div v-else>
-          <p
-            v-for="note in notesByLastModifiedDesc"
-            :key="note.filename"
-            class="text-center clickable-link mb-2"
-            @click="loadNote(note.filename)"
-          >
-            {{ note.title }}
-          </p>
-        </div>
+      <!-- Home -->
+      <div v-if="currentView == views.home">
+        <p
+          v-for="note in notesByLastModifiedDesc"
+          class="text-center clickable-link mb-2"
+          :key="note.filename"
+        >
+          <a :href="note.href">{{ note.title }}</a>
+        </p>
       </div>
     </div>
   </div>
index 513fe23521b997c10f17bd72016d18b0f8966ebf..d55e562fbd3feaafa4df7c1b213f9e39c5aae968 100644 (file)
@@ -1,3 +1,5 @@
+import * as constants from "../constants";
+
 class Note {
   constructor(filename, lastModified, content) {
     this.filename = filename;
@@ -8,6 +10,10 @@ class Note {
   get title() {
     return this.filename.slice(0, -3);
   }
+
+  get href() {
+    return `/${constants.basePaths.note}/${this.title}`
+  }
 }
 
 class SearchResult extends Note {
diff --git a/flatnotes/src/constants.js b/flatnotes/src/constants.js
new file mode 100644 (file)
index 0000000..03a6d21
--- /dev/null
@@ -0,0 +1,7 @@
+export const markdownExt = "md";
+
+// Base Paths
+export const basePaths = { login: "login", note: "note", search: "search" };
+
+// Params
+export const params = { searchTerm: "term", redirect: "redirect" };
diff --git a/flatnotes/src/helpers.js b/flatnotes/src/helpers.js
new file mode 100644 (file)
index 0000000..8e9bb7d
--- /dev/null
@@ -0,0 +1,4 @@
+export function getSearchParam(paramName) {
+  let urlSearchParams = new URLSearchParams(window.location.search);
+  return urlSearchParams.get(paramName);
+}
index a2185bc097652138857a42919162e3bcaee016f7..55a0ac5050093a03c18de9705c80a52175ce5a5f 100644 (file)
@@ -1,6 +1,10 @@
 .clickable-link {
-    cursor: pointer;
-    &:hover {
-        font-weight: bold;
-    }
+  cursor: pointer;
+  &:hover {
+    font-weight: bold;
+  }
+  a {
+    text-decoration: none;
+    color: inherit;
+  }
 }
git clone https://git.99rst.org/PROJECT