Add date to pin and timeline page

tutorial
Alexis Feron 2 weeks ago
parent d2e6f8d567
commit a83133a6f0

@ -3,9 +3,11 @@ import { AuthGuard } from './auth.guard';
import { HomePageComponent } from './components/home-page/home-page.component'; 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';
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: '**', component: NotFoundComponent }, { path: '**', component: NotFoundComponent },
]; ];

@ -26,12 +26,12 @@
'opacity-0 scale-50 pointer-events-none': !isPinModalOpen, 'opacity-0 scale-50 pointer-events-none': !isPinModalOpen,
'opacity-100 scale-100': isPinModalOpen 'opacity-100 scale-100': isPinModalOpen
}" }"
class="fixed top-0 right-0 left-0 z-50 flex justify-center items-center w-full h-full transition-transform duration-300 ease-in-out" class="fixed top-0 right-0 left-0 z-50 flex justify-center items-center w-full h-full transition-transform duration-300 ease-in-out overflow-y-auto"
> >
<div class="relative p-4 w-full max-w-xl max-h-full"> <div class="relative p-4 w-full max-w-xl max-h-full">
<!-- Modal content --> <!-- Modal content -->
<div <div
class="relative bg-white rounded-lg shadow dark:bg-gray-700 transition-transform duration-300 ease-in-out" class="relative bg-white rounded-lg shadow dark:bg-gray-700 transition-transform duration-300 ease-in-out my-8"
> >
<!-- Modal header --> <!-- Modal header -->
<div <div
@ -113,6 +113,11 @@
</div> </div>
<div> <div>
<label
for="files"
class="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
>Images</label
>
<app-drag-drop <app-drag-drop
(filesSelected)="onFilesReceived($event)" (filesSelected)="onFilesReceived($event)"
></app-drag-drop> ></app-drag-drop>
@ -143,25 +148,47 @@
</div> </div>
</div> --> </div> -->
<div class="flex justify-between"> <!-- <div *ngIf="files.length > 0">
<button <div *ngFor="let file of files">
type="reset" <img
(click)="closePinModal()" [src]="getImagePreview(file)"
class="w-full text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-800" alt="Image preview"
> width="100"
Annuler />
</button> </div>
</div> </div> -->
<div class="flex justify-between">
<button <div>
type="submit" <label
(click)="submitForm()" for="date"
class="w-full text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
>Date (optionnel)</label
> >
Valider <input
</button> type="date"
id="date"
formControlName="date"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white"
/>
</div> </div>
</form> </form>
<!-- Boutons alignés sous la grille -->
<div class="flex gap-4 mt-6">
<button
type="reset"
(click)="closePinModal()"
class="w-1/2 text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-800"
>
Annuler
</button>
<button
type="submit"
(click)="submitForm()"
class="w-1/2 text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
>
Valider
</button>
</div>
</div> </div>
</div> </div>
</div> </div>

