Skip to content

Commit

Permalink
feat: character switcher
Browse files Browse the repository at this point in the history
  • Loading branch information
ManuelRauber committed Feb 5, 2024
1 parent 03a43e3 commit cc160ef
Show file tree
Hide file tree
Showing 16 changed files with 204 additions and 50 deletions.
2 changes: 1 addition & 1 deletion src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const routes: Routes = [
loadComponent: () => import('./components/characters/create-character/create-character.component'),
},
{
path: ':id',
path: ':characterId',
children: [
{
path: '',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<section class="flex items-center space-x-2">
<div class="relative inline-flex h-12 w-12 shrink-0 items-center justify-center overflow-hidden rounded-full bg-gray-100 dark:bg-gray-600">
<span class="font-medium text-gray-600 dark:text-gray-300">{{ initials() }}</span>
</div>
<div class="flex min-w-0 flex-col justify-center text-left">
<div class="truncate font-semibold">
{{ characterName() }}
</div>
@if (group) {
<div class="truncate text-sm text-gray-500 dark:text-gray-400">{{ group }}</div>
}
</div>
</section>
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { ChangeDetectionStrategy, Component, computed, input, Input } from '@angular/core';

const createInitials = (name: string) => {
const regExp = new RegExp(/(\p{L}{1})\p{L}+/, 'gu');

const initials = [...name.matchAll(regExp)] || [];

return ((initials.shift()?.[1] || '') + (initials.pop()?.[1] || '')).toUpperCase();
};

@Component({
selector: 'pap-character-switcher-item',
standalone: true,
imports: [],
templateUrl: './character-switcher-item.component.html',
styleUrl: './character-switcher-item.component.css',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CharacterSwitcherItemComponent {
@Input() group = '';
characterName = input.required<string>();

initials = computed(() => {
const characterName = this.characterName();
return createInitials(characterName);
});
}
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<div class="flex items-center space-x-2">
<pap-character-switcher-item
#dropdownTrigger
[characterName]="currentCharacter().name"
[group]="currentCharacter().group"
class="min-w-0 flex-1 cursor-pointer" />
<fa-icon [icon]="faRepeat" class="text-gray-500 dark:text-gray-400" />
</div>

<div class="z-10 hidden" #dropdownTarget id="character-menu-dropdown">
<div class="ml-1 rounded-lg bg-gray-50 shadow dark:bg-gray-800">
@if (charactersWithoutCurrent().length) {
<div class="max-h-48 space-y-2 overflow-y-auto px-2 py-2">
@for (character of charactersWithoutCurrent(); track character.id) {
<pap-character-switcher-item
(click)="selectCharacter(character)"
class="block"
[characterName]="character.name"
[group]="character.group"></pap-character-switcher-item>
}
</div>
<hr class="h-px border-0 bg-gray-200 dark:bg-gray-700" />
}

<a [routerLink]="['/characters/create']" class="block px-2 py-3">
<fa-icon [icon]="faUser" class="mr-1" />
Neuen Charakter erstellen</a
>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { AfterViewInit, ChangeDetectionStrategy, Component, computed, ElementRef, EventEmitter, input, Output, ViewChild } from '@angular/core';
import { CharacterSwitcherItemComponent } from './character-switcher-item/character-switcher-item.component';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { faRepeat, faUser } from '@fortawesome/free-solid-svg-icons';
import { RouterLink } from '@angular/router';
import { CharacterEntity } from '../../models/character/character.entity';
import { Dropdown } from 'flowbite';

@Component({
selector: 'pap-character-switcher',
standalone: true,
imports: [CharacterSwitcherItemComponent, FaIconComponent, RouterLink],
templateUrl: './character-switcher.component.html',
styleUrl: './character-switcher.component.css',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CharacterSwitcherComponent implements AfterViewInit {
characters = input.required<CharacterEntity[]>();
currentCharacter = input.required<CharacterEntity>();

@Output() characterSelected = new EventEmitter<CharacterEntity>();
protected readonly charactersWithoutCurrent = computed(() => {
const characters = this.characters();
const currentCharacter = this.currentCharacter();

return characters.filter(character => character.id !== currentCharacter.id);
});
protected readonly faRepeat = faRepeat;
protected readonly faUser = faUser;
@ViewChild('dropdownTrigger', { read: ElementRef, static: true })
private readonly dropdownTrigger!: ElementRef<HTMLElement>;
@ViewChild('dropdownTarget', { static: true })
private readonly dropdownTarget!: ElementRef<HTMLElement>;
private dropdown!: Dropdown;

ngAfterViewInit() {
this.dropdown = new Dropdown(this.dropdownTarget.nativeElement, this.dropdownTrigger.nativeElement, {
placement: 'top',
});
}

protected selectCharacter(character: CharacterEntity) {
this.dropdown.hide();
this.characterSelected.emit(character);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export default class CharacterDashboardComponent {
private readonly charactersService = inject(CharactersService);
private readonly id = toSignal(
this.activatedRoute.paramMap.pipe(
map(params => params.get('id')),
map(params => params.get('characterId')),
filter(id => !!id),
map(id => +id!),
),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,12 @@
@if (charactersStore.count() > 0) {
<pap-h1>Keine Charaktererstellung möglich</pap-h1>
<pap-h1>Neuen Charakter erstellen</pap-h1>

<p>
Du hast bereits einen Charakter erstellt, gehe daher bitte zu seinem
<a class="underline" [routerLink]="['/characters', charactersStore.entities()[0].id, 'dashboard']">Dashboard</a>.
</p>
} @else {
<pap-h1>Neuen Charakter erstellen</pap-h1>
<form (ngSubmit)="submit()" [formGroup]="formGroup" class="space-y-4">
<pap-text-input [autofocus]="true" [control]="formGroup.controls['name']" autoComplete="nickname" class="block" label="Name" />
<pap-text-input [control]="formGroup.controls['age']" [min]="0" class="block" label="Alter" valueType="number" />
<pap-text-input [control]="formGroup.controls['gender']" class="block" label="Geschlecht" />
<pap-text-input [control]="formGroup.controls['nation']" class="block" label="Volk" />
<pap-text-input [control]="formGroup.controls['religion']" class="block" label="Religion" />
<pap-text-input [control]="formGroup.controls['group']" class="block" label="Spielrunde" />

<form [formGroup]="formGroup" (ngSubmit)="submit()" class="space-y-4">
<pap-text-input class="block" [control]="formGroup.controls['name']" label="Name" [autofocus]="true" autoComplete="nickname" />
<pap-text-input class="block" [control]="formGroup.controls['age']" label="Alter" valueType="number" [min]="0" />
<pap-text-input class="block" [control]="formGroup.controls['gender']" label="Geschlecht" />
<pap-text-input class="block" [control]="formGroup.controls['nation']" label="Volk" />
<pap-text-input class="block" [control]="formGroup.controls['religion']" label="Religion" />

<pap-submit-button class="mt-4" [disabled]="formGroup.invalid">Charakter erstellen</pap-submit-button>
</form>
}
<pap-submit-button [disabled]="formGroup.invalid" class="mt-4">Charakter erstellen</pap-submit-button>
</form>
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { TextInputComponent } from '../../inputs/text-input/text-input.component
import { SubmitButtonComponent } from '../../inputs/submit-button/submit-button.component';
import { Router, RouterLink } from '@angular/router';
import { CharactersService } from '../../../services/characters.service';
import { CharactersStore } from '../../../stores/characters.store';

@Component({
selector: 'pap-create-character',
Expand All @@ -22,9 +21,9 @@ export default class CreateCharacterComponent {
gender: ['', Validators.required],
age: [0, [Validators.required, Validators.min(0)]],
religion: ['', Validators.required],
group: ['', Validators.required],
});
protected readonly charactersService = inject(CharactersService);
protected readonly charactersStore = inject(CharactersStore);
private readonly router = inject(Router);

protected async submit(): Promise<void> {
Expand All @@ -38,6 +37,7 @@ export default class CreateCharacterComponent {
this.formGroup.value.age!,
this.formGroup.value.nation!,
this.formGroup.value.religion!,
this.formGroup.value.group!,
);

await this.router.navigate(['/characters', character.id, 'dashboard']);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { TauriService } from '../../../services/tauri.service';
import { DateTime } from 'luxon';
import { H2Component } from '../../headings/h2/h2.component';
import { H3Component } from '../../headings/h3/h3.component';
import { Router } from '@angular/router';
import { FormControl, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { FileDirective } from '../../../directives/file.directive';

Expand All @@ -22,7 +21,6 @@ export default class BackupRestoreComponent {
protected readonly fileControl = new FormControl<File | null>(null, { validators: [Validators.required] });
private readonly databaseService = inject(DatabaseService);
private readonly tauriService = inject(TauriService);
private readonly router = inject(Router);
@ViewChild('fileInput')
private readonly fileInputElement!: ElementRef<HTMLInputElement>;

Expand Down Expand Up @@ -64,7 +62,7 @@ export default class BackupRestoreComponent {
return;
}

await this.router.navigateByUrl('/');
window.location.reload();
}

async reset(): Promise<void> {
Expand All @@ -75,6 +73,6 @@ export default class BackupRestoreComponent {
}

await this.databaseService.clear();
await this.router.navigateByUrl('/');
window.location.reload();
}
}
18 changes: 14 additions & 4 deletions src/app/components/sidebar/sidebar.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
<div class="flex h-full flex-col overflow-y-auto bg-white px-3 py-5 dark:bg-gray-950">
<div class="space-y-2">
<pap-sidebar-item-group [icon]="faDragon" label="Charakter">
<pap-sidebar-item [icon]="faDashboard" [link]="['/characters/1/dashboard']">Dashboard</pap-sidebar-item>
<pap-sidebar-item [icon]="faDashboard" [link]="['/characters', currentCharacterIdOrDefault() ,'dashboard']"
>Dashboard</pap-sidebar-item
>
</pap-sidebar-item-group>
</div>
<div class="space-y-2">
Expand All @@ -16,11 +18,19 @@
</div>
<div class="space-y-2">
<pap-sidebar-item-group [icon]="faGears" label="Einstellungen">
<pap-sidebar-item [icon]="faArrowRotateBackward" [link]="['/settings/backup-restore']">Backup</pap-sidebar-item>
<pap-sidebar-item [icon]="faArrowRotateBackward" [link]="['/settings/backup-restore']">Backup </pap-sidebar-item>
</pap-sidebar-item-group>
</div>

<div class="mt-auto flex flex-col p-4 pt-2 text-center">
<div class="mt-auto flex flex-col pt-2 text-center">
@if (charactersStore.count() && currentCharacter()) {
<hr class="my-4 h-px border-0 bg-gray-200 dark:bg-gray-700" />
<pap-character-switcher
[currentCharacter]="currentCharacter()!"
[characters]="charactersStore.entities()"
(characterSelected)="switchCharacter($event)" />
}
<hr class="my-4 h-px border-0 bg-gray-200 dark:bg-gray-700" />
<div>
<a href="https://github.com/boundfoxstudios/tipis-pap-tools" target="_blank">
<fa-icon [icon]="faGithub" />
Expand All @@ -34,7 +44,7 @@
}
</a>
</div>
<div class="mt-1 text-xs font-extralight text-gray-400">
<div class="mb-4 mt-1 text-xs font-extralight text-gray-400">
With
<fa-icon [icon]="faHeart" class="text-red-600" />
by Boundfox Studios
Expand Down
66 changes: 49 additions & 17 deletions src/app/components/sidebar/sidebar.component.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,22 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import {
faArrowCircleLeft,
faArrowRotateBackward,
faDashboard,
faDiceD6,
faDragon,
faGears,
faHeart,
faRecycle,
faTools,
} from '@fortawesome/free-solid-svg-icons';
import { faArrowRotateBackward, faDashboard, faDiceD6, faDragon, faGears, faHeart, faTools } from '@fortawesome/free-solid-svg-icons';
import { SidebarItemComponent } from './sidebar-item/sidebar-item.component';
import { faGithub } from '@fortawesome/free-brands-svg-icons';
import { VersionService } from '../../services/version.service';
import { toSignal } from '@angular/core/rxjs-interop';
import { AsyncPipe } from '@angular/common';
import { AsyncPipe, JsonPipe } from '@angular/common';
import { SidebarItemGroupComponent } from './sidebar-item-group/sidebar-item-group.component';
import { RouterLinkActive } from '@angular/router';
import { ActivatedRoute, NavigationEnd, Router, RouterLinkActive } from '@angular/router';
import { CharacterSwitcherComponent } from '../character-switcher/character-switcher.component';
import { CharactersStore } from '../../stores/characters.store';
import { filter, map } from 'rxjs';
import { CharacterEntity } from '../../models/character/character.entity';

@Component({
selector: 'pap-sidebar',
standalone: true,
imports: [FaIconComponent, SidebarItemComponent, AsyncPipe, SidebarItemGroupComponent, RouterLinkActive],
imports: [FaIconComponent, SidebarItemComponent, AsyncPipe, SidebarItemGroupComponent, RouterLinkActive, CharacterSwitcherComponent, JsonPipe],
templateUrl: './sidebar.component.html',
styleUrl: './sidebar.component.css',
changeDetection: ChangeDetectionStrategy.OnPush,
Expand All @@ -35,9 +29,47 @@ export class SidebarComponent {
protected readonly faDashboard = faDashboard;
protected readonly faTools = faTools;
protected readonly faGears = faGears;
protected readonly faRecycle = faRecycle;
protected readonly faArrowCircleLeft = faArrowCircleLeft;
protected readonly faArrowRotateBackward = faArrowRotateBackward;
protected readonly charactersStore = inject(CharactersStore);
protected readonly currentCharacter = computed(() => {
const id = this.characterId();

if (!id) {
return null;
}

const entities = this.charactersStore.entityMap();
return entities[id];
});
protected readonly currentCharacterIdOrDefault = computed(() => {
const currentCharacter = this.currentCharacter();

return currentCharacter?.id ?? 1;
});
private readonly versionService = inject(VersionService);
protected readonly versionSignal = toSignal(this.versionService.load());
private readonly router = inject(Router);
private readonly route = inject(ActivatedRoute);
readonly characterId = toSignal(
this.router.events.pipe(
filter(event => event instanceof NavigationEnd),
map(() => this.getParams(this.route)),
map(params => params.characterId ?? null),
),
);

protected switchCharacter(character: CharacterEntity) {
void this.router.navigate(['/characters', character.id, 'dashboard']);
}

private getParams(route: ActivatedRoute): { characterId?: string } {
let params = route.snapshot.params;
params = { ...route.snapshot.queryParams, ...params };
if (route.children) {
for (const r of route.children) {
params = { ...this.getParams(r), ...params };
}
}
return params;
}
}
6 changes: 6 additions & 0 deletions src/app/models/character/character.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ export interface CharacterEntity {
nation: string;
age: number;
religion: string;

/**
* Group is the "Spielrunde" where the character is played.
*/
group: string;

main: MainStatistics;
value: ValueStatistics;
}
3 changes: 2 additions & 1 deletion src/app/services/characters.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ export class CharactersService {
await this.charactersStore.update(character);
}

async add(name: string, gender: string, age: number, nation: string, religion: string): Promise<CharacterEntity> {
async add(name: string, gender: string, age: number, nation: string, religion: string, group: string): Promise<CharacterEntity> {
return this.charactersStore.add({
name,
gender,
age,
nation,
religion,
group,
main: {
agility: 0,
magic: 0,
Expand Down
3 changes: 1 addition & 2 deletions src/app/services/tables/characters.table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ export class CharactersTable {
async add(character: Omit<CharacterEntity, 'id'>) {
const id = await this.databaseService.characters.add({
...character,
id: 1, // prevent creating more than one character
});
} as CharacterEntity);

const newCharacter = await this.databaseService.characters.get(id);
return newCharacter!;
Expand Down

0 comments on commit cc160ef

Please sign in to comment.