Jelajahi Sumber

Merge branch 'release/0.12.2'

Warafear 9 bulan lalu
induk
melakukan
50dbc8acc8

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "dndtools",
-  "version": "0.12.1",
+  "version": "0.12.2",
   "scripts": {
     "ng": "ng",
     "start": "nx serve",

+ 1 - 1
src/app/character/character-creator/character-creator.component.ts

@@ -384,7 +384,7 @@ export class CharacterCreatorComponent {
       this.dataAccessor.addData(
         this.characterName,
         {
-          data: {},
+          data: [],
         },
         'notes',
       ),

+ 7 - 6
src/app/journal/journal-home/navigation-panel/navigation-panel.component.html

@@ -62,14 +62,15 @@
         {{ "navigation.notes" | translate }}
       </div>
     </li>
+
     <li>
       <div
         class="navigation-entry"
         [class]="active === 7 ? 'active' : ''"
         (click)="setActiveProperty(7); closeAll(); closePanel()"
-        [routerLink]="'./quests'"
+        [routerLink]="'./npcs'"
       >
-        {{ "navigation.quests" | translate }}
+        {{ "navigation.npcs" | translate }}
       </div>
     </li>
     <li>
@@ -77,9 +78,9 @@
         class="navigation-entry"
         [class]="active === 8 ? 'active' : ''"
         (click)="setActiveProperty(8); closeAll(); closePanel()"
-        [routerLink]="'./npcs'"
+        [routerLink]="'./places'"
       >
-        {{ "navigation.npcs" | translate }}
+        {{ "navigation.places" | translate }}
       </div>
     </li>
     <li>
@@ -87,9 +88,9 @@
         class="navigation-entry"
         [class]="active === 9 ? 'active' : ''"
         (click)="setActiveProperty(9); closeAll(); closePanel()"
-        [routerLink]="'./places'"
+        [routerLink]="'./quests'"
       >
-        {{ "navigation.places" | translate }}
+        {{ "navigation.quests" | translate }}
       </div>
     </li>
     <li>

+ 3 - 3
src/app/journal/journal-home/navigation-panel/navigation-panel.component.ts

@@ -73,13 +73,13 @@ export class NavigationPanelComponent {
       case 'notes':
         this.active = 6;
         break;
-      case 'quests':
+      case 'npcs':
         this.active = 7;
         break;
-      case 'npcs':
+      case 'places':
         this.active = 8;
         break;
-      case 'places':
+      case 'quests':
         this.active = 9;
         break;
       case 'maps':

+ 118 - 22
src/app/journal/journal-notes/journal-notes.component.html

@@ -1,24 +1,120 @@
-<!-- <div
-  style="
-    display: flex;
-    justify-content: center;
-    align-items: center;
-    height: 100%;
-    width: 100%;
-  "
->
-  <img style="height: 100%" src="assets/images/notes_coming_soon.jpeg" alt="" />
-</div> -->
-
-<div class="NgxEditor__Wrapper">
-  <ngx-editor-menu [editor]="editor"> </ngx-editor-menu>
-  <ngx-editor
-    [editor]="editor"
-    [(ngModel)]="html"
-    [disabled]="false"
-    [placeholder]="'Coming soon...'"
-  ></ngx-editor>
+<div class="entries-list">
+  <!-- Add Button or temporary unsaved new entry -->
+  @if (isNewEntry) {
+    <div class="entry active">
+      <div class="entry-title">
+        @if (currentEntry.title !== "") {
+          {{ currentEntry.title }}
+        } @else {
+          Noch kein Titel
+        }
+      </div>
+      <div class="entry-date">
+        {{ currentEntry.created | date: "shortDate" : "" : "de" }}
+      </div>
+      <div class="unsaved">Nicht gespeichert</div>
+    </div>
+  } @else {
+    <div class="entry add-button" (click)="addEntry()">
+      <img src="assets/icons/UIIcons/add.svg" />
+    </div>
+  }
+  <!-- List of entries -->
+  @for (entry of entries; let index = $index; track entry) {
+    <div
+      class="entry-wrapper"
+      [ngClass]="{
+        active: currentEntryIndex === index,
+        'edit-mode': isInEditMode
+      }"
+    >
+      <div (click)="selectEntry(index)" class="entry">
+        <div class="entry-title">
+          @if (entries[index].title !== "") {
+            {{ entries[index].title }}
+          } @else {
+            Kein Titel
+          }
+        </div>
+        <div class="entry-date">
+          {{ entries[index].created | date: "shortDate" : "" : "de" }}
+        </div>
+        @if (isInEditMode && currentEntryIndex === index) {
+          <div class="unsaved">Nicht gespeichert</div>
+        }
+      </div>
+      <div class="control-button-wrapper">
+        <icon-button [icon]="'edit-large'" (click)="editEntry()"></icon-button>
+        <icon-button [icon]="'delete'" (click)="deleteEntry()"></icon-button>
+      </div>
+    </div>
+  }
 </div>
-<p [innerHTML]="htmlString"></p>
 
-<div [innerHTML]="html"></div>
+<!-- Entry container, shows the currentEntry -->
+@if (currentEntryIndex !== -1 || isNewEntry) {
+  <div class="entry-container">
+    @if (isInEditMode) {
+      <!-- Title -->
+      <mat-form-field class="title-write" appearance="outline">
+        <input
+          matInput
+          name="name"
+          placeholder="Titel des Eintrags"
+          [(ngModel)]="currentEntry.title"
+        />
+      </mat-form-field>
+
+      <!-- Datepicker -->
+      <mat-form-field class="date-write" appearance="outline">
+        <input
+          matInput
+          [matDatepicker]="picker"
+          placeholder="TT.MM.JJJJ"
+          [(ngModel)]="currentEntry.created"
+        />
+        <mat-datepicker-toggle
+          matIconSuffix
+          [for]="picker"
+        ></mat-datepicker-toggle>
+        <mat-datepicker #picker></mat-datepicker>
+      </mat-form-field>
+      <div class="NgxEditor__Wrapper">
+        <ngx-editor-menu [editor]="editor" [toolbar]="toolbar">
+        </ngx-editor-menu>
+        <ngx-editor
+          [editor]="editor"
+          [placeholder]="'notes.placeholder' | translate"
+          [(ngModel)]="currentEntry.content"
+        ></ngx-editor>
+      </div>
+    } @else {
+      <div class="read-container">
+        <div class="title-row">
+          <div class="title-read">{{ entries[currentEntryIndex].title }}</div>
+          <div class="date-read">
+            {{ currentEntry.created | date: "longDate" : "" : "de" }}
+          </div>
+        </div>
+        <divider appearance="gold-2"></divider>
+        <div class="content-read" [innerHTML]="currentEntry.content"></div>
+      </div>
+    }
+    <div class="button-row">
+      @if (isInEditMode) {
+        <ui-button width="w16" (click)="saveEntry()">Speichern</ui-button>
+        <ui-button width="w16" (click)="discardEntry()">Verwerfen</ui-button>
+      }
+      <!-- @else {
+        <ui-button width="w16" (click)="editEntry()">Bearbeiten</ui-button>
+        <ui-button width="w16" (click)="deleteEntry()"
+          >Eintrag löschen</ui-button
+        >
+      } -->
+    </div>
+  </div>
+} @else {
+  <div style="margin: 5rem auto; text-align: center">
+    Noch kein Eintrag vorhanden
+  </div>
+}

+ 182 - 0
src/app/journal/journal-notes/journal-notes.component.scss

@@ -0,0 +1,182 @@
+.entries-list {
+  width: 18rem;
+  height: 100%;
+  overflow-y: auto;
+  position: fixed;
+  top: 0;
+  left: 0;
+  display: flex;
+  flex-direction: column;
+  gap: 1rem;
+  background-image: url("../../../assets/images/texture-10.jpg");
+  border-right: var(--gold-3);
+  box-shadow: var(--shadow);
+  padding-top: 3rem;
+
+  .entry {
+    width: 15rem;
+    margin-left: 1rem;
+    padding: 1rem;
+    border: var(--gold-2);
+    border-radius: 10px;
+    box-shadow: var(--shadow);
+    cursor: pointer;
+    background-image: url("../../../assets/images/texture-0.jpg");
+    &:hover {
+      background-image: url("../../../assets/images/texture-5.jpg");
+    }
+
+    &.active {
+      background-image: url("../../../assets/images/texture-30.jpg") !important;
+    }
+    .entry-title {
+      overflow: hidden;
+      white-space: nowrap;
+      text-overflow: ellipsis;
+      font-weight: 500;
+    }
+
+    .entry-date {
+    }
+
+    .unsaved {
+      margin-top: 0.375rem;
+      text-align: center;
+      color: rgb(52, 33, 33);
+      font-size: 0.75rem;
+      font-weight: 600;
+    }
+  }
+
+  .control-button-wrapper {
+    display: flex;
+    flex-direction: column;
+    gap: 0.75rem;
+  }
+
+  .entry-wrapper {
+    display: flex;
+    gap: 0.5rem;
+    align-items: center;
+    .control-button-wrapper {
+      display: none;
+    }
+
+    &.active:not(.edit-mode) {
+      .entry {
+        width: 13rem;
+        background-image: url("../../../assets/images/texture-30.jpg") !important;
+      }
+      .control-button-wrapper {
+        display: flex;
+      }
+    }
+    &.active {
+      .entry {
+        background-image: url("../../../assets/images/texture-30.jpg") !important;
+      }
+    }
+  }
+
+  .add-button {
+    height: 5rem;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+  }
+}
+
+.entry-container {
+  width: 800px;
+  margin-left: calc(50vw - 400px + 9rem);
+  margin-top: 3rem;
+
+  // WRITE VIEW
+  .title-write {
+    border: var(--gold-2);
+    border-radius: 6px;
+    box-shadow: var(--shadow);
+    ::ng-deep .mat-mdc-text-field-wrapper {
+      width: 26rem !important;
+      background-image: url("../../../assets/images/texture-0.jpg") !important;
+    }
+  }
+  .date-write {
+    border: var(--gold-2);
+    margin-left: 13.5rem;
+    border-radius: 6px;
+    box-shadow: var(--shadow);
+    ::ng-deep .mat-mdc-text-field-wrapper {
+      width: 10rem !important;
+      background-image: url("../../../assets/images/texture-0.jpg") !important;
+    }
+  }
+
+  ::ng-deep .mat-calendar-body-selected {
+    background-color: var(--primary) !important;
+  }
+
+  .NgxEditor__Wrapper {
+    margin-top: 1.5rem;
+    border: var(--gold-3) !important;
+    border-radius: 6px;
+    box-shadow: var(--shadow);
+  }
+
+  ::ng-deep .NgxEditor__MenuBar {
+    background-image: url("../../../assets/images/texture-10.jpg");
+  }
+  ::ng-deep .ProseMirror {
+    height: calc(100vh - 17rem);
+    background-image: url("../../../assets/images/texture-0.jpg");
+    border-radius: 0 0 4px 4px;
+  }
+
+  // READ VIEW
+
+  .read-container {
+    margin-top: 1.5rem;
+    height: calc(100vh - 6rem);
+    padding: 1rem 2rem 2rem;
+    overflow: auto;
+    border-radius: 6px;
+    border: var(--gold-3);
+    background-image: url("/assets/images/texture-0.jpg");
+    box-shadow: var(--shadow);
+  }
+
+  .title-row {
+    display: flex;
+    justify-content: space-between;
+  }
+
+  .title-read {
+    display: inline-block;
+    width: 26rem;
+    height: 3.75rem;
+    display: flex;
+    align-items: center;
+    font-size: 1.75rem;
+    font-weight: 500;
+  }
+
+  .date-read {
+    display: inline-block;
+    text-align: center;
+    display: flex;
+    align-items: center;
+    font-size: 1.25rem;
+    font-weight: 500;
+    height: 3.75rem;
+  }
+
+  .content-read {
+    margin-top: 1rem;
+  }
+
+  .button-row {
+    display: flex;
+    justify-content: space-around;
+    margin-top: 1rem;
+  }
+}

+ 122 - 17
src/app/journal/journal-notes/journal-notes.component.ts

@@ -1,6 +1,10 @@
-import { Component, OnInit, OnDestroy } from '@angular/core';
+import { Component, OnInit, OnDestroy, Inject } from '@angular/core';
 import { Editor } from 'ngx-editor';
-import { marked } from 'marked';
+import { DateAdapter } from '@angular/material/core';
+import { JournalEntry } from 'src/interfaces/interfaces';
+import localeDe from '@angular/common/locales/de';
+import { registerLocaleData } from '@angular/common';
+import { DataService } from 'src/services/data/data.service';
 
 @Component({
   selector: 'app-journal-notes',
@@ -9,27 +13,128 @@ import { marked } from 'marked';
 })
 export class JournalNotesComponent implements OnInit, OnDestroy {
   editor: Editor = new Editor();
-  html = '';
+  toolbar: any = [
+    // default value
+    ['bold', 'italic'],
+    ['bullet_list'],
+    [{ heading: ['h3', 'h4', 'h5', 'h6'] }],
+  ];
 
-  markdownString =
-    ' # Hello \n \
-  this is a test \
-  **hi** \
-   \
-  - 1\
-  - 2';
+  /** Used to show the interactale form or the display version of an entry. */
+  public isInEditMode = false;
 
-  htmlString = '';
+  /** The index of the currently active entry */
+  public currentEntryIndex: number = 0;
 
-  constructor() {
-    this.htmlString = marked(this.markdownString);
-    console.log(this.markdownString);
-    console.log(this.htmlString);
+  private backupIndex: number = -1;
+  /** Indicates, if the currentEntry is a newly generated one that is still not saved. */
+  public isNewEntry = false;
+
+  /**The array of JournalEntries, synched to the   */
+  public entries: JournalEntry[] = [];
+
+  /** Holds the data for the current entry */
+  public currentEntry: JournalEntry = {
+    title: '',
+    content: '',
+    created: new Date(),
+  };
+
+  constructor(
+    private _adapter: DateAdapter<any>,
+    private dataService: DataService,
+  ) {
+    registerLocaleData(localeDe);
+    this.entries = this.dataService.notesData;
+  }
+
+  ngOnInit(): void {
+    this._adapter.setLocale('de');
+    this.entries = this.dataService.notesData;
+    console.log('JournalNotesComponent: ', this.entries);
+
+    // if the list is empty, set the currentEntryIndex to -1 to hide the entry-container
+    if (this.entries.length === 0) {
+      this.currentEntryIndex = -1;
+    } else {
+      this.currentEntry = this.entries[0];
+    }
   }
 
-  ngOnInit(): void {}
+  /**
+   * Sets the currentEntry variable when being clicked on in the entries list on the left.
+   * @param index The index of the selected entry.
+   */
+  public selectEntry(index: number): void {
+    this.currentEntryIndex = index;
+    this.currentEntry = this.getEntryAt(index);
+    this.isNewEntry = false;
+    this.isInEditMode = false;
+  }
+
+  // DONE
+  addEntry(): void {
+    this.currentEntry = {
+      title: '',
+      content: '',
+      created: new Date(),
+    };
+    this.isNewEntry = true;
+    this.isInEditMode = true;
+    // Hightlight no entry because the placeholder is shown as active
+    this.backupIndex = this.currentEntryIndex;
+    this.currentEntryIndex = -1;
+  }
+
+  public editEntry(): void {
+    this.isInEditMode = true;
+  }
+
+  // DONE
+  public saveEntry(): void {
+    if (this.isNewEntry) {
+      // Prepend the new JournalEntry
+      this.entries.unshift(this.currentEntry);
+      this.isNewEntry = false;
+      this.currentEntryIndex = 0;
+    }
+    this.isInEditMode = false;
+    this.entries[this.currentEntryIndex] = this.currentEntry;
+    this.uploadNotes();
+  }
+
+  public discardEntry(): void {
+    if (this.isNewEntry) {
+      this.currentEntryIndex = this.backupIndex;
+      this.isNewEntry = false;
+    }
+    if (this.entries.length > 0) {
+      // Reset the currentEntry to the last saved version
+      this.currentEntry = this.getEntryAt(this.currentEntryIndex);
+    }
+    this.isInEditMode = false;
+  }
+
+  deleteEntry(): void {
+    this.entries.splice(this.currentEntryIndex, 1);
+    if (this.entries.length === 0) {
+      this.currentEntryIndex = -1;
+    } else {
+      this.currentEntryIndex = Math.max(this.currentEntryIndex - 1, 0);
+      this.currentEntry = this.getEntryAt(this.currentEntryIndex);
+    }
+    this.uploadNotes();
+  }
+
+  private getEntryAt(index: number): JournalEntry {
+    return JSON.parse(JSON.stringify(this.entries[index]));
+  }
+
+  private uploadNotes(): void {
+    console.log('Uploading notes to the server: ', this.entries);
+    this.dataService.notesData = this.entries;
+  }
 
-  // make sure to destory the editor
   ngOnDestroy(): void {
     this.editor.destroy();
   }

+ 0 - 2
src/app/journal/journal-stats/ability-panel/proficiencies-table/proficiencies-table.component.scss

@@ -13,8 +13,6 @@ mat-expansion-panel {
   margin-right: 10px;
   margin-left: 10px;
   margin-bottom: 1rem !important;
-  // background-color: var(--items);
-  // border: var(--border); !important;
   border: var(--gold-2) !important;
   background-image: url("/assets/images/texture-0.jpg");
   border-radius: 10px !important;

+ 7 - 0
src/app/journal/journal.module.ts

@@ -18,6 +18,11 @@ import { MatTabsModule } from '@angular/material/tabs';
 import { MatExpansionModule } from '@angular/material/expansion';
 import { MatRadioModule } from '@angular/material/radio';
 import { MatDividerModule } from '@angular/material/divider';
+import {
+  MatCalendarCellClassFunction,
+  MatDatepickerModule,
+} from '@angular/material/datepicker';
+import { MatNativeDateModule } from '@angular/material/core';
 
 import { JournalRoutingModule } from './journal-routing.module';
 import { JournalHomeComponent } from './journal-home/journal-home.component';
@@ -195,6 +200,8 @@ import { DividerComponent } from '../shared-components/divider/divider.component
     MatDividerModule,
     DurationPipe,
     DividerComponent,
+    MatDatepickerModule,
+    MatNativeDateModule,
   ],
 })
 export class JournalModule {}

+ 1 - 1
src/app/shared-components/icon-button/icon-button.component.scss

@@ -13,6 +13,6 @@ button {
 }
 
 .hover-effect:hover {
-  background-color: rgba(211, 211, 211, 0.5);
+  background-color: rgba(138, 138, 138, 0.5);
   cursor: pointer;
 }

+ 5 - 1
src/assets/i18n/de.json

@@ -820,6 +820,10 @@
       }
     }
   },
