Compare commits
No commits in common. 'master' and 'show-pins' have entirely different histories.
@ -1,45 +0,0 @@
|
|||||||
kind: pipeline
|
|
||||||
type: docker
|
|
||||||
name: Frontend CI/CD
|
|
||||||
|
|
||||||
trigger:
|
|
||||||
event:
|
|
||||||
- push
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: code-analysis
|
|
||||||
image: hub.codefirst.iut.uca.fr/alix.jeudi--lemoine/codefirst-dronesonarplugin-node:latest
|
|
||||||
commands:
|
|
||||||
- npm install
|
|
||||||
- /opt/sonar-scanner/bin/sonar-scanner
|
|
||||||
-Dsonar.login=$PLUGIN_SONAR_TOKEN
|
|
||||||
-Dsonar.projectKey=SAE3A_MemoryMap-front
|
|
||||||
-Dsonar.sources=src
|
|
||||||
-Dsonar.exclusions=**/*.spec.ts,**/node_modules/**
|
|
||||||
settings:
|
|
||||||
sonar_token:
|
|
||||||
from_secret: SECRET_SONAR_TOKEN
|
|
||||||
|
|
||||||
- name: docker-build-and-push
|
|
||||||
image: plugins/docker
|
|
||||||
settings:
|
|
||||||
dockerfile: docker/Dockerfile
|
|
||||||
context: .
|
|
||||||
registry: hub.codefirst.iut.uca.fr
|
|
||||||
mirror: https://proxy.iut.uca.fr:8443
|
|
||||||
repo: hub.codefirst.iut.uca.fr/alix.jeudi--lemoine/memorymap_front
|
|
||||||
username:
|
|
||||||
from_secret: SECRET_REGISTRY_USERNAME
|
|
||||||
password:
|
|
||||||
from_secret: SECRET_REGISTRY_PASSWORD
|
|
||||||
depends_on: [ code-analysis ]
|
|
||||||
|
|
||||||
- name: deploy-frontend
|
|
||||||
image: hub.codefirst.iut.uca.fr/thomas.bellembois/codefirst-dockerproxy-clientdrone:latest
|
|
||||||
environment:
|
|
||||||
IMAGENAME: hub.codefirst.iut.uca.fr/alix.jeudi--lemoine/memorymap_front:latest
|
|
||||||
CONTAINERNAME: frontend
|
|
||||||
COMMAND: create
|
|
||||||
OVERWRITE: true
|
|
||||||
ADMINS: alixjeudi--lemoine,alexisferon,mathisframit,maxencejouannet
|
|
||||||
depends_on: [ docker-build-and-push ]
|
|
@ -1,17 +0,0 @@
|
|||||||
# Official Node.js image
|
|
||||||
FROM node:22-slim
|
|
||||||
|
|
||||||
# Set workdir
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy the Angular app folder in the container
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Install dependencies
|
|
||||||
RUN npm install
|
|
||||||
|
|
||||||
# Expose port
|
|
||||||
EXPOSE 80
|
|
||||||
|
|
||||||
# Start the application
|
|
||||||
CMD [ "npm", "run", "production" ]
|
|
@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "./node_modules/@angular/service-worker/config/schema.json",
|
|
||||||
"index": "/",
|
|
||||||
"assetGroups": [
|
|
||||||
{
|
|
||||||
"name": "app",
|
|
||||||
"installMode": "prefetch",
|
|
||||||
"resources": {
|
|
||||||
"files": [
|
|
||||||
"/favicon.ico",
|
|
||||||
"/index.csr.html",
|
|
||||||
"/index.html",
|
|
||||||
"/manifest.webmanifest",
|
|
||||||
"/*.css",
|
|
||||||
"/*.js"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "assets",
|
|
||||||
"installMode": "lazy",
|
|
||||||
"updateMode": "prefetch",
|
|
||||||
"resources": {
|
|
||||||
"files": [
|
|
||||||
"/**/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 89 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 154 KiB |
Before Width: | Height: | Size: 257 KiB |
Before Width: | Height: | Size: 7.0 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 239 KiB After Width: | Height: | Size: 244 KiB |
@ -1,67 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Memory Map",
|
|
||||||
"short_name": "Memory Map",
|
|
||||||
"theme_color": "#111827",
|
|
||||||
"background_color": "#FFFFFF",
|
|
||||||
"display": "standalone",
|
|
||||||
"scope": "./",
|
|
||||||
"start_url": "./",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "icons/icon-72x72.png",
|
|
||||||
"sizes": "72x72",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "maskable any",
|
|
||||||
"background_color": "transparent"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "icons/icon-96x96.png",
|
|
||||||
"sizes": "96x96",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "maskable any",
|
|
||||||
"background_color": "transparent"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "icons/icon-128x128.png",
|
|
||||||
"sizes": "128x128",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "maskable any",
|
|
||||||
"background_color": "transparent"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "icons/icon-144x144.png",
|
|
||||||
"sizes": "144x144",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "maskable any",
|
|
||||||
"background_color": "transparent"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "icons/icon-152x152.png",
|
|
||||||
"sizes": "152x152",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "maskable any",
|
|
||||||
"background_color": "transparent"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "icons/icon-192x192.png",
|
|
||||||
"sizes": "192x192",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "maskable any",
|
|
||||||
"background_color": "transparent"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "icons/icon-384x384.png",
|
|
||||||
"sizes": "384x384",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "maskable any",
|
|
||||||
"background_color": "transparent"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "icons/icon-512x512.png",
|
|
||||||
"sizes": "512x512",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "maskable any",
|
|
||||||
"background_color": "transparent"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,25 +1,13 @@
|
|||||||
import { provideHttpClient } from '@angular/common/http';
|
import { provideHttpClient } from '@angular/common/http';
|
||||||
import {
|
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
|
||||||
ApplicationConfig,
|
|
||||||
isDevMode,
|
|
||||||
LOCALE_ID,
|
|
||||||
provideZoneChangeDetection,
|
|
||||||
} from '@angular/core';
|
|
||||||
import { provideRouter } from '@angular/router';
|
import { provideRouter } from '@angular/router';
|
||||||
import { provideServiceWorker } from '@angular/service-worker';
|
|
||||||
import { routes } from './app.routes';
|
import { routes } from './app.routes';
|
||||||
import { CookieService } from 'ngx-cookie-service';
|
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
provideZoneChangeDetection({ eventCoalescing: true }),
|
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||||
provideRouter(routes),
|
provideRouter(routes),
|
||||||
provideHttpClient(),
|
provideHttpClient(),
|
||||||
{ provide: LOCALE_ID, useValue: 'fr-FR' },
|
|
||||||
provideServiceWorker('ngsw-worker.js', {
|
|
||||||
enabled: !isDevMode(),
|
|
||||||
registrationStrategy: 'registerWhenStable:30000',
|
|
||||||
}),
|
|
||||||
CookieService,
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
@ -1,15 +1,12 @@
|
|||||||
import { Routes } from '@angular/router';
|
import { Routes } from '@angular/router';
|
||||||
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 { LoginPageComponent } from './components/login-page/login-page.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 { 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 },
|
||||||
{ path: 'timeline', component: TimelineComponent, canActivate: [AuthGuard] },
|
{ path: 'sign', component: LoginPageComponent },
|
||||||
{ path: 'pin/:id', component: PinDetailComponent, canActivate: [AuthGuard] },
|
|
||||||
{ path: '**', component: NotFoundComponent },
|
{ path: '**', component: NotFoundComponent },
|
||||||
];
|
];
|
||||||
|
@ -1,49 +0,0 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
|
||||||
import { Router } from '@angular/router';
|
|
||||||
import { AuthGuard } from './auth.guard';
|
|
||||||
import { CookiesService } from './services/cookies/cookies.service';
|
|
||||||
import { ModalService } from './services/modal/modal.service';
|
|
||||||
|
|
||||||
describe('AuthGuard', () => {
|
|
||||||
let guard: AuthGuard;
|
|
||||||
let cookiesServiceSpy: jasmine.SpyObj<CookiesService>;
|
|
||||||
let routerSpy: jasmine.SpyObj<Router>;
|
|
||||||
let loginModalServiceSpy: jasmine.SpyObj<ModalService>;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
cookiesServiceSpy = jasmine.createSpyObj('CookiesService', [
|
|
||||||
'getToken',
|
|
||||||
]);
|
|
||||||
routerSpy = jasmine.createSpyObj('Router', ['navigate']);
|
|
||||||
loginModalServiceSpy = jasmine.createSpyObj('LoginModalService', [
|
|
||||||
'openModal',
|
|
||||||
]);
|
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
providers: [
|
|
||||||
AuthGuard,
|
|
||||||
{ provide: CookiesService, useValue: cookiesServiceSpy },
|
|
||||||
{ provide: Router, useValue: routerSpy },
|
|
||||||
{ provide: ModalService, useValue: loginModalServiceSpy },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
guard = TestBed.inject(AuthGuard);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should allow activation when token exists', () => {
|
|
||||||
cookiesServiceSpy.getToken.and.returnValue('valid-token');
|
|
||||||
const result = guard.canActivate();
|
|
||||||
expect(result).toBeTrue();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should deny activation and trigger redirect and modal when token is missing', async () => {
|
|
||||||
cookiesServiceSpy.getToken.and.returnValue(null);
|
|
||||||
routerSpy.navigate.and.returnValue(Promise.resolve(true));
|
|
||||||
|
|
||||||
const result = guard.canActivate();
|
|
||||||
expect(result).toBeFalse();
|
|
||||||
expect(routerSpy.navigate).toHaveBeenCalledWith(['/']);
|
|
||||||
expect(loginModalServiceSpy.openModal).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,26 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { CanActivate, Router } from '@angular/router';
|
|
||||||
import { ModalService } from './services/modal/modal.service';
|
|
||||||
import { AuthService } from './services/auth/auth.service';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class AuthGuard implements CanActivate {
|
|
||||||
constructor(
|
|
||||||
private authService: AuthService,
|
|
||||||
private router: Router,
|
|
||||||
private loginModalService: ModalService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
canActivate(): boolean {
|
|
||||||
if (this.authService.isLoggedIn()) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
this.router.navigate(['/']).then(() => {
|
|
||||||
this.loginModalService.openModal('login-modal');
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
<div class="fixed bottom-0 left-0 w-full bg-gray-900 dark:bg-gray-900 border-t border-gray-700 dark:border-gray-700">
|
|
||||||
<div class="container mx-auto px-4 py-2">
|
|
||||||
<div class="flex justify-center items-center">
|
|
||||||
<a href="https://administration.memorymap.fr" class="text-m text-gray-300 dark:text-gray-300 hover:text-white dark:hover:text-white transition-colors duration-200">
|
|
||||||
Accès panneau d'administration
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,9 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-admin-footer',
|
|
||||||
imports: [CommonModule],
|
|
||||||
templateUrl: './admin-footer.component.html'
|
|
||||||
})
|
|
||||||
export class AdminFooterComponent { }
|
|
@ -1,71 +0,0 @@
|
|||||||
<div id="confirm-modal-{{pinId}}">
|
|
||||||
<!-- Fond assombri -->
|
|
||||||
<div
|
|
||||||
class="fixed inset-0 bg-gray-900 bg-opacity-50 w-full h-full z-40 transition-opacity duration-300 ease-in-out"
|
|
||||||
[ngClass]="{
|
|
||||||
'opacity-0 pointer-events-none': !isOpen,
|
|
||||||
'opacity-100': isOpen
|
|
||||||
}"
|
|
||||||
(click)="cancel()"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<!-- Contenu principal -->
|
|
||||||
<div
|
|
||||||
class="fixed inset-0 z-50 flex justify-center items-center w-full h-full overflow-y-auto"
|
|
||||||
[ngClass]="{
|
|
||||||
'opacity-0 scale-50 pointer-events-none': !isOpen,
|
|
||||||
'opacity-100 scale-100': isOpen
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="bg-white dark:bg-gray-700 rounded-lg shadow p-6 w-full max-w-md transition-transform duration-300 ease-in-out my-8"
|
|
||||||
>
|
|
||||||
<!-- Modal header -->
|
|
||||||
<div
|
|
||||||
class="flex items-center justify-between border-b rounded-t dark:border-gray-600 mb-6 pb-2"
|
|
||||||
>
|
|
||||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
|
||||||
Confirmation
|
|
||||||
</h2>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
(click)="closeModal()"
|
|
||||||
class="end-2.5 text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-3 h-3"
|
|
||||||
aria-hidden="true"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 14 14"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span class="sr-only">Fermer la modal</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p class="text-sm text-gray-700 dark:text-gray-300 mb-6">{{ message }}</p>
|
|
||||||
|
|
||||||
<div class="flex justify-end space-x-4">
|
|
||||||
<button
|
|
||||||
class="px-4 py-2 text-white bg-red-600 hover:bg-red-700 rounded"
|
|
||||||
(click)="confirm()"
|
|
||||||
>
|
|
||||||
Supprimer
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="px-4 py-2 bg-gray-300 dark:bg-gray-600 text-gray-800 dark:text-white rounded hover:bg-gray-400 dark:hover:bg-gray-500"
|
|
||||||
(click)="cancel()"
|
|
||||||
>
|
|
||||||
Annuler
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,23 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { ConfirmModalComponent } from './confirm-modal.component';
|
|
||||||
|
|
||||||
describe('ConfirmModalComponent', () => {
|
|
||||||
let component: ConfirmModalComponent;
|
|
||||||
let fixture: ComponentFixture<ConfirmModalComponent>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
imports: [ConfirmModalComponent]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(ConfirmModalComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,73 +0,0 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import {
|
|
||||||
Component,
|
|
||||||
EventEmitter,
|
|
||||||
Input,
|
|
||||||
OnDestroy,
|
|
||||||
OnInit,
|
|
||||||
Output,
|
|
||||||
} from '@angular/core';
|
|
||||||
import { Subscription } from 'rxjs';
|
|
||||||
import { ModalService } from '../../services/modal/modal.service';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-confirm-modal',
|
|
||||||
standalone: true,
|
|
||||||
imports: [CommonModule],
|
|
||||||
templateUrl: './confirm-modal.component.html',
|
|
||||||
})
|
|
||||||
export class ConfirmModalComponent implements OnInit, OnDestroy {
|
|
||||||
@Input() message: string = 'Es-tu sûr de vouloir supprimer ?';
|
|
||||||
@Input() pinId: string = '';
|
|
||||||
@Input() pinOpened!: EventEmitter<void>;
|
|
||||||
|
|
||||||
@Output() confirmed = new EventEmitter<void>();
|
|
||||||
@Output() cancelled = new EventEmitter<void>();
|
|
||||||
|
|
||||||
modalId: string = '';
|
|
||||||
|
|
||||||
isOpen = false;
|
|
||||||
private subscription!: Subscription;
|
|
||||||
|
|
||||||
constructor(private modalService: ModalService) {}
|
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
this.modalId = 'confirm-modal-' + this.pinId;
|
|
||||||
|
|
||||||
this.subscription = this.modalService
|
|
||||||
.getModalState(this.modalId)
|
|
||||||
.subscribe((state) => {
|
|
||||||
this.isOpen = state;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.pinOpened.subscribe(() => {
|
|
||||||
this.moveModalToBody();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy() {
|
|
||||||
this.subscription?.unsubscribe();
|
|
||||||
}
|
|
||||||
|
|
||||||
confirm() {
|
|
||||||
this.confirmed.emit();
|
|
||||||
this.modalService.closeModal(this.modalId);
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel() {
|
|
||||||
this.cancelled.emit();
|
|
||||||
this.modalService.closeModal(this.modalId);
|
|
||||||
}
|
|
||||||
|
|
||||||
closeModal() {
|
|
||||||
this.isOpen = false;
|
|
||||||
this.modalService.closeModal(this.modalId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private moveModalToBody(): void {
|
|
||||||
const modal = document.getElementById(this.modalId);
|
|
||||||
if (modal && modal.parentElement !== document.body) {
|
|
||||||
document.body.appendChild(modal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,231 +0,0 @@
|
|||||||
<!-- Modal toggle -->
|
|
||||||
<button
|
|
||||||
class="p-2 text-blue-500 rounded-full hover:bg-blue-200 focus:outline-none flex items-center shadow-sm transition duration-200"
|
|
||||||
aria-label="Edit"
|
|
||||||
(click)="openPinModal()"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-5 h-5 text-gray-800"
|
|
||||||
aria-hidden="true"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M10.779 17.779 4.36 19.918 6.5 13.5m4.279 4.279 8.364-8.643a3.027 3.027 0 0 0-2.14-5.165 3.03 3.03 0 0 0-2.14.886L6.5 13.5m4.279 4.279L6.499 13.5m2.14 2.14 6.213-6.504M12.75 7.04 17 11.28"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div id="edit-pin-popup-{{ pinId }}">
|
|
||||||
<!-- Fond assombri -->
|
|
||||||
<div
|
|
||||||
class="fixed inset-0 bg-gray-900 bg-opacity-50 w-full h-full transition-opacity duration-300 ease-in-out z-40"
|
|
||||||
[ngClass]="{
|
|
||||||
'opacity-0 pointer-events-none': !isPinModalOpen,
|
|
||||||
'opacity-100': isPinModalOpen
|
|
||||||
}"
|
|
||||||
(click)="closePinModal()"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<!-- Main modal -->
|
|
||||||
<div
|
|
||||||
tabindex="-1"
|
|
||||||
aria-hidden="true"
|
|
||||||
[ngClass]="{
|
|
||||||
'opacity-0 scale-50 pointer-events-none': !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 overflow-y-auto"
|
|
||||||
>
|
|
||||||
<div class="relative p-4 w-full max-w-xl max-h-full">
|
|
||||||
<!-- Modal content -->
|
|
||||||
<div
|
|
||||||
class="relative bg-white rounded-lg shadow dark:bg-gray-700 transition-transform duration-300 ease-in-out my-8"
|
|
||||||
>
|
|
||||||
<!-- Modal header -->
|
|
||||||
<div
|
|
||||||
class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600"
|
|
||||||
>
|
|
||||||
<h3 class="text-xl font-semibold text-gray-900 dark:text-white">
|
|
||||||
Modifie ton souvenir
|
|
||||||
</h3>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
(click)="closePinModal()"
|
|
||||||
class="end-2.5 text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-3 h-3"
|
|
||||||
aria-hidden="true"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 14 14"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span class="sr-only">Fermer la modal</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Modal body -->
|
|
||||||
<div class="p-4 md:p-5">
|
|
||||||
<form class="grid gap-6 mb-1 md:grid-cols-2" [formGroup]="form">
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
for="title"
|
|
||||||
class="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
|
|
||||||
>Titre</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="title"
|
|
||||||
formControlName="title"
|
|
||||||
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"
|
|
||||||
placeholder="Mont Saint-Michel"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
*ngIf="form.get('title')?.invalid && form.get('title')?.touched"
|
|
||||||
class="text-red-500 text-sm mt-1"
|
|
||||||
>
|
|
||||||
<span *ngIf="form.get('title')?.errors?.['required']"
|
|
||||||
>Le titre est requis</span
|
|
||||||
>
|
|
||||||
<span *ngIf="form.get('title')?.errors?.['minlength']"
|
|
||||||
>Le titre doit contenir au moins 3 caractères</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
for="localisation"
|
|
||||||
class="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
|
|
||||||
>Localisation</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="localisation"
|
|
||||||
formControlName="location"
|
|
||||||
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"
|
|
||||||
placeholder="Saisir la localisation"
|
|
||||||
(focus)="onFocus()"
|
|
||||||
(blur)="onBlur()"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
*ngIf="
|
|
||||||
form.get('location')?.invalid && form.get('location')?.touched
|
|
||||||
"
|
|
||||||
class="text-red-500 text-sm mt-1"
|
|
||||||
>
|
|
||||||
<span *ngIf="form.get('location')?.errors?.['required']"
|
|
||||||
>La localisation est requise</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<ul
|
|
||||||
*ngIf="suggestions.length > 0 && inputFocused"
|
|
||||||
class="bg-white dark:bg-gray-700 border border-gray-300 mt-2 rounded shadow absolute z-10 mr-5 max-h-60 overflow-auto"
|
|
||||||
>
|
|
||||||
<li
|
|
||||||
*ngFor="let suggestion of suggestions"
|
|
||||||
(click)="selectSuggestion(suggestion)"
|
|
||||||
class="p-2 block mb-2 text-sm font-medium text-gray-900 dark:text-white hover:bg-gray-500 cursor-pointer"
|
|
||||||
>
|
|
||||||
{{ suggestion.display_name }}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
for="files"
|
|
||||||
class="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
|
|
||||||
>Images</label
|
|
||||||
>
|
|
||||||
<app-drag-drop
|
|
||||||
*ngIf="isPinModalOpen"
|
|
||||||
[initialFiles]="getFileNames()"
|
|
||||||
(filesSelected)="onFilesReceived($event)"
|
|
||||||
(fileRemoved)="removeFile($event)"
|
|
||||||
[errorMessage]="uploadError"
|
|
||||||
></app-drag-drop>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
for="description"
|
|
||||||
class="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
|
|
||||||
>Description</label
|
|
||||||
>
|
|
||||||
<textarea
|
|
||||||
id="description"
|
|
||||||
rows="4"
|
|
||||||
formControlName="description"
|
|
||||||
class="block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
|
||||||
placeholder="Décrit ton souvenir..."
|
|
||||||
></textarea>
|
|
||||||
<div
|
|
||||||
*ngIf="
|
|
||||||
form.get('description')?.invalid &&
|
|
||||||
form.get('description')?.touched
|
|
||||||
"
|
|
||||||
class="text-red-500 text-sm mt-1"
|
|
||||||
>
|
|
||||||
<span *ngIf="form.get('description')?.errors?.['required']"
|
|
||||||
>La description est requise</span
|
|
||||||
>
|
|
||||||
<span *ngIf="form.get('description')?.errors?.['minlength']"
|
|
||||||
>La description doit contenir au moins 3 caractères</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
for="date"
|
|
||||||
class="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
|
|
||||||
>Date (optionnel)</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
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>
|
|
||||||
</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>
|
|
@ -1,22 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { EditPinPopupComponent } from './edit-pin-popup.component';
|
|
||||||
|
|
||||||
describe('EditPinPopupComponent', () => {
|
|
||||||
let component: EditPinPopupComponent;
|
|
||||||
let fixture: ComponentFixture<EditPinPopupComponent>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
imports: [EditPinPopupComponent],
|
|
||||||
}).compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(EditPinPopupComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,300 +0,0 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import {
|
|
||||||
Component,
|
|
||||||
EventEmitter,
|
|
||||||
Input,
|
|
||||||
OnDestroy,
|
|
||||||
OnInit,
|
|
||||||
ViewChild,
|
|
||||||
} from '@angular/core';
|
|
||||||
import {
|
|
||||||
FormBuilder,
|
|
||||||
FormControl,
|
|
||||||
FormGroup,
|
|
||||||
ReactiveFormsModule,
|
|
||||||
Validators,
|
|
||||||
} from '@angular/forms';
|
|
||||||
import { NavigationEnd, Router } from '@angular/router';
|
|
||||||
import { forkJoin, of, Subscription } from 'rxjs';
|
|
||||||
import {
|
|
||||||
catchError,
|
|
||||||
debounceTime,
|
|
||||||
distinctUntilChanged,
|
|
||||||
filter,
|
|
||||||
switchMap,
|
|
||||||
take,
|
|
||||||
} from 'rxjs/operators';
|
|
||||||
import { Pin } from '../../model/Pin';
|
|
||||||
import { AutocompleteService } from '../../services/auto-complete/auto-complete.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 { ModalService } from '../../services/modal/modal.service';
|
|
||||||
import { PinService } from '../../services/pin/pin.service';
|
|
||||||
import { DragDropComponent } from '../drag-drop/drag-drop.component';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-edit-pin-popup',
|
|
||||||
standalone: true,
|
|
||||||
imports: [ReactiveFormsModule, CommonModule, DragDropComponent],
|
|
||||||
templateUrl: './edit-pin-popup.component.html',
|
|
||||||
})
|
|
||||||
export class EditPinPopupComponent implements OnInit, OnDestroy {
|
|
||||||
@Input() isHomePage: boolean = false;
|
|
||||||
@Input() pin!: Pin;
|
|
||||||
@Input() pinId!: string;
|
|
||||||
|
|
||||||
@Input() pinOpened!: EventEmitter<void>;
|
|
||||||
|
|
||||||
@ViewChild(DragDropComponent) dragDropComponent!: DragDropComponent;
|
|
||||||
|
|
||||||
private modalOpenSubscription!: Subscription;
|
|
||||||
|
|
||||||
form!: FormGroup;
|
|
||||||
suggestions: any[] = [];
|
|
||||||
inputFocused: boolean = false;
|
|
||||||
files: File[] = [];
|
|
||||||
isPinModalOpen: boolean = false;
|
|
||||||
uploadError: string = '';
|
|
||||||
modalId: string = '';
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private fb: FormBuilder,
|
|
||||||
private autocompleteService: AutocompleteService,
|
|
||||||
private pinService: PinService,
|
|
||||||
private exifService: ExifService,
|
|
||||||
private modalService: ModalService,
|
|
||||||
private mapReloadService: MapReloadService,
|
|
||||||
private imageService: ImageService
|
|
||||||
) {
|
|
||||||
// Initialiser le formulaire avec des valeurs par défaut
|
|
||||||
this.form = this.fb.group({
|
|
||||||
title: new FormControl('', [Validators.required, Validators.minLength(3)]),
|
|
||||||
description: new FormControl('', [Validators.required, Validators.minLength(3)]),
|
|
||||||
location: new FormControl('', [Validators.required]),
|
|
||||||
complete_address: new FormControl('', [Validators.required]),
|
|
||||||
coordinates: new FormControl<number[]>([]),
|
|
||||||
files: new FormControl(null),
|
|
||||||
date: new FormControl(''),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onFocus(): void {
|
|
||||||
this.inputFocused = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
onBlur(): void {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.inputFocused = false; // Désactiver le focus après un petit délai pour permettre un clic sur la liste
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.modalId = 'edit-pin-popup-' + this.pinId;
|
|
||||||
|
|
||||||
// S'abonner aux changements d'état du modal
|
|
||||||
this.modalOpenSubscription = this.modalService
|
|
||||||
.getModalState(this.modalId)
|
|
||||||
.subscribe((state) => {
|
|
||||||
this.isPinModalOpen = state;
|
|
||||||
});
|
|
||||||
|
|
||||||
// S'abonner aux événements de navigation du router
|
|
||||||
this.pinOpened.subscribe(() => {
|
|
||||||
this.moveModalToBody();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Configuration de l'autocomplétion pour le champ d'adresse
|
|
||||||
this.form
|
|
||||||
.get('location')
|
|
||||||
?.valueChanges.pipe(
|
|
||||||
debounceTime(300), // Attendre 300ms après la dernière frappe
|
|
||||||
distinctUntilChanged(), // Ignorer si la nouvelle valeur est la même que la précédente
|
|
||||||
switchMap((query) => {
|
|
||||||
// Vérifier que query est une chaîne de caractères
|
|
||||||
if (typeof query !== 'string') {
|
|
||||||
return of([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const trimmedQuery = query.trim();
|
|
||||||
if (trimmedQuery.length > 2) {
|
|
||||||
return this.autocompleteService.getAddressSuggestions(trimmedQuery);
|
|
||||||
}
|
|
||||||
return of([]);
|
|
||||||
}),
|
|
||||||
catchError((error) => {
|
|
||||||
console.error('Error fetching suggestions:', error);
|
|
||||||
return of([]);
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.subscribe((data) => {
|
|
||||||
this.suggestions = data;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy() {
|
|
||||||
// Nettoyage des abonnements pour éviter les fuites de mémoire
|
|
||||||
if (this.modalOpenSubscription) {
|
|
||||||
this.modalOpenSubscription.unsubscribe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Méthode dédiée pour déplacer le modal vers le body
|
|
||||||
private moveModalToBody(): void {
|
|
||||||
const modal = document.getElementById(this.modalId);
|
|
||||||
if (modal && modal.parentElement !== document.body) {
|
|
||||||
document.body.appendChild(modal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
selectSuggestion(suggestion: any): void {
|
|
||||||
const locationControl = this.form.get('location');
|
|
||||||
if (locationControl instanceof FormControl) {
|
|
||||||
locationControl.setValue(suggestion.display_name);
|
|
||||||
this.form.get('complete_address')?.setValue(suggestion.display_name);
|
|
||||||
this.form.get('coordinates')?.setValue([suggestion.lat, suggestion.lon]);
|
|
||||||
}
|
|
||||||
this.suggestions = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
async onFilesReceived(files: FileList): Promise<void> {
|
|
||||||
// Ajouter les nouveaux fichiers à la liste existante
|
|
||||||
this.files = [...this.files, ...Array.from(files).map((file) => file)];
|
|
||||||
this.uploadError = ''; // Réinitialiser l'erreur
|
|
||||||
|
|
||||||
if (this.dragDropComponent) {
|
|
||||||
this.dragDropComponent.updateFileNamesFromFileList(files);
|
|
||||||
} else {
|
|
||||||
console.warn('EditPinPopupComponent - dragDropComponent not available');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ne traiter que la première photo pour les métadonnées EXIF
|
|
||||||
if (files.length > 0) {
|
|
||||||
try {
|
|
||||||
const data = await this.exifService.getLocation(files[0]);
|
|
||||||
if (data && data.latitude !== undefined && data.longitude !== undefined) {
|
|
||||||
this.autocompleteService.getAddressFromCoordinates(data.latitude, data.longitude).subscribe((address) => {
|
|
||||||
if (address) {
|
|
||||||
this.form.get('location')?.setValue(address.display_name);
|
|
||||||
this.form.get('complete_address')?.setValue(address.display_name);
|
|
||||||
this.form.get('coordinates')?.setValue([data.latitude, data.longitude]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
'EditPinPopupComponent - Error processing EXIF data:',
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async submitForm(): Promise<void> {
|
|
||||||
// Marquer tous les champs comme touched pour afficher les erreurs
|
|
||||||
Object.keys(this.form.controls).forEach(key => {
|
|
||||||
const control = this.form.get(key);
|
|
||||||
control?.markAsTouched();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.form.valid) {
|
|
||||||
const uploadObservables = await Promise.all(this.files.map(async (file) => {
|
|
||||||
if(file.size === 0) {
|
|
||||||
if(file.name.includes("|")) {
|
|
||||||
return of({id: file.name.split("|")[1]});
|
|
||||||
} else {
|
|
||||||
this.uploadError = file.name + ' : ' + 'Image vide';
|
|
||||||
return of(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let fileDate = await this.exifService.getDateTime(file);
|
|
||||||
|
|
||||||
return this.imageService.postImage(file, fileDate).pipe(
|
|
||||||
catchError(async error => {
|
|
||||||
this.uploadError = file.name + ' : ' + error.error.detail || 'Erreur lors de l\'upload de l\'image';
|
|
||||||
if (this.dragDropComponent) {
|
|
||||||
this.dragDropComponent.errorMessage = this.uploadError;
|
|
||||||
}
|
|
||||||
return of(null);
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
|
|
||||||
forkJoin(uploadObservables).subscribe(async (responses) => {
|
|
||||||
// Vérifier si toutes les réponses sont valides
|
|
||||||
if (responses.some(response => response === null)) {
|
|
||||||
return; // Ne pas continuer si une erreur s'est produite
|
|
||||||
}
|
|
||||||
|
|
||||||
this.files = responses.map((res: any) => res.id);
|
|
||||||
|
|
||||||
const coordinates = this.form.get('coordinates')?.value;
|
|
||||||
const pinData = {
|
|
||||||
...this.form.value,
|
|
||||||
files: this.files,
|
|
||||||
date: this.form.get('date')?.value || null,
|
|
||||||
location: coordinates || [0, 0],
|
|
||||||
complete_address: this.form.get('complete_address')?.value || this.form.get('location')?.value,
|
|
||||||
};
|
|
||||||
|
|
||||||
delete pinData.coordinates;
|
|
||||||
|
|
||||||
this.pinService.updatePin(this.pin.id, pinData)?.subscribe(() => {
|
|
||||||
this.mapReloadService.requestReload();
|
|
||||||
this.closePinModal();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.error('Le formulaire est invalide');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
openPinModal() {
|
|
||||||
// Initialiser le formulaire avec les valeurs de base
|
|
||||||
this.form.patchValue({
|
|
||||||
title: this.pin?.title || '',
|
|
||||||
description: this.pin?.description || '',
|
|
||||||
location: this.pin?.complete_address || '',
|
|
||||||
complete_address: this.pin?.complete_address || '',
|
|
||||||
coordinates: this.pin?.location || [],
|
|
||||||
files: this.pin?.files || [],
|
|
||||||
date: this.pin?.date
|
|
||||||
? new Date(this.pin.date).toISOString().split('T')[0]
|
|
||||||
: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
this.pin.files.forEach((file) => {
|
|
||||||
this.imageService.getImageMetadata(file).subscribe((metadata) => {
|
|
||||||
this.files.push(new File([], metadata.metadata.original_filename + "|" + file.toString(), { type: metadata.metadata.content_type }));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.modalService.openModal(this.modalId);
|
|
||||||
}
|
|
||||||
|
|
||||||
closePinModal() {
|
|
||||||
this.files = [];
|
|
||||||
this.modalService.closeModal(this.modalId);
|
|
||||||
}
|
|
||||||
|
|
||||||
removeFile(fileName: string): void {
|
|
||||||
const index = this.files.findIndex((file) => file.name === fileName || file.name.split("|")[0] === fileName);
|
|
||||||
if (index > -1) {
|
|
||||||
this.files.splice(index, 1);
|
|
||||||
this.uploadError = ''; // Réinitialiser l'erreur lors de la suppression d'un fichier
|
|
||||||
if (this.dragDropComponent) {
|
|
||||||
this.dragDropComponent.errorMessage = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mettre à jour le form control
|
|
||||||
const dataTransfer = new DataTransfer();
|
|
||||||
this.files.forEach((file) => dataTransfer.items.add(file as File));
|
|
||||||
this.form.patchValue({ files: dataTransfer.files });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getFileNames(): string[] {
|
|
||||||
return this.files.map((file) => file.name ? file.name.split("|")[0] : '');
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,256 +0,0 @@
|
|||||||
<!-- Bouton d'ouverture du modal -->
|
|
||||||
<button
|
|
||||||
(click)="openFriendModal()"
|
|
||||||
class="block py-2 text-gray-900 dark:text-white hover:text-gray-700 dark:hover:text-gray-300"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
Amis
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Fond assombri -->
|
|
||||||
<div
|
|
||||||
class="fixed inset-0 bg-gray-900 bg-opacity-50 w-full h-full transition-opacity duration-300 ease-in-out z-40"
|
|
||||||
[ngClass]="{
|
|
||||||
'opacity-0 pointer-events-none': !isFriendModalOpen,
|
|
||||||
'opacity-100': isFriendModalOpen
|
|
||||||
}"
|
|
||||||
(click)="closeFriendModal()"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<!-- Modal principal -->
|
|
||||||
<div
|
|
||||||
id="friends-modal"
|
|
||||||
tabindex="-1"
|
|
||||||
aria-hidden="true"
|
|
||||||
[ngClass]="{
|
|
||||||
'opacity-0 scale-50 pointer-events-none': !isFriendModalOpen,
|
|
||||||
'opacity-100 scale-100': isFriendModalOpen
|
|
||||||
}"
|
|
||||||
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="bg-white dark:bg-gray-800 rounded-lg shadow-lg w-96 max-w-full my-8"
|
|
||||||
>
|
|
||||||
<!-- En-tête du modal -->
|
|
||||||
<div
|
|
||||||
class="flex items-center justify-between p-4 border-b dark:border-gray-700"
|
|
||||||
>
|
|
||||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
|
||||||
Liste d'amis
|
|
||||||
</h3>
|
|
||||||
<button
|
|
||||||
(click)="closeFriendModal()"
|
|
||||||
class="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
|
|
||||||
>
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Barre de recherche -->
|
|
||||||
<div class="p-4" id="friend-search-bar">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="search-friends"
|
|
||||||
class="w-full p-2 border rounded-lg dark:bg-gray-700 dark:text-white"
|
|
||||||
placeholder="Rechercher un ami..."
|
|
||||||
[(ngModel)]="searchTerm"
|
|
||||||
(ngModelChange)="onSearchTermChange($event)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div *ngIf="listUser" class="text-gray-500 text-sm">
|
|
||||||
<div
|
|
||||||
*ngFor="let user of listUser"
|
|
||||||
class="friend flex items-center justify-between space-x-3 pt-10"
|
|
||||||
>
|
|
||||||
<div class="friend flex items-center space-x-3">
|
|
||||||
<img
|
|
||||||
class="w-10 h-10 rounded-full"
|
|
||||||
src="/avatar.png"
|
|
||||||
alt="Friend 2"
|
|
||||||
/>
|
|
||||||
<span class="text-gray-900 dark:text-white">{{
|
|
||||||
user.username
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
(click)="addUser(user.uid)"
|
|
||||||
class="p-2 bg-green-500 text-white rounded-full"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-6 h-6 text-gray-800 dark:text-white"
|
|
||||||
aria-hidden="true"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
fill="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M9 4a4 4 0 1 0 0 8 4 4 0 0 0 0-8Zm-2 9a4 4 0 0 0-4 4v1a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-1a4 4 0 0 0-4-4H7Zm8-1a1 1 0 0 1 1-1h1v-1a1 1 0 1 1 2 0v1h1a1 1 0 1 1 0 2h-1v1a1 1 0 1 1-2 0v-1h-1a1 1 0 0 1-1-1Z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="p-4 space-y-3" id="friend-list">
|
|
||||||
<p class="dark:text-white">Amis</p>
|
|
||||||
<div
|
|
||||||
*ngIf="hasNoAcceptedFriends()"
|
|
||||||
class="text-gray-500 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
<p>
|
|
||||||
<small><em>Aucun amis</em></small>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div *ngFor="let friend of listFriend">
|
|
||||||
<div
|
|
||||||
*ngIf="friend.status == 'accepted'"
|
|
||||||
class="flex justify-between items-center w-full space-x-2"
|
|
||||||
>
|
|
||||||
<div class="friend flex items-center space-x-3">
|
|
||||||
<img
|
|
||||||
class="w-10 h-10 rounded-full"
|
|
||||||
src="/avatar.png"
|
|
||||||
alt="Friend 2"
|
|
||||||
/>
|
|
||||||
<span class="text-gray-900 dark:text-white">{{
|
|
||||||
friend.username
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
(click)="deleteFriend(friend.id)"
|
|
||||||
class="p-2 bg-red-500 text-white rounded-full"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="w-4 h-4"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m3 0v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6h16zM10 11v6m4-6v6"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div *ngIf="hasPendingApprovalFriend()">
|
|
||||||
<p class="dark:text-white">Demandes</p>
|
|
||||||
<hr class="border-gray-300 my-3" />
|
|
||||||
</div>
|
|
||||||
<div *ngFor="let friend of listFriend">
|
|
||||||
<div
|
|
||||||
*ngIf="friend.status == 'pending_approval'"
|
|
||||||
class="flex justify-between items-center w-full"
|
|
||||||
>
|
|
||||||
<div class="friend flex items-center space-x-3">
|
|
||||||
<img
|
|
||||||
class="w-10 h-10 rounded-full"
|
|
||||||
src="/avatar.png"
|
|
||||||
alt="Friend 2"
|
|
||||||
/>
|
|
||||||
<span class="text-gray-900 dark:text-white">{{
|
|
||||||
friend.username
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex space-x-2">
|
|
||||||
<button
|
|
||||||
(click)="onAcceptOrDeny(friend.id, 'accept')"
|
|
||||||
class="p-2 bg-green-500 text-white rounded-full"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="w-4 h-4"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M5 13l4 4L19 7"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
(click)="onAcceptOrDeny(friend.id, 'deny')"
|
|
||||||
class="p-2 bg-red-500 text-white rounded-full"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="w-4 h-4"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M6 18L18 6M6 6l12 12"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div *ngIf="hasPendingFriend()">
|
|
||||||
<p class="dark:text-white">En attente</p>
|
|
||||||
<hr class="border-gray-300 my-3" />
|
|
||||||
</div>
|
|
||||||
<div *ngFor="let friend of listFriend">
|
|
||||||
<div
|
|
||||||
*ngIf="friend.status == 'pending'"
|
|
||||||
class="flex justify-between items-center w-full"
|
|
||||||
>
|
|
||||||
<div class="friend flex items-center space-x-3">
|
|
||||||
<img
|
|
||||||
class="w-10 h-10 rounded-full"
|
|
||||||
src="/avatar.png"
|
|
||||||
alt="Friend 2"
|
|
||||||
/>
|
|
||||||
<span class="text-gray-900 dark:text-white">{{
|
|
||||||
friend.username
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
<svg
|
|
||||||
class="w-6 h-6 text-gray-800 dark:text-white"
|
|
||||||
aria-hidden="true"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M18.5 4h-13m13 16h-13M8 20v-3.333a2 2 0 0 1 .4-1.2L10 12.6a1 1 0 0 0 0-1.2L8.4 8.533a2 2 0 0 1-.4-1.2V4h8v3.333a2 2 0 0 1-.4 1.2L13.957 11.4a1 1 0 0 0 0 1.2l1.643 2.867a2 2 0 0 1 .4 1.2V20H8Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Pied du modal -->
|
|
||||||
<div class="flex justify-end p-4 border-t dark:border-gray-700">
|
|
||||||
<button
|
|
||||||
(click)="closeFriendModal()"
|
|
||||||
class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
Fermer
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,23 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { FriendPageComponent } from './friend-page.component';
|
|
||||||
|
|
||||||
describe('FriendPageComponent', () => {
|
|
||||||
let component: FriendPageComponent;
|
|
||||||
let fixture: ComponentFixture<FriendPageComponent>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
imports: [FriendPageComponent]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(FriendPageComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,194 +0,0 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
|
||||||
import { FormsModule } from '@angular/forms';
|
|
||||||
import { debounceTime, distinctUntilChanged, Subject, Subscription } from 'rxjs';
|
|
||||||
import { FriendsService } from '../../services/friends/friends.service';
|
|
||||||
import { UserService } from '../../services/user/user.service';
|
|
||||||
import { ModalService } from '../../services/modal/modal.service';
|
|
||||||
import { AuthService } from '../../services/auth/auth.service';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-friend-page',
|
|
||||||
imports: [CommonModule, FormsModule],
|
|
||||||
templateUrl: './friend-page.component.html',
|
|
||||||
})
|
|
||||||
export class FriendPageComponent implements OnInit, OnDestroy {
|
|
||||||
protected listFriend: {
|
|
||||||
username: string;
|
|
||||||
status: string;
|
|
||||||
friend_user_id: string;
|
|
||||||
id: string;
|
|
||||||
}[] = [];
|
|
||||||
|
|
||||||
protected listUser: {
|
|
||||||
uid: string;
|
|
||||||
username: string;
|
|
||||||
}[] = [];
|
|
||||||
|
|
||||||
userId: string = '';
|
|
||||||
status: string = '';
|
|
||||||
isFriendModalOpen: boolean = false;
|
|
||||||
searchTerm: string = '';
|
|
||||||
searchTermChanged = new Subject<string>();
|
|
||||||
modalId: string = 'friend-modal';
|
|
||||||
|
|
||||||
private modalSub!: Subscription;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private friendService: FriendsService,
|
|
||||||
private userService: UserService,
|
|
||||||
private authService: AuthService,
|
|
||||||
private modalService: ModalService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.getFriendData();
|
|
||||||
this.modalSub = this.modalService.getModalState(this.modalId).subscribe(open => {
|
|
||||||
this.isFriendModalOpen = open;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.searchTermChanged
|
|
||||||
.pipe(debounceTime(200), distinctUntilChanged())
|
|
||||||
.subscribe((username: string) => {
|
|
||||||
this.searchUser(username);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this.modalSub.unsubscribe();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected searchUser(username: string) {
|
|
||||||
this.searchTerm = username;
|
|
||||||
if (this.searchTerm) {
|
|
||||||
this.getUserData(this.searchTerm.trim());
|
|
||||||
} else {
|
|
||||||
this.listUser = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onSearchTermChange(username: string) {
|
|
||||||
this.searchTermChanged.next(username);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getUserData(search: string): void {
|
|
||||||
const username = this.authService.getUsername();
|
|
||||||
this.userService
|
|
||||||
.getUser('^(?!' + username + ')' + search)
|
|
||||||
.subscribe((data: any[]) => {
|
|
||||||
if (data.length > 0) {
|
|
||||||
const existingFriendIds = this.listFriend.map(
|
|
||||||
(friend) => friend.friend_user_id
|
|
||||||
);
|
|
||||||
this.listUser = data.filter(
|
|
||||||
(user) => !existingFriendIds.includes(user.uid)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected addUser(user_id: string): void {
|
|
||||||
this.friendService.addFriend(user_id).subscribe((data: any) => {
|
|
||||||
if (data.id) {
|
|
||||||
const add_user = this.listUser.find((x) => x.uid == user_id);
|
|
||||||
if (add_user) {
|
|
||||||
this.listFriend.push({
|
|
||||||
username: add_user.username,
|
|
||||||
status: 'pending',
|
|
||||||
friend_user_id: add_user.uid,
|
|
||||||
id: data.id,
|
|
||||||
});
|
|
||||||
this.searchTerm = '';
|
|
||||||
this.listUser = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private getFriendData(): void {
|
|
||||||
this.friendService.getFriend().subscribe((data: any[]) => {
|
|
||||||
if (data.length > 0) {
|
|
||||||
data.forEach((friend) => {
|
|
||||||
let status = friend['status'];
|
|
||||||
let userId = friend['friend_user_id'];
|
|
||||||
let id = friend['id'];
|
|
||||||
this.friendService
|
|
||||||
.getFriendById(userId)
|
|
||||||
.subscribe((friendData: any) => {
|
|
||||||
this.listFriend.push({
|
|
||||||
username: friendData.username,
|
|
||||||
status: status,
|
|
||||||
friend_user_id: userId,
|
|
||||||
id: id,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onAcceptOrDeny(id: string, choice: string) {
|
|
||||||
if (choice == 'accept') {
|
|
||||||
this.friendService.acceptFriendById(id).subscribe((data: any) => {
|
|
||||||
if (data.message == 'Friend request accepted') {
|
|
||||||
this.listFriend.forEach((friend) => {
|
|
||||||
if (friend.id == id) {
|
|
||||||
friend.status = 'accepted';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.friendService.denyFriendById(id).subscribe((data: any) => {
|
|
||||||
if (data.message == 'Friend request denied') {
|
|
||||||
this.listFriend.forEach((friend, index) => {
|
|
||||||
if (friend.id == id) {
|
|
||||||
this.listFriend.splice(index, 1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
openFriendModal() {
|
|
||||||
this.modalService.openModal(this.modalId);
|
|
||||||
}
|
|
||||||
|
|
||||||
closeFriendModal() {
|
|
||||||
this.modalService.closeModal(this.modalId);
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteFriend(id: string) {
|
|
||||||
this.friendService.deleteFriend(id).subscribe((data: any) => {
|
|
||||||
if (data.message == 'Friend deleted') {
|
|
||||||
this.listFriend.forEach((friend, index) => {
|
|
||||||
if (friend.id == id) {
|
|
||||||
this.listFriend.splice(index, 1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
hasNoAcceptedFriends(): boolean {
|
|
||||||
return (
|
|
||||||
this.listFriend.filter((friend) => friend.status === 'accepted')
|
|
||||||
.length === 0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
hasPendingApprovalFriend(): boolean {
|
|
||||||
return (
|
|
||||||
this.listFriend.filter((friend) => friend.status === 'pending_approval')
|
|
||||||
.length !== 0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
hasPendingFriend(): boolean {
|
|
||||||
return (
|
|
||||||
this.listFriend.filter((friend) => friend.status === 'pending').length !==
|
|
||||||
0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,26 +1,11 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { ModalService } from '../../services/modal/modal.service';
|
import { RouterLink } from '@angular/router';
|
||||||
import { Router } from '@angular/router';
|
|
||||||
import { AuthService } from '../../services/auth/auth.service';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-home-page',
|
selector: 'app-home-page',
|
||||||
|
imports: [RouterLink],
|
||||||
templateUrl: './home-page.component.html',
|
templateUrl: './home-page.component.html',
|
||||||
})
|
})
|
||||||
export class HomePageComponent {
|
export class HomePageComponent {
|
||||||
currentYear = new Date().getFullYear();
|
currentYear = new Date().getFullYear();
|
||||||
|
|
||||||
constructor(
|
|
||||||
private loginModalService: ModalService,
|
|
||||||
private router: Router,
|
|
||||||
private authService: AuthService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
openLogin() {
|
|
||||||
if (!this.authService.isLoggedIn()) {
|
|
||||||
this.loginModalService.openModal('login-modal');
|
|
||||||
} else {
|
|
||||||
this.router.navigate(['/map']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,95 +1,3 @@
|
|||||||
<div class="map-container h-[calc(100vh_-_72px)] relative">
|
<div class="map-container h-[calc(100vh_-_72px)]">
|
||||||
<div
|
<div id="map" class="h-full w-full z-0"></div>
|
||||||
id="map"
|
|
||||||
class="h-full w-full z-0"
|
|
||||||
(drop)="onDrop($event)"
|
|
||||||
(dragover)="onDragOver($event)"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<!-- Bouton vertical d'ouverture des filtres (mobile uniquement, caché si ouvert) -->
|
|
||||||
<button
|
|
||||||
*ngIf="!isFiltersVisible && !isDesktop()"
|
|
||||||
(click)="toggleFilters()"
|
|
||||||
class="md:hidden absolute top-4 right-0 z-40 h-12 w-8 flex items-center justify-center bg-white dark:bg-gray-900 dark:text-white rounded-l-xl shadow-lg border border-gray-200 dark:border-gray-700 border-r-0"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="h-5 w-5 text-black dark:text-white"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="m15 19-7-7 7-7"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Filtres : même div pour mobile et desktop -->
|
|
||||||
<div
|
|
||||||
*ngIf="isFiltersVisible || isDesktop()"
|
|
||||||
[ngClass]="{
|
|
||||||
'absolute top-4 right-2 z-30 p-4': !isDesktop(),
|
|
||||||
'absolute top-4 right-4 p-3': isDesktop(),
|
|
||||||
'bg-white absolute top-4 right-2 dark:bg-gray-900 dark:text-white rounded-xl shadow-lg border border-gray-200 dark:border-gray-700': true
|
|
||||||
}"
|
|
||||||
class="box-border"
|
|
||||||
>
|
|
||||||
<!-- Ligne du haut : Pays + croix à droite (mobile uniquement) -->
|
|
||||||
<div class="flex flex-row items-center justify-between mb-2">
|
|
||||||
<label class="flex items-center space-x-2 m-0">
|
|
||||||
Pays :
|
|
||||||
<select
|
|
||||||
[(ngModel)]="selectedCountry"
|
|
||||||
(change)="onCountryChange(selectedCountry)"
|
|
||||||
class="bg-white dark:bg-gray-900 dark:text-white ml-2"
|
|
||||||
>
|
|
||||||
<option value="__all__">Tous</option>
|
|
||||||
<option *ngFor="let country of availableCountries" [value]="country">
|
|
||||||
{{ country }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<button
|
|
||||||
*ngIf="!isDesktop()"
|
|
||||||
(click)="toggleFilters()"
|
|
||||||
class="text-xl text-gray-700 dark:text-white focus:outline-none p-0 ml-4"
|
|
||||||
aria-label="Fermer les filtres"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="h-6 w-6"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M6 18L18 6M6 6l12 12"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col md:flex-row md:items-center gap-4">
|
|
||||||
<label class="flex items-center space-x-2">
|
|
||||||
Amis :
|
|
||||||
<select
|
|
||||||
[(ngModel)]="selectedPerson"
|
|
||||||
(change)="onPersonChange(selectedPerson)"
|
|
||||||
class="bg-white dark:bg-gray-900 dark:text-white ml-2"
|
|
||||||
>
|
|
||||||
<option value="__all__">Tous</option>
|
|
||||||
<option value="__none__">Aucun</option>
|
|
||||||
<option *ngFor="let person of availablePersons" [value]="person">
|
|
||||||
{{ person }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
#authentication-modal.show {
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
#authentication-modal.hidden {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease-in-out;
|
||||||
|
}
|
@ -0,0 +1,81 @@
|
|||||||
|
<div class="text-center">
|
||||||
|
<strong>{{ monument.title }}</strong>
|
||||||
|
<div
|
||||||
|
*ngIf="monument.files.length > 0"
|
||||||
|
class="relative carousel overflow-hidden"
|
||||||
|
>
|
||||||
|
<!-- Carousel wrapper -->
|
||||||
|
<div
|
||||||
|
class="relative h-40 mt-2 overflow-hidden rounded-lg flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
*ngFor="let image of monument.files; let index = index"
|
||||||
|
[class]="
|
||||||
|
'absolute inset-0 transition-opacity duration-700 ease-in-out' +
|
||||||
|
(index === currentIndex ? ' opacity-100' : ' opacity-0')
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
[src]="image"
|
||||||
|
[alt]="monument.title"
|
||||||
|
class="object-contain max-h-full max-w-full h-full w-auto mx-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Slider controls -->
|
||||||
|
<div *ngIf="monument.files.length > 1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="absolute top-0 left-0 z-30 flex items-center justify-center h-full 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 dark:bg-black-800/30 group-hover:bg-black/50 dark:group-hover:bg-black-800/60"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-4 h-4 text-white rtl:rotate-180"
|
||||||
|
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 class="sr-only">Previous</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="absolute top-0 right-0 z-30 flex items-center justify-center h-full 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 dark:bg-black-800/30 group-hover:bg-black/50 dark:group-hover:bg-black-800/60"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-4 h-4 text-white rtl:rotate-180"
|
||||||
|
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 class="sr-only">Next</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p [innerHTML]="formattedDescription"></p>
|
||||||
|
</div>
|
@ -1,17 +1,17 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { PinMarkerComponent } from './pin-marker.component';
|
import { MonumentMarkerComponent } from './monument-marker.component';
|
||||||
|
|
||||||
describe('PinmarkerComponent', () => {
|
describe('MonumentmarkerComponent', () => {
|
||||||
let component: PinMarkerComponent;
|
let component: MonumentMarkerComponent;
|
||||||
let fixture: ComponentFixture<PinMarkerComponent>;
|
let fixture: ComponentFixture<MonumentMarkerComponent>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [PinMarkerComponent],
|
imports: [MonumentMarkerComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
||||||
fixture = TestBed.createComponent(PinMarkerComponent);
|
fixture = TestBed.createComponent(MonumentMarkerComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
@ -0,0 +1,36 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { Monument } from '../../model/Monument';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-monument-marker',
|
||||||
|
templateUrl: './monument-marker.component.html',
|
||||||
|
imports: [CommonModule],
|
||||||
|
})
|
||||||
|
export class MonumentMarkerComponent {
|
||||||
|
@Input() monument!: Monument;
|
||||||
|
|
||||||
|
currentIndex: number = 0;
|
||||||
|
|
||||||
|
get formattedDescription(): string {
|
||||||
|
return this.formatDescription(this.monument.description);
|
||||||
|
}
|
||||||
|
|
||||||
|
formatDescription(description: string): string {
|
||||||
|
const regex = /@(\w+(-\w+)*(\.\w+(-\w+)*)*)/g;
|
||||||
|
return description.replace(
|
||||||
|
regex,
|
||||||
|
`<a href="/profile/$1" class="text-blue-500 hover:underline">@$1</a>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
prevSlide(): void {
|
||||||
|
this.currentIndex =
|
||||||
|
(this.currentIndex - 1 + this.monument.files.length) %
|
||||||
|
this.monument.files.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextSlide(): void {
|
||||||
|
this.currentIndex = (this.currentIndex + 1) % this.monument.files.length;
|
||||||
|
}
|
||||||
|
}
|
@ -1,171 +1,25 @@
|
|||||||
import { CommonModule, NgIf } from '@angular/common';
|
import { NgIf } from '@angular/common';
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import {
|
import { NavigationEnd, Router } from '@angular/router';
|
||||||
FormBuilder,
|
|
||||||
FormControl,
|
|
||||||
FormGroup,
|
|
||||||
ReactiveFormsModule,
|
|
||||||
} from '@angular/forms';
|
|
||||||
import {
|
|
||||||
ActivatedRoute,
|
|
||||||
NavigationEnd,
|
|
||||||
Router,
|
|
||||||
RouterLink,
|
|
||||||
} from '@angular/router';
|
|
||||||
import {
|
|
||||||
catchError,
|
|
||||||
debounceTime,
|
|
||||||
distinctUntilChanged,
|
|
||||||
of,
|
|
||||||
switchMap,
|
|
||||||
} from 'rxjs';
|
|
||||||
import { Pin } from '../../model/Pin';
|
|
||||||
import { AuthService } from '../../services/auth/auth.service';
|
|
||||||
import { ModalService } from '../../services/modal/modal.service';
|
|
||||||
import { NavbarService } from '../../services/navbar/navbar.service';
|
|
||||||
import { PinService } from '../../services/pin/pin.service';
|
|
||||||
import { AddPinPopupComponent } from '../add-pin-popup/add-pin-popup.component';
|
import { AddPinPopupComponent } from '../add-pin-popup/add-pin-popup.component';
|
||||||
import { FriendPageComponent } from '../friend-page/friend-page.component';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-navbar',
|
selector: 'app-navbar',
|
||||||
imports: [
|
imports: [AddPinPopupComponent, NgIf],
|
||||||
AddPinPopupComponent,
|
|
||||||
NgIf,
|
|
||||||
FriendPageComponent,
|
|
||||||
CommonModule,
|
|
||||||
ReactiveFormsModule,
|
|
||||||
RouterLink,
|
|
||||||
],
|
|
||||||
templateUrl: './navbar.component.html',
|
templateUrl: './navbar.component.html',
|
||||||
})
|
})
|
||||||
export class NavbarComponent implements OnInit {
|
export class NavbarComponent implements OnInit {
|
||||||
showTimeline: boolean = false;
|
isHome: boolean = false;
|
||||||
isSearchOpen: boolean = false;
|
isModalOpen: boolean = false;
|
||||||
isNavbarOpen: boolean = false;
|
|
||||||
|
|
||||||
pins: Pin[] = [];
|
constructor(private router: Router) {}
|
||||||
pinsFiltered: Pin[] = [];
|
|
||||||
inputFocus: Boolean = false;
|
|
||||||
searchForm: FormGroup;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private router: Router,
|
|
||||||
private route: ActivatedRoute,
|
|
||||||
private pinService: PinService,
|
|
||||||
private fb: FormBuilder,
|
|
||||||
private authService: AuthService,
|
|
||||||
private navbarService: NavbarService,
|
|
||||||
private modalService: ModalService
|
|
||||||
) {
|
|
||||||
this.searchForm = this.fb.group({
|
|
||||||
searchControl: new FormControl(''),
|
|
||||||
});
|
|
||||||
|
|
||||||
this.navbarService.isSearchOpen$.subscribe((isOpen) => {
|
|
||||||
this.isSearchOpen = isOpen;
|
|
||||||
this.isNavbarOpen = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.navbarService.isNavbarOpen$.subscribe((isOpen) => {
|
|
||||||
this.isNavbarOpen = isOpen;
|
|
||||||
this.isSearchOpen = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleSearch(): void {
|
|
||||||
this.navbarService.toggleSearch();
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleNavbar(): void {
|
|
||||||
this.navbarService.toggleNavbar();
|
|
||||||
}
|
|
||||||
|
|
||||||
openPinModal() {
|
|
||||||
this.modalService.openModal('add-pin-modal');
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.pins = this.pinService.getPins().subscribe((pins: Pin[]) => {
|
this.isHome = this.router.url === '/';
|
||||||
this.pins = pins;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.showTimeline =
|
|
||||||
this.router.url !== '/timeline' && this.router.url !== '/';
|
|
||||||
this.router.events.subscribe((event) => {
|
this.router.events.subscribe((event) => {
|
||||||
if (event instanceof NavigationEnd) {
|
if (event instanceof NavigationEnd) {
|
||||||
this.showTimeline =
|
this.isHome = event.url === '/';
|
||||||
event.url !== '/timeline' && this.router.url !== '/';
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.searchForm
|
|
||||||
.get('searchControl')
|
|
||||||
?.valueChanges.pipe(
|
|
||||||
debounceTime(300),
|
|
||||||
distinctUntilChanged(),
|
|
||||||
switchMap((searchTerm) => {
|
|
||||||
const trimmedQuery = searchTerm?.trim();
|
|
||||||
if (trimmedQuery && trimmedQuery.length > 1) {
|
|
||||||
return of(this.filterPins(trimmedQuery));
|
|
||||||
}
|
|
||||||
return of([]);
|
|
||||||
}),
|
|
||||||
catchError((error) => {
|
|
||||||
console.error(
|
|
||||||
'Erreur lors de la récupération des suggestions :',
|
|
||||||
error
|
|
||||||
);
|
|
||||||
return of([]);
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.subscribe((filteredPins) => {
|
|
||||||
this.pinsFiltered = filteredPins;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
filterPins(searchTerm: string): Pin[] {
|
|
||||||
const filteredPins: Pin[] = [];
|
|
||||||
|
|
||||||
if (this.pins.length === 0) {
|
|
||||||
this.pins = this.pinService.getPins();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.pins.forEach((pin: Pin) => {
|
|
||||||
if (
|
|
||||||
pin.title &&
|
|
||||||
pin.title.toLowerCase().includes(searchTerm.toLowerCase())
|
|
||||||
) {
|
|
||||||
filteredPins.push(pin);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return filteredPins;
|
|
||||||
}
|
|
||||||
|
|
||||||
clickSuggestion(pin: Pin): void {
|
|
||||||
this.searchForm.reset();
|
|
||||||
|
|
||||||
const queryParams = { pin: pin.id };
|
|
||||||
this.router.navigate([], {
|
|
||||||
relativeTo: this.route,
|
|
||||||
queryParams: queryParams,
|
|
||||||
queryParamsHandling: 'merge',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onFocus(): void {
|
|
||||||
this.inputFocus = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
onBlur(): void {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.inputFocus = false;
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
public logout() {
|
|
||||||
this.authService.logout();
|
|
||||||
this.router.navigate(['/']);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,37 +0,0 @@
|
|||||||
.pin-detail {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 100%;
|
|
||||||
min-height: calc(100vh - 15rem);
|
|
||||||
padding: 1rem;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* .pin-detail {
|
|
||||||
height: 100vh;
|
|
||||||
overflow-y: auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin: 0;
|
|
||||||
padding: 1rem;
|
|
||||||
box-sizing: border-box;
|
|
||||||
} */
|
|
||||||
|
|
||||||
.card-pin-detail {
|
|
||||||
background: linear-gradient(145deg, #ffffff, #f9fafb);
|
|
||||||
border: 1px solid #e5e7eb;
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
|
||||||
0 4px 6px -4px rgba(0, 0, 0, 0.1);
|
|
||||||
padding: 2.5rem;
|
|
||||||
max-width: 48rem;
|
|
||||||
width: 100%;
|
|
||||||
transition: box-shadow 0.3s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-pin-detail:hover {
|
|
||||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,163 +0,0 @@
|
|||||||
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';
|
|
||||||
import { CookiesService } from '../../services/cookies/cookies.service';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-pin-detail',
|
|
||||||
templateUrl: './pin-detail.component.html',
|
|
||||||
styleUrls: ['./pin-detail.component.css'],
|
|
||||||
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;
|
|
||||||
|
|
||||||
username_session: string = '';
|
|
||||||
|
|
||||||
@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,
|
|
||||||
private cookiesService: CookiesService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
// this.renderer.addClass(document.body, 'no-scroll-body');
|
|
||||||
|
|
||||||
const pinId = this.route.snapshot.paramMap.get('id');
|
|
||||||
this.username_session = this.cookiesService.getUsername() || '';
|
|
||||||
|
|
||||||
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),
|
|
||||||
});
|
|
||||||
|
|
||||||
this.pinService.getSharedUsersForPin(pinId).subscribe({
|
|
||||||
next: (shares) => {
|
|
||||||
this.sharedUsers = shares.map((share) => ({
|
|
||||||
user_id: share.user_id,
|
|
||||||
username: share.username,
|
|
||||||
can_edit: share.can_edit,
|
|
||||||
can_delete: share.can_delete,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
error: (err) => {
|
|
||||||
console.error('Erreur récupération utilisateurs partagés:', err);
|
|
||||||
this.sharedUsers = [];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 } });
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,229 +0,0 @@
|
|||||||
<!-- Conteneur cliquable -->
|
|
||||||
<div class="text-center px-2 sm:px-4"></div>
|
|
||||||
|
|
||||||
<!-- Modals -->
|
|
||||||
<app-confirm-modal
|
|
||||||
(confirmed)="handleConfirm()"
|
|
||||||
(cancelled)="handleCancel()"
|
|
||||||
[pinId]="pin.id"
|
|
||||||
[pinOpened]="pinOpened"
|
|
||||||
></app-confirm-modal>
|
|
||||||
<app-share-modal [pinOpened]="pinOpened" [pinId]="pin.id"></app-share-modal>
|
|
||||||
|
|
||||||
<!-- Boutons d'action -->
|
|
||||||
<div class="flex justify-between items-center flex-wrap mb-2">
|
|
||||||
<!-- Bouton Voir / Détail à gauche -->
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
[routerLink]="['/pin', pin.id]"
|
|
||||||
(click)="$event.stopPropagation()"
|
|
||||||
class="p-2 text-gray-800 rounded-full hover:bg-gray-200 focus:outline-none flex items-center shadow-sm transition duration-200"
|
|
||||||
aria-label="Voir le détail du pin"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-5 h-5 text-gray-800"
|
|
||||||
aria-hidden="true"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M16 4h4m0 0v4m0-4-5 5M8 20H4m0 0v-4m0 4 5-5"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Autres boutons à droite -->
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<app-edit-pin-popup
|
|
||||||
*ngIf="!this.pin.is_poi"
|
|
||||||
[pin]="pin"
|
|
||||||
[pinId]="pin.id"
|
|
||||||
[pinOpened]="pinOpened"
|
|
||||||
></app-edit-pin-popup>
|
|
||||||
|
|
||||||
<button
|
|
||||||
*ngIf="!this.pin.is_poi"
|
|
||||||
class="p-2 text-green-500 rounded-full hover:bg-green-200 focus:outline-none flex items-center shadow-sm transition duration-200"
|
|
||||||
(click)="sharePin()"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-4 h-4 sm:w-5 sm:h-5 text-gray-800"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M7.926 10.898 15 7.727m-7.074 5.39L15 16.29M8 12a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0Zm12 5.5a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0Zm0-11a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
*ngIf="!pin.is_poi"
|
|
||||||
class="p-2 text-red-500 rounded-full hover:bg-red-100 focus:outline-none flex items-center shadow-sm transition duration-200"
|
|
||||||
(click)="onDelete()"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-4 h-4 sm:w-5 sm:h-5 text-gray-800"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M5 7h14m-9 3v8m4-8v8M10 3h4a1 1 0 0 1 1 1v3H9V4a1 1 0 0 1 1-1ZM6 7h12v13a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="p-2 text-gray-500 rounded-full hover:bg-gray-200 focus:outline-none flex items-center shadow-sm transition duration-200"
|
|
||||||
(click)="onClosePopup()"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-4 h-4 sm:w-5 sm:h-5 text-gray-800"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M6 18 17.94 6M18 18 6.06 6"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Contenu du pin -->
|
|
||||||
<div class="text-center px-2 sm:px-4">
|
|
||||||
<strong class="block text-base sm:text-lg">{{ pin.title }}</strong>
|
|
||||||
|
|
||||||
<div *ngIf="!this.pin.is_poi">
|
|
||||||
<ng-container
|
|
||||||
*ngIf="pin.files.length > 0; else noImagesPlaceholder"
|
|
||||||
class="relative carousel overflow-hidden"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="relative mt-2 overflow-hidden rounded-lg flex items-center justify-center"
|
|
||||||
[ngClass]="{
|
|
||||||
'h-32 sm:h-40 md:h-52 lg:h-60': true
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Slider controls -->
|
|
||||||
<div *ngIf="pin.files.length > 1">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="absolute top-0 left-0 z-30 flex items-center justify-center h-full 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>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="absolute top-0 right-0 z-30 flex items-center justify-center h-full 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>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="text-lg mb-4 text-left"
|
|
||||||
[ngClass]="{
|
|
||||||
'whitespace-nowrap overflow-hidden truncate': !pin.is_poi,
|
|
||||||
'text-justify': pin.is_poi
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
{{ pin.description || "Aucune description" }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,124 +0,0 @@
|
|||||||
import { CommonModule, NgIf } from '@angular/common';
|
|
||||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
|
||||||
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
|
|
||||||
import * as L from 'leaflet';
|
|
||||||
import { Pin } from '../../model/Pin';
|
|
||||||
import { ImageService } from '../../services/image/image.service';
|
|
||||||
import { ModalService } from '../../services/modal/modal.service';
|
|
||||||
import { PinService } from '../../services/pin/pin.service';
|
|
||||||
import { ConfirmModalComponent } from '../confirm-modal/confirm-modal.component';
|
|
||||||
import { EditPinPopupComponent } from '../edit-pin-popup/edit-pin-popup.component';
|
|
||||||
import { ShareModalComponent } from '../share-modal/share-modal.component';
|
|
||||||
import { Router } from '@angular/router';
|
|
||||||
import { RouterModule } from '@angular/router';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-pin-marker',
|
|
||||||
templateUrl: './pin-marker.component.html',
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
EditPinPopupComponent,
|
|
||||||
ConfirmModalComponent,
|
|
||||||
ShareModalComponent,
|
|
||||||
RouterModule,
|
|
||||||
NgIf
|
|
||||||
],
|
|
||||||
standalone: true,
|
|
||||||
})
|
|
||||||
export class PinMarkerComponent {
|
|
||||||
@Input() pin!: Pin;
|
|
||||||
@Input() marker!: L.Marker;
|
|
||||||
|
|
||||||
currentIndex: number = 0;
|
|
||||||
imageUrls: SafeUrl[] = [];
|
|
||||||
imagesLoaded = false;
|
|
||||||
|
|
||||||
@Output() pinOpened = new EventEmitter<void>();
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private pinService: PinService,
|
|
||||||
private modalService: ModalService,
|
|
||||||
private imageService: ImageService,
|
|
||||||
private sanitizer: DomSanitizer,
|
|
||||||
private router: Router
|
|
||||||
) {}
|
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
// Écouter l'événement d'ouverture du popup
|
|
||||||
this.marker.on('popupopen', () => {
|
|
||||||
if (!this.imagesLoaded) {
|
|
||||||
this.loadImages();
|
|
||||||
this.pinOpened.emit();
|
|
||||||
// this.formatDescription(this.pin.description);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
sharePin() {
|
|
||||||
this.modalService.openModal('share-modal-' + this.pin.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
onClosePopup() {
|
|
||||||
this.marker.closePopup();
|
|
||||||
}
|
|
||||||
|
|
||||||
onDelete() {
|
|
||||||
this.modalService.openModal('confirm-modal-' + this.pin.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleConfirm() {
|
|
||||||
this.pinService.deletePin(this.pin.id).subscribe(() => {
|
|
||||||
this.marker.remove();
|
|
||||||
this.modalService.closeModal('confirm-modal-' + this.pin.id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCancel() {
|
|
||||||
this.modalService.closeModal('confirm-modal-' + this.pin.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// get formattedDescription(): string {
|
|
||||||
// return this.formatDescription(this.pin.description);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// formatDescription(description: string): string {
|
|
||||||
// const regex = /@(\w+(-\w+)*(\.\w+(-\w+)*)*)/g;
|
|
||||||
// return description.replace(
|
|
||||||
// regex,
|
|
||||||
// `<a href="/profile/$1" class="text-blue-500 hover:underline">@$1</a>`
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
prevSlide(): void {
|
|
||||||
this.currentIndex =
|
|
||||||
(this.currentIndex - 1 + this.imageUrls.length) % this.imageUrls.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
nextSlide(): void {
|
|
||||||
this.currentIndex = (this.currentIndex + 1) % this.imageUrls.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy() {
|
|
||||||
// Clean up object URLs to prevent memory leaks
|
|
||||||
this.imageUrls.forEach((url) => {
|
|
||||||
URL.revokeObjectURL(url.toString());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
navigateToDetail(): void {
|
|
||||||
this.router.navigate(['/pin', this.pin.id]);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,140 +0,0 @@
|
|||||||
<div id="share-modal-{{ pinId }}">
|
|
||||||
<!-- Fond assombri -->
|
|
||||||
<div
|
|
||||||
class="fixed inset-0 bg-gray-900 bg-opacity-50 w-full h-full transition-opacity duration-300 ease-in-out z-40"
|
|
||||||
[ngClass]="{
|
|
||||||
'opacity-0 pointer-events-none': !isShareModalOpen,
|
|
||||||
'opacity-100': isShareModalOpen
|
|
||||||
}"
|
|
||||||
(click)="closeShareModal()"
|
|
||||||
id="share-modal-background-{{ pinId }}"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<!-- Main modal -->
|
|
||||||
<div
|
|
||||||
id="share-modal-{{ pinId }}"
|
|
||||||
tabindex="-1"
|
|
||||||
aria-hidden="true"
|
|
||||||
[ngClass]="{
|
|
||||||
'opacity-0 scale-50 pointer-events-none': !isShareModalOpen,
|
|
||||||
'opacity-100 scale-100': isShareModalOpen
|
|
||||||
}"
|
|
||||||
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-md max-h-full">
|
|
||||||
<!-- Modal content -->
|
|
||||||
<div
|
|
||||||
class="relative bg-white rounded-lg shadow dark:bg-gray-700 transition-transform duration-300 ease-in-out my-8"
|
|
||||||
>
|
|
||||||
<!-- Modal header -->
|
|
||||||
<div
|
|
||||||
class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600"
|
|
||||||
>
|
|
||||||
<h3 class="text-xl font-semibold text-gray-900 dark:text-white">
|
|
||||||
Partager ce souvenir
|
|
||||||
</h3>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
(click)="closeShareModal()"
|
|
||||||
class="end-2.5 text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-3 h-3"
|
|
||||||
aria-hidden="true"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 14 14"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span class="sr-only">Fermer la modal</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Modal body -->
|
|
||||||
<div class="p-4 md:p-5 space-y-4">
|
|
||||||
<!-- Options de partage -->
|
|
||||||
<div class="flex flex-col space-y-4">
|
|
||||||
<div class="p-4">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="search-friends"
|
|
||||||
class="w-full p-2 mb-2 border rounded-lg dark:bg-gray-700 dark:text-white"
|
|
||||||
placeholder="Rechercher un ami..."
|
|
||||||
[(ngModel)]="searchTerm"
|
|
||||||
(ngModelChange)="onSearchTermChange($event)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div *ngIf="listUser" class="text-gray-500 text-sm">
|
|
||||||
<div
|
|
||||||
*ngFor="let user of listUser"
|
|
||||||
class="friend flex items-center justify-between space-x-3 pt-10"
|
|
||||||
>
|
|
||||||
<div class="friend flex items-center space-x-3">
|
|
||||||
<img
|
|
||||||
class="w-10 h-10 rounded-full"
|
|
||||||
src="/avatar.png"
|
|
||||||
alt="Friend 2"
|
|
||||||
/>
|
|
||||||
<span class="text-gray-900 dark:text-white">{{
|
|
||||||
user.username
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
*ngIf="!user.isShared"
|
|
||||||
(click)="sharePin(user.friend_user_id)"
|
|
||||||
class="p-2 bg-green-500 text-white rounded-full hover:bg-green-600 transition-colors"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-6 h-6 text-white"
|
|
||||||
aria-hidden="true"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M7.926 10.898 15 7.727m-7.074 5.39L15 16.29M8 12a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0Zm12 5.5a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0Zm0-11a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
*ngIf="user.isShared"
|
|
||||||
(click)="unsharePin(user.friend_user_id)"
|
|
||||||
class="p-2 bg-red-500 text-white rounded-full hover:bg-red-600 transition-colors"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-6 h-6"
|
|
||||||
aria-hidden="true"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M5 7h14m-9 3v8m4-8v8M10 3h4a1 1 0 0 1 1 1v3H9V4a1 1 0 0 1 1-1ZM6 7h12v13a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,23 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { ShareModalComponent } from './share-modal.component';
|
|
||||||
|
|
||||||
describe('ShareModalComponent', () => {
|
|
||||||
let component: ShareModalComponent;
|
|
||||||
let fixture: ComponentFixture<ShareModalComponent>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
imports: [ShareModalComponent]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(ShareModalComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,146 +0,0 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import {
|
|
||||||
Component,
|
|
||||||
EventEmitter,
|
|
||||||
Input,
|
|
||||||
OnDestroy,
|
|
||||||
OnInit,
|
|
||||||
} from '@angular/core';
|
|
||||||
import { FormsModule } from '@angular/forms';
|
|
||||||
import { Subject, Subscription } from 'rxjs';
|
|
||||||
import { FriendsService } from '../../services/friends/friends.service';
|
|
||||||
import { ModalService } from '../../services/modal/modal.service';
|
|
||||||
import { PinService } from '../../services/pin/pin.service';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-share-modal',
|
|
||||||
standalone: true,
|
|
||||||
imports: [CommonModule, FormsModule],
|
|
||||||
templateUrl: './share-modal.component.html',
|
|
||||||
})
|
|
||||||
export class ShareModalComponent implements OnInit, OnDestroy {
|
|
||||||
modalId: string = 'share-modal';
|
|
||||||
|
|
||||||
isShareModalOpen = false;
|
|
||||||
private modalSub!: Subscription;
|
|
||||||
isFriendModalOpen: boolean = false;
|
|
||||||
hasAcceptedFriends: boolean = false;
|
|
||||||
hasPendingFriends: boolean = false;
|
|
||||||
searchTerm: string = '';
|
|
||||||
searchTermChanged = new Subject<string>();
|
|
||||||
listUser: any[] = [];
|
|
||||||
listFriend: any[] = [];
|
|
||||||
pinShares: any[] = [];
|
|
||||||
|
|
||||||
@Input() pinId!: string;
|
|
||||||
@Input() pinOpened!: EventEmitter<void>;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private modalService: ModalService,
|
|
||||||
private friendService: FriendsService,
|
|
||||||
private pinService: PinService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
this.modalId = 'share-modal-' + this.pinId;
|
|
||||||
|
|
||||||
this.modalSub = this.modalService
|
|
||||||
.getModalState(this.modalId)
|
|
||||||
.subscribe((open) => {
|
|
||||||
this.isShareModalOpen = open;
|
|
||||||
if (open) {
|
|
||||||
this.getFriend();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.pinOpened.subscribe(() => {
|
|
||||||
this.moveModalToBody();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy() {
|
|
||||||
this.modalSub?.unsubscribe();
|
|
||||||
}
|
|
||||||
|
|
||||||
openShareModal() {
|
|
||||||
this.modalService.openModal(this.modalId);
|
|
||||||
}
|
|
||||||
|
|
||||||
closeShareModal() {
|
|
||||||
this.modalService.closeModal(this.modalId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private moveModalToBody(): void {
|
|
||||||
const modal = document.getElementById(this.modalId);
|
|
||||||
if (modal && modal.parentElement !== document.body) {
|
|
||||||
document.body.appendChild(modal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onSearchTermChange(value: string): void {
|
|
||||||
if (!this.listFriend) return;
|
|
||||||
|
|
||||||
if (value.trim() === '') {
|
|
||||||
this.listUser = [...this.listFriend];
|
|
||||||
} else {
|
|
||||||
this.listUser = this.listFriend.filter((friend) =>
|
|
||||||
friend.username?.toLowerCase().includes(value.toLowerCase())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getFriend() {
|
|
||||||
// Récupérer d'abord les partages du pin
|
|
||||||
this.pinService.getPinShares(this.pinId).subscribe((response: any) => {
|
|
||||||
this.pinShares = response.shares || [];
|
|
||||||
|
|
||||||
// Ensuite récupérer les amis
|
|
||||||
this.friendService.getFriend().subscribe((friends: any[]) => {
|
|
||||||
this.listFriend = [];
|
|
||||||
this.listUser = [];
|
|
||||||
|
|
||||||
// Récupérer les détails de chaque ami
|
|
||||||
friends.forEach((friend) => {
|
|
||||||
if (friend.status === 'accepted') {
|
|
||||||
this.friendService
|
|
||||||
.getFriendById(friend.friend_user_id)
|
|
||||||
.subscribe((userDetails: any) => {
|
|
||||||
const friendWithDetails = {
|
|
||||||
...friend,
|
|
||||||
username: userDetails.username,
|
|
||||||
isShared: this.pinShares.some(
|
|
||||||
(share) => share.user_id === friend.friend_user_id
|
|
||||||
),
|
|
||||||
};
|
|
||||||
this.listFriend.push(friendWithDetails);
|
|
||||||
this.listUser.push(friendWithDetails);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
sharePin(friendId: string) {
|
|
||||||
if (!this.pinId) {
|
|
||||||
console.error('No pin ID available');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.pinService.sharePin(this.pinId, friendId).subscribe((data: any) => {
|
|
||||||
this.closeShareModal();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
unsharePin(friendId: string) {
|
|
||||||
if (!this.pinId) {
|
|
||||||
console.error('No pin ID available');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.pinService.deletePinShare(this.pinId, friendId).subscribe(() => {
|
|
||||||
// Mettre à jour la liste des amis après la suppression
|
|
||||||
this.getFriend();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,240 +0,0 @@
|
|||||||
<!-- Spinner pendant le chargement -->
|
|
||||||
<div *ngIf="loading" class="flex justify-center items-center h-64">
|
|
||||||
<div
|
|
||||||
class="animate-spin rounded-full h-16 w-16 border-4 border-blue-500 border-t-transparent"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Timeline principale -->
|
|
||||||
<div
|
|
||||||
*ngIf="!loading && pins.length > 0"
|
|
||||||
class="relative mx-auto max-w-7xl py-20 px-6 z-0"
|
|
||||||
>
|
|
||||||
<!-- Barre centrale -->
|
|
||||||
<div
|
|
||||||
class="absolute left-1/2 transform -translate-x-1/2 h-full bg-blue-500 w-6 rounded-full z-0"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<!-- Groupement par années -->
|
|
||||||
<ng-container *ngFor="let year of sortedYears">
|
|
||||||
<!-- Marqueur d'année -->
|
|
||||||
<div class="relative mb-24 flex justify-center items-center">
|
|
||||||
<div
|
|
||||||
class="absolute left-1/2 transform -translate-x-1/2 h-16 w-8 bg-blue-500 z-0"
|
|
||||||
></div>
|
|
||||||
<div
|
|
||||||
class="bg-blue-600 text-white text-2xl font-bold px-10 py-5 rounded-full shadow-2xl z-10 border-4 border-white"
|
|
||||||
>
|
|
||||||
{{ year }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Pins de l'année -->
|
|
||||||
<ng-container *ngFor="let pin of groupedPins[year]; let i = index">
|
|
||||||
<div
|
|
||||||
class="mb-32 flex flex-col sm:flex-row justify-between items-center w-full relative z-10"
|
|
||||||
>
|
|
||||||
<!-- Espace vide -->
|
|
||||||
<div
|
|
||||||
class="w-full sm:w-5/12"
|
|
||||||
[ngClass]="{ 'sm:order-1': i % 2 === 0, 'sm:order-2': i % 2 !== 0 }"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<!-- Bulle centrale avec la date -->
|
|
||||||
<div
|
|
||||||
class="z-20 flex items-center justify-center bg-white border-[6px] border-b-0 sm:border-b-[6px] sm:border-blue-600 border-gray-800 sm:text-blue-700 text-gray-800 font-bold text-base sm:text-lg shadow-2xl sm:rounded-full rounded-t-3xl sm:w-32 sm:h-32 text-center leading-tight px-4 py-2 sm:px-10 sm:py-5 date-bubble"
|
|
||||||
[ngClass]="{
|
|
||||||
'sm:order-2': i % 2 === 0,
|
|
||||||
'sm:order-1': i % 2 !== 0
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<span>{{ pin.date | date : "d MMMM yyyy" }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Ligne de liaison (desktop uniquement) -->
|
|
||||||
<div
|
|
||||||
class="hidden -z-10 sm:block absolute top-1/2 transform -translate-y-1/2 h-2 w-[calc(50%-8rem)] bg-blue-500"
|
|
||||||
[ngClass]="{
|
|
||||||
'left-1/2': i % 2 === 0,
|
|
||||||
'right-1/2': i % 2 !== 0
|
|
||||||
}"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<!-- Carte de contenu -->
|
|
||||||
<div
|
|
||||||
class="bg-white dark:bg-gray-800 rounded-3xl shadow-2xl px-10 py-8 w-full sm:w-5/12 transition-all duration-300 hover:scale-[1.02] cursor-pointer"
|
|
||||||
[ngClass]="{
|
|
||||||
'sm:order-3 sm:text-left text-center': i % 2 === 0,
|
|
||||||
'sm:order-0 sm:text-right text-left': i % 2 !== 0
|
|
||||||
}"
|
|
||||||
(click)="navigateToPinOnMap(pin.id)"
|
|
||||||
>
|
|
||||||
<!-- Titre centré -->
|
|
||||||
<h3
|
|
||||||
class="text-2xl font-extrabold text-gray-900 dark:text-white mb-4 text-center"
|
|
||||||
>
|
|
||||||
{{ pin.title || "Titre inconnu" }}
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<!-- Description justifiée tronquée -->
|
|
||||||
<div
|
|
||||||
class="text-md text-gray-700 dark:text-gray-300 mb-4 text-justify transition-all duration-300"
|
|
||||||
[ngClass]="{
|
|
||||||
'line-clamp-5 overflow-hidden':
|
|
||||||
!expandedDescriptions[pins.indexOf(pin)]
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
{{ pin.description || "Aucune description" }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Bouton "voir plus / moins" -->
|
|
||||||
<div *ngIf="pin.description.length > 200" class="text-right mb-6">
|
|
||||||
<button
|
|
||||||
class="text-blue-600 font-semibold hover:underline"
|
|
||||||
(click)="toggleDescription(pins.indexOf(pin))"
|
|
||||||
>
|
|
||||||
{{
|
|
||||||
expandedDescriptions[pins.indexOf(pin)]
|
|
||||||
? "Voir moins"
|
|
||||||
: "Voir plus"
|
|
||||||
}}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Carrousel d'images -->
|
|
||||||
<ng-container *ngIf="imageUrls[pins.indexOf(pin)].length > 0">
|
|
||||||
<div
|
|
||||||
class="relative h-64 mt-2 overflow-hidden rounded-lg flex items-center justify-center"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
*ngFor="
|
|
||||||
let imageId of imageUrls[pins.indexOf(pin)];
|
|
||||||
let index = index
|
|
||||||
"
|
|
||||||
[class]="
|
|
||||||
'absolute inset-0 transition-opacity duration-700 ease-in-out' +
|
|
||||||
(index === carouselIndexes[pins.indexOf(pin)]
|
|
||||||
? ' opacity-100'
|
|
||||||
: ' opacity-0')
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
[src]="imageId"
|
|
||||||
class="object-contain max-h-full max-w-full h-full w-auto mx-auto"
|
|
||||||
alt="image"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Slider controls -->
|
|
||||||
<div *ngIf="imageUrls[pins.indexOf(pin)].length > 1">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="absolute top-0 left-0 z-30 flex items-center justify-center h-full cursor-pointer group focus:outline-none"
|
|
||||||
(click)="
|
|
||||||
prevImage(pins.indexOf(pin)); $event.stopPropagation()
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<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 class="sr-only">Précédent</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="absolute top-0 right-0 z-30 flex items-center justify-center h-full cursor-pointer group focus:outline-none"
|
|
||||||
(click)="
|
|
||||||
nextImage(pins.indexOf(pin)); $event.stopPropagation()
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<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 class="sr-only">Suivant</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Indicateur de position -->
|
|
||||||
<div
|
|
||||||
*ngIf="imageUrls[pins.indexOf(pin)].length > 1"
|
|
||||||
class="flex justify-center mt-2 space-x-2"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
*ngFor="let img of imageUrls[pins.indexOf(pin)]; let j = index"
|
|
||||||
class="w-3 h-3 rounded-full"
|
|
||||||
[ngClass]="{
|
|
||||||
'bg-blue-600': j === carouselIndexes[pins.indexOf(pin)],
|
|
||||||
'bg-blue-200': j !== carouselIndexes[pins.indexOf(pin)]
|
|
||||||
}"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- Fallback s'il n'y a pas d'image -->
|
|
||||||
<ng-container
|
|
||||||
*ngIf="
|
|
||||||
!imageUrls[pins.indexOf(pin)] ||
|
|
||||||
imageUrls[pins.indexOf(pin)].length === 0
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<div class="text-gray-400 italic text-center">Aucune image</div>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Message si vide -->
|
|
||||||
<div
|
|
||||||
*ngIf="!loading && pins.length === 0"
|
|
||||||
class="text-center text-gray-500 py-12 text-xl"
|
|
||||||
>
|
|
||||||
Aucun souvenir à afficher pour le moment.
|
|
||||||
</div>
|
|
||||||
</div></ng-container
|
|
||||||
></ng-container
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
*ngIf="!loading && pins.length === 0"
|
|
||||||
class="flex flex-col items-center justify-center h-64 space-y-6"
|
|
||||||
>
|
|
||||||
<p class="text-xl text-gray-800 text-center">
|
|
||||||
Commencez à créer votre histoire en ajoutant des souvenirs sur la carte !
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
(click)="openPinModal()"
|
|
||||||
class="px-6 py-3 bg-gray-800 text-white rounded-lg hover:bg-gray-700 transition-colors duration-200 shadow-lg"
|
|
||||||
>
|
|
||||||
Ajouter un souvenir
|
|
||||||
</button>
|
|
||||||
</div>
|
|
@ -1,140 +0,0 @@
|
|||||||
import { CommonModule, ViewportScroller } from '@angular/common';
|
|
||||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
|
||||||
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
|
|
||||||
import { NavigationEnd, Router } from '@angular/router';
|
|
||||||
import { Subscription } from 'rxjs';
|
|
||||||
import { filter } from 'rxjs/operators';
|
|
||||||
import { Pin } from '../../model/Pin';
|
|
||||||
import { ImageService } from '../../services/image/image.service';
|
|
||||||
import { ModalService } from '../../services/modal/modal.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, OnDestroy {
|
|
||||||
pins: Pin[] = [];
|
|
||||||
imageUrls: SafeUrl[][] = [];
|
|
||||||
loading = true;
|
|
||||||
groupedPins: { [year: string]: Pin[] } = {};
|
|
||||||
sortedYears: string[] = [];
|
|
||||||
carouselIndexes: number[] = [];
|
|
||||||
expandedDescriptions: { [index: number]: boolean } = {};
|
|
||||||
private navigationSubscription: Subscription;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private pinService: PinService,
|
|
||||||
private imageService: ImageService,
|
|
||||||
private sanitizer: DomSanitizer,
|
|
||||||
private modalService: ModalService,
|
|
||||||
private router: Router,
|
|
||||||
private viewportScroller: ViewportScroller
|
|
||||||
) {
|
|
||||||
// Écouter les événements de navigation
|
|
||||||
this.navigationSubscription = this.router.events
|
|
||||||
.pipe(filter((event) => event instanceof NavigationEnd))
|
|
||||||
.subscribe(() => {
|
|
||||||
// Attendre que le contenu soit chargé
|
|
||||||
if (!this.loading) {
|
|
||||||
this.restoreScrollPosition();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy() {
|
|
||||||
// Nettoyer la souscription lors de la destruction du composant
|
|
||||||
if (this.navigationSubscription) {
|
|
||||||
this.navigationSubscription.unsubscribe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private restoreScrollPosition() {
|
|
||||||
const scrollPosition = sessionStorage.getItem('timelineScrollPosition');
|
|
||||||
if (scrollPosition) {
|
|
||||||
window.scrollTo({
|
|
||||||
top: parseInt(scrollPosition),
|
|
||||||
behavior: 'smooth',
|
|
||||||
});
|
|
||||||
sessionStorage.removeItem('timelineScrollPosition');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
openPinModal() {
|
|
||||||
this.modalService.openModal('add-pin-modal');
|
|
||||||
}
|
|
||||||
|
|
||||||
navigateToPinOnMap(pinId: string) {
|
|
||||||
const scrollPosition = window.scrollY;
|
|
||||||
sessionStorage.setItem('timelineScrollPosition', scrollPosition.toString());
|
|
||||||
this.router.navigate(['/map'], { queryParams: { pin: pinId } });
|
|
||||||
}
|
|
||||||
|
|
||||||
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.carouselIndexes = this.pins.map(() => 0);
|
|
||||||
this.loading = false;
|
|
||||||
this.groupPinsByYear();
|
|
||||||
|
|
||||||
// Attendre que le DOM soit mis à jour avant de restaurer la position
|
|
||||||
setTimeout(() => {
|
|
||||||
this.restoreScrollPosition();
|
|
||||||
}, 100);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private groupPinsByYear(): void {
|
|
||||||
this.groupedPins = {};
|
|
||||||
|
|
||||||
for (const pin of this.pins) {
|
|
||||||
const year = new Date(pin.date!).getFullYear().toString();
|
|
||||||
if (!this.groupedPins[year]) {
|
|
||||||
this.groupedPins[year] = [];
|
|
||||||
}
|
|
||||||
this.groupedPins[year].push(pin);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trie les pins dans chaque groupe (au cas où)
|
|
||||||
for (const year in this.groupedPins) {
|
|
||||||
this.groupedPins[year].sort((a, b) => a.date!.localeCompare(b.date!));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trie les années dans l'ordre croissant (utilisé dans le template)
|
|
||||||
this.sortedYears = Object.keys(this.groupedPins).sort((a, b) => +a - +b);
|
|
||||||
}
|
|
||||||
|
|
||||||
nextImage(index: number) {
|
|
||||||
const images = this.imageUrls[index];
|
|
||||||
this.carouselIndexes[index] =
|
|
||||||
(this.carouselIndexes[index] + 1) % images.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
prevImage(index: number) {
|
|
||||||
const images = this.imageUrls[index];
|
|
||||||
this.carouselIndexes[index] =
|
|
||||||
(this.carouselIndexes[index] - 1 + images.length) % images.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleDescription(index: number): void {
|
|
||||||
this.expandedDescriptions[index] = !this.expandedDescriptions[index];
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,32 @@
|
|||||||
|
import { Monument } from '../model/Monument';
|
||||||
|
|
||||||
|
export const monuments: Monument[] = [
|
||||||
|
{
|
||||||
|
coords: [48.85837, 2.294481],
|
||||||
|
name: 'Tour Eiffel',
|
||||||
|
images: [
|
||||||
|
'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQGGib245tSFiK1Qcx0cB0dZsoVyJElwsY3kA&s',
|
||||||
|
'https://encrypted-tbn2.gstatic.com/licensed-image?q=tbn:ANd9GcTLB9B0j50rJbcSbdja9_hySHS6_KATbhTK_iCeWeNKtA92hTmTX5nTW3udjjovZrnU1JxqAjMS_VqHnMwHGhTs35-sU-7B29_X_T3uLV8',
|
||||||
|
],
|
||||||
|
description: 'Visité en 2020 avec la famille, un moment inoubliable.',
|
||||||
|
visited: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
coords: [43.296482, 5.36978],
|
||||||
|
name: 'Vieux Port de Marseille',
|
||||||
|
images: [
|
||||||
|
'https://upload.wikimedia.org/wikipedia/commons/thumb/7/74/Marseille_Old_Port.jpg/390px-Marseille_Old_Port.jpg',
|
||||||
|
],
|
||||||
|
description:
|
||||||
|
"Découvert lors d'un week-end ensoleillé en 2019 avec @John-Doe.",
|
||||||
|
visited: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
coords: [48.636063, -1.511457],
|
||||||
|
name: 'Mont Saint-Michel',
|
||||||
|
images: [],
|
||||||
|
description: '',
|
||||||
|
visited: false,
|
||||||
|
},
|
||||||
|
// ...
|
||||||
|
];
|
@ -1,6 +0,0 @@
|
|||||||
export interface AuthResponse {
|
|
||||||
access_token: string;
|
|
||||||
token_type: string;
|
|
||||||
user_id: string;
|
|
||||||
is_admin: boolean;
|
|
||||||
}
|
|
@ -0,0 +1,6 @@
|
|||||||
|
export interface Monument {
|
||||||
|
location: number[];
|
||||||
|
title: string;
|
||||||
|
files: string[];
|
||||||
|
description: string;
|
||||||
|
}
|
@ -1,11 +0,0 @@
|
|||||||
export interface Pin {
|
|
||||||
id: string;
|
|
||||||
location: number[];
|
|
||||||
complete_address: string;
|
|
||||||
title: string;
|
|
||||||
files: string[];
|
|
||||||
description: string;
|
|
||||||
is_poi: boolean;
|
|
||||||
user_id: string;
|
|
||||||
date?: string;
|
|
||||||
}
|
|
@ -1,13 +1,13 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { ExifService } from './exif.service';
|
import { AddPinService } from './add-pin.service';
|
||||||
|
|
||||||
describe('ExifService', () => {
|
describe('AddPinService', () => {
|
||||||
let service: ExifService;
|
let service: AddPinService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({});
|
TestBed.configureTestingModule({});
|
||||||
service = TestBed.inject(ExifService);
|
service = TestBed.inject(AddPinService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
@ -0,0 +1,45 @@
|
|||||||
|
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { switchMap } from 'rxjs';
|
||||||
|
import { environment } from '../../environments/environment';
|
||||||
|
import { AutocompleteService } from './auto-complete.service';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class AddPinService {
|
||||||
|
private apiURL = environment.apiURL;
|
||||||
|
private token = localStorage.getItem('auth_token');
|
||||||
|
constructor(
|
||||||
|
private http: HttpClient,
|
||||||
|
private autoCompleteService: AutocompleteService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
addPin(pin: {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
location: string;
|
||||||
|
files: any[];
|
||||||
|
}) {
|
||||||
|
const url = `${this.apiURL}/pin/add`;
|
||||||
|
const headers = new HttpHeaders({
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: 'Bearer ' + this.token,
|
||||||
|
});
|
||||||
|
return this.autoCompleteService.getAdressCoordinates(pin.location).pipe(
|
||||||
|
switchMap((response: any) => {
|
||||||
|
const coords: [string, string] = [response[0].lat, response[0].lon];
|
||||||
|
return this.http.post<any>(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
title: pin.title,
|
||||||
|
description: pin.description,
|
||||||
|
location: coords,
|
||||||
|
files: pin.files,
|
||||||
|
},
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,104 +0,0 @@
|
|||||||
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
|
|
||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { BehaviorSubject, Observable, tap } from 'rxjs';
|
|
||||||
import { environment } from '../../../environment';
|
|
||||||
import { AuthResponse } from '../../model/AuthResponse';
|
|
||||||
import { CookiesService } from '../cookies/cookies.service';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class AuthService {
|
|
||||||
private isAdminSubject = new BehaviorSubject<boolean>(false);
|
|
||||||
private userIdSubject = new BehaviorSubject<string>('');
|
|
||||||
isAdmin$ = this.isAdminSubject.asObservable();
|
|
||||||
username$ = new BehaviorSubject<string>('');
|
|
||||||
isLoggedIn$ = new BehaviorSubject<boolean>(false);
|
|
||||||
userId$ = this.userIdSubject.asObservable();
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private http: HttpClient,
|
|
||||||
private cookiesService: CookiesService
|
|
||||||
) {
|
|
||||||
const token = this.cookiesService.getToken();
|
|
||||||
if (token) {
|
|
||||||
this.isAdminSubject.next(this.cookiesService.getIsAdmin() === 'true');
|
|
||||||
this.username$.next(this.cookiesService.getUsername() || '');
|
|
||||||
this.userIdSubject.next(this.cookiesService.getUserId() || '');
|
|
||||||
this.isLoggedIn$.next(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
login(username: string, password: string): Observable<AuthResponse> {
|
|
||||||
const payload = new HttpParams()
|
|
||||||
.set('username', username)
|
|
||||||
.set('password', password);
|
|
||||||
|
|
||||||
return this.http
|
|
||||||
.post<AuthResponse>(`${environment.apiURL}/login`, payload)
|
|
||||||
.pipe(
|
|
||||||
tap((response) => {
|
|
||||||
this.cookiesService.setToken(response.access_token);
|
|
||||||
this.cookiesService.setIsAdmin(response.is_admin);
|
|
||||||
this.cookiesService.setUsername(username);
|
|
||||||
this.cookiesService.setUserId(response.user_id);
|
|
||||||
this.isAdminSubject.next(response.is_admin);
|
|
||||||
this.username$.next(username);
|
|
||||||
this.userIdSubject.next(response.user_id);
|
|
||||||
this.isLoggedIn$.next(true);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
logout(): void {
|
|
||||||
this.cookiesService.removeToken();
|
|
||||||
this.cookiesService.removeIsAdmin();
|
|
||||||
this.cookiesService.removeUsername();
|
|
||||||
this.cookiesService.removeUserId();
|
|
||||||
this.isAdminSubject.next(false);
|
|
||||||
this.username$.next('');
|
|
||||||
this.userIdSubject.next('');
|
|
||||||
this.isLoggedIn$.next(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
register(username: string, password: string): Observable<AuthResponse> {
|
|
||||||
return this.http
|
|
||||||
.post<AuthResponse>(`${environment.apiURL}/register`, {
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
})
|
|
||||||
.pipe(
|
|
||||||
tap((response) => {
|
|
||||||
this.cookiesService.setToken(response.access_token);
|
|
||||||
this.cookiesService.setIsAdmin(response.is_admin);
|
|
||||||
this.cookiesService.setUsername(username);
|
|
||||||
this.cookiesService.setUserId(response.user_id);
|
|
||||||
this.isAdminSubject.next(response.is_admin);
|
|
||||||
this.username$.next(username);
|
|
||||||
this.userIdSubject.next(response.user_id);
|
|
||||||
this.isLoggedIn$.next(true);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
isAdmin(): boolean {
|
|
||||||
return this.isAdminSubject.value || false;
|
|
||||||
}
|
|
||||||
|
|
||||||
getAuthHeaders(): HttpHeaders {
|
|
||||||
const token = this.cookiesService.getToken();
|
|
||||||
return new HttpHeaders().set('Authorization', `Bearer ${token}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
getUsername(): string {
|
|
||||||
return this.username$.value || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
getUserId(): string {
|
|
||||||
return this.userIdSubject.value || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoggedIn(): boolean {
|
|
||||||
return this.isLoggedIn$.value || false;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +1,13 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { AutocompleteService } from './auto-complete.service';
|
import { AutoCompleteService } from './auto-complete.service';
|
||||||
|
|
||||||
describe('AutocompleteService', () => {
|
describe('AutoCompleteService', () => {
|
||||||
let service: AutocompleteService;
|
let service: AutoCompleteService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({});
|
TestBed.configureTestingModule({});
|
||||||
service = TestBed.inject(AutocompleteService);
|
service = TestBed.inject(AutoCompleteService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
@ -1,106 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { Router } from '@angular/router';
|
|
||||||
import { CookieService } from 'ngx-cookie-service';
|
|
||||||
import { ModalService } from '../modal/modal.service';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class CookiesService {
|
|
||||||
private readonly AUTH_TOKEN_KEY = 'auth_token';
|
|
||||||
private readonly USERNAME_KEY = 'username';
|
|
||||||
private readonly USER_ID = 'userId';
|
|
||||||
private readonly IS_ADMIN_KEY = 'isAdmin';
|
|
||||||
private readonly COOKIE_OPTIONS = {
|
|
||||||
path: '/',
|
|
||||||
domain: window.location.hostname,
|
|
||||||
secure: true,
|
|
||||||
sameSite: 'Strict' as const,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private router: Router,
|
|
||||||
private modalService: ModalService,
|
|
||||||
private cookieService: CookieService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
setToken(token: string): void {
|
|
||||||
this.cookieService.set(this.AUTH_TOKEN_KEY, token, this.COOKIE_OPTIONS);
|
|
||||||
}
|
|
||||||
|
|
||||||
setUsername(username: string): void {
|
|
||||||
this.cookieService.set(this.USERNAME_KEY, username, this.COOKIE_OPTIONS);
|
|
||||||
}
|
|
||||||
|
|
||||||
setUserId(user_id: string): void {
|
|
||||||
this.cookieService.set(this.USER_ID, user_id, this.COOKIE_OPTIONS);
|
|
||||||
}
|
|
||||||
|
|
||||||
getUserId(): string | null {
|
|
||||||
return this.cookieService.get(this.USER_ID) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
getUsername(): string | null {
|
|
||||||
return this.cookieService.get(this.USERNAME_KEY) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
removeUsername(): void {
|
|
||||||
this.cookieService.delete(
|
|
||||||
this.USERNAME_KEY,
|
|
||||||
this.COOKIE_OPTIONS.path,
|
|
||||||
this.COOKIE_OPTIONS.domain
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
getToken(): string | null {
|
|
||||||
const token = this.cookieService.get(this.AUTH_TOKEN_KEY);
|
|
||||||
if (token) {
|
|
||||||
const payload = JSON.parse(atob(token.split('.')[1]));
|
|
||||||
const expirationDate = new Date(payload.exp * 1000);
|
|
||||||
if (expirationDate < new Date()) {
|
|
||||||
this.removeToken();
|
|
||||||
this.router.navigate(['/']).then(() => {
|
|
||||||
this.modalService.openModal('login-modal');
|
|
||||||
});
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return token || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
removeToken(): void {
|
|
||||||
this.cookieService.delete(
|
|
||||||
this.AUTH_TOKEN_KEY,
|
|
||||||
this.COOKIE_OPTIONS.path,
|
|
||||||
this.COOKIE_OPTIONS.domain
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
removeUserId(): void {
|
|
||||||
this.cookieService.delete(
|
|
||||||
this.USER_ID,
|
|
||||||
this.COOKIE_OPTIONS.path,
|
|
||||||
this.COOKIE_OPTIONS.domain
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsAdmin(isAdmin: boolean): void {
|
|
||||||
this.cookieService.set(
|
|
||||||
this.IS_ADMIN_KEY,
|
|
||||||
isAdmin.toString(),
|
|
||||||
this.COOKIE_OPTIONS
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
getIsAdmin(): string | null {
|
|
||||||
return this.cookieService.get(this.IS_ADMIN_KEY) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
removeIsAdmin(): void {
|
|
||||||
this.cookieService.delete(
|
|
||||||
this.IS_ADMIN_KEY,
|
|
||||||
this.COOKIE_OPTIONS.path,
|
|
||||||
this.COOKIE_OPTIONS.domain
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
import * as exifr from 'exifr';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class ExifService {
|
|
||||||
async getOrientation(file: File): Promise<number | undefined> {
|
|
||||||
try {
|
|
||||||
return await exifr.orientation(file);
|
|
||||||
} catch (error) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getLocation(
|
|
||||||
file: File
|
|
||||||
): Promise<{ latitude?: number; longitude?: number }> {
|
|
||||||
try {
|
|
||||||
return exifr.gps(file);
|
|
||||||
} catch (error) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getDateTime(file: File): Promise<string> {
|
|
||||||
try {
|
|
||||||
const data = await exifr.parse(file);
|
|
||||||
return data.DateTimeOriginal.toISOString();
|
|
||||||
} catch (error) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,60 +0,0 @@
|
|||||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
|
||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { environment } from '../../../environment';
|
|
||||||
import { AuthService } from '../auth/auth.service';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class FriendsService {
|
|
||||||
private apiURL = environment.apiURL;
|
|
||||||
constructor(private http: HttpClient, private authService: AuthService) {}
|
|
||||||
|
|
||||||
getFriend() {
|
|
||||||
const url = `${this.apiURL}/friends`;
|
|
||||||
const headers = this.authService.getAuthHeaders();
|
|
||||||
headers.set('Content-Type', 'application/json');
|
|
||||||
|
|
||||||
return this.http.get<any[]>(url, { headers });
|
|
||||||
}
|
|
||||||
|
|
||||||
getFriendById(id: string) {
|
|
||||||
const url = `${this.apiURL}/user/${id}`;
|
|
||||||
const headers = this.authService.getAuthHeaders();
|
|
||||||
headers.set('Content-Type', 'application/json');
|
|
||||||
|
|
||||||
return this.http.get<any>(url, { headers });
|
|
||||||
}
|
|
||||||
|
|
||||||
addFriend(user_id: string) {
|
|
||||||
const url = `${this.apiURL}/friend/add`;
|
|
||||||
const headers = this.authService.getAuthHeaders();
|
|
||||||
headers.set('Content-Type', 'application/json');
|
|
||||||
|
|
||||||
return this.http.post<any>(url, { friend_user_id: user_id }, { headers });
|
|
||||||
}
|
|
||||||
|
|
||||||
acceptFriendById(id: string) {
|
|
||||||
const url = `${this.apiURL}/friend/${id}/accept`;
|
|
||||||
const headers = this.authService.getAuthHeaders();
|
|
||||||
headers.set('Content-Type', 'application/json');
|
|
||||||
|
|
||||||
return this.http.patch<any>(url, [], { headers });
|
|
||||||
}
|
|
||||||
|
|
||||||
denyFriendById(id: string) {
|
|
||||||
const url = `${this.apiURL}/friend/${id}/deny`;
|
|
||||||
const headers = this.authService.getAuthHeaders();
|
|
||||||
headers.set('Content-Type', 'application/json');
|
|
||||||
|
|
||||||
return this.http.delete<any>(url, { headers });
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteFriend(id: string) {
|
|
||||||
const url = `${this.apiURL}/friend/${id}/delete`;
|
|
||||||
const headers = this.authService.getAuthHeaders();
|
|
||||||
headers.set('Content-Type', 'application/json');
|
|
||||||
|
|
||||||
return this.http.delete<any>(url, { headers });
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +1,13 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { ImageService } from './image.service';
|
import { GetPinService } from './get-pin.service';
|
||||||
|
|
||||||
describe('ImageService', () => {
|
describe('GetPinService', () => {
|
||||||
let service: ImageService;
|
let service: GetPinService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({});
|
TestBed.configureTestingModule({});
|
||||||
service = TestBed.inject(ImageService);
|
service = TestBed.inject(GetPinService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
@ -0,0 +1,21 @@
|
|||||||
|
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { environment } from '../../environments/environment';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class GetPinService {
|
||||||
|
private apiURL = environment.apiURL;
|
||||||
|
private token = localStorage.getItem('auth_token');
|
||||||
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
|
getPins(): any {
|
||||||
|
const url = `${this.apiURL}/pins`;
|
||||||
|
const headers = new HttpHeaders({
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: 'Bearer ' + this.token,
|
||||||
|
});
|
||||||
|
return this.http.get<any>(url, { headers });
|
||||||
|
}
|
||||||
|
}
|
@ -1,47 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { environment } from '../../../environment';
|
|
||||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
import { AuthService } from '../auth/auth.service';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root'
|
|
||||||
})
|
|
||||||
export class ImageService {
|
|
||||||
private apiUrl = environment.apiURL;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private http: HttpClient,
|
|
||||||
private authService: AuthService
|
|
||||||
) { }
|
|
||||||
|
|
||||||
getImage(imageId: string): Observable<Blob> {
|
|
||||||
const headers = this.authService.getAuthHeaders();
|
|
||||||
|
|
||||||
return this.http.get(`${this.apiUrl}/image/${imageId}`, {
|
|
||||||
headers,
|
|
||||||
responseType: 'blob'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
postImage(image: File, date: string): Observable<any> {
|
|
||||||
let url = `${this.apiUrl}/image/pin/null/add`;
|
|
||||||
|
|
||||||
const headers = this.authService.getAuthHeaders();
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('image', image);
|
|
||||||
|
|
||||||
if(date !== '') {
|
|
||||||
url += `?exif_date=${date}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.http.post(url, formData, { headers });
|
|
||||||
}
|
|
||||||
|
|
||||||
getImageMetadata(imageId: string): Observable<any> {
|
|
||||||
const headers = this.authService.getAuthHeaders();
|
|
||||||
|
|
||||||
return this.http.get(`${this.apiUrl}/image/${imageId}/metadata`, { headers });
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { IntroService } from './intro.service';
|
|
||||||
|
|
||||||
describe('IntroService', () => {
|
|
||||||
let service: IntroService;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
TestBed.configureTestingModule({});
|
|
||||||
service = TestBed.inject(IntroService);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be created', () => {
|
|
||||||
expect(service).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,138 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
import introJs from 'intro.js';
|
|
||||||
import { ModalService } from '../modal/modal.service';
|
|
||||||
import { NavbarService } from '../navbar/navbar.service';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root'
|
|
||||||
})
|
|
||||||
export class IntroService {
|
|
||||||
constructor(
|
|
||||||
private modalService: ModalService,
|
|
||||||
private navbarService: NavbarService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
private sleep(ms: number): Promise<void> {
|
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
|
||||||
}
|
|
||||||
|
|
||||||
async startIntro() {
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
const intro = introJs();
|
|
||||||
intro.setOptions({
|
|
||||||
tooltipClass: 'custom-tooltip-with-avatar',
|
|
||||||
steps: [
|
|
||||||
{ intro: 'Bienvenue sur MemoryMap ! ' },
|
|
||||||
{ intro: 'Ensemble nous allons explorer les différentes fonctionnalités disponibles !'},
|
|
||||||
],
|
|
||||||
exitOnOverlayClick: false,
|
|
||||||
disableInteraction: false,
|
|
||||||
});
|
|
||||||
intro.oncomplete(() => {
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
|
|
||||||
intro.start();
|
|
||||||
});
|
|
||||||
await this.sleep(300);
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
const intro = introJs();
|
|
||||||
intro.setOptions({
|
|
||||||
tooltipClass: 'custom-tooltip-with-avatar',
|
|
||||||
steps: [
|
|
||||||
{ element: '#timeline', intro: 'Ici retrouvez tous vos souvenirs grâce à une frise chronologique de vos voyages!' },
|
|
||||||
{ element: '#quete', intro: "N'hésitez pas à réaliser les différentes quêtes, pour un petit plaisir personnel, que vous pourrez retrouver ici !" },
|
|
||||||
],
|
|
||||||
exitOnOverlayClick: false,
|
|
||||||
disableInteraction: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
intro.onstart(async () => {
|
|
||||||
this.navbarService.onpenNavbar();
|
|
||||||
await this.sleep(100);
|
|
||||||
});
|
|
||||||
|
|
||||||
intro.oncomplete(() => {
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
|
|
||||||
intro.start();
|
|
||||||
});
|
|
||||||
await this.sleep(300);
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
const intro = introJs();
|
|
||||||
|
|
||||||
intro.setOptions({
|
|
||||||
tooltipClass: 'introjs-tooltip custom-tooltip-with-avatar',
|
|
||||||
steps: [
|
|
||||||
{ element: '#add', intro: "Le point important : l'ajout de pin ! <br>Allons voir ensemble comment cela fonctionne." },
|
|
||||||
{ element: '#add-pin-modal-title', intro: 'Ajoutez le titre de votre pin, le lieu du pin par exemple !' },
|
|
||||||
{ element: '#add-pin-modal-image', intro: 'Glissez et déposez toutes les images que vous souhaitez ! <br> <br>TIP : La localisation de la première image sera récupérée automatiquement 😎' },
|
|
||||||
{ element: '#add-pin-modal-localisation', intro: "Si l'adresse n'a pas été récupérée automatiquement, vous pouvez la renseigner manuellement." },
|
|
||||||
{ element: '#add-pin-modal-description', intro: 'Une petite description de votre voyage pour vous souvenir des points importants !' },
|
|
||||||
{ element: '#add-pin-modal-date', intro: 'Ajoutez la date de votre voyage, très important pour la frise chronologique !' },
|
|
||||||
{ element: '#add-pin-modal-validate', intro: "Et voilà vous n'avez plus qu'à valider et ajouter votre pin !" },
|
|
||||||
],
|
|
||||||
exitOnOverlayClick: false,
|
|
||||||
disableInteraction: true
|
|
||||||
});
|
|
||||||
|
|
||||||
intro.onchange(async (element) => {
|
|
||||||
if (element?.id === 'add-pin-modal-title') {
|
|
||||||
this.modalService.openModal('add-pin-modal');
|
|
||||||
await this.sleep(300);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
intro.onexit(() => {
|
|
||||||
this.modalService.closeModal('add-pin-modal');
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
|
|
||||||
intro.oncomplete(() => {
|
|
||||||
this.modalService.closeModal('add-pin-modal');
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
|
|
||||||
intro.start();
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.sleep(300);
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
const intro = introJs();
|
|
||||||
intro.setOptions({
|
|
||||||
tooltipClass: 'custom-tooltip-with-avatar',
|
|
||||||
steps: [
|
|
||||||
{ element: '#friend', intro: "Memory Map, c'est aussi du social. Voyons voir comment ajouter un ami !" },
|
|
||||||
{ element: '#friend-search-bar', intro: "Cherchez votre ami avec son pseudo afin de lui envoyer une demande d'ami !" },
|
|
||||||
{ element: '#friend-list', intro: "Ici vous retrouverez vos amis, ainsi que vos demandes d'amis, acceptez ou refusez les demandes en attente." }
|
|
||||||
],
|
|
||||||
exitOnOverlayClick: false,
|
|
||||||
disableInteraction: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
intro.onchange(async (element) => {
|
|
||||||
if (element?.id === 'friend-search-bar') {
|
|
||||||
this.modalService.openModal('friend-modal');
|
|
||||||
await this.sleep(300);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
intro.onexit(() => {
|
|
||||||
this.modalService.closeModal('friend-modal');
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
|
|
||||||
intro.oncomplete(() => {
|
|
||||||
this.modalService.closeModal('friend-modal');
|
|
||||||
this.navbarService.closeNavbar();
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
|
|
||||||
intro.start();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,23 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class LocalStorageService {
|
||||||
|
|
||||||
|
private readonly AUTH_TOKEN_KEY = 'auth_token';
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
setToken(token: string): void {
|
||||||
|
localStorage.setItem(this.AUTH_TOKEN_KEY, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
getToken(): string | null {
|
||||||
|
return localStorage.getItem(this.AUTH_TOKEN_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeToken(): void {
|
||||||
|
localStorage.removeItem(this.AUTH_TOKEN_KEY);
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +1,13 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { AuthService } from './auth.service';
|
import { LoginService } from './login.service';
|
||||||
|
|
||||||
describe('AuthService', () => {
|
describe('LoginService', () => {
|
||||||
let service: AuthService;
|
let service: LoginService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({});
|
TestBed.configureTestingModule({});
|
||||||
service = TestBed.inject(AuthService);
|
service = TestBed.inject(LoginService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
@ -0,0 +1,21 @@
|
|||||||
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { environment } from '../../environments/environment';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class LoginService {
|
||||||
|
private apiUrl = environment.apiURL;
|
||||||
|
|
||||||
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
|
login(username: string, password: string): Observable<any> {
|
||||||
|
const payload = new HttpParams()
|
||||||
|
.set('username', username)
|
||||||
|
.set('password', password);
|
||||||
|
|
||||||
|
return this.http.post(this.apiUrl + '/login', payload);
|
||||||
|
}
|
||||||
|
}
|
@ -1,16 +0,0 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { MapReloadService } from './map-reload.service';
|
|
||||||
|
|
||||||
describe('MapReloadService', () => {
|
|
||||||
let service: MapReloadService;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
TestBed.configureTestingModule({});
|
|
||||||
service = TestBed.inject(MapReloadService);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be created', () => {
|
|
||||||
expect(service).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,14 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { Subject } from 'rxjs';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class MapReloadService {
|
|
||||||
private reloadSubject = new Subject<void>();
|
|
||||||
public reload$ = this.reloadSubject.asObservable();
|
|
||||||
|
|
||||||
requestReload(): void {
|
|
||||||
this.reloadSubject.next();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { ModalService } from './modal.service';
|
|
||||||
|
|
||||||
describe('ModalService', () => {
|
|
||||||
let service: ModalService;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
TestBed.configureTestingModule({});
|
|
||||||
service = TestBed.inject(ModalService);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be created', () => {
|
|
||||||
expect(service).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,41 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { BehaviorSubject } from 'rxjs';
|
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
|
||||||
export class ModalService {
|
|
||||||
private modals: Map<string, BehaviorSubject<boolean>> = new Map();
|
|
||||||
private imageFilesSubject = new BehaviorSubject<File[] | null>(null);
|
|
||||||
private formDataSubject = new BehaviorSubject<any>(null);
|
|
||||||
|
|
||||||
getModalState(id: string): BehaviorSubject<boolean> {
|
|
||||||
if (!this.modals.has(id)) {
|
|
||||||
this.modals.set(id, new BehaviorSubject<boolean>(false));
|
|
||||||
}
|
|
||||||
return this.modals.get(id)!;
|
|
||||||
}
|
|
||||||
|
|
||||||
openModal(id: string, images?: File[], formData?: any) {
|
|
||||||
if (images) {
|
|
||||||
this.imageFilesSubject.next(images);
|
|
||||||
}
|
|
||||||
if (formData) {
|
|
||||||
this.formDataSubject.next(formData);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.getModalState(id).next(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
closeModal(id: string) {
|
|
||||||
this.getModalState(id).next(false);
|
|
||||||
this.imageFilesSubject.next(null);
|
|
||||||
this.formDataSubject.next(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
getImageFiles(): BehaviorSubject<File[] | null> {
|
|
||||||
return this.imageFilesSubject;
|
|
||||||
}
|
|
||||||
|
|
||||||
getFormData(): BehaviorSubject<any> {
|
|
||||||
return this.formDataSubject;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { BehaviorSubject } from 'rxjs';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root'
|
|
||||||
})
|
|
||||||
export class NavbarService {
|
|
||||||
private isSearchOpenSubject = new BehaviorSubject<boolean>(false);
|
|
||||||
private isNavbarOpenSubject = new BehaviorSubject<boolean>(false);
|
|
||||||
|
|
||||||
isSearchOpen$ = this.isSearchOpenSubject.asObservable();
|
|
||||||
isNavbarOpen$ = this.isNavbarOpenSubject.asObservable();
|
|
||||||
|
|
||||||
toggleSearch(): void {
|
|
||||||
this.isSearchOpenSubject.next(!this.isSearchOpenSubject.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleNavbar(): void {
|
|
||||||
this.isNavbarOpenSubject.next(!this.isNavbarOpenSubject.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
onpenNavbar(): void {
|
|
||||||
this.isNavbarOpenSubject.next(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
closeNavbar(): void {
|
|
||||||
this.isNavbarOpenSubject.next(false);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { PinService } from './pin.service';
|
|
||||||
|
|
||||||
describe('PinService', () => {
|
|
||||||
let service: PinService;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
TestBed.configureTestingModule({});
|
|
||||||
service = TestBed.inject(PinService);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be created', () => {
|
|
||||||
expect(service).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,13 +1,13 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { FriendsService } from './friends.service';
|
import { RegisterService } from './register.service';
|
||||||
|
|
||||||
describe('FriendsService', () => {
|
describe('RegisterService', () => {
|
||||||
let service: FriendsService;
|
let service: RegisterService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({});
|
TestBed.configureTestingModule({});
|
||||||
service = TestBed.inject(FriendsService);
|
service = TestBed.inject(RegisterService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
@ -0,0 +1,17 @@
|
|||||||
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { environment } from '../../environments/environment';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class RegisterService {
|
||||||
|
private apiUrl = environment.apiURL;
|
||||||
|
|
||||||
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
|
register(username: string, password: string): Observable<any> {
|
||||||
|
return this.http.post(this.apiUrl + '/register', { username, password });
|
||||||
|
}
|
||||||
|
}
|
@ -1,16 +0,0 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { UserService } from './user.service';
|
|
||||||
|
|
||||||
describe('UserService', () => {
|
|
||||||
let service: UserService;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
TestBed.configureTestingModule({});
|
|
||||||
service = TestBed.inject(UserService);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be created', () => {
|
|
||||||
expect(service).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|