Ver código fonte

Merge branch 'release/0.13.2'

Warafear 6 meses atrás
pai
commit
4478cd44d1

+ 1 - 1
package.json

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

+ 3 - 21
src/app/character/character-creator/character-creator.component.ts

@@ -388,13 +388,7 @@ export class CharacterCreatorComponent {
         },
         'notes',
       ),
-      this.dataAccessor.addData(
-        this.characterName,
-        {
-          data: {},
-        },
-        'quests',
-      ),
+      this.dataAccessor.addData(this.characterName, [], 'quests'),
       this.dataAccessor.addData(
         this.characterName,
         {
@@ -407,20 +401,8 @@ export class CharacterCreatorComponent {
         },
         'npcs',
       ),
-      this.dataAccessor.addData(
-        this.characterName,
-        {
-          data: {},
-        },
-        'locations',
-      ),
-      this.dataAccessor.addData(
-        this.characterName,
-        {
-          data: {},
-        },
-        'maps',
-      ),
+      this.dataAccessor.addData(this.characterName, [], 'locations'),
+      this.dataAccessor.addData(this.characterName, [], 'maps'),
       this.dataAccessor.addData(
         this.characterName,
         {

+ 11 - 4
src/app/journal/journal-maps/journal-maps.component.html

@@ -1,4 +1,11 @@
-<ng-template #tooltip><div [innerHTML]="currentText"></div></ng-template>
-
-<button [ngbTooltip]="tooltip" (mouseover)="setNewText('hans')">Hans</button>
-<button [ngbTooltip]="tooltip" (mouseover)="setNewText('peter')">Peter</button>
+<div
+  style="
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    height: 100%;
+    width: 100%;
+  "
+>
+  <img style="height: 100%" src="assets/images/maps_coming_soon.jpeg" alt="" />
+</div>

+ 12 - 5
src/app/journal/journal-notes/journal-notes.component.html

@@ -26,14 +26,19 @@
     <div
       class="entry-wrapper"
       [ngClass]="{
-        active: currentEntryIndex === index,
+        active: currentIndex === index,
         'edit-mode': isInEditMode
       }"
     >
       <div (click)="selectEntry(index)" class="entry">
         <div class="entry-title">
           @if (entries[index].title !== "") {
-            {{ entries[index].title }}
+            <!-- {{ entries[index].title }} -->
+            @if (isInEditMode && index === currentIndex) {
+              {{ currentEntry.title }}
+            } @else {
+              {{ entries[index].title }}
+            }
           } @else {
             {{ "notes.noTitle" | translate }}
           }
@@ -41,7 +46,7 @@
         <div class="entry-date">
           {{ entries[index].created | date: "shortDate" : "" : "de" }}
         </div>
-        @if (isInEditMode && currentEntryIndex === index) {
+        @if (isInEditMode && currentIndex === index) {
           <div class="unsaved">{{ "notes.unsaved" | translate }}</div>
         }
       </div>
@@ -54,7 +59,7 @@
 </div>
 
 <!-- Entry container, shows the currentEntry -->
-@if (currentEntryIndex !== -1 || isNewEntry) {
+@if (currentIndex !== -1 || isNewEntry) {
   <div class="entry-container">
     @if (isInEditMode) {
       <!-- Title -->
@@ -126,5 +131,7 @@
     {{ "notes.empty" | translate }}
   </div>
 }
-<ng-template #tooltip><div [innerHTML]="tooltipText"></div></ng-template>
+<ng-template #tooltip
+  ><div class="tooltip-content" [innerHTML]="tooltipText"></div
+></ng-template>
 <div #creationAnchor></div>

+ 20 - 21
src/app/journal/journal-notes/journal-notes.component.ts

@@ -47,7 +47,7 @@ export class JournalNotesComponent implements OnInit, OnDestroy {
   };
 
   public isInEditMode = false;
-  public currentEntryIndex: number = 0;
+  public currentIndex: number = -1;
   private backupIndex: number = -1;
   public isNewEntry = false;
 
@@ -72,14 +72,12 @@ export class JournalNotesComponent implements OnInit, OnDestroy {
     this._adapter.setLocale('de');
     this.entries = this.dataService.notesData;
 
-    // if the list is empty, set the currentEntryIndex to -1 to hide the entry-container
+    // if the list is empty, set the currentIndex to -1 to hide the entry-container
     if (this.entries.length === 0) {
-      this.currentEntryIndex = -1;
+      this.currentIndex = -1;
     } else {
-      this.currentEntry = this.entries[0];
-      this.tooltipifiedEntry = JSON.parse(JSON.stringify(this.currentEntry));
+      this.selectEntry(0);
     }
-    this.tooltipify();
   }
 
   // ACTIONS FROM THE TEMPLATE
@@ -91,8 +89,9 @@ export class JournalNotesComponent implements OnInit, OnDestroy {
    * @param index The index of the selected entry.
    */
   public selectEntry(index: number): void {
-    if (this.isInEditMode || index !== this.currentEntryIndex) {
-      this.currentEntryIndex = index;
+    // hier
+    if (this.isInEditMode || index !== this.currentIndex) {
+      this.currentIndex = index;
       this.currentEntry = this.getEntryAt(index);
       this.isNewEntry = false;
       this.isInEditMode = false;
@@ -111,9 +110,9 @@ export class JournalNotesComponent implements OnInit, OnDestroy {
     };
     this.isNewEntry = true;
     this.isInEditMode = true;
-    this.backupIndex = this.currentEntryIndex;
+    this.backupIndex = this.currentIndex;
     // Hightlight no entry because the placeholder is shown as active
-    this.currentEntryIndex = -1;
+    this.currentIndex = -1;
   }
 
   /**
@@ -133,27 +132,27 @@ export class JournalNotesComponent implements OnInit, OnDestroy {
       // Prepend the new JournalEntry
       this.entries.unshift(this.currentEntry);
       this.isNewEntry = false;
-      this.currentEntryIndex = 0;
+      this.currentIndex = 0;
     }
     this.isInEditMode = false;
-    this.entries[this.currentEntryIndex] = this.currentEntry;
+    this.entries[this.currentIndex] = this.currentEntry;
     this.tooltipify();
     this.uploadNotes();
   }
 
   /**
    * Discards the current entry and resets the currentEntry to the last saved version.
-   * If the entry was a new entry, the currentEntryIndex is reset to the last selected entry.
+   * If the entry was a new entry, the currentIndex is reset to the last selected entry.
    * The content is also tooltipified again.
    */
   public discardEntry(): void {
     if (this.isNewEntry) {
-      this.currentEntryIndex = this.backupIndex;
+      this.currentIndex = 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.currentEntry = this.getEntryAt(this.currentIndex);
     }
     this.isInEditMode = false;
     this.tooltipify();
@@ -161,16 +160,16 @@ export class JournalNotesComponent implements OnInit, OnDestroy {
 
   /**
    * Deletes the current entry and removes it from the entries list.
-   * If the last entry was deleted, the currentEntryIndex is reset to -1.
+   * If the last entry was deleted, the currentIndex is reset to -1.
    * The content is saved in the database.
    */
   public deleteEntry(): void {
-    this.entries.splice(this.currentEntryIndex, 1);
+    this.entries.splice(this.currentIndex, 1);
     if (this.entries.length === 0) {
-      this.currentEntryIndex = -1;
+      this.currentIndex = -1;
     } else {
-      this.currentEntryIndex = Math.max(this.currentEntryIndex - 1, 0);
-      this.currentEntry = this.getEntryAt(this.currentEntryIndex);
+      this.currentIndex = Math.max(this.currentIndex - 1, 0);
+      this.currentEntry = this.getEntryAt(this.currentIndex);
       // Update the tooltipified entry
       this.tooltipify();
     }
@@ -200,7 +199,7 @@ export class JournalNotesComponent implements OnInit, OnDestroy {
    */
   public tooltipify(): void {
     let result: any = this.tooltipService.tooltipifyEntry(
-      this.getEntryAt(this.currentEntryIndex).content,
+      this.getEntryAt(this.currentIndex).content,
     );
 
     this.tooltipifiedEntry.content = result.content;

+ 45 - 8
src/app/journal/journal-npcs/journal-npcs.component.html

@@ -17,7 +17,7 @@
               @if (currentNpc.name !== "") {
                 {{ currentNpc.name }}
               } @else {
-                {{ "notes.noName" | translate }}
+                {{ "npcs.noName" | translate }}
               }
             </div>
             <div class="unsaved">{{ "notes.unsaved" | translate }}</div>
@@ -46,7 +46,29 @@
               }"
             >
               <div class="npc" (click)="selectNpc(index, group)">
-                <div class="npc-title">{{ npc.name }}</div>
+                <div class="npc-title">
+                  <!-- {{ npc.name }} -->
+                  @if (npcs[group][index].name !== "") {
+                    @if (
+                      isInEditMode &&
+                      index === currentIndex &&
+                      group === currentType
+                    ) {
+                      {{ currentNpc.name }}
+                    } @else {
+                      {{ npcs[group][index].name }}
+                    }
+                  } @else {
+                    {{ "npcs.noName" | translate }}
+                  }
+                </div>
+                @if (
+                  isInEditMode &&
+                  currentIndex === index &&
+                  currentType === group
+                ) {
+                  <div class="unsaved">{{ "npcs.unsaved" | translate }}</div>
+                }
               </div>
               <div class="control-button-wrapper">
                 <icon-button
@@ -65,6 +87,8 @@
     }
   </mat-accordion>
 </div>
+
+<!-- Read/Write Container -->
 @if (!npcsIsEmpty) {
   @if (isInEditMode) {
     <div class="write-container">
@@ -83,7 +107,9 @@
         </div>
       </div>
       <div class="t-2">
-        <label class="write-label">{{ "npcs.short" | translate }}</label>
+        <label class="write-label">{{
+          "npcs.shortPlaceholder" | translate
+        }}</label>
         <div class="NgxEditor__Wrapper">
           <ngx-editor-menu [editor]="shortEditor" [toolbar]="toolbar">
           </ngx-editor-menu>
@@ -96,7 +122,9 @@
         </div>
       </div>
       <div class="t-2">
-        <label class="write-label">{{ "npcs.long" | translate }}</label>
+        <label class="write-label">{{
+          "npcs.longPlaceholder" | translate
+        }}</label>
         <div class="NgxEditor__Wrapper">
           <ngx-editor-menu [editor]="longEditor" [toolbar]="toolbar">
           </ngx-editor-menu>
@@ -121,10 +149,19 @@
     <div class="read-container">
       <div class="title-row">
         <div class="name-read">{{ currentNpc.name }}</div>
-        <icon-button
-          [icon]="'flip'"
-          (click)="showShortDescription = !showShortDescription"
-        ></icon-button>
+        <mat-button-toggle-group name="fontStyle" aria-label="Font Style">
+          <mat-button-toggle
+            value="short"
+            (click)="showShortDescription = true"
+            >{{ "npcs.short" | translate }}</mat-button-toggle
+          >
+          <mat-button-toggle
+            value="long"
+            [class]="showShortDescription ? '' : 'mat-button-toggle-checked'"
+            (click)="showShortDescription = false"
+            >{{ "npcs.long" | translate }}</mat-button-toggle
+          >
+        </mat-button-toggle-group>
       </div>
       <divider [appearance]="'gold-2'" class="b-1 t-1"></divider>
       <div class="short-read">

+ 11 - 0
src/app/journal/journal-npcs/journal-npcs.component.scss

@@ -204,3 +204,14 @@ ngx-editor {
     }
   }
 }
+
+.mat-button-toggle-appearance-standard.mat-button-toggle-checked {
+  background-color: rgb(169, 169, 169);
+}
+
+.mat-button-toggle-standalone.mat-button-toggle-appearance-standard:not(
+    [class*="mat-elevation-z"]
+  ),
+.mat-button-toggle-group-appearance-standard:not([class*="mat-elevation-z"]) {
+  border: var(--gold-2);
+}

+ 144 - 10
src/app/journal/journal-places/journal-places.component.html

@@ -1,11 +1,145 @@
-<div
-  style="
-    display: flex;
-    justify-content: center;
-    align-items: center;
-    height: 100%;
-    width: 100%;
-  "
->
-  <img style="height: 100%" src="assets/images/place_coming_soon.jpeg" alt="" />
+<div class="places-list">
+  <div class="title t-0">{{ "places.title" | translate }}</div>
+  <divider appearance="gold-2" style="padding: 0 1rem"></divider>
+  <!-- Add Button or temporary unsaved new Place -->
+  @if (isNewPlace) {
+    <div class="place active">
+      <div class="place-title">
+        @if (currentPlace.name !== "") {
+          {{ currentPlace.name }}
+        } @else {
+          {{ "places.noName" | translate }}
+        }
+      </div>
+      <div class="unsaved">{{ "places.unsaved" | translate }}</div>
+    </div>
+  } @else {
+    <div class="place add-button" (click)="addPlace()">
+      <img src="assets/icons/UIIcons/add.svg" />
+    </div>
+  }
+  <!-- List of entries -->
+  @for (place of places; let index = $index; track place) {
+    <div
+      class="place-wrapper"
+      [ngClass]="{
+        active: currentIndex === index,
+        'edit-mode': isInEditMode
+      }"
+    >
+      <div (click)="selectPlace(index)" class="place">
+        <div class="place-title">
+          @if (places[index].name !== "") {
+            @if (isInEditMode && index === currentIndex) {
+              {{ currentPlace.name }}
+            } @else {
+              {{ places[index].name }}
+            }
+          } @else {
+            {{ "places.noName" | translate }}
+          }
+        </div>
+        @if (isInEditMode && currentIndex === index) {
+          <div class="unsaved">{{ "places.unsaved" | translate }}</div>
+        }
+      </div>
+      <div class="control-button-wrapper">
+        <icon-button [icon]="'edit-large'" (click)="editPlace()"></icon-button>
+        <icon-button [icon]="'delete'" (click)="deletePlace()"></icon-button>
+      </div>
+    </div>
+  }
 </div>
+
+@if (places.length > 0 || isNewPlace) {
+  @if (isInEditMode) {
+    <div class="write-container">
+      <div>
+        <label class="write-label">{{
+          "places.namePlaceholder" | translate
+        }}</label>
+        <div>
+          <mat-form-field class="name-write" appearance="outline">
+            <input
+              matInput
+              [(ngModel)]="currentPlace.name"
+              [placeholder]="'places.namePlaceholder' | translate"
+            />
+          </mat-form-field>
+        </div>
+      </div>
+      <div class="t-2">
+        <label class="write-label">{{
+          "places.shortPlaceholder" | translate
+        }}</label>
+        <div class="NgxEditor__Wrapper">
+          <ngx-editor-menu [editor]="shortEditor" [toolbar]="toolbar">
+          </ngx-editor-menu>
+          <ngx-editor
+            class="short-editor"
+            [editor]="shortEditor"
+            [placeholder]="'places.shortPlaceholder' | translate"
+            [(ngModel)]="currentPlace.shortDescription"
+          ></ngx-editor>
+        </div>
+      </div>
+      <div class="t-2">
+        <label class="write-label">{{
+          "places.longPlaceholder" | translate
+        }}</label>
+        <div class="NgxEditor__Wrapper">
+          <ngx-editor-menu [editor]="longEditor" [toolbar]="toolbar">
+          </ngx-editor-menu>
+          <ngx-editor
+            class="long-editor"
+            [editor]="longEditor"
+            [placeholder]="'places.longPlaceholder' | translate"
+            [(ngModel)]="currentPlace.longDescription"
+          ></ngx-editor>
+        </div>
+      </div>
+      <div class="button-row">
+        <ui-button width="w16" (click)="savePlace()">{{
+          "places.save" | translate
+        }}</ui-button>
+        <ui-button width="w16" (click)="discardPlace()">{{
+          "places.discard" | translate
+        }}</ui-button>
+      </div>
+    </div>
+  } @else {
+    <div class="read-container">
+      <div class="title-row">
+        <div class="name-read">{{ currentPlace.name }}</div>
+        <mat-button-toggle-group name="fontStyle" aria-label="Font Style">
+          <mat-button-toggle
+            value="short"
+            (click)="showShortDescription = true"
+            >{{ "places.short" | translate }}</mat-button-toggle
+          >
+          <mat-button-toggle
+            value="long"
+            [class]="showShortDescription ? '' : 'mat-button-toggle-checked'"
+            (click)="showShortDescription = false"
+            >{{ "places.long" | translate }}</mat-button-toggle
+          >
+        </mat-button-toggle-group>
+      </div>
+      <divider [appearance]="'gold-2'" class="b-1 t-1"></divider>
+      <div class="short-read">
+        <div
+          class="description"
+          [innerHTML]="
+            showShortDescription
+              ? currentPlace.shortDescription
+              : currentPlace.longDescription
+          "
+        ></div>
+      </div>
+    </div>
+  }
+} @else {
+  <div class="empty-container">
+    {{ "places.empty" | translate }}
+  </div>
+}

+ 211 - 0
src/app/journal/journal-places/journal-places.component.scss

@@ -0,0 +1,211 @@
+.places-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: 2rem;
+
+  .place {
+    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;
+    }
+    .place-title {
+      overflow: hidden;
+      white-space: nowrap;
+      text-overflow: ellipsis;
+      font-weight: 500;
+    }
+
+    .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;
+  }
+
+  .place-wrapper {
+    display: flex;
+    gap: 0.5rem;
+    align-items: center;
+    .control-button-wrapper {
+      display: none;
+    }
+
+    &.active:not(.edit-mode) {
+      .place {
+        width: 13rem;
+        background-image: url("../../../assets/images/texture-30.jpg") !important;
+      }
+      .control-button-wrapper {
+        display: flex;
+      }
+    }
+    &.active {
+      .place {
+        background-image: url("../../../assets/images/texture-30.jpg") !important;
+      }
+    }
+  }
+
+  .add-button {
+    height: 5rem;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+  }
+}
+
+.read-container {
+  width: 800px;
+  height: calc(100vh - 6rem);
+  margin-top: 1.5rem;
+  margin-left: calc(50vw - 400px + 9rem);
+  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;
+
+    icon-button {
+      width: 2rem;
+      height: 2rem;
+    }
+  }
+
+  .name-read {
+    font-size: 1.5rem;
+    font-weight: 600;
+  }
+
+  .section-name {
+    font-size: 1.25rem;
+    font-weight: 500;
+  }
+}
+
+// Write view
+
+.write-container {
+  width: 800px;
+  height: calc(100vh - 2rem);
+  margin-top: 1.5rem;
+  margin-left: calc(50vw - 400px + 9rem);
+  padding: 1rem 2rem 2rem;
+  overflow: auto;
+
+  .name-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;
+    }
+  }
+  .button-row {
+    display: flex;
+    justify-content: space-around;
+    margin-top: 2.5rem;
+  }
+}
+
+.write-label {
+  margin: 0 0 0.25rem 0.25rem;
+  font-weight: 600;
+}
+
+.empty-container {
+  width: 800px;
+  margin-top: 45vh;
+  margin-left: calc(50vw - 5rem);
+  font-weight: 600;
+  font-size: 1.5rem;
+}
+
+// MATERIAL
+
+.mat-expansion-panel {
+  border: var(--gold-2);
+}
+
+::ng-deep .mat-expansion-panel-body {
+  padding: 0 1rem 1rem !important;
+}
+
+.mat-accordion {
+  width: 16rem;
+}
+
+// Editor
+
+.NgxEditor__Wrapper {
+  border: var(--gold-3) !important;
+  border-radius: 6px;
+  box-shadow: var(--shadow);
+}
+
+::ng-deep .NgxEditor__MenuBar {
+  background-image: url("../../../assets/images/texture-10.jpg");
+}
+
+ngx-editor {
+  &.short-editor {
+    ::ng-deep .ProseMirror {
+      height: calc((100vh - 40rem) / 2);
+      background-image: url("../../../assets/images/texture-0.jpg");
+      border-radius: 0 0 4px 4px;
+    }
+  }
+
+  &.long-editor {
+    ::ng-deep .ProseMirror {
+      height: calc((100vh - 18rem) / 2);
+      background-image: url("../../../assets/images/texture-0.jpg");
+      border-radius: 0 0 4px 4px;
+    }
+  }
+}
+
+.mat-button-toggle-appearance-standard.mat-button-toggle-checked {
+  background-color: rgb(169, 169, 169);
+}
+
+.mat-button-toggle-standalone.mat-button-toggle-appearance-standard:not(
+    [class*="mat-elevation-z"]
+  ),
+.mat-button-toggle-group-appearance-standard:not([class*="mat-elevation-z"]) {
+  border: var(--gold-2);
+}

+ 141 - 2
src/app/journal/journal-places/journal-places.component.ts

@@ -1,8 +1,147 @@
-import { Component } from '@angular/core';
+import { Component, inject } from '@angular/core';
+import { Editor } from 'ngx-editor';
+import { Place } from 'src/interfaces/interfaces';
+import { DataService } from 'src/services/data/data.service';
 
 @Component({
   selector: 'app-journal-places',
   templateUrl: './journal-places.component.html',
   styleUrl: './journal-places.component.scss',
 })
-export class JournalPlacesComponent {}
+export class JournalPlacesComponent {
+  shortEditor: Editor = new Editor();
+  longEditor: Editor = new Editor();
+  toolbar: any = [
+    // default value
+    ['bold', 'italic'],
+    ['bullet_list'],
+    [{ heading: ['h3', 'h4', 'h5', 'h6'] }],
+  ];
+
+  public isNewPlace = false;
+  public isInEditMode = false;
+  public currentPlace: Place = {
+    name: '',
+    shortDescription: '',
+    longDescription: '',
+  };
+  public places: Place[] = [];
+  public currentIndex: number = 0;
+  private backupIndex: number = -1;
+  public showShortDescription: boolean = false;
+
+  private dataService: DataService = inject(DataService);
+
+  ngOnInit(): void {
+    this.places = this.dataService.places;
+
+    // if the list is empty, set the currentIndex to -1 to hide the entry-container
+    if (this.places.length === 0) {
+      this.currentIndex = -1;
+    } else {
+      this.selectPlace(0);
+    }
+  }
+
+  // Template functions
+
+  /**
+   * Selectes a place from the list indeicated by the index param. It also sets all the needed variables to display the place in read mode.
+   * @param index The index of the place to select
+   */
+  public selectPlace(index: number): void {
+    this.currentIndex = index;
+    this.currentIndex = index;
+    this.currentPlace = this.getPlace();
+    this.isNewPlace = false;
+    this.isInEditMode = false;
+  }
+
+  /**
+   * Adds a new place and starts the edit mode.
+   */
+  public addPlace(): void {
+    this.isNewPlace = true;
+    this.currentPlace = {
+      name: '',
+      shortDescription: '',
+      longDescription: '',
+    };
+    this.isNewPlace = true;
+    this.isInEditMode = true;
+    this.backupIndex = this.currentIndex;
+    // Hightlight no place because the placeholder is shown as active
+    this.currentIndex = -1;
+  }
+
+  public editPlace(): void {
+    this.isInEditMode = true;
+  }
+
+  /**
+   * Saves the current place at the start of the list of places and uploads it to the data service.
+   */
+  public savePlace(): void {
+    if (this.isNewPlace) {
+      // Prepend the new JournalEntry
+      this.places.unshift(this.currentPlace);
+      this.isNewPlace = false;
+      this.currentIndex = 0;
+    }
+    this.isInEditMode = false;
+    this.places[this.currentIndex] = this.currentPlace;
+    this.uploadPlaces();
+  }
+
+  /**
+   * Deletes the current place from the list and uploads the new list to the data service.
+   */
+  public deletePlace(): void {
+    this.places.splice(this.currentIndex, 1);
+    if (this.places.length === 0) {
+      this.currentIndex = -1;
+    } else {
+      this.currentIndex = Math.max(this.currentIndex - 1, 0);
+      this.currentPlace = this.getPlace();
+    }
+    this.uploadPlaces();
+  }
+
+  /**
+   * Discards the current place and resets the currentPlace to the last saved version.
+   */
+  public discardPlace(): void {
+    if (this.isNewPlace) {
+      this.currentIndex = this.backupIndex;
+      this.isNewPlace = false;
+    }
+    if (this.places.length > 0) {
+      // Reset the currentEntry to the last saved version
+      this.currentPlace = this.getPlace();
+    }
+    this.isInEditMode = false;
+  }
+
+  /**
+   * Saves the current entries in the data service.
+   */
+  private uploadPlaces(): void {
+    console.log('Uploading places in places component');
+
+    this.dataService.places = this.places;
+  }
+
+  // Helper functions
+
+  /**
+   * Returns a deep copy of the current place.
+   */
+  private getPlace(): Place {
+    return JSON.parse(JSON.stringify(this.places[this.currentIndex]));
+  }
+
+  ngOnDestroy(): void {
+    this.shortEditor.destroy();
+    this.longEditor.destroy();
+  }
+}

+ 1 - 1
src/app/shared-components/highlight/highlight.component.html

@@ -1 +1 @@
-<span [ngbTooltip]="tooltip!">{{ text }}</span>
+<span [ngbTooltip]="tooltip!" tooltipClass="my-custom-class">{{ text }}</span>

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

@@ -822,11 +822,11 @@
   },
   "notes": {
     "title": "Titel",
+    "noTitle": "Noch kein Titel",
     "placeholder": "Hier die Notizen einfügen",
     "save": "Speichern",
     "discard": "Verwerfen",
     "sessions": "Sessions",
-    "noName": "Noch kein Name",
     "unsaved": "Nicht gespeichert",
     "empty": "Noch kein Eintrag vorhanden"
   },
@@ -839,10 +839,25 @@
     "shortPlaceholder": "Kurze Beschreibung",
     "longPlaceholder": "Ausführliche Beschreibung",
     "namePlaceholder": "Name",
-    "short": "Kurzbeschreibung",
-    "long": "Ausführliche Beschreibung",
+    "noName": "Noch kein Name",
+    "short": "Kurz",
+    "long": " Lang",
     "empty": "Noch keine Personen hinzugefügt",
     "save": "Speichern",
+    "unsaved": "Nicht gespeichert",
+    "discard": "Verwerfen"
+  },
+  "places": {
+    "title": "Orte",
+    "shortPlaceholder": "Kurze Beschreibung",
+    "longPlaceholder": "Ausführliche Beschreibung",
+    "namePlaceholder": "Name",
+    "noName": "Noch kein Name",
+    "short": "Kurz",
+    "long": " Lang",
+    "empty": "Noch keine Orte hinzugefügt",
+    "unsaved": "Nicht gespeichert",
+    "save": "Speichern",
     "discard": "Verwerfen"
   },
   "creator": {
@@ -860,6 +875,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.13.1"
+    "version": "0.13.2"
   }
 }

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

@@ -817,11 +817,11 @@
   },
   "notes": {
     "title": "Title",
+    "noTitle": "No title yet",
     "placeholder": "Add notes here",
     "save": "Save",
     "discard": "Discard",
     "sessions": "Sessions",
-    "noName": "No Name yet",
     "unsaved": "Not saved",
     "empty": "No entries added yet"
   },
@@ -834,9 +834,24 @@
     "shortPlaceholder": "Short description",
     "longPlaceholder": "Detailed description",
     "namePlaceholder": "Name",
-    "short": "Short Description",
-    "long": "Detailed Description",
+    "noName": "No Name yet",
     "empty": "No persons added yet",
+    "unsaved": "Not saved",
+    "save": "Save",
+    "discard": "Discard",
+    "long": "Long",
+    "short": "Short"
+  },
+  "places": {
+    "title": "Places",
+    "shortPlaceholder": "Short description",
+    "longPlaceholder": "Detailed description",
+    "namePlaceholder": "Name",
+    "noName": "No Name yet",
+    "long": "Long",
+    "short": "Short",
+    "empty": "No places added yet",
+    "unsaved": "Not saved",
     "save": "Save",
     "discard": "Discard"
   },