@ -15,11 +15,11 @@ import {
} from 'rxjs/operators'; } from 'rxjs/operators';
import { AutocompleteService } from '../../services/auto-complete/auto-complete.service'; import { AutocompleteService } from '../../services/auto-complete/auto-complete.service';
import { ExifService } from '../../services/exif/exif.service'; import { ExifService } from '../../services/exif/exif.service';
import { ImageService } from '../../services/image/image.service';
import { MapReloadService } from '../../services/map-reload/map-reload.service'; import { MapReloadService } from '../../services/map-reload/map-reload.service';
import { ModalService } from '../../services/modal/modal.service';
import { PinService } from '../../services/pin/pin.service'; import { PinService } from '../../services/pin/pin.service';
import { DragDropComponent } from '../drag-drop/drag-drop.component'; import { DragDropComponent } from '../drag-drop/drag-drop.component';
import { ModalService } from '../../services/modal/modal.service';
import { ImageService } from '../../services/image/image.service';
@Component({ @Component({
selector: 'app-add-pin-popup', selector: 'app-add-pin-popup',
@ -51,6 +51,7 @@ export class AddPinPopupComponent implements OnInit {
description: new FormControl(''), description: new FormControl(''),
location: new FormControl(''), location: new FormControl(''),
files: new FormControl(null), files: new FormControl(null),
date: new FormControl(''),
}); });
} }
@ -159,6 +160,7 @@ export class AddPinPopupComponent implements OnInit {
const pinData = { const pinData = {
...this.form.value, ...this.form.value,
files: this.files, files: this.files,
date: this.form.get('date')?.value || null,
}; };
console.log('Files : ' + JSON.stringify(this.files)); console.log('Files : ' + JSON.stringify(this.files));

@ -43,12 +43,12 @@
'opacity-0 scale-50 pointer-events-none': !isPinModalOpen, 'opacity-0 scale-50 pointer-events-none': !isPinModalOpen,
'opacity-100 scale-100': isPinModalOpen 'opacity-100 scale-100': isPinModalOpen
}" }"
class="fixed top-0 right-0 left-0 z-50 flex justify-center items-center w-full h-full transition-transform duration-300 ease-in-out" class="fixed top-0 right-0 left-0 z-50 flex justify-center items-center w-full h-full transition-transform duration-300 ease-in-out overflow-y-auto"
> >
<div class="relative p-4 w-full max-w-xl max-h-full"> <div class="relative p-4 w-full max-w-xl max-h-full">
<!-- Modal content --> <!-- Modal content -->
<div <div
class="relative bg-white rounded-lg shadow dark:bg-gray-700 transition-transform duration-300 ease-in-out" class="relative bg-white rounded-lg shadow dark:bg-gray-700 transition-transform duration-300 ease-in-out my-8"
> >
<!-- Modal header --> <!-- Modal header -->
<div <div
@ -130,6 +130,11 @@
</div> </div>
<div> <div>
<label
for="files"
class="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
>Images</label
>
<app-drag-drop <app-drag-drop
(filesSelected)="onFilesReceived($event)" (filesSelected)="onFilesReceived($event)"
></app-drag-drop> ></app-drag-drop>
@ -150,25 +155,37 @@
></textarea> ></textarea>
</div> </div>
<div class="flex justify-between"> <div>
<button <label
type="reset" for="date"
(click)="closePinModal()" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
class="w-full text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-800" >Date (optionnel)</label
>
Annuler
</button>
</div>
<div class="flex justify-between">
<button
type="submit"
(click)="submitForm()"
class="w-full text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
> >
Valider <input
</button> type="date"
id="date"
formControlName="date"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white"
/>
</div> </div>
</form> </form>
<!-- Boutons alignés sous la grille -->
<div class="flex gap-4 mt-6">
<button
type="reset"
(click)="closePinModal()"
class="w-1/2 text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-800"
>
Annuler
</button>
<button
type="submit"
(click)="submitForm()"
class="w-1/2 text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
>
Valider
</button>
</div>
</div> </div>
</div> </div>
</div> </div>

