Mise en place du picker d'image pour la partie header / illustration des fiches personnage
Some checks failed
E2E Tests / e2e (push) Has been cancelled
Build & Push Images / build (brain) (push) Successful in 1m1s
Build & Push Images / build (core) (push) Successful in 1m32s
Build & Push Images / build (web) (push) Successful in 1m42s

Migration pour l'ancienne partie des fiches perso vers les nouvelles pages
Vue retravaillée pour les fiches perso
This commit is contained in:
2026-04-30 10:54:27 +02:00
parent 52e389db24
commit 7c4a42327d
28 changed files with 1103 additions and 112 deletions

View File

@@ -35,29 +35,26 @@
/>
</div>
<div class="field-row">
<div class="field">
<label>Portrait (ID image)</label>
<input
type="text"
[(ngModel)]="portraitImageId"
name="portraitImageId"
placeholder="ID de l'image portrait"
/>
<div class="field-row image-row">
<div class="field portrait-field">
<label>Portrait</label>
<app-single-image-picker
[imageId]="portraitImageId"
aspectRatio="1 / 1"
hint="Carre conseille (400×400)."
(imageIdChange)="portraitImageId = $event">
</app-single-image-picker>
</div>
<div class="field">
<label>Bandeau / Header (ID image)</label>
<input
type="text"
[(ngModel)]="headerImageId"
name="headerImageId"
placeholder="ID de l'image bandeau"
/>
<div class="field header-field">
<label>Bandeau / Header</label>
<app-single-image-picker
[imageId]="headerImageId"
aspectRatio="3 / 1"
hint="Format paysage conseille (1200×400)."
(imageIdChange)="headerImageId = $event">
</app-single-image-picker>
</div>
</div>
<p class="hint">
Les portraits et bandeaux acceptent un ID d'image (MVP). Picker visuel a venir.
</p>
<div class="template-fields">
<app-dynamic-fields-form

View File