@@ -855,7 +870,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.13.1",
+    "version": "0.13.2",
     "test": "Test"
   }
 }

+ 7 - 0
src/interfaces/interfaces.ts

@@ -193,3 +193,10 @@ export interface Npc {
   shortDescription: string;
   organization?: string;
 }
+
+export interface Place {
+  name: string;
+  identifier?: string;
+  longDescription: string;
+  shortDescription: string;
+}

+ 24 - 1
src/services/data/data.service.ts

@@ -12,6 +12,7 @@ import {
   Attribute,
   JournalEntry,
   Npcs,
+  Place,
 } from 'src/interfaces/interfaces';
 import { SpellsService } from '../spells/spells.service';
 
@@ -172,6 +173,17 @@ export class DataService {
         others: [],
       };
     }
+
+    // Locations
+
+    // Migration
+
+    if (!Array.isArray(locationsData.data)) {
+      this.places = [];
+      console.log('migrated places data to array');
+    } else {
+      this.places = locationsData.data;
+    }
   }
 
   // #endregion
@@ -1100,7 +1112,7 @@ export class DataService {
 
   // #endregion
 
-  // #region Notes and Entries
+  // #region Notes, NPCs and Locations
 
   private _notesData: JournalEntry[] = [];
 
@@ -1129,6 +1141,17 @@ export class DataService {
     this.setData('npcs', newValue);
   }
 
+  private _places: Place[] = [];
+
+  public get places(): Place[] {
+    return this._places;
+  }
+
+  public set places(newValue: Place[]) {
+    this._places = newValue;
+    this.setData('locations', { data: newValue });
+  }
+
   // #endregion
 
   // #region database calls

+ 30 - 0
src/styles.scss

@@ -285,6 +285,36 @@ input[type="checkbox"]:checked::after {
   border: var(--gold-1) !important;
 }
 
+// Tooltip
+
+.my-custom-class .tooltip-inner {
+  background-color: rgb(42, 42, 42);
+  border: var(--gold-dark-3);
+}
+.my-custom-class.bs-tooltip-end .tooltip-arrow::before {
+  border-right-color: var(--gold-dark);
+}
+.my-custom-class.bs-tooltip-start .tooltip-arrow::before {
+  border-left-color: var(--gold-dark);
+}
+.my-custom-class.bs-tooltip-top .tooltip-arrow::before {
+  border-top-color: var(--gold-dark);
+}
+.my-custom-class.bs-tooltip-bottom .tooltip-arrow::before {
+  border-bottom-color: var(--gold-dark);
+}
+
+.tooltip-content {
+  padding: 0.5rem;
+  p {
+    margin-bottom: 0;
+  }
+}
+
+.tooltip.show {
+  opacity: 1;
+}
+
 // Used to remove the empty space under form fields
 .mat-mdc-form-field-subscript-wrapper {
   display: none;