💥 pin detail page

master
Mathis FRAMIT 1 week ago
parent b57475551c
commit 30534df9ae

@ -5,6 +5,7 @@ import { HomeNavbarComponent } from './components/home-navbar/home-navbar.compon
import { NavbarComponent } from './components/navbar/navbar.component'; import { NavbarComponent } from './components/navbar/navbar.component';
import { AdminFooterComponent } from './components/admin-footer/admin-footer.component'; import { AdminFooterComponent } from './components/admin-footer/admin-footer.component';
import { AuthService } from './services/auth/auth.service'; import { AuthService } from './services/auth/auth.service';
import { PinDetailComponent } from './components/pin-detail/pin-detail.component';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',

@ -4,10 +4,12 @@ import { HomePageComponent } from './components/home-page/home-page.component';
import { LeafletMapComponent } from './components/leaflet-map/leaflet-map.component'; import { LeafletMapComponent } from './components/leaflet-map/leaflet-map.component';
import { NotFoundComponent } from './components/not-found/not-found.component'; import { NotFoundComponent } from './components/not-found/not-found.component';
import { TimelineComponent } from './components/timeline/timeline.component'; import { TimelineComponent } from './components/timeline/timeline.component';
import { PinDetailComponent } from './components/pin-detail/pin-detail.component';
export const routes: Routes = [ export const routes: Routes = [
{ path: '', component: HomePageComponent }, { path: '', component: HomePageComponent },
{ path: 'map', component: LeafletMapComponent, canActivate: [AuthGuard] }, { path: 'map', component: LeafletMapComponent, canActivate: [AuthGuard] },
{ path: 'timeline', component: TimelineComponent, canActivate: [AuthGuard] }, { path: 'timeline', component: TimelineComponent, canActivate: [AuthGuard] },
{ path: 'pin/:id', component: PinDetailComponent, canActivate: [AuthGuard] },
{ path: '**', component: NotFoundComponent }, { path: '**', component: NotFoundComponent },
]; ];

@ -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…
Cancel
Save