@ -66,6 +66,7 @@ export class EditPinPopupComponent implements OnInit, AfterViewInit, OnDestroy {
description: new FormControl(''), description: new FormControl(''),
location: new FormControl(''), location: new FormControl(''),
files: new FormControl(null), files: new FormControl(null),
date: new FormControl(''),
}); });
} }
@ -85,6 +86,7 @@ export class EditPinPopupComponent implements OnInit, AfterViewInit, OnDestroy {
title: this.pin?.title || '', title: this.pin?.title || '',
description: this.pin?.description || '', description: this.pin?.description || '',
location: "Chargement de l'adresse...", location: "Chargement de l'adresse...",
date: this.pin?.date || '',
}); });
// Vérifier si nous avons des coordonnées valides dans pin.location // Vérifier si nous avons des coordonnées valides dans pin.location
@ -251,6 +253,7 @@ export class EditPinPopupComponent implements OnInit, AfterViewInit, OnDestroy {
...this.form.value, ...this.form.value,
files: this.files, files: this.files,
user_id: this.pin.user_id, user_id: this.pin.user_id,
date: this.form.get('date')?.value || null,
}; };
this.pinService.updatePin(this.pin.id, pinData).subscribe(() => { this.pinService.updatePin(this.pin.id, pinData).subscribe(() => {

@ -1,48 +1,46 @@
<div class="map-container h-[calc(100vh_-_72px)]"> <div class="map-container h-[calc(100vh_-_72px)] relative">
<div <div
id="map" id="map"
class="h-full w-full z-0" class="h-full w-full z-0"
(drop)="onDrop($event)" (drop)="onDrop($event)"
(dragover)="onDragOver($event)" (dragover)="onDragOver($event)"
></div> ></div>
</div>
<div
class="floating-filters text-center absolute top-[86px] right-[14px] flex flex-col space-y-2 z-10 bg-white p-3 rounded-xl shadow-lg dark:bg-gray-900 dark:text-white"
>
<div <div
class="filters flex md:items-center flex-col md:flex-row align-top justify-start gap-4" class="z-50 floating-filters text-center absolute top-4 right-4 flex flex-col space-y-2 bg-white p-3 rounded-xl shadow-lg dark:bg-gray-900 dark:text-white"
> >
<label class="flex items-center space-x-2"> <div
Pays : class="filters flex md:items-center flex-col md:flex-row align-top justify-start gap-4"
<select >
[(ngModel)]="selectedCountry" <label class="flex items-center space-x-2">
(change)="onCountryChange(selectedCountry)" Pays :
class="bg-white dark:bg-gray-900 dark:text-white ml-2" <select
> [(ngModel)]="selectedCountry"
<option value="">Tous</option> (change)="onCountryChange(selectedCountry)"
<option *ngFor="let country of availableCountries" [value]="country"> class="bg-white dark:bg-gray-900 dark:text-white ml-2"
{{ country }} >
</option> <option value="">Tous</option>
</select> <option *ngFor="let country of availableCountries" [value]="country">
</label> {{ country }}
<div class="hidden md:block h-5 border-l-2"></div> </option>
</select>
</label>
<div class="hidden md:block h-5 border-l-2"></div>
<!-- Amis taguées --> <!-- Amis taguées -->
<label class="flex items-center space-x-2"> <label class="flex items-center space-x-2">
Amis : Amis :
<select <select
[(ngModel)]="selectedPerson" [(ngModel)]="selectedPerson"
(change)="onPersonChange(selectedPerson)" (change)="onPersonChange(selectedPerson)"
class="bg-white dark:bg-gray-900 dark:text-white ml-2" class="bg-white dark:bg-gray-900 dark:text-white ml-2"
> >
<option value="__all__">Tous</option> <option value="__all__">Tous</option>
<option value="__none__">Aucun</option> <option value="__none__">Aucun</option>
<option *ngFor="let person of availablePersons" [value]="person"> <option *ngFor="let person of availablePersons" [value]="person">
{{ person }} {{ person }}
</option> </option>
</select> </select>
</label> </label>
</div>
</div> </div>
</div> </div>

@ -169,16 +169,16 @@
> >
<li> <li>
<a <a
routerLink="/" routerLink="/map"
*ngIf="!isHome" *ngIf="isTimeline"
class="block py-2 text-gray-900 dark:text-white hover:text-gray-700 dark:hover:text-gray-300" class="block py-2 text-gray-900 dark:text-white hover:text-gray-700 dark:hover:text-gray-300"
>Accueil</a >Carte</a
> >
<a <a
routerLink="/map" routerLink="/timeline"
*ngIf="isHome" *ngIf="!isTimeline"
class="block py-2 text-gray-900 dark:text-white hover:text-gray-700 dark:hover:text-gray-300" class="block py-2 text-gray-900 dark:text-white hover:text-gray-700 dark:hover:text-gray-300"
>Carte</a >Timeline</a
> >
</li> </li>
<li> <li>

@ -38,7 +38,7 @@ import { FriendPageComponent } from '../friend-page/friend-page.component';
templateUrl: './navbar.component.html', templateUrl: './navbar.component.html',
}) })
export class NavbarComponent implements OnInit { export class NavbarComponent implements OnInit {
isHome: boolean = false; isTimeline: boolean = false;
isSearchOpen: boolean = false; isSearchOpen: boolean = false;
isNavbarOpen: boolean = false; isNavbarOpen: boolean = false;
@ -75,10 +75,10 @@ export class NavbarComponent implements OnInit {
this.pins = pins; this.pins = pins;
}); });
this.isHome = this.router.url === '/'; this.isTimeline = this.router.url === '/timeline';
this.router.events.subscribe((event) => { this.router.events.subscribe((event) => {
if (event instanceof NavigationEnd) { if (event instanceof NavigationEnd) {
this.isHome = event.url === '/'; this.isTimeline = event.url === '/timeline';
} }
}); });