+  "notes": {
+    "title": "Titel",
+    "placeholder": "Hier die Notizen einfügen"
+  },
 
   "creator": {
     "new": "Neuen Charakter erstellen",
@@ -836,6 +840,6 @@
     "hint": "Die App befindet sich immer noch in einem Entwicklungsstadium und es können Fehler auftreten",
     "issues": "<p>Fehler und Anmerkungen bitte auf dem <a href='https://gogs.koljastrohm-games.com/Warafear/DNDTools/issues'>Git-Server in Issues</a> vermerken.<p>",
     "okay": "Verstanden",
-    "version": "0.12.1"
+    "version": "0.12.2"
   }
 }

+ 7 - 3
src/assets/i18n/en.json

@@ -574,7 +574,7 @@
     "concentration": "Concentration"
   },
   "abilities": {
-    "label": "Features",
+    "label": "Abilities",
     "empty": "No features added yet",
     "uses": "Uses:",
     "modal": {
@@ -599,7 +599,7 @@
   "magic": {
     "spellCasting": "Spell Casting",
     "ki": "Ki Points",
-    "spell": "Spell",
+    "spell": "Magic",
     "spellslots": "Spell Slots",
     "empty": "No spell slots added yet",
     "spellcastingAttribute": "Spell Casting Attribute",
@@ -815,6 +815,10 @@
       }
     }
   },
