Kaynağa Gözat

Merge branch 'feature/notes' into develop

Warafear 7 ay önce
ebeveyn
işleme
8562bd05bf

+ 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();
   }

+ 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;
 }

+ 4 - 0
src/assets/i18n/de.json

@@ -820,6 +820,10 @@
       }
     }
   },
+  "notes": {
+    "title": "Titel",
+    "placeholder": "Hier die Notizen einfügen"
+  },
 
   "creator": {
     "new": "Neuen Charakter erstellen",

+ 4 - 0
src/assets/i18n/en.json

@@ -815,6 +815,10 @@
       }
     }
   },
+  "notes": {
+    "title": "Title",
+    "placeholder": "Add notes here"
+  },
   "creator": {
     "new": "Create New Character",
     "name": "Name",

+ 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>

BIN
src/assets/images/eliane.jpg


BIN
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;
+}

+ 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(