@ -0,0 +1,73 @@
<!-- Spinner pendant le chargement -->
<div *ngIf="loading" class="flex justify-center items-center h-64">
<div
class="animate-spin rounded-full h-12 w-12 border-4 border-blue-500 border-t-transparent"
></div>
</div>
<!-- Timeline -->
<div
*ngIf="!loading && pins.length > 0"
class="relative mx-auto max-w-3xl py-10 px-4"
>
<div
class="absolute h-full left-1/2 transform -translate-x-1/2 border-l-2 border-blue-500"
></div>
<div
*ngFor="let pin of pins; let i = index"
class="mb-12 flex justify-between items-center w-full"
>
<!-- Si pair, carte à droite, point à gauche -->
<div
class="w-5/12"
[ngClass]="{ 'order-1': i % 2 === 0, 'order-2': i % 2 !== 0 }"
></div>
<div
class="z-20 flex items-center bg-blue-600 shadow-lg w-10 h-10 rounded-full"
[ngClass]="{ 'order-2': i % 2 === 0, 'order-1': i % 2 !== 0 }"
>
<span class="text-white font-semibold mx-auto">{{ i + 1 }}</span>
</div>
<!-- Content card -->
<div
class="bg-white dark:bg-gray-800 rounded-xl shadow-xl px-6 py-5 w-5/12 transition-all duration-300 hover:scale-[1.02]"
[ngClass]="{
'order-3': i % 2 === 0,
'order-0': i % 2 !== 0,
'text-right': i % 2 !== 0
}"
>
<div class="text-sm text-gray-400 mb-1">
{{ pin.date ? (pin.date | date : "dd/MM/yyyy") : "Date inconnue" }}
</div>
<div class="text-lg font-bold text-gray-900 dark:text-white mb-1">
{{ pin.title || "Titre inconnu" }}
</div>
<div class="text-gray-700 dark:text-gray-300 mb-3">
{{ pin.description || "Aucune description" }}
</div>
<ng-container *ngIf="imageUrls[i] && imageUrls[i].length > 0">
<img
[src]="imageUrls[i][0]"
alt="image"
class="rounded-lg mx-auto max-h-40 object-cover shadow"
/>
</ng-container>
<ng-container *ngIf="!imageUrls[i] || imageUrls[i].length === 0">
<div class="text-gray-400 italic text-center">Aucune image</div>
</ng-container>
</div>
</div>
</div>
<!-- Message si vide -->
<div
*ngIf="!loading && pins.length === 0"
class="text-center text-gray-500 py-12 text-lg"
>
Aucun souvenir à afficher pour le moment.
</div>

@ -0,0 +1,47 @@
import { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { Pin } from '../../model/Pin';
import { ImageService } from '../../services/image/image.service';
import { PinService } from '../../services/pin/pin.service';
@Component({
selector: 'app-timeline',
standalone: true,
imports: [CommonModule],
templateUrl: './timeline.component.html',
})
export class TimelineComponent implements OnInit {
pins: Pin[] = [];
imageUrls: SafeUrl[][] = [];
loading = true;
constructor(
private pinService: PinService,
private imageService: ImageService,
private sanitizer: DomSanitizer
) {}
ngOnInit(): void {
this.pinService.getPins().subscribe((pins: Pin[]) => {
this.pins = pins
.filter((pin) => !!pin.date)
.sort((a, b) => (a.date! > b.date! ? 1 : -1));
this.imageUrls = this.pins.map(() => []); // initialise le tableau d'images
this.pins.forEach((pin, index) => {
if (pin.files && pin.files.length > 0) {
pin.files.forEach((imageId) => {
this.imageService.getImage(imageId).subscribe((blob) => {
const objectUrl = URL.createObjectURL(blob);
const safeUrl = this.sanitizer.bypassSecurityTrustUrl(objectUrl);
this.imageUrls[index].push(safeUrl);
});
});
}
});
this.loading = false;
});
}
}

@ -5,4 +5,5 @@ export interface Pin {
files: string[]; files: string[];
description: string; description: string;
user_id: string; user_id: string;
date?: string;
} }

@ -34,6 +34,7 @@ export class PinService {
description: string; description: string;
location: string; location: string;
files: string[]; files: string[];
date: string;
}) { }) {
const url = `${this.apiURL}/pin/add`; const url = `${this.apiURL}/pin/add`;
const headers = new HttpHeaders({ const headers = new HttpHeaders({
@ -51,6 +52,7 @@ export class PinService {
location: coords, location: coords,
files: pin.files, files: pin.files,
user_id: '', user_id: '',
date: pin.date,
}, },
{ headers } { headers }
); );

Loading…
Cancel
Save