+  "notes": {
+    "title": "Title",
+    "placeholder": "Add notes here"
+  },
   "creator": {
     "new": "Create New Character",
     "name": "Name",
@@ -830,7 +834,7 @@
     "hint": "The app is still in a development stage and errors may occur",
     "issues": "<p>Please note errors and comments on the <a href='https://gogs.koljastrohm-games.com/Warafear/DNDTools/issues'>Git server in Issues</a>.<p>",
     "okay": "Understood",
-    "version": "0.12.1",
+    "version": "0.12.2",
     "test": "Test"
   }
 }

+ 1 - 0
src/assets/icons/UIIcons/edit-large.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h357l-80 80H200v560h560v-278l80-80v358q0 33-23.5 56.5T760-120H200Zm280-360ZM360-360v-170l367-367q12-12 27-18t30-6q16 0 30.5 6t26.5 18l56 57q11 12 17 26.5t6 29.5q0 15-5.5 29.5T897-728L530-360H360Zm481-424-56-56 56 56ZM440-440h56l232-232-28-28-29-28-231 231v57Zm260-260-29-28 29 28 28 28-28-28Z"/></svg>

+ 1 - 0
src/assets/icons/UIIcons/warn.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m40-120 440-760 440 760H40Zm138-80h604L480-720 178-200Zm302-40q17 0 28.5-11.5T520-280q0-17-11.5-28.5T480-320q-17 0-28.5 11.5T440-280q0 17 11.5 28.5T480-240Zm-40-120h80v-200h-80v200Zm40-100Z"/></svg>

