Added A-Z index page. Resolves #5.
authorAdam Dullage <redacted>
Sat, 20 Aug 2022 13:09:09 +0000 (14:09 +0100)
committerAdam Dullage <redacted>
Sat, 20 Aug 2022 13:09:09 +0000 (14:09 +0100)
15 files changed:
flatnotes/main.py
flatnotes/src/api.js
flatnotes/src/classes.js
flatnotes/src/colours.scss
flatnotes/src/components/App.js
flatnotes/src/components/App.vue
flatnotes/src/components/LoadingIndicator.vue
flatnotes/src/components/NavBar.vue
flatnotes/src/components/NoteList.vue [new file with mode: 0644]
flatnotes/src/components/RecentlyModified.vue [deleted file]
flatnotes/src/components/SearchInput.vue
flatnotes/src/components/SearchResults.vue
flatnotes/src/constants.js
flatnotes/src/global.scss [moved from flatnotes/src/main.scss with 77% similarity]
flatnotes/src/index.js

index 787724159e2171cf8548004854b1fad01f7f0937..4ac37cfea5644737482cdf66de3e385c7e31b181 100644 (file)
@@ -48,6 +48,7 @@ async def token(data: LoginModel):
 @app.get("/login")
 @app.get("/search")
 @app.get("/new")
+@app.get("/notes")
 @app.get("/note/{title}")
 async def root(title: str = ""):
     with open("flatnotes/dist/index.html", "r", encoding="utf-8") as f:
index 7f970de1ee6d54d6c68eb8a859ff8b88cd201c5e..0ab3a26ae66363495f8e3bf1f11d93ef6f9108ae 100644 (file)
@@ -29,7 +29,7 @@ api.interceptors.response.use(
     ) {
       EventBus.$emit(
         "navigate",
-        `/${constants.basePaths.login}?${constants.params.redirect}=${encodeURI(
+        `${constants.basePaths.login}?${constants.params.redirect}=${encodeURI(
           window.location.pathname + window.location.search
         )}`
       );
index 7a58597952bc7990b23ae68a2651ca667e831f70..4349e29bd0b5a03f703751b9d7d640b75be5bb6c 100644 (file)
@@ -8,7 +8,15 @@ class Note {
   }
 
   get href() {
-    return `/${constants.basePaths.note}/${this.title}`;
+    return `${constants.basePaths.note}/${this.title}`;
+  }
+
+  get lastModifiedAsDate() {
+    return new Date(this.lastModified * 1000);
+  }
+
+  get lastModifiedAsString() {
+    return this.lastModifiedAsDate.toLocaleString();
   }
 }
 
index 1cc3862f8ffaff6c6c66f301455974ac713915b0..1c2b2a7a6e54b457a3e8c014f8ea00d8fe553b6b 100644 (file)
@@ -2,7 +2,8 @@ $off-white: #f8f9fd70;
 $form-control-border: #ced4da;
 $drop-shadow: #0000000a;
 $muted-text: #6c757d;
+$very-muted-text: #d8dbdd;
 $text: #222222;
-$button-background: #00000010;
+$button-background: #00000008;
 $input-highlight: #bbcdff;
 $logo-key-colour: #f9a76b;
index 813b5eff82d39ecc193e1da663c3d2719d615589..f9f8314965be000907ce7abead2c2765edec9d82 100644 (file)
@@ -7,8 +7,8 @@ import Login from "./Login";
 import Logo from "./Logo";
 import Mousetrap from "mousetrap";
 import NavBar from "./NavBar";
+import NoteList from "./NoteList";
 import NoteViewerEditor from "./NoteViewerEditor";
-import RecentlyModified from "./RecentlyModified";
 import SearchInput from "./SearchInput";
 import SearchResults from "./SearchResults";
 
@@ -16,7 +16,7 @@ export default {
   name: "App",
 
   components: {
-    RecentlyModified,
+    NoteList,
     LoadingIndicator,
     Login,
     NavBar,
@@ -33,6 +33,7 @@ export default {
         home: 1,
         note: 2,
         search: 3,
+        notes: 4,
       },
       currentView: 1,
 
@@ -44,12 +45,12 @@ export default {
   methods: {
     route: function() {
       let path = window.location.pathname.split("/");
-      let basePath = path[1];
+      let basePath = `/${path[1]}`;
 
       this.$bvModal.hide("search-modal");
 
       // Home Page
-      if (basePath == "") {
+      if (basePath == constants.basePaths.home) {
         this.updateDocumentTitle();
         this.currentView = this.views.home;
         this.$nextTick(function() {
@@ -77,6 +78,12 @@ export default {
         this.currentView = this.views.note;
       }
 
+      // Notes
+      else if (basePath == constants.basePaths.notes) {
+        this.updateDocumentTitle();
+        this.currentView = this.views.notes;
+      }
+
       // Login
       else if (basePath == constants.basePaths.login) {
         this.updateDocumentTitle("Log In");
@@ -102,11 +109,11 @@ export default {
     logout: function() {
       sessionStorage.removeItem("token");
       localStorage.removeItem("token");
-      this.navigate(`/${constants.basePaths.login}`);
+      this.navigate(constants.basePaths.login);
     },
 
     newNote: function() {
-      this.navigate(`/${constants.basePaths.new}`);
+      this.navigate(constants.basePaths.new);
     },
 
     noteDeletedToast: function() {
@@ -148,6 +155,8 @@ export default {
   created: function() {
     let parent = this;
 
+    this.constants = constants;
+
     EventBus.$on("navigate", this.navigate);
     EventBus.$on("unhandledServerError", this.unhandledServerErrorToast);
     EventBus.$on("updateDocumentTitle", this.updateDocumentTitle);
index 9667281b11b29cf02ed87038d5e1007f40664b99..d9b66ee12e46fc5b0202615682ab2b96df8ed651 100644 (file)
@@ -13,8 +13,9 @@
       v-if="currentView != views.login"
       class="w-100 mb-5"
       :show-logo="currentView != views.home"
-      @navigate-home="navigate('/')"
+      @navigate-home="navigate(constants.basePaths.home)"
       @new-note="newNote()"
+      @a-z="navigate(constants.basePaths.notes)"
       @logout="logout()"
       @search="openSearch()"
     ></NavBar>
         :initial-value="searchTerm"
         class="search-input mb-4"
       ></SearchInput>
-      <RecentlyModified class="recently-modified"></RecentlyModified>
+      <NoteList
+        class="recently-modified"
+        mini-header="Recently Modified"
+        :num-recently-modified="5"
+        :show-loader="false"
+        centered
+      ></NoteList>
     </div>
 
     <!-- Search Results -->
     <div
       v-if="currentView == views.search"
-      class="flex-grow-1 search-results-view"
+      class="flex-grow-1 search-results-view d-flex flex-column"
     >
       <SearchInput
         :initial-value="searchTerm"
         class="search-input mb-4"
       ></SearchInput>
-      <SearchResults :search-term="searchTerm" class="h-100"></SearchResults>
+      <SearchResults
+        :search-term="searchTerm"
+        class="flex-grow-1"
+      ></SearchResults>
     </div>
 
+    <!-- Notes -->
+    <NoteList
+      v-if="currentView == views.notes"
+      class="flex-grow-1"
+      grouped
+      show-last-modified
+    ></NoteList>
+
     <!-- Note -->
     <NoteViewerEditor
       v-if="currentView == this.views.note"
   </div>
 </template>
 
+<style lang="scss" scoped>
+@import "../colours";
+
+.home-view {
+  max-width: 500px;
+}
+
+.search-results-view {
+  max-width: 700px;
+}
+
+.search-input {
+  box-shadow: 0 0 20px $drop-shadow;
+}
+
+.recently-modified {
+  // Prevent UI from moving during load
+  min-height: 180px;
+}
+</style>
+
 <script>
 export { default } from "./App.js";
 </script>
index 39195440a8e35ee73d440582249cdedade5d72f7..fc8b11db8f4f78605ca713ff2663ee96ad2dbbff 100644 (file)
@@ -1,10 +1,13 @@
 <template>
-  <div>
+  <div class="d-flex justify-content-center">
     <div v-if="showLoader && !failed" class="loader"></div>
-    <div v-else-if="failed" class="d-flex flex-column align-items-center">
+    <div
+      v-else-if="failed"
+      class="d-flex flex-column align-items-center failure-message"
+    >
       <b-icon
         class="failed-icon mb-3"
-        :icon="failedBootstrapIcon || 'cloud-slash'"
+        :icon="failedBootstrapIcon || 'cone-striped'"
       ></b-icon>
       <p>{{ failedMessage }}</p>
     </div>
@@ -23,6 +26,11 @@ p {
   font-size: 60px;
 }
 
+.failure-message {
+  max-width: 300px;
+  text-align: center;
+}
+
 .loader,
 .loader:before,
 .loader:after {
index 67588d5061d06cd699f316f890d0db33f61024cc..4092d8f1086c035308295b85bf342007d4fffca6 100644 (file)
@@ -7,10 +7,12 @@
       @click.native="$emit('navigate-home')"
       responsive
     ></Logo>
+
+    <!-- Buttons -->
     <div>
       <!-- New Note -->
       <button type="button" class="bttn" @click="$emit('new-note')">
-        <b-icon icon="plus-circle"></b-icon> New Note
+        <b-icon icon="plus-circle"></b-icon> New
       </button>
 
       <!-- Log Out -->
@@ -18,6 +20,9 @@
         <b-icon icon="box-arrow-right"></b-icon> Log Out
       </button>
 
+      <!-- A-Z -->
+      <button type="button" class="bttn" @click="$emit('a-z')">A-Z</button>
+
       <!-- Search -->
       <button
         type="button"
diff --git a/flatnotes/src/components/NoteList.vue b/flatnotes/src/components/NoteList.vue
new file mode 100644 (file)
index 0000000..e9c6825
--- /dev/null
@@ -0,0 +1,214 @@
+<template>
+  <div>
+    <!-- Loading -->
+    <div
+      v-if="notes == null || notes.length == 0"
+      class="h-100 d-flex flex-column justify-content-center"
+    >
+      <LoadingIndicator
+        :failed="loadingFailed"
+        :failedMessage="loadingFailedMessage"
+        :failedBootstrapIcon="loadingFailedIcon"
+        :show-loader="showLoader"
+      />
+    </div>
+
+    <!-- Notes Loaded -->
+    <div v-else>
+      <p
+        v-if="miniHeader"
+        class="mini-header mb-1"
+        :class="{ centered: centered }"
+      >
+        {{ miniHeader }}
+      </p>
+      <div
+        v-for="group in notesGrouped"
+        :key="group.name"
+        :class="{ centered: centered, 'mb-5': grouped }"
+      >
+        <p v-if="grouped" class="group-name">{{ group.name }}</p>
+        <a
+          v-for="note in group.notes"
+          :key="note.title"
+          class="d-flex justify-content-between align-items-center note-row"
+          :href="note.href"
+          @click.prevent="openNote(note.href, $event)"
+        >
+          <span>{{ note.title }}</span>
+          <span v-if="showLastModified" class="last-modified d-none d-md-block">
+            {{ note.lastModifiedAsString }}
+          </span>
+        </a>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" >
+@import "../colours";
+
+.centered {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.mini-header {
+  text-transform: uppercase;
+  font-size: 12px;
+  font-weight: bold;
+  color: $very-muted-text;
+}
+
+.group-name {
+  padding-left: 8px;
+  font-weight: bold;
+  font-size: 32px;
+  color: $very-muted-text;
+  margin-bottom: 1px solid $very-muted-text;
+}
+
+.note-row {
+  padding: 4px 8px;
+  margin: 2px 0;
+  border-radius: 4px;
+  &:hover {
+    background-color: $button-background;
+  }
+}
+
+a {
+  &:hover {
+    filter: none;
+    cursor: pointer;
+  }
+}
+
+.last-modified {
+  color: $muted-text;
+  font-size: 12px;
+}
+</style>
+
+<script>
+import * as constants from "../constants";
+
+import EventBus from "../eventBus";
+import LoadingIndicator from "./LoadingIndicator.vue";
+import { Note } from "../classes";
+import api from "../api";
+
+const alphaGroups = ["#", ...constants.alphabet];
+
+export default {
+  components: {
+    LoadingIndicator,
+  },
+
+  props: {
+    numRecentlyModified: { type: Number },
+    grouped: { type: Boolean, default: false },
+    showLastModified: { type: Boolean, default: false },
+    centered: { type: Boolean, default: false },
+    miniHeader: { type: String },
+    showLoader: { type: Boolean, default: true },
+  },
+
+  data: function () {
+    return {
+      notes: null,
+      loadingFailed: false,
+      loadingFailedMessage: "Failed to load notes",
+      loadingFailedIcon: null,
+    };
+  },
+
+  computed: {
+    notesGrouped: function () {
+      if (!this.grouped) {
+        return [{ name: "all", notes: this.notes }];
+      }
+
+      let notesGroupedDict = {};
+      alphaGroups.forEach(function (group) {
+        notesGroupedDict[group] = [];
+      });
+
+      this.notes.forEach(function (note) {
+        let firstCharUpper = note.title[0].toUpperCase();
+        if (constants.alphabet.includes(firstCharUpper)) {
+          notesGroupedDict[firstCharUpper].push(note);
+        } else {
+          notesGroupedDict["#"].push(note);
+        }
+      });
+
+      // Convert dict to an array skipping empty groups
+      let notesGroupedArray = [];
+      Object.entries(notesGroupedDict).forEach(function (item) {
+        if (item[1].length) {
+          notesGroupedArray.push({
+            name: item[0],
+            notes: item[1].sort(function (noteA, noteB) {
+              return noteA.title.localeCompare(noteB.title);
+            }),
+          });
+        }
+      });
+
+      // Ensure the array is ordered correctly
+      notesGroupedArray.sort(function (groupA, groupB) {
+        return groupA.name.localeCompare(groupB.name);
+      });
+
+      return notesGroupedArray;
+    },
+  },
+
+  methods: {
+    getNotes: function () {
+      let parent = this;
+      api
+        .get("/api/notes", {
+          params: {
+            limit: this.numRecentlyModified,
+            sort: "lastModified",
+            order: "desc",
+          },
+        })
+        .then(function (response) {
+          parent.notes = [];
+          if (response.data.length) {
+            response.data.forEach(function (note) {
+              parent.notes.push(new Note(note.title, note.lastModified));
+            });
+          } else {
+            parent.loadingFailedMessage =
+              "Click the 'New' button at the top of the page to add your first note";
+            parent.loadingFailedIcon = "pencil";
+            parent.loadingFailed = true;
+          }
+        })
+        .catch(function (error) {
+          parent.loadingFailed = true;
+          if (!error.handled) {
+            EventBus.$emit("unhandledServerError");
+          }
+        });
+    },
+
+    notesByTitle: function () {
+      return null;
+    },
+
+    openNote: function (href, event) {
+      EventBus.$emit("navigate", href, event);
+    },
+  },
+
+  created: function () {
+    this.getNotes();
+  },
+};
+</script>
diff --git a/flatnotes/src/components/RecentlyModified.vue b/flatnotes/src/components/RecentlyModified.vue
deleted file mode 100644 (file)
index 794a108..0000000
+++ /dev/null
@@ -1,76 +0,0 @@
-<template>
-  <div>
-    <!-- Loading -->
-    <div v-if="notes == null" class="h-100 d-flex flex-column justify-content-center">
-      <LoadingIndicator
-        :showLoader="false"
-        :failed="loadingFailed"
-        failedMessage="Failed to load Recently Modified"
-      />
-    </div>
-
-    <!-- Notes Loaded -->
-    <div v-else-if="notes.length > 0">
-      <h6 class="text-center text-muted text-bold">Recently Modified</h6>
-      <p
-        v-for="note in notes"
-        class="text-center clickable-link mb-2"
-        :key="note.title"
-      >
-        <a :href="note.href" @click.prevent="openNote(note.href, $event)">{{
-          note.title
-        }}</a>
-      </p>
-    </div>
-  </div>
-</template>
-
-<script>
-import EventBus from "../eventBus";
-import LoadingIndicator from "./LoadingIndicator.vue";
-import { Note } from "../classes";
-import api from "../api";
-
-export default {
-  components: {
-    LoadingIndicator,
-  },
-
-  data: function () {
-    return {
-      notes: null,
-      loadingFailed: false,
-    };
-  },
-
-  methods: {
-    getNotes: function (limit = null, sort = "title", order = "asc") {
-      let parent = this;
-      api
-        .get("/api/notes", {
-          params: { limit: limit, sort: sort, order: order },
-        })
-        .then(function (response) {
-          parent.notes = [];
-          response.data.forEach(function (note) {
-            parent.notes.push(new Note(note.title, note.lastModified));
-          });
-        })
-        .catch(function (error) {
-          parent.loadingFailed = true;
-          if (!error.handled) {
-            EventBus.$emit("unhandledServerError");
-          }
-        });
-    },
-
-    openNote: function (href, event) {
-      EventBus.$emit("navigate", href, event);
-    },
-  },
-
-  created: function () {
-    this.getNotes(5, "lastModified", "desc");
-  },
-};
-</script>
\ No newline at end of file
index 22749d66a7feb626a30fbd3ff8f1992d5037951c..561f042b90498f96a4df29651e54ff145e98a3a0 100644 (file)
@@ -72,7 +72,7 @@ export default {
       if (this.searchTermInput) {
         EventBus.$emit(
           "navigate",
-          `/${constants.basePaths.search}?${
+          `${constants.basePaths.search}?${
             constants.params.searchTerm
           }=${encodeURI(this.searchTermInput)}`
         );
index 96e1bf0fcf9966114fbaf293aedc948a125b53f8..23f835a5bb030acc31d1dc166bf332ae86e65688 100644 (file)
   </div>
 </template>
 
-<style lang="scss">
+<style lang="scss" scoped>
 @import "../colours";
 
+a {
+  &:hover {
+    filter: opacity(70%);
+  }
+}
+
 .result-contents {
   color: $muted-text;
 }
+</style>
+
+<style lang="scss">
+@import "../colours";
+
 .match {
   font-weight: bold;
   color: $logo-key-colour;
index 92fb975bee34fc1cf89e434611fd845008fb5394..4733166b7387faddb225560ec385cdacc2d3bcf4 100644 (file)
@@ -1,10 +1,42 @@
 // Base Paths
 export const basePaths = {
-  login: "login",
-  note: "note",
-  search: "search",
-  new: "new",
+  home: "/",
+  login: "/login",
+  note: "/note",
+  search: "/search",
+  new: "/new",
+  notes: "/notes",
 };
 
 // Params
 export const params = { searchTerm: "term", redirect: "redirect" };
+
+// Other
+export const alphabet = [
+  "A",
+  "B",
+  "C",
+  "D",
+  "E",
+  "F",
+  "G",
+  "H",
+  "I",
+  "J",
+  "K",
+  "L",
+  "M",
+  "N",
+  "O",
+  "P",
+  "Q",
+  "R",
+  "S",
+  "T",
+  "U",
+  "V",
+  "W",
+  "X",
+  "Y",
+  "Z",
+];
similarity index 77%
rename from flatnotes/src/main.scss
rename to flatnotes/src/global.scss
index 1675c9bf4cb1c4c47af07f70ca5599516515d85d..e6ee3178d6086467194324a82ffcce0274660d40 100644 (file)
@@ -21,7 +21,7 @@ a {
   color: inherit;
   &:hover {
     text-decoration: none;
-    filter: opacity(70%);
+    color: inherit;
   }
 }
 
@@ -30,23 +30,6 @@ a {
   border-color: $form-control-border;
 }
 
-.home-view {
-  max-width: 500px;
-}
-
-.search-results-view {
-  max-width: 700px;
-}
-
-.search-input {
-  box-shadow: 0 0 20px $drop-shadow;
-}
-
-.recently-modified {
-  // Prevent UI from moving during load
-  min-height: 190px;
-}
-
 .bttn {
   border: 0;
   background-color: transparent;
index c0ce22d0463f9a321b857322a6462ca4fe8b505d..1c2e1ed717d30ebbd893c912b10b0a3c454d1daf 100644 (file)
@@ -1,4 +1,4 @@
-import "./main.scss"
+import "./global.scss"
 
 import { BootstrapVue, IconsPlugin } from "bootstrap-vue";
 
git clone https://git.99rst.org/PROJECT