Mise en ligne de la version 0.2.0
All checks were successful
Build & Push Images / build (brain) (push) Successful in 46s
Build & Push Images / build (core) (push) Successful in 1m21s
Build & Push Images / build (web) (push) Successful in 1m25s

This commit is contained in:
2026-04-21 14:25:17 +02:00
parent ebee8e106b
commit ba8a503b3e
300 changed files with 35329 additions and 1 deletions

View File

@@ -0,0 +1,69 @@
<div class="modal-backdrop" (click)="onCancel()">
<div class="modal" (click)="$event.stopPropagation()">
<div class="modal-header">
<h2>Créer une nouvelle Campagne</h2>
<button class="btn-close" (click)="onCancel()">
<lucide-icon [img]="X" [size]="18"></lucide-icon>
</button>
</div>
<form [formGroup]="form" (ngSubmit)="submit()">
<div class="field">
<label>Nom de la campagne *</label>
<input
type="text"
formControlName="name"
placeholder="Ex: L'Ombre du Nord, Les Héritiers Oubliés..."
[class.invalid]="form.get('name')?.invalid && form.get('name')?.touched"
/>
</div>
<div class="field">
<label>Description / Pitch</label>
<textarea
formControlName="description"
placeholder="Résumez l'intrigue principale de votre campagne..."
rows="5"
></textarea>
</div>
<div class="field">
<label>Nombre de joueurs</label>
<input type="number" formControlName="playerCount" min="1" />
</div>
<div class="field">
<label>Univers associé</label>
<select formControlName="loreId">
<option value="">— Aucun univers (campagne libre) —</option>
<option *ngFor="let lore of availableLores" [value]="lore.id">{{ lore.name }}</option>
</select>
<p class="hint">
Optionnel. Si associée, vous pourrez lier arcs, chapitres et scènes aux pages du Lore.
Laissez vide pour un one-shot ou si vous créerez le Lore plus tard.
</p>
</div>
<div class="info-box">
<p><strong>💡 Organisation :</strong> Votre campagne sera structurée en :</p>
<ul>
<li><strong>Arcs</strong> - Les grandes phases narratives</li>
<li><strong>Chapitres</strong> - Les segments d'un arc</li>
<li><strong>Scènes</strong> - Les moments de jeu individuels</li>
</ul>
</div>
<div class="modal-actions">
<button type="submit" class="btn-primary" [disabled]="form.invalid">
<lucide-icon [img]="BookCopy" [size]="16"></lucide-icon>
Créer la campagne
</button>
<button type="button" class="btn-secondary" (click)="onCancel()">Annuler</button>
</div>
</form>
</div>
</div>

View File

@@ -0,0 +1,123 @@
.modal-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.modal {
background: #111827;
border: 1px solid #1f2937;
border-radius: 16px;
padding: 2rem;
width: 100%;
max-width: 600px;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
h2 { color: white; font-size: 1.25rem; font-weight: 600; }
}
.btn-close {
background: transparent;
border: none;
color: #6b7280;
cursor: pointer;
padding: 0.25rem;
border-radius: 6px;
display: flex;
transition: color 0.2s;
&:hover { color: white; }
}
.field {
margin-bottom: 1.5rem;
label {
display: block;
font-size: 0.875rem;
color: #d1d5db;
margin-bottom: 0.5rem;
}
input, textarea {
width: 100%;
background: #1f2937;
border: 1px solid #374151;
border-radius: 8px;
padding: 0.75rem 1rem;
color: white;
font-size: 0.9rem;
outline: none;
resize: none;
transition: border-color 0.2s;
&::placeholder { color: #4b5563; }
&:focus { border-color: #6c63ff; }
&.invalid { border-color: #ef4444; }
}
input[type="number"] { width: 120px; }
}
.info-box {
background: #1f2937;
border-radius: 8px;
padding: 1rem 1.25rem;
margin-bottom: 2rem;
font-size: 0.875rem;
color: #9ca3af;
line-height: 1.8;
ul {
margin: 0.5rem 0 0 1.25rem;
li strong { color: #d1d5db; }
}
}
.modal-actions {
display: flex;
gap: 1rem;
}
.btn-primary {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
background: #6c63ff;
color: white;
border: none;
border-radius: 8px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
&:hover:not(:disabled) { background: #5b52e0; }
&:disabled { opacity: 0.4; cursor: not-allowed; }
}
.btn-secondary {
padding: 0.75rem 1.5rem;
background: #1f2937;
color: #d1d5db;
border: 1px solid #374151;
border-radius: 8px;
font-size: 0.9rem;
cursor: pointer;
transition: background 0.2s;
&:hover { background: #374151; }
}

View File

@@ -0,0 +1,69 @@
import { Component, EventEmitter, OnInit, Output } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { LucideAngularModule, BookCopy, X } from 'lucide-angular';
import { LoreService } from '../../services/lore.service';
import { Lore } from '../../services/lore.model';
/**
* Payload émis vers le parent à la création d'une campagne.
* `loreId` est optionnel (null = campagne sans univers associé).
*/
export interface CampaignCreatePayload {
name: string;
description: string;
playerCount: number;
loreId: string | null;
}
@Component({
selector: 'app-campaign-create',
standalone: true,
imports: [CommonModule, ReactiveFormsModule, LucideAngularModule],
templateUrl: './campaign-create.component.html',
styleUrls: ['./campaign-create.component.scss']
})
export class CampaignCreateComponent implements OnInit {
@Output() close = new EventEmitter<void>();
@Output() created = new EventEmitter<CampaignCreatePayload>();
readonly BookCopy = BookCopy;
readonly X = X;
form: FormGroup;
/** Lores disponibles pour association. Chargés à l'ouverture de la modal. */
availableLores: Lore[] = [];
constructor(private fb: FormBuilder, private loreService: LoreService) {
this.form = this.fb.group({
name: ['', Validators.required],
description: [''],
playerCount: [4, [Validators.required, Validators.min(1)]],
// Valeur par défaut : chaîne vide = "— Aucun lore associé —".
// Le service normalise ensuite ""/null en null côté backend.
loreId: ['']
});
}
ngOnInit(): void {
this.loreService.getAllLores().subscribe({
next: (lores) => this.availableLores = lores,
error: () => this.availableLores = []
});
}
submit(): void {
if (this.form.invalid) return;
const raw = this.form.value;
this.created.emit({
name: raw.name,
description: raw.description,
playerCount: raw.playerCount,
loreId: raw.loreId ? raw.loreId : null
});
}
onCancel(): void {
this.close.emit();
}
}