@@ -6,9 +6,10 @@ import { LucideAngularModule, Save, ArrowLeft, User, Trash2, Sparkles } from 'lu
import { CharacterService } from '../../../services/character.service';
import { CampaignService } from '../../../services/campaign.service';
import { GameSystemService } from '../../../services/game-system.service';
import { TemplateField } from '../../../services/template-field.model';
import { TemplateField } from '../../../services/template.model';
import { AiChatDrawerComponent } from '../../../shared/ai-chat-drawer/ai-chat-drawer.component';
import { DynamicFieldsFormComponent } from '../../../shared/dynamic-fields-form/dynamic-fields-form.component';
import { SingleImagePickerComponent } from '../../../shared/single-image-picker/single-image-picker.component';
/**
* Editeur plein ecran d'une fiche de personnage (PJ).
@@ -24,7 +25,7 @@ import { DynamicFieldsFormComponent } from '../../../shared/dynamic-fields-form/
@Component({
selector: 'app-character-edit',
standalone: true,
imports: [CommonModule, FormsModule, LucideAngularModule, AiChatDrawerComponent, DynamicFieldsFormComponent],
imports: [CommonModule, FormsModule, LucideAngularModule, AiChatDrawerComponent, DynamicFieldsFormComponent, SingleImagePickerComponent],
templateUrl: './character-edit.component.html',
styleUrls: ['./character-edit.component.scss']
})
@@ -48,8 +49,8 @@ export class CharacterEditComponent implements OnInit {
characterId: string | null = null;
name = '';
portraitImageId = '';
headerImageId = '';
portraitImageId: string | null = null;
headerImageId: string | null = null;
values: Record<string, string> = {};
imageValues: Record<string, string[]> = {};
templateFields: TemplateField[] = [];
@@ -76,8 +77,8 @@ export class CharacterEditComponent implements OnInit {
this.service.getById(this.characterId).subscribe({
next: (c) => {
this.name = c.name;
this.portraitImageId = c.portraitImageId ?? '';
this.headerImageId = c.headerImageId ?? '';
this.portraitImageId = c.portraitImageId ?? null;
this.headerImageId = c.headerImageId ?? null;
this.values = c.values ?? {};
this.imageValues = c.imageValues ?? {};
this.order = c.order ?? 0;
@@ -107,8 +108,8 @@ export class CharacterEditComponent implements OnInit {
if (!this.name.trim() || !this.campaignId) return;
const payload = {
name: this.name.trim(),
portraitImageId: this.portraitImageId.trim() || null,
headerImageId: this.headerImageId.trim() || null,
portraitImageId: this.portraitImageId,
headerImageId: this.headerImageId,
values: this.values,
imageValues: this.imageValues,
campaignId: this.campaignId

View File

@@ -0,0 +1,28 @@
<div class="cv-page">
<div class="cv-toolbar">
<button class="btn-back" (click)="back()">
<lucide-icon [img]="ArrowLeft" [size]="14"></lucide-icon>
Retour
</button>
<span class="spacer"></span>
<button class="btn-ai" (click)="toggleChat()" [class.active]="chatOpen" *ngIf="characterId">
<lucide-icon [img]="Sparkles" [size]="14"></lucide-icon>
Assistant IA
</button>
<button class="btn-edit" (click)="edit()">
<lucide-icon [img]="Edit3" [size]="14"></lucide-icon>
Editer
</button>
</div>
<app-persona-view [persona]="character" [templateFields]="templateFields"></app-persona-view>
</div>
<app-ai-chat-drawer
*ngIf="characterId && campaignId"
[campaignId]="campaignId"
entityType="character"
[entityId]="characterId"
[isOpen]="chatOpen"
(close)="chatOpen = false">
</app-ai-chat-drawer>

View File

@@ -0,0 +1,53 @@
.cv-page {
padding: 16px 0 48px;
min-height: 100vh;
}
.cv-toolbar {
display: flex;
align-items: center;
gap: 8px;
padding: 0 32px 16px;
max-width: 1100px;
margin: 0 auto;
.spacer { flex: 1; }
}
.btn-back,
.btn-edit,
.btn-ai {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 4px;
color: #d1d5db;
font-size: 0.85rem;
cursor: pointer;
transition: all 120ms;
&:hover {
background: rgba(255, 255, 255, 0.1);
color: #fff;
}
}
.btn-edit {
border-color: rgba(209, 168, 120, 0.4);
color: #d1a878;
&:hover {
background: rgba(209, 168, 120, 0.15);
}
}
.btn-ai {
&.active {
background: rgba(168, 85, 247, 0.2);
border-color: rgba(168, 85, 247, 0.5);
color: #d8b4fe;
}
}

View File

@@ -0,0 +1,80 @@
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActivatedRoute, Router } from '@angular/router';
import { LucideAngularModule, ArrowLeft, Edit3, Sparkles } from 'lucide-angular';
import { CharacterService } from '../../../services/character.service';
import { CampaignService } from '../../../services/campaign.service';
import { GameSystemService } from '../../../services/game-system.service';
import { TemplateField } from '../../../services/template.model';
import { Character } from '../../../services/character.model';
import { PersonaViewComponent } from '../../../shared/persona-view/persona-view.component';
import { AiChatDrawerComponent } from '../../../shared/ai-chat-drawer/ai-chat-drawer.component';
/**
* Vue lecture seule "WorldAnvil" d'une fiche PJ.
* Route : /campaigns/:campaignId/characters/:characterId
*/
@Component({
selector: 'app-character-view',
standalone: true,
imports: [CommonModule, LucideAngularModule, PersonaViewComponent, AiChatDrawerComponent],
templateUrl: './character-view.component.html',
styleUrls: ['./character-view.component.scss']
})
export class CharacterViewComponent implements OnInit {
readonly ArrowLeft = ArrowLeft;
readonly Edit3 = Edit3;
readonly Sparkles = Sparkles;
campaignId: string | null = null;
characterId: string | null = null;
character: Character | null = null;
templateFields: TemplateField[] = [];
chatOpen = false;
toggleChat(): void { this.chatOpen = !this.chatOpen; }
constructor(
private route: ActivatedRoute,
private router: Router,
private service: CharacterService,
private campaignService: CampaignService,
private gameSystemService: GameSystemService
) {}
ngOnInit(): void {
const params = this.route.snapshot.paramMap;
this.campaignId = params.get('campaignId');
this.characterId = params.get('characterId');
if (this.characterId) {
this.service.getById(this.characterId).subscribe({
next: c => { this.character = c; },
error: () => this.back()
});
}
if (this.campaignId) {
this.campaignService.getCampaignById(this.campaignId).subscribe(camp => {
if (camp.gameSystemId) {
this.gameSystemService.getById(camp.gameSystemId).subscribe(gs => {
this.templateFields = gs.characterTemplate ?? [];
});
}
});
}
}
edit(): void {
if (this.campaignId && this.characterId) {
this.router.navigate(['/campaigns', this.campaignId, 'characters', this.characterId, 'edit']);
}
}
back(): void {
if (this.campaignId) {
this.router.navigate(['/campaigns', this.campaignId]);
} else {
this.router.navigate(['/campaigns']);
}
}
}