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