TEMPAT SAMPAH
src/assets/images/eliane.jpg


TEMPAT SAMPAH
src/assets/images/tel.jpg


+ 5 - 0
src/colors.scss

@@ -99,4 +99,9 @@
     0 0 9px #69088f, 0 0 12px #3a088f, 0 0 15px #080a8f;
   --transmutation-border-large: 0 0 0 3px #171314, 0 0 4px #8f088f,
     0 0 5px #7f088f, 0 0 11px #69088f, 0 0 14px #3a088f, 0 0 17px #080a8f;
+
+  --mat-datepicker-calendar-date-selected-state-background-color: var(
+    --primary
+  );
+  --mat-datepicker-toggle-active-state-icon-color: var(--primary);
 }

+ 10 - 0
src/interfaces/interfaces.ts

@@ -165,3 +165,13 @@ export interface Spell {
   heal?: Heal;
 }
 // #endregion
+
+// #region Notes
+
+export interface JournalEntry {
+  title: string;
+  created: Date;
+  content: string;
+  startDate?: string;
+  endDate?: string;
+}

+ 1 - 1
src/services/background/background.service.ts

@@ -38,7 +38,7 @@ export class BackgroundService {
   private acolyte: any = {
     title: 'acolyte',
     description: `
-        p><strong><em>You have spent your life in the service of a temple to a specific god or pantheon of gods. You act as an intermediary between the realm of the holy and the mortal world, performing sacred rites and offering sacrifices in order to conduct worshipers into the presence of the divine. You are not necessarily a cleric –&nbsp;performing sacred rites is not the same thing as channeling divine power.</em></strong></p>
+        <p><strong><em>You have spent your life in the service of a temple to a specific god or pantheon of gods. You act as an intermediary between the realm of the holy and the mortal world, performing sacred rites and offering sacrifices in order to conduct worshipers into the presence of the divine. You are not necessarily a cleric –&nbsp;performing sacred rites is not the same thing as channeling divine power.</em></strong></p>
         <p><strong><em>Choose a god, a pantheon of gods, or some other quasi-divine being, and work with your DM to detail the nature of your religious service. Were you a lesser functionary in a temple, raised from childhood to assist the priests in the sacred rites? Or were you a high priest who suddenly experienced a call to serve your god in a different way? Perhaps you were the leader of a small cult outside of any established temple structure, or even an occult group that served a fiendish master that you now deny.</em></strong></p>
         <p>Source: Player's Handbook</p>
         <p><strong>Skill Proficiencies:</strong> Insight, Religion<br />

+ 25 - 0
src/services/data/data.service.ts

@@ -10,6 +10,7 @@ import {
   Spell,
   Skill,
   Attribute,
+  JournalEntry,
 } from 'src/interfaces/interfaces';
 import { SpellsService } from '../spells/spells.service';
 
@@ -145,6 +146,15 @@ export class DataService {
     this.traits = traitsData.data;
     this.abilities = abilitiesData.data;
     this.proficiencies = proficienciesData;
+
+    // Notes
+
+    if (!Array.isArray(notesData.data)) {
+      this.notesData = [];
+      console.log('migrated notes data to array');
+    } else {
+      this.notesData = notesData.data;
+    }
   }
 
   // #endregion
@@ -1073,6 +1083,21 @@ export class DataService {
 
   // #endregion
 
+  // #region Notes
+
+  private _notesData: JournalEntry[] = [];
+
+  public get notesData(): JournalEntry[] {
+    return this._notesData;
+  }
+
+  public set notesData(newValue: JournalEntry[]) {
+    this._notesData = newValue;
+    this.setData('notes', { data: newValue });
+  }
+
+  // #endregion
+
   // #region database calls
 
   public async addData(