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,51 @@
<!-- Mode compact : bouton "+ ajouter" carre, utilise dans les galeries. -->
<ng-container *ngIf="compact; else dropZone">
<label class="upload-compact" [class.loading]="uploading" [title]="errorMessage || 'Ajouter une image'">
<ng-container *ngIf="!uploading; else spinner">
<lucide-icon [img]="Upload" [size]="18"></lucide-icon>
<span>Ajouter</span>
</ng-container>
<input type="file"
accept="image/jpeg,image/png,image/webp,image/gif"
(change)="onFileSelected($event)"
[disabled]="uploading"
hidden />
</label>
<p *ngIf="errorMessage" class="upload-error-inline">
<lucide-icon [img]="AlertCircle" [size]="12"></lucide-icon>
{{ errorMessage }}
</p>
</ng-container>
<!-- Mode standard : grande drop-zone cliquable. -->
<ng-template #dropZone>
<label class="upload-zone"
[class.drag-over]="dragOver"
[class.loading]="uploading"
(dragover)="onDragOver($event)"
(dragleave)="onDragLeave()"
(drop)="onDrop($event)">
<ng-container *ngIf="!uploading; else spinner">
<lucide-icon [img]="Upload" [size]="32"></lucide-icon>
<p class="upload-zone-title">Glisse une image ici</p>
<p class="upload-zone-hint">ou clique pour choisir un fichier (JPEG, PNG, WebP, GIF, max 10 Mo)</p>
</ng-container>
<input type="file"
accept="image/jpeg,image/png,image/webp,image/gif"
(change)="onFileSelected($event)"
[disabled]="uploading"
hidden />
</label>
<p *ngIf="errorMessage" class="upload-error" role="alert">
<lucide-icon [img]="AlertCircle" [size]="14"></lucide-icon>
{{ errorMessage }}
</p>
</ng-template>
<ng-template #spinner>
<div class="upload-spinner" aria-label="Upload en cours"></div>
<p class="upload-zone-hint">Upload en cours...</p>
</ng-template>

View File

@@ -0,0 +1,88 @@
// Drop-zone standard
.upload-zone {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.4rem;
min-height: 140px;
padding: 1.5rem;
background: #1a1a2e;
border: 2px dashed #2a2a3d;
border-radius: 8px;
color: #9ca3af;
cursor: pointer;
transition: background 0.15s, border-color 0.15s, color 0.15s;
&:hover { border-color: #6c63ff; color: #d1d5db; }
&.drag-over { border-color: #6c63ff; background: #1f1f3a; color: white; }
&.loading { cursor: progress; opacity: 0.7; }
}
.upload-zone-title {
margin: 0;
font-size: 0.92rem;
font-weight: 500;
color: #d1d5db;
}
.upload-zone-hint {
margin: 0;
font-size: 0.78rem;
color: #6b7280;
text-align: center;
}
// Variante compacte (bouton carre pour galerie)
.upload-compact {
display: inline-flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.3rem;
width: 120px;
height: 120px;
background: #1a1a2e;
border: 2px dashed #2a2a3d;
border-radius: 6px;
color: #6b7280;
font-size: 0.78rem;
cursor: pointer;
transition: background 0.15s, border-color 0.15s, color 0.15s;
&:hover { border-color: #6c63ff; color: #a5b4fc; background: #1f1f3a; }
&.loading { cursor: progress; opacity: 0.7; }
}
.upload-error, .upload-error-inline {
display: flex;
align-items: center;
gap: 0.4rem;
margin: 0.5rem 0 0;
padding: 0.5rem 0.7rem;
background: #3f1f1f;
color: #fca5a5;
border: 1px solid #7f1d1d;
border-radius: 6px;
font-size: 0.82rem;
}
.upload-error-inline {
padding: 0.3rem 0.5rem;
font-size: 0.72rem;
margin-top: 0.3rem;
}
// Spinner CSS simple (pas de dep externe)
.upload-spinner {
width: 28px;
height: 28px;
border: 3px solid #2a2a3d;
border-top-color: #6c63ff;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}

View File

@@ -0,0 +1,100 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { CommonModule } from '@angular/common';
import { LucideAngularModule, Upload, AlertCircle } from 'lucide-angular';
import { ImageService } from '../../services/image.service';
import { Image } from '../../services/image.model';
/**
* Composant reutilisable d'upload d'image (drop-zone + bouton file).
*
* Usage :
* <app-image-uploader (uploaded)="onImageUploaded($event)"></app-image-uploader>
*
* Responsabilites :
* - Accepter un fichier via drag&drop OU clic sur la zone
* - Valider cote client (MIME + taille) pour eviter un aller-retour inutile
* - POSTer vers /api/images (service ImageService)
* - Emettre (uploaded) avec l'objet Image recu
* - Afficher l'etat loading et les erreurs
*/
@Component({
selector: 'app-image-uploader',
standalone: true,
imports: [CommonModule, LucideAngularModule],
templateUrl: './image-uploader.component.html',
styleUrls: ['./image-uploader.component.scss']
})
export class ImageUploaderComponent {
readonly Upload = Upload;
readonly AlertCircle = AlertCircle;
/** Compact mode : bouton "+ ajouter" plutot que grande drop-zone. */
@Input() compact = false;
/** Emit quand l'image est uploadee avec succes. */
@Output() uploaded = new EventEmitter<Image>();
uploading = false;
errorMessage: string | null = null;
dragOver = false;
/** MIME types alignes avec le backend (ImageService.java). */
private readonly ALLOWED_MIMES = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
private readonly MAX_BYTES = 10 * 1024 * 1024;
constructor(private imageService: ImageService) {}
onFileSelected(event: Event): void {
const input = event.target as HTMLInputElement;
if (input.files && input.files.length > 0) {
this.handleFile(input.files[0]);
// Reset pour permettre de re-uploader la meme image si besoin.
input.value = '';
}
}
onDragOver(event: DragEvent): void {
event.preventDefault();
this.dragOver = true;
}
onDragLeave(): void {
this.dragOver = false;
}
onDrop(event: DragEvent): void {
event.preventDefault();
this.dragOver = false;
if (event.dataTransfer?.files && event.dataTransfer.files.length > 0) {
this.handleFile(event.dataTransfer.files[0]);
}
}
private handleFile(file: File): void {
this.errorMessage = null;
// Validation cote client (premier filet de securite).
if (!this.ALLOWED_MIMES.includes(file.type)) {
this.errorMessage = 'Format non supporte (JPEG, PNG, WebP, GIF uniquement).';
return;
}
if (file.size > this.MAX_BYTES) {
this.errorMessage = `Fichier trop volumineux (max ${this.MAX_BYTES / 1024 / 1024} Mo).`;
return;
}
this.uploading = true;
this.imageService.upload(file).subscribe({
next: (image) => {
this.uploading = false;
this.uploaded.emit(image);
},
error: (err) => {
this.uploading = false;
this.errorMessage = err?.status === 413
? 'Fichier refuse par le serveur (trop volumineux).'
: 'Echec de l\'upload. Verifiez que le backend et MinIO tournent.';
}
});
}
}