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,71 @@
<div class="node-create-page">
<div class="page-header">
<h1>Créer un nouveau dossier</h1>
<p class="subtitle">Les dossiers permettent d'organiser vos pages par catégorie</p>
</div>
<form [formGroup]="form" (ngSubmit)="submit()" class="node-form">
<div class="field">
<label>Nom du dossier *</label>
<input
type="text"
formControlName="name"
placeholder="Ex: Personnages, Créatures..."
[class.invalid]="form.get('name')?.invalid && form.get('name')?.touched"
/>
</div>
<div class="field">
<label>Dossier parent <span class="optional">(optionnel)</span></label>
<select formControlName="parentId">
<option value="">— Racine du Lore —</option>
<option *ngFor="let parent of availableParents" [value]="parent.id">{{ parent.name }}</option>
</select>
<p class="hint">Laissez vide pour créer un dossier à la racine du lore</p>
</div>
<div class="field">
<label>Icône</label>
<div class="icon-grid">
<button
type="button"
class="icon-btn"
*ngFor="let option of iconOptions"
[class.selected]="selectedIcon === option.key"
(click)="selectIcon(option.key)">
<lucide-icon [img]="option.icon" [size]="18"></lucide-icon>
</button>
</div>
</div>
<div class="field">
<label>Description <span class="optional">(optionnel)</span></label>
<textarea
formControlName="description"
placeholder="Décrivez le type de contenu que ce dossier contiendra..."
rows="4">
</textarea>
</div>
<div class="field">
<label>Adresse</label>
<input
type="text"
formControlName="address"
placeholder="nom-du-dossier"
/>
</div>
<div class="form-actions">
<button type="submit" class="btn-primary" [disabled]="form.invalid">
<lucide-icon [img]="getIcon(selectedIcon)" [size]="16"></lucide-icon>
Créer le dossier
</button>
<button type="button" class="btn-secondary" (click)="cancel()">Annuler</button>
</div>
</form>
</div>

View File

@@ -0,0 +1,116 @@
.node-create-page {
padding: 2.5rem 2rem;
max-width: 640px;
}
.page-header {
margin-bottom: 2.5rem;
h1 { font-size: 1.5rem; font-weight: 700; color: white; margin-bottom: 0.4rem; }
.subtitle { color: #6b7280; font-size: 0.9rem; }
}
.node-form {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.field {
display: flex;
flex-direction: column;
gap: 0.5rem;
label {
font-size: 0.875rem;
color: #d1d5db;
font-weight: 500;
}
.optional { color: #6b7280; font-weight: 400; }
input, textarea, select {
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; }
}
}
.icon-grid {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
background: #1f2937;
border: 1px solid #374151;
border-radius: 8px;
padding: 0.75rem;
}
.icon-btn {
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: 1px solid transparent;
border-radius: 6px;
color: #6b7280;
cursor: pointer;
transition: all 0.15s;
&:hover { background: #374151; color: white; }
&.selected {
background: #1e1b4b;
border-color: #6c63ff;
color: #a5b4fc;
}
}
.form-actions {
display: flex;
gap: 1rem;
padding-top: 0.5rem;
}
.btn-primary {
display: flex;
align-items: 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,104 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { LucideAngularModule, LucideIconData } from 'lucide-angular';
import { LoreService } from '../../services/lore.service';
import { TemplateService } from '../../services/template.service';
import { PageService } from '../../services/page.service';
import { LayoutService } from '../../services/layout.service';
import { LoreNode } from '../../services/lore.model';
import { loadLoreSidebarData, buildLoreSidebarConfig } from '../lore-sidebar.helper';
import { LORE_ICON_OPTIONS, IconOption, resolveIcon } from '../lore-icons';
@Component({
selector: 'app-lore-node-create',
standalone: true,
imports: [CommonModule, ReactiveFormsModule, LucideAngularModule],
templateUrl: './lore-node-create.component.html',
styleUrls: ['./lore-node-create.component.scss']
})
export class LoreNodeCreateComponent implements OnInit, OnDestroy {
readonly iconOptions: IconOption[] = LORE_ICON_OPTIONS;
form: FormGroup;
loreId = '';
/** parentId optionnel depuis la route — si présent, pré-remplit le champ. */
preselectedParentId: string | null = null;
/** Liste des dossiers existants pour le select "Dossier parent". */
availableParents: LoreNode[] = [];
selectedIcon = this.iconOptions[0].key;
constructor(
private fb: FormBuilder,
private route: ActivatedRoute,
private router: Router,
private loreService: LoreService,
private templateService: TemplateService,
private pageService: PageService,
private layoutService: LayoutService
) {
this.form = this.fb.group({
name: ['', Validators.required],
description: [''],
address: ['', Validators.required],
parentId: [''] // '' = racine
});
// Auto-génère l'adresse depuis le nom
this.form.get('name')!.valueChanges.subscribe(name => {
const slug = (name as string).toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
this.form.get('address')!.setValue(slug, { emitEvent: false });
});
}
ngOnInit(): void {
this.loreId = this.route.snapshot.paramMap.get('loreId')!;
this.preselectedParentId = this.route.snapshot.paramMap.get('parentId');
this.loadLayout();
}
private loadLayout(): void {
loadLoreSidebarData(this.loreId, this.loreService, this.templateService, this.pageService)
.subscribe(data => {
this.availableParents = data.nodes;
if (this.preselectedParentId) {
this.form.patchValue({ parentId: this.preselectedParentId });
}
this.layoutService.show(buildLoreSidebarConfig(data));
});
}
selectIcon(key: string): void {
this.selectedIcon = key;
}
getIcon(key: string): LucideIconData {
return this.iconOptions.find(o => o.key === key)!.icon;
}
submit(): void {
if (this.form.invalid) return;
const raw = this.form.value;
this.loreService.createLoreNode({
name: raw.name,
description: raw.description,
address: raw.address,
icon: this.selectedIcon,
parentId: raw.parentId && raw.parentId !== '' ? raw.parentId : null,
loreId: this.loreId
}).subscribe({
next: () => this.router.navigate(['/lore', this.loreId]),
error: () => console.error('Erreur lors de la création du dossier')
});
}
cancel(): void {
this.router.navigate(['/lore', this.loreId]);
}
ngOnDestroy(): void {
this.layoutService.hide();
}
}