Ajout de la partie "Système de jeu" avec toute la partie stockage de règles de notre jeu.
Ajout de possibilité de stocker des fiches de personnages associés à une campagne également (personnages joueurs pour le moment)
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
<div class="gse-page">
|
||||
|
||||
<div class="gse-header">
|
||||
<button class="btn-back" (click)="back()">
|
||||
<lucide-icon [img]="ArrowLeft" [size]="14"></lucide-icon>
|
||||
Retour à la liste
|
||||
</button>
|
||||
<h1>
|
||||
<lucide-icon [img]="Dices" [size]="22"></lucide-icon>
|
||||
{{ id ? 'Éditer le système' : 'Nouveau système de JDR' }}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="gse-form">
|
||||
|
||||
<div class="field">
|
||||
<label>Nom *</label>
|
||||
<input type="text" [(ngModel)]="name" name="name" placeholder="Ex: Nimble, D&D 5.1 SRD, Mon Homebrew..." />
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Description courte</label>
|
||||
<textarea [(ngModel)]="description" name="description" rows="2" placeholder="En une ligne, de quoi parle ce système ?"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Auteur</label>
|
||||
<input type="text" [(ngModel)]="author" name="author" placeholder="Ex: Hasbro, Homebrew, moi-même..." />
|
||||
</div>
|
||||
|
||||
<!-- Sections de règles -->
|
||||
<div class="sections-area">
|
||||
<h2 class="sections-title">Règles du système</h2>
|
||||
<p class="sections-hint">
|
||||
Une section = un thème. L'IA injectera automatiquement les sections pertinentes
|
||||
selon ce qu'elle génère (combat → Combat/Monstres, PNJ → Classes, arc → Lore/Factions).
|
||||
</p>
|
||||
|
||||
<div class="section-list">
|
||||
<div class="section-card" *ngFor="let section of sections; let i = index" [class.collapsed]="section.collapsed">
|
||||
|
||||
<div class="section-head">
|
||||
<button type="button" class="btn-collapse" (click)="toggleCollapse(section)">
|
||||
<lucide-icon [img]="section.collapsed ? ChevronRight : ChevronDown" [size]="16"></lucide-icon>
|
||||
</button>
|
||||
<input
|
||||
type="text"
|
||||
class="section-title-input"
|
||||
[(ngModel)]="section.title"
|
||||
[name]="'title-' + i"
|
||||
placeholder="Nom de la section (ex: Combat)"
|
||||
/>
|
||||
<button type="button" class="btn-remove" (click)="removeSection(i)" title="Supprimer cette section">
|
||||
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
*ngIf="!section.collapsed"
|
||||
class="section-content"
|
||||
[(ngModel)]="section.content"
|
||||
[name]="'content-' + i"
|
||||
rows="6"
|
||||
placeholder="Décrivez les règles de cette section..."
|
||||
></textarea>
|
||||
|
||||
</div>
|
||||
|
||||
<div *ngIf="sections.length === 0" class="empty-hint">
|
||||
Aucune section pour l'instant — ajoutez-en une avec les boutons ci-dessous.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="add-row">
|
||||
<span class="add-label">Ajouter une section :</span>
|
||||
<button
|
||||
type="button"
|
||||
*ngFor="let name of suggestedSections"
|
||||
class="chip"
|
||||
[class.disabled]="isSectionUsed(name)"
|
||||
(click)="addSuggested(name)"
|
||||
[disabled]="isSectionUsed(name)">
|
||||
<lucide-icon [img]="Plus" [size]="12"></lucide-icon>
|
||||
{{ name }}
|
||||
</button>
|
||||
<button type="button" class="chip chip-custom" (click)="addBlank()">
|
||||
<lucide-icon [img]="Plus" [size]="12"></lucide-icon>
|
||||
Autre…
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button type="button" class="btn-primary" [disabled]="!name.trim()" (click)="submit()">
|
||||
<lucide-icon [img]="Save" [size]="16"></lucide-icon>
|
||||
{{ id ? 'Enregistrer' : 'Créer' }}
|
||||
</button>
|
||||
<button type="button" class="btn-secondary" (click)="back()">Annuler</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,256 @@
|
||||
.gse-page {
|
||||
padding: 2rem 3rem;
|
||||
color: #e5e7eb;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.gse-header {
|
||||
margin-bottom: 2rem;
|
||||
|
||||
h1 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 1.75rem;
|
||||
color: white;
|
||||
margin: 0.75rem 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-back {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.25rem 0;
|
||||
font-size: 0.85rem;
|
||||
|
||||
&:hover { color: #e5e7eb; }
|
||||
}
|
||||
|
||||
.gse-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
label {
|
||||
color: #e5e7eb;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
input[type="text"], textarea {
|
||||
background: #0b1220;
|
||||
border: 1px solid #1f2937;
|
||||
border-radius: 8px;
|
||||
color: #e5e7eb;
|
||||
padding: 0.6rem 0.75rem;
|
||||
font-size: 0.95rem;
|
||||
font-family: inherit;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #a78bfa;
|
||||
}
|
||||
}
|
||||
|
||||
textarea { resize: vertical; }
|
||||
}
|
||||
|
||||
/* ─── Sections ──────────────────────────────────────────────── */
|
||||
|
||||
.sections-area {
|
||||
border-top: 1px solid #1f2937;
|
||||
padding-top: 1.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.sections-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
margin: 0 0 0.25rem;
|
||||
}
|
||||
|
||||
.sections-hint {
|
||||
color: #6b7280;
|
||||
font-size: 0.85rem;
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
.section-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.section-card {
|
||||
background: #0b1220;
|
||||
border: 1px solid #1f2937;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
transition: border-color 0.15s;
|
||||
|
||||
&:focus-within { border-color: #374151; }
|
||||
&.collapsed { background: #0a0f1a; }
|
||||
}
|
||||
|
||||
.section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.5rem 0.5rem 0.25rem;
|
||||
}
|
||||
|
||||
.btn-collapse {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #6b7280;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover { color: #e5e7eb; }
|
||||
}
|
||||
|
||||
.section-title-input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
padding: 0.3rem 0.25rem;
|
||||
|
||||
&:focus { outline: none; }
|
||||
&::placeholder { color: #4b5563; font-weight: 400; font-style: italic; }
|
||||
}
|
||||
|
||||
.btn-remove {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #6b7280;
|
||||
cursor: pointer;
|
||||
padding: 4px 6px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
|
||||
&:hover { color: #f87171; background: rgba(248, 113, 113, 0.08); }
|
||||
}
|
||||
|
||||
.section-content {
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-top: 1px solid #1f2937;
|
||||
color: #d1d5db;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.9rem;
|
||||
font-family: inherit;
|
||||
line-height: 1.5;
|
||||
resize: vertical;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:focus { outline: none; }
|
||||
}
|
||||
|
||||
.empty-hint {
|
||||
color: #6b7280;
|
||||
font-size: 0.85rem;
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
padding: 1.5rem;
|
||||
border: 1px dashed #1f2937;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* ─── Chips d'ajout ─────────────────────────────────────────── */
|
||||
|
||||
.add-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.add-label {
|
||||
color: #9ca3af;
|
||||
font-size: 0.85rem;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.chip {
|
||||
background: #111827;
|
||||
border: 1px solid #1f2937;
|
||||
color: #d1d5db;
|
||||
padding: 0.3rem 0.7rem;
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
transition: all 0.15s;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
border-color: #a78bfa;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.disabled, &:disabled {
|
||||
opacity: 0.35;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&.chip-custom {
|
||||
border-style: dashed;
|
||||
color: #a78bfa;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Actions ──────────────────────────────────────────────── */
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #a78bfa;
|
||||
color: #0b1220;
|
||||
border: none;
|
||||
padding: 0.6rem 1.25rem;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
|
||||
&:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
&:hover:not(:disabled) { background: #c4b5fd; }
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: transparent;
|
||||
border: 1px solid #1f2937;
|
||||
color: #9ca3af;
|
||||
padding: 0.6rem 1.25rem;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover { border-color: #374151; color: #e5e7eb; }
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { LucideAngularModule, Save, ArrowLeft, Dices, Plus, Trash2, ChevronDown, ChevronRight } from 'lucide-angular';
|
||||
import { GameSystemService } from '../../services/game-system.service';
|
||||
|
||||
/**
|
||||
* Éditeur plein écran d'un GameSystem. Rôle double création/édition :
|
||||
* - `/game-systems/create` → POST au submit
|
||||
* - `/game-systems/:id/edit` → PUT au submit
|
||||
*
|
||||
* UI par sections : au lieu d'un gros textarea de markdown, on présente
|
||||
* une liste de cartes repliables (une par section H2). Au load on parse
|
||||
* le markdown existant, au submit on le reconstruit. Les noms suggérés
|
||||
* (Combat, Classes…) guident sans imposer — c'est le même mapping que
|
||||
* le backend utilise pour filtrer les sections à injecter dans l'IA.
|
||||
*/
|
||||
interface RuleSection {
|
||||
title: string;
|
||||
content: string;
|
||||
collapsed: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sections suggérées : mécaniques de jeu uniquement. Lore/factions/PNJ
|
||||
* appartiennent au module Lore de LoreMind (décrit l'univers de l'utilisateur),
|
||||
* pas au GameSystem (décrit les règles). Séparation volontaire.
|
||||
*/
|
||||
const SUGGESTED_SECTIONS = [
|
||||
'Combat', 'Classes', 'Stats', 'Magie', 'Monstres', 'Progression'
|
||||
];
|
||||
|
||||
@Component({
|
||||
selector: 'app-game-system-edit',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, LucideAngularModule],
|
||||
templateUrl: './game-system-edit.component.html',
|
||||
styleUrls: ['./game-system-edit.component.scss']
|
||||
})
|
||||
export class GameSystemEditComponent implements OnInit {
|
||||
readonly Save = Save;
|
||||
readonly ArrowLeft = ArrowLeft;
|
||||
readonly Dices = Dices;
|
||||
readonly Plus = Plus;
|
||||
readonly Trash2 = Trash2;
|
||||
readonly ChevronDown = ChevronDown;
|
||||
readonly ChevronRight = ChevronRight;
|
||||
|
||||
id: string | null = null;
|
||||
|
||||
name = '';
|
||||
description = '';
|
||||
author = '';
|
||||
sections: RuleSection[] = [];
|
||||
|
||||
readonly suggestedSections = SUGGESTED_SECTIONS;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private service: GameSystemService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.id = this.route.snapshot.paramMap.get('id');
|
||||
if (this.id) {
|
||||
this.service.getById(this.id).subscribe({
|
||||
next: (gs) => {
|
||||
this.name = gs.name;
|
||||
this.description = gs.description ?? '';
|
||||
this.author = gs.author ?? '';
|
||||
this.sections = this.parseMarkdown(gs.rulesMarkdown ?? '');
|
||||
},
|
||||
error: () => this.back()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Noms de section déjà utilisés — pour désactiver les suggestions en doublon. */
|
||||
isSectionUsed(title: string): boolean {
|
||||
const lower = title.toLowerCase();
|
||||
return this.sections.some(s => s.title.toLowerCase() === lower);
|
||||
}
|
||||
|
||||
/** Ajoute une section avec un nom suggéré. */
|
||||
addSuggested(title: string): void {
|
||||
if (this.isSectionUsed(title)) return;
|
||||
this.sections.push({ title, content: '', collapsed: false });
|
||||
}
|
||||
|
||||
/** Ajoute une section vierge (titre à remplir). */
|
||||
addBlank(): void {
|
||||
this.sections.push({ title: '', content: '', collapsed: false });
|
||||
}
|
||||
|
||||
removeSection(index: number): void {
|
||||
this.sections.splice(index, 1);
|
||||
}
|
||||
|
||||
toggleCollapse(section: RuleSection): void {
|
||||
section.collapsed = !section.collapsed;
|
||||
}
|
||||
|
||||
submit(): void {
|
||||
if (!this.name.trim()) return;
|
||||
const payload = {
|
||||
name: this.name.trim(),
|
||||
description: this.description.trim() || null,
|
||||
author: this.author.trim() || null,
|
||||
rulesMarkdown: this.serializeMarkdown(),
|
||||
isPublic: false
|
||||
};
|
||||
const req = this.id
|
||||
? this.service.update(this.id, payload)
|
||||
: this.service.create(payload);
|
||||
req.subscribe({
|
||||
next: () => this.back(),
|
||||
error: () => console.error('Erreur sauvegarde GameSystem')
|
||||
});
|
||||
}
|
||||
|
||||
back(): void {
|
||||
this.router.navigate(['/game-systems']);
|
||||
}
|
||||
|
||||
// --- Parse / Serialize markdown ------------------------------------------
|
||||
|
||||
/**
|
||||
* Découpe un markdown par titres H2 en sections. Doit rester cohérent avec
|
||||
* le parser Java côté backend (regex `^##\s+…$`). Le texte avant le premier
|
||||
* H2 est volontairement perdu — le backend l'ignore aussi pour l'injection IA.
|
||||
*/
|
||||
private parseMarkdown(markdown: string): RuleSection[] {
|
||||
if (!markdown) return [];
|
||||
const sections: RuleSection[] = [];
|
||||
const lines = markdown.split('\n');
|
||||
let current: RuleSection | null = null;
|
||||
const buffer: string[] = [];
|
||||
|
||||
const flush = () => {
|
||||
if (current) {
|
||||
current.content = buffer.join('\n').trim();
|
||||
sections.push(current);
|
||||
}
|
||||
buffer.length = 0;
|
||||
};
|
||||
|
||||
for (const line of lines) {
|
||||
const match = line.match(/^##\s+(.+?)\s*$/);
|
||||
if (match) {
|
||||
flush();
|
||||
current = { title: match[1].trim(), content: '', collapsed: false };
|
||||
} else if (current) {
|
||||
buffer.push(line);
|
||||
}
|
||||
}
|
||||
flush();
|
||||
return sections;
|
||||
}
|
||||
|
||||
/** Reconstruit le markdown à partir des sections (ignore les sections vides). */
|
||||
private serializeMarkdown(): string | null {
|
||||
const valid = this.sections.filter(s => s.title.trim() || s.content.trim());
|
||||
if (valid.length === 0) return null;
|
||||
return valid
|
||||
.map(s => `## ${s.title.trim() || 'Sans titre'}\n${s.content.trim()}`)
|
||||
.join('\n\n');
|
||||
}
|
||||
}
|
||||
39
web/src/app/game-systems/game-systems.component.html
Normal file
39
web/src/app/game-systems/game-systems.component.html
Normal file
@@ -0,0 +1,39 @@
|
||||
<div class="gs-page">
|
||||
|
||||
<div class="gs-hero">
|
||||
<lucide-icon [img]="Dices" [size]="56" class="hero-icon"></lucide-icon>
|
||||
<h1>Systèmes de JDR</h1>
|
||||
<p class="hero-subtitle">Les règles que l'IA connaîtra pour vos campagnes</p>
|
||||
</div>
|
||||
|
||||
<div class="gs-grid">
|
||||
|
||||
<div class="gs-card" *ngFor="let gs of gameSystems" (click)="edit(gs.id!)">
|
||||
<div class="card-header">
|
||||
<h2>{{ gs.name }}</h2>
|
||||
<div class="card-actions">
|
||||
<button class="icon-btn" (click)="delete(gs, $event)" title="Supprimer">
|
||||
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="card-description">{{ gs.description || '(Pas de description)' }}</p>
|
||||
<div class="card-footer">
|
||||
<span *ngIf="gs.author" class="author">par {{ gs.author }}</span>
|
||||
<span *ngIf="gs.isPublic" class="badge-public">public</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gs-card card-new" (click)="create()">
|
||||
<div class="new-icon">
|
||||
<lucide-icon [img]="Plus" [size]="20"></lucide-icon>
|
||||
</div>
|
||||
<h2>Nouveau système</h2>
|
||||
<p class="card-description">Saisir les règles d'un JDR (markdown)</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<p class="tip">💡 Astuce : organisez vos règles par sections avec des titres <code>## Combat</code>, <code>## Classes</code>, <code>## Lore</code>… Le système injectera les sections pertinentes selon ce que l'IA doit générer.</p>
|
||||
|
||||
</div>
|
||||
111
web/src/app/game-systems/game-systems.component.scss
Normal file
111
web/src/app/game-systems/game-systems.component.scss
Normal file
@@ -0,0 +1,111 @@
|
||||
.gs-page {
|
||||
padding: 2rem 3rem;
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
.gs-hero {
|
||||
text-align: center;
|
||||
margin-bottom: 2.5rem;
|
||||
|
||||
.hero-icon { color: #a78bfa; margin-bottom: 0.75rem; }
|
||||
h1 { font-size: 2rem; margin: 0.5rem 0 0.25rem; color: white; }
|
||||
.hero-subtitle { color: #9ca3af; margin: 0; }
|
||||
}
|
||||
|
||||
.gs-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.gs-card {
|
||||
background: #111827;
|
||||
border: 1px solid #1f2937;
|
||||
border-radius: 12px;
|
||||
padding: 1.25rem;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s, transform 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #a78bfa;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
h2 { color: white; font-size: 1.1rem; margin: 0; }
|
||||
}
|
||||
|
||||
.card-description {
|
||||
color: #9ca3af;
|
||||
font-size: 0.9rem;
|
||||
min-height: 2.5em;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 0.75rem;
|
||||
font-size: 0.8rem;
|
||||
|
||||
.author { color: #6b7280; }
|
||||
.badge-public {
|
||||
background: #1e3a8a;
|
||||
color: #bfdbfe;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #6b7280;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
|
||||
&:hover { color: #f87171; background: rgba(248, 113, 113, 0.1); }
|
||||
}
|
||||
|
||||
.card-new {
|
||||
border-style: dashed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
|
||||
.new-icon {
|
||||
background: rgba(167, 139, 250, 0.1);
|
||||
color: #a78bfa;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
h2 { margin: 0 0 0.25rem; color: white; }
|
||||
}
|
||||
|
||||
.tip {
|
||||
margin-top: 2rem;
|
||||
color: #6b7280;
|
||||
font-size: 0.85rem;
|
||||
|
||||
code {
|
||||
background: #1f2937;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
color: #a78bfa;
|
||||
}
|
||||
}
|
||||
56
web/src/app/game-systems/game-systems.component.ts
Normal file
56
web/src/app/game-systems/game-systems.component.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Router } from '@angular/router';
|
||||
import { LucideAngularModule, Dices, Plus, Pencil, Trash2 } from 'lucide-angular';
|
||||
import { GameSystemService } from '../services/game-system.service';
|
||||
import { GameSystem } from '../services/game-system.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-game-systems',
|
||||
standalone: true,
|
||||
imports: [CommonModule, LucideAngularModule],
|
||||
templateUrl: './game-systems.component.html',
|
||||
styleUrls: ['./game-systems.component.scss']
|
||||
})
|
||||
export class GameSystemsComponent implements OnInit {
|
||||
readonly Dices = Dices;
|
||||
readonly Plus = Plus;
|
||||
readonly Pencil = Pencil;
|
||||
readonly Trash2 = Trash2;
|
||||
|
||||
gameSystems: GameSystem[] = [];
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private gameSystemService: GameSystemService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.load();
|
||||
}
|
||||
|
||||
load(): void {
|
||||
this.gameSystemService.getAll().subscribe({
|
||||
next: (data) => this.gameSystems = data,
|
||||
error: () => this.gameSystems = []
|
||||
});
|
||||
}
|
||||
|
||||
create(): void {
|
||||
this.router.navigate(['/game-systems/create']);
|
||||
}
|
||||
|
||||
edit(id: string): void {
|
||||
this.router.navigate(['/game-systems', id, 'edit']);
|
||||
}
|
||||
|
||||
delete(system: GameSystem, event: MouseEvent): void {
|
||||
event.stopPropagation();
|
||||
if (!system.id) return;
|
||||
if (!confirm(`Supprimer le système "${system.name}" ? Les campagnes qui l'utilisent ne seront plus associées à aucun système.`)) return;
|
||||
this.gameSystemService.delete(system.id).subscribe({
|
||||
next: () => this.load(),
|
||||
error: () => console.error('Erreur suppression GameSystem')
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user