parent
b57475551c
commit
30534df9ae
@ -0,0 +1,150 @@
|
|||||||
|
<!-- Container parent qui centre verticalement et horizontalement -->
|
||||||
|
<div class="pin-detail">
|
||||||
|
<!-- Conteneur principal -->
|
||||||
|
<div
|
||||||
|
class="w-full max-w-3xl p-10 rounded-lg shadow-lg bg-white card-pin-detail"
|
||||||
|
>
|
||||||
|
<!-- Titre -->
|
||||||
|
<h2 class="text-3xl font-semibold mb-4 text-gray-800 text-center">
|
||||||
|
{{ pin.title }}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<!-- Carousel -->
|
||||||
|
<div
|
||||||
|
*ngIf="pin.files.length > 0; else noImagesPlaceholder"
|
||||||
|
class="relative mt-2 mb-4 overflow-hidden rounded-lg flex items-center justify-center"
|
||||||
|
[ngClass]="{ 'h-32 sm:h-40 md:h-52 lg:h-60': true }"
|
||||||
|
>
|
||||||
|
<!-- Images -->
|
||||||
|
<div
|
||||||
|
*ngFor="let imageId of pin.files; let index = index"
|
||||||
|
[class]="
|
||||||
|
'absolute inset-0 transition-opacity duration-700 ease-in-out' +
|
||||||
|
(index === currentIndex ? ' opacity-100' : ' opacity-0')
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="relative w-full h-full overflow-hidden rounded-lg flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
[src]="imageUrls[index]"
|
||||||
|
[hidden]="!imagesLoaded"
|
||||||
|
class="object-contain max-h-full max-w-full h-full w-auto mx-auto"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
*ngIf="!imagesLoaded"
|
||||||
|
class="w-full h-full bg-gray-200 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<span class="text-gray-500">Loading image...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Contrôles gauche/droite -->
|
||||||
|
<ng-container *ngIf="pin.files.length > 1">
|
||||||
|
<!-- Précédent -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="absolute top-1/2 left-2 z-30 -translate-y-1/2 flex items-center justify-center px-2 cursor-pointer group focus:outline-none"
|
||||||
|
(click)="prevSlide()"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-black/30 group-hover:bg-black/50"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-4 h-4 text-white"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 6 10"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M5 1 1 5l4 4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Suivant -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="absolute top-1/2 right-2 z-30 -translate-y-1/2 flex items-center justify-center px-2 cursor-pointer group focus:outline-none"
|
||||||
|
(click)="nextSlide()"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-black/30 group-hover:bg-black/50"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-4 h-4 text-white"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 6 10"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M1 9l4-4-4-4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Fallback si pas d'image -->
|
||||||
|
<ng-template #noImagesPlaceholder>
|
||||||
|
<div
|
||||||
|
class="relative mt-2 overflow-hidden rounded-lg flex items-center justify-center bg-gray-200"
|
||||||
|
[ngClass]="{ 'h-32 sm:h-40 md:h-52 lg:h-60': true }"
|
||||||
|
>
|
||||||
|
<span class="text-gray-500">No images available</span>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<!-- Adresse complète -->
|
||||||
|
<p class="text-sm text-gray-500 mb-2">📍 {{ pin.complete_address }}</p>
|
||||||
|
|
||||||
|
<!-- Date si disponible -->
|
||||||
|
<p class="text-sm text-gray-500 mb-6" *ngIf="pin.date">
|
||||||
|
📅 {{ pin.date | date : "longDate" }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<div
|
||||||
|
#desc
|
||||||
|
class="text-lg mb-4 text-justify transition-all duration-300"
|
||||||
|
[ngClass]="{
|
||||||
|
'max-h-[7.5rem] overflow-hidden whitespace-normal':
|
||||||
|
!expandedDescription,
|
||||||
|
'max-h-60 overflow-y-auto whitespace-normal': expandedDescription
|
||||||
|
}"
|
||||||
|
style="line-height: 1.5rem; word-break: break-word"
|
||||||
|
>
|
||||||
|
{{ pin.description || "Aucune description" }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bouton Voir plus / Voir moins -->
|
||||||
|
<div *ngIf="showToggleButton" class="text-right mb-6">
|
||||||
|
<button
|
||||||
|
(click)="toggleDescription()"
|
||||||
|
class="text-blue-600 font-semibold hover:underline"
|
||||||
|
>
|
||||||
|
{{ expandedDescription ? "Voir moins" : "Voir plus" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ID utilisateur -->
|
||||||
|
<div class="text-xs text-gray-400">Utilisateur : {{ username }}</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
(click)="goBack()"
|
||||||
|
class="mb-4 mt-10 px-4 py-2 rounded bg-blue-600 text-white hover:bg-blue-700 transition"
|
||||||
|
>
|
||||||
|
← Retour à la carte
|
||||||
|
</button>
|
||||||
|
</div>
|
@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { PinDetailComponent } from './pin-detail.component';
|
||||||
|
|
||||||
|
describe('PinDetailComponent', () => {
|
||||||
|
let component: PinDetailComponent;
|
||||||
|
let fixture: ComponentFixture<PinDetailComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [PinDetailComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(PinDetailComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,141 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component, ElementRef, ViewChild, AfterViewInit } from '@angular/core';
|
||||||
|
import { Pin } from '../../model/Pin';
|
||||||
|
import { PinService } from '../../services/pin/pin.service';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { AuthService } from '../../services/auth/auth.service';
|
||||||
|
import { UserService } from '../../services/user/user.service';
|
||||||
|
import { ImageService } from '../../services/image/image.service';
|
||||||
|
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { Renderer2 } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-pin-detail',
|
||||||
|
templateUrl: './pin-detail.component.html',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, RouterModule],
|
||||||
|
})
|
||||||
|
export class PinDetailComponent {
|
||||||
|
pin!: Pin;
|
||||||
|
username: string = '';
|
||||||
|
imageUrls: SafeUrl[] = [];
|
||||||
|
imagesLoaded = false;
|
||||||
|
sharedUsers: {
|
||||||
|
user_id: string;
|
||||||
|
username: string;
|
||||||
|
can_edit: boolean;
|
||||||
|
can_delete: boolean;
|
||||||
|
}[] = [];
|
||||||
|
expandedDescription = false;
|
||||||
|
currentIndex: number = 0;
|
||||||
|
|
||||||
|
@ViewChild('desc') descriptionDiv!: ElementRef<HTMLDivElement>;
|
||||||
|
showToggleButton = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private pinService: PinService,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private authService: AuthService,
|
||||||
|
private userService: UserService,
|
||||||
|
private imageService: ImageService,
|
||||||
|
private sanitizer: DomSanitizer,
|
||||||
|
private router: Router,
|
||||||
|
private renderer: Renderer2
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.renderer.addClass(document.body, 'no-scroll-body');
|
||||||
|
|
||||||
|
const pinId = this.route.snapshot.paramMap.get('id');
|
||||||
|
if (pinId) {
|
||||||
|
this.pinService.getPinById(pinId).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
this.pin = data;
|
||||||
|
this.loadUsername(data.user_id);
|
||||||
|
this.loadImages();
|
||||||
|
},
|
||||||
|
error: (err) =>
|
||||||
|
console.error('Erreur lors du chargement du pin :', err),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadUsername(userId: string) {
|
||||||
|
this.userService.getUserById(userId).subscribe({
|
||||||
|
next: (user) => {
|
||||||
|
this.username = user.username; // ou user.name selon ton backend
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error('Erreur lors de la récupération du pseudo :', err);
|
||||||
|
this.username = 'Utilisateur inconnu';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadImages() {
|
||||||
|
this.pin.files.forEach((imageId) => {
|
||||||
|
this.imageService.getImage(imageId).subscribe((blob) => {
|
||||||
|
const objectUrl = URL.createObjectURL(blob);
|
||||||
|
const safeUrl = this.sanitizer.bypassSecurityTrustUrl(objectUrl);
|
||||||
|
this.imageUrls.push(safeUrl);
|
||||||
|
if (this.imageUrls.length === this.pin.files.length) {
|
||||||
|
this.imagesLoaded = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSharedUsers(pinId: string) {
|
||||||
|
this.pinService.getSharedUsersForPin(pinId).subscribe({
|
||||||
|
next: (shares) => {
|
||||||
|
this.sharedUsers = shares;
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error('Erreur récupération utilisateurs partagés:', err);
|
||||||
|
this.sharedUsers = [];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// toggleDescription(): void {
|
||||||
|
// this.expandedDescription = !this.expandedDescription;
|
||||||
|
// }
|
||||||
|
|
||||||
|
ngAfterViewInit(): void {
|
||||||
|
this.checkIfDescriptionIsTruncated();
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleDescription() {
|
||||||
|
this.expandedDescription = !this.expandedDescription;
|
||||||
|
if (!this.expandedDescription) {
|
||||||
|
// re-check quand on replie
|
||||||
|
setTimeout(() => this.checkIfDescriptionIsTruncated(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkIfDescriptionIsTruncated() {
|
||||||
|
if (!this.descriptionDiv) return;
|
||||||
|
|
||||||
|
const el = this.descriptionDiv.nativeElement;
|
||||||
|
this.showToggleButton = el.scrollHeight > el.clientHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
prevSlide(): void {
|
||||||
|
this.currentIndex =
|
||||||
|
(this.currentIndex - 1 + this.imageUrls.length) % this.imageUrls.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextSlide(): void {
|
||||||
|
this.currentIndex = (this.currentIndex + 1) % this.imageUrls.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.renderer.removeClass(document.body, 'no-scroll-body');
|
||||||
|
}
|
||||||
|
|
||||||
|
goBack() {
|
||||||
|
this.router.navigate(['/map'], { queryParams: { pin: this.pin.id } });
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue