Image Upload, spinner aktivirt, listings & details überarbeitet
This commit is contained in:
parent
2b27ab8ba5
commit
fd91adda57
|
|
@ -42,6 +42,7 @@
|
|||
"redis-om": "^0.4.3",
|
||||
"reflect-metadata": "^0.2.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"sharp": "^0.33.2",
|
||||
"urlcat": "^3.1.0",
|
||||
"winston": "^3.11.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
import { Controller, Param, Post, UploadedFile, UseInterceptors } from '@nestjs/common';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
|
||||
@Controller('account')
|
||||
export class AccountController {
|
||||
constructor(private fileService:FileService){}
|
||||
|
||||
@Post('uploadProfile/:id')
|
||||
@UseInterceptors(FileInterceptor('file'),)
|
||||
uploadProfile(@UploadedFile() file: Express.Multer.File,@Param('id') id:string) {
|
||||
this.fileService.storeProfilePicture(file,id);
|
||||
}
|
||||
@Post('uploadCompanyLogo/:id')
|
||||
@UseInterceptors(FileInterceptor('file'),)
|
||||
uploadCompanyLogo(@UploadedFile() file: Express.Multer.File,@Param('id') id:string) {
|
||||
this.fileService.storeCompanyLogo(file,id);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { AccountController } from './account.controller.js';
|
||||
import { AccountService } from './account.service.js';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
|
||||
@Module({
|
||||
controllers: [AccountController],
|
||||
providers: [AccountService,FileService]
|
||||
})
|
||||
export class AccountModule {}
|
||||
|
|
@ -10,8 +10,6 @@ import { SelectOptionsService } from './select-options/select-options.service.js
|
|||
import { SubscriptionsController } from './subscriptions/subscriptions.controller.js';
|
||||
import { RedisModule } from './redis/redis.module.js';
|
||||
import { ListingsService } from './listings/listings.service.js';
|
||||
import { AccountController } from './account/account.controller.js';
|
||||
import { AccountService } from './account/account.service.js';
|
||||
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||
import path, { join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
|
@ -22,9 +20,9 @@ import { AuthModule } from './auth/auth.module.js';
|
|||
import { GeoModule } from './geo/geo.module.js';
|
||||
import { UserModule } from './user/user.module.js';
|
||||
import { ListingsModule } from './listings/listings.module.js';
|
||||
import { AccountModule } from './account/account.module.js';
|
||||
import { SelectOptionsModule } from './select-options/select-options.module.js';
|
||||
import { CommercialPropertyListingsController } from './listings/commercial-property-listings.controller.js';
|
||||
import { ImageModule } from './image/image.module.js';
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
|
|
@ -52,9 +50,9 @@ const __dirname = path.dirname(__filename);
|
|||
GeoModule,
|
||||
UserModule,
|
||||
ListingsModule,
|
||||
AccountModule,
|
||||
SelectOptionsModule,
|
||||
RedisModule
|
||||
RedisModule,
|
||||
ImageModule
|
||||
],
|
||||
controllers: [AppController, SubscriptionsController],
|
||||
providers: [AppService, FileService],
|
||||
|
|
|
|||
|
|
@ -1,18 +1,20 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { fstat, readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import { ImageProperty } from 'src/models/main.model.js';
|
||||
|
||||
import { ImageProperty } from '../models/main.model.js';
|
||||
import sharp from 'sharp';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { Logger } from 'winston';
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
@Injectable()
|
||||
export class FileService {
|
||||
private subscriptions: any;
|
||||
constructor() {
|
||||
constructor(@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {
|
||||
this.loadSubscriptions();
|
||||
fs.ensureDirSync(`./pictures`);
|
||||
fs.ensureDirSync(`./pictures/profile`);
|
||||
|
|
@ -20,42 +22,60 @@ export class FileService {
|
|||
fs.ensureDirSync(`./pictures/property`);
|
||||
}
|
||||
private loadSubscriptions(): void {
|
||||
const filePath = join(__dirname,'..', 'assets', 'subscriptions.json');
|
||||
const filePath = join(__dirname, '..', 'assets', 'subscriptions.json');
|
||||
const rawData = readFileSync(filePath, 'utf8');
|
||||
this.subscriptions = JSON.parse(rawData);
|
||||
}
|
||||
getSubscriptions() {
|
||||
return this.subscriptions
|
||||
}
|
||||
async storeProfilePicture(file: Express.Multer.File,userId: string){
|
||||
const suffix = file.mimetype.includes('png')?'png':'jpg'
|
||||
await fs.outputFile(`./pictures/profile/${userId}`,file.buffer);
|
||||
async storeProfilePicture(file: Express.Multer.File, userId: string) {
|
||||
const suffix = file.mimetype.includes('png') ? 'png' : 'jpg'
|
||||
await fs.outputFile(`./pictures/profile/${userId}`, file.buffer);
|
||||
}
|
||||
async storeCompanyLogo(file: Express.Multer.File,userId: string){
|
||||
const suffix = file.mimetype.includes('png')?'png':'jpg'
|
||||
await fs.outputFile(`./pictures/logo/${userId}`,file.buffer);
|
||||
hasProfile(userId: string){
|
||||
return fs.existsSync(`./pictures/profile/${userId}`)
|
||||
}
|
||||
async getPropertyImages(listingId: string):Promise<ImageProperty[]>{
|
||||
const result:ImageProperty[]=[]
|
||||
|
||||
async storeCompanyLogo(file: Express.Multer.File, userId: string) {
|
||||
const suffix = file.mimetype.includes('png') ? 'png' : 'jpg'
|
||||
await fs.outputFile(`./pictures/logo/${userId}`, file.buffer);
|
||||
}
|
||||
hasCompanyLogo(userId: string){
|
||||
return fs.existsSync(`./pictures/logo/${userId}`)
|
||||
}
|
||||
|
||||
async getPropertyImages(listingId: string): Promise<ImageProperty[]> {
|
||||
const result: ImageProperty[] = []
|
||||
const directory = `./pictures/property/${listingId}`
|
||||
if (fs.existsSync(directory)){
|
||||
if (fs.existsSync(directory)) {
|
||||
const files = await fs.readdir(directory);
|
||||
files.forEach(f=>{
|
||||
const image:ImageProperty={name:f,id:'',code:''};
|
||||
files.forEach(f => {
|
||||
const image: ImageProperty = { name: f, id: '', code: '' };
|
||||
result.push(image)
|
||||
})
|
||||
return result;
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
|
||||
}
|
||||
async storePropertyPicture(file: Express.Multer.File,listingId: string){
|
||||
const suffix = file.mimetype.includes('png')?'png':'jpg'
|
||||
async hasPropertyImages(listingId: string): Promise<boolean> {
|
||||
const result: ImageProperty[] = []
|
||||
const directory = `./pictures/property/${listingId}`
|
||||
if (fs.existsSync(directory)) {
|
||||
const files = await fs.readdir(directory);
|
||||
return files.length>0
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
async storePropertyPicture(file: Express.Multer.File, listingId: string) {
|
||||
const suffix = file.mimetype.includes('png') ? 'png' : 'jpg'
|
||||
const directory = `./pictures/property/${listingId}`
|
||||
fs.ensureDirSync(`${directory}`);
|
||||
const imageName = await this.getNextImageName(directory);
|
||||
await fs.outputFile(`${directory}/${imageName}`,file.buffer);
|
||||
//await fs.outputFile(`${directory}/${imageName}`, file.buffer);
|
||||
await this.resizeImageToAVIF(file.buffer,150 * 1024,imageName,directory);
|
||||
}
|
||||
async getNextImageName(directory) {
|
||||
try {
|
||||
|
|
@ -72,4 +92,24 @@ export class FileService {
|
|||
return null;
|
||||
}
|
||||
}
|
||||
async resizeImageToAVIF(buffer: Buffer, maxSize: number,imageName:string,directory:string) {
|
||||
let quality = 50; // AVIF kann mit niedrigeren Qualitätsstufen gute Ergebnisse erzielen
|
||||
let output;
|
||||
let start = Date.now();
|
||||
do {
|
||||
output = await sharp(buffer)
|
||||
.resize({ width: 1500 })
|
||||
.avif({ quality }) // Verwende AVIF
|
||||
//.webp({ quality }) // Verwende Webp
|
||||
.toBuffer();
|
||||
|
||||
if (output.byteLength > maxSize) {
|
||||
quality -= 5; // Justiere Qualität in feineren Schritten
|
||||
}
|
||||
} while (output.byteLength > maxSize && quality > 0);
|
||||
let timeTaken = Date.now() - start;
|
||||
this.logger.info(`Quality: ${quality} - Time: ${timeTaken} milliseconds`)
|
||||
await sharp(output).toFile(`${directory}/${imageName}.avif`); // Ersetze Dateierweiterung
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
import { Body, Controller, Delete, Get, Inject, Param, Post, UploadedFile, UseInterceptors } from '@nestjs/common';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { Logger } from 'winston';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
|
||||
@Controller('image')
|
||||
export class ImageController {
|
||||
|
||||
constructor(private fileService:FileService,@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {
|
||||
}
|
||||
|
||||
@Post('uploadPropertyPicture/:id')
|
||||
@UseInterceptors(FileInterceptor('file'),)
|
||||
async uploadFile(@UploadedFile() file: Express.Multer.File,@Param('id') id:string) {
|
||||
await this.fileService.storePropertyPicture(file,id);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
async getPropertyImagesById(@Param('id') id:string): Promise<any> {
|
||||
return await this.fileService.getPropertyImages(id);
|
||||
}
|
||||
|
||||
@Post('uploadProfile/:id')
|
||||
@UseInterceptors(FileInterceptor('file'),)
|
||||
uploadProfile(@UploadedFile() file: Express.Multer.File,@Param('id') id:string) {
|
||||
this.fileService.storeProfilePicture(file,id);
|
||||
}
|
||||
|
||||
@Post('uploadCompanyLogo/:id')
|
||||
@UseInterceptors(FileInterceptor('file'),)
|
||||
uploadCompanyLogo(@UploadedFile() file: Express.Multer.File,@Param('id') id:string) {
|
||||
this.fileService.storeCompanyLogo(file,id);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { ImageController } from './image.controller.js';
|
||||
import { ImageService } from './image.service.js';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
|
||||
@Module({
|
||||
controllers: [ImageController],
|
||||
providers: [ImageService,FileService]
|
||||
})
|
||||
export class ImageModule {}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class AccountService {}
|
||||
export class ImageService {}
|
||||
|
|
@ -11,10 +11,6 @@ export class CommercialPropertyListingsController {
|
|||
constructor(private readonly listingsService:ListingsService,private fileService:FileService,@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {
|
||||
}
|
||||
|
||||
@Get()
|
||||
findAll(): any {
|
||||
return this.listingsService.getAllCommercialListings();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
findById(@Param('id') id:string): any {
|
||||
|
|
@ -43,14 +39,5 @@ export class CommercialPropertyListingsController {
|
|||
this.listingsService.deleteCommercialPropertyListing(id)
|
||||
}
|
||||
|
||||
@Post('uploadPropertyPicture/:id')
|
||||
@UseInterceptors(FileInterceptor('file'),)
|
||||
uploadFile(@UploadedFile() file: Express.Multer.File,@Param('id') id:string) {
|
||||
this.fileService.storePropertyPicture(file,id);
|
||||
}
|
||||
|
||||
@Get('images/:id')
|
||||
getPropertyImagesById(@Param('id') id:string): any {
|
||||
return this.fileService.getPropertyImages(id);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,8 +95,6 @@ export class ListingsService {
|
|||
async getAllCommercialListings(start?: number, end?: number) {
|
||||
return await this.commercialPropertyListingRepository.search().return.all()
|
||||
}
|
||||
|
||||
|
||||
async findBusinessListings(criteria:ListingCriteria): Promise<any> {
|
||||
let listings = await this.getAllBusinessListings();
|
||||
return this.find(criteria,listings);
|
||||
|
|
|
|||
|
|
@ -20,9 +20,9 @@ export class UserController {
|
|||
return this.userService.saveUser(user);
|
||||
}
|
||||
|
||||
// @Put()
|
||||
// update(@Body() user: any):Promise<UserEntity>{
|
||||
// this.logger.info(`update User`);
|
||||
// return this.userService.saveUser(user);
|
||||
// }
|
||||
@Post('search')
|
||||
find(@Body() criteria: any): any {
|
||||
return this.userService.findUser(criteria);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,12 @@ import { Module } from '@nestjs/common';
|
|||
import { UserController } from './user.controller.js';
|
||||
import { UserService } from './user.service.js';
|
||||
import { RedisModule } from '../redis/redis.module.js';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
|
||||
@Module({
|
||||
imports: [RedisModule],
|
||||
controllers: [UserController],
|
||||
providers: [UserService]
|
||||
providers: [UserService,FileService]
|
||||
})
|
||||
export class UserModule {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { Entity, Repository, Schema } from 'redis-om';
|
|||
import { ListingCriteria, User } from '../models/main.model.js';
|
||||
import { REDIS_CLIENT } from '../redis/redis.module.js';
|
||||
import { UserEntity } from '../models/server.model.js';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
|
|
@ -24,12 +25,15 @@ export class UserService {
|
|||
}, {
|
||||
dataStructure: 'JSON'
|
||||
})
|
||||
constructor(@Inject(REDIS_CLIENT) private readonly redis: any){
|
||||
constructor(@Inject(REDIS_CLIENT) private readonly redis: any,private fileService:FileService){
|
||||
this.userRepository = new Repository(this.userSchema, redis)
|
||||
this.userRepository.createIndex();
|
||||
}
|
||||
async getUserById( id:string){
|
||||
return await this.userRepository.fetch(id);
|
||||
const user = await this.userRepository.fetch(id) as UserEntity;
|
||||
user.hasCompanyLogo=this.fileService.hasCompanyLogo(id);
|
||||
user.hasProfile=this.fileService.hasProfile(id);
|
||||
return user;
|
||||
}
|
||||
async saveUser(user:any):Promise<UserEntity>{
|
||||
return await this.userRepository.save(user.id,user) as UserEntity
|
||||
|
|
|
|||
|
|
@ -4,11 +4,20 @@
|
|||
<header></header>
|
||||
}
|
||||
<router-outlet></router-outlet>
|
||||
@if (loadingService.isLoading$ | async) {
|
||||
<div class="progress-spinner flex h-full align-items-center justify-content-center">
|
||||
<p-progressSpinner></p-progressSpinner>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<footer></footer>
|
||||
</div>
|
||||
<!-- @if (loadingService.isLoading$ | async) { -->
|
||||
<!-- <div class="progress-spinner flex h-full align-items-center justify-content-center">
|
||||
<div class="spinner-text">Please wait - we're processing your image...</div>
|
||||
<p-progressSpinner></p-progressSpinner>
|
||||
</div> -->
|
||||
<!-- } -->
|
||||
@if (loadingService.isLoading$ | async) {
|
||||
<div class="spinner-overlay">
|
||||
<div class="spinner-container">
|
||||
<p-progressSpinner></p-progressSpinner>
|
||||
<div class="spinner-text" *ngIf="loadingService.loadingText$ | async as loadingText">{{loadingText}}</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
|
@ -8,3 +8,57 @@
|
|||
/* Optional: Padding für den Inhalt, um sicherzustellen, dass er nicht direkt am Footer klebt */
|
||||
// padding-bottom: 20px;
|
||||
}
|
||||
.progress-spinner {
|
||||
position: fixed;
|
||||
z-index: 999;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.progress-spinner:before {
|
||||
content: '';
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.spinner-text {
|
||||
margin-top: 20px; /* Abstand zwischen Spinner und Text anpassen */
|
||||
font-size: 20px; /* Schriftgröße nach Bedarf anpassen */
|
||||
color: #FFF;
|
||||
text-shadow: 0 0 8px rgba(255, 255, 255, 0.6); /* Hinzufügen eines leichten Glows */
|
||||
font-weight: bold; /* Macht den Text fett */
|
||||
}
|
||||
.spinner-overlay {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: fixed; /* oder 'absolute', abhängig vom Kontext */
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
z-index: 1000; /* Stellt sicher, dass der Overlay über anderen Elementen liegt */
|
||||
}
|
||||
|
||||
.spinner-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
/* Keine Hintergrundfarbe hier, um Transparenz nur im Overlay zu haben */
|
||||
}
|
||||
|
||||
// .spinner-text {
|
||||
// margin-top: 10px; /* Abstand zwischen Spinner und Text anpassen */
|
||||
// font-size: 16px; /* Schriftgröße nach Bedarf anpassen */
|
||||
// color: #FFF; /* Schriftfarbe für bessere Lesbarkeit auf dunklem Hintergrund */
|
||||
// }
|
||||
|
|
@ -39,17 +39,6 @@ export class AppComponent {
|
|||
// Hier haben Sie Zugriff auf den aktuellen Route-Pfad
|
||||
this.actualRoute=currentRoute.snapshot.url[0].path
|
||||
});
|
||||
|
||||
// keycloakService.keycloakEvents$.subscribe({
|
||||
// next(event) {
|
||||
// if (event.type == KeycloakEventType.OnTokenExpired) {
|
||||
// keycloakService.updateToken(20);
|
||||
// }
|
||||
// if (event.type == KeycloakEventType.OnActionUpdate) {
|
||||
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
}
|
||||
ngOnInit(){
|
||||
this.user = this.userService.getUser();
|
||||
|
|
|
|||
|
|
@ -4,11 +4,12 @@ import { provideRouter } from '@angular/router';
|
|||
import { routes } from './app.routes';
|
||||
import { provideClientHydration } from '@angular/platform-browser';
|
||||
import { provideAnimations } from '@angular/platform-browser/animations';
|
||||
import { HTTP_INTERCEPTORS, provideHttpClient, withFetch, withInterceptorsFromDi } from '@angular/common/http';
|
||||
import { HTTP_INTERCEPTORS, provideHttpClient, withFetch, withInterceptors, withInterceptorsFromDi } from '@angular/common/http';
|
||||
import { environment } from '../environments/environment';
|
||||
import { SelectOptionsService } from './services/select-options.service';
|
||||
import { KeycloakService } from './services/keycloak.service';
|
||||
import { UserService } from './services/user.service';
|
||||
import { LoadingInterceptor } from './interceptors/loading.interceptor';
|
||||
// provideClientHydration()
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
|
|
@ -26,6 +27,11 @@ export const appConfig: ApplicationConfig = {
|
|||
multi: true,
|
||||
deps: [SelectOptionsService],
|
||||
},
|
||||
{
|
||||
provide:HTTP_INTERCEPTORS,
|
||||
useClass:LoadingInterceptor,
|
||||
multi:true
|
||||
},
|
||||
provideRouter(routes),provideAnimations(),
|
||||
// {provide: LOCALE_ID, useValue: 'en-US' }
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,21 +1,39 @@
|
|||
import { HttpInterceptorFn } from '@angular/common/http';
|
||||
import { inject } from '@angular/core';
|
||||
import { tap } from 'rxjs';
|
||||
import { HttpEvent, HttpHandler, HttpInterceptor, HttpInterceptorFn, HttpRequest } from '@angular/common/http';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { Observable, tap } from 'rxjs';
|
||||
import { v4 } from 'uuid';
|
||||
import { LoadingService } from '../services/loading.service';
|
||||
|
||||
export const loadingInterceptor: HttpInterceptorFn = (req, next) => {
|
||||
const loadingService = inject(LoadingService);
|
||||
// export const loadingInterceptor: HttpInterceptorFn = (req, next) => {
|
||||
// const loadingService = inject(LoadingService);
|
||||
|
||||
// const requestId = `HTTP-${v4()}`;
|
||||
|
||||
// loadingService.startLoading(requestId);
|
||||
|
||||
// return next(req).pipe(
|
||||
// tap({
|
||||
// finalize: () => loadingService.stopLoading(requestId),
|
||||
// error: () => loadingService.stopLoading(requestId),
|
||||
// complete: () => loadingService.stopLoading(requestId),
|
||||
// })
|
||||
// );
|
||||
// };
|
||||
@Injectable()
|
||||
export class LoadingInterceptor implements HttpInterceptor {
|
||||
constructor(private loadingService:LoadingService) { }
|
||||
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
|
||||
console.log("Intercepting Requests")
|
||||
const requestId = `HTTP-${v4()}`;
|
||||
this.loadingService.startLoading(requestId,request.url);
|
||||
|
||||
loadingService.startLoading(requestId);
|
||||
|
||||
return next(req).pipe(
|
||||
// return next.handle(request);
|
||||
return next.handle(request).pipe(
|
||||
tap({
|
||||
finalize: () => loadingService.stopLoading(requestId),
|
||||
error: () => loadingService.stopLoading(requestId),
|
||||
complete: () => loadingService.stopLoading(requestId),
|
||||
finalize: () => this.loadingService.stopLoading(requestId), // Stoppt den Ladevorgang, wenn die Anfrage abgeschlossen ist
|
||||
// Beachte, dass 'error' und 'complete' hier entfernt wurden, da 'finalize' in allen Fällen aufgerufen wird,
|
||||
// egal ob die Anfrage erfolgreich war, einen Fehler geworfen hat oder abgeschlossen wurde.
|
||||
})
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -7,22 +7,11 @@
|
|||
<p-button icon="pi pi-times" [rounded]="true" severity="danger" (click)="back()"></p-button>
|
||||
</div>
|
||||
<!-- <div class="text-500 mb-5">Egestas sed tempus urna et pharetra pharetra massa massa ultricies.</div> -->
|
||||
@if (listing){
|
||||
<div class="grid">
|
||||
<div class="col-12 md:col-6">
|
||||
<ul class="list-none p-0 m-0 border-top-1 border-300">
|
||||
<!-- @if (listing && (listing.listingsCategory==='business')){
|
||||
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
||||
<div class="text-500 w-full md:w-2 font-medium">Summary</div>
|
||||
<div class="w-full md:w-10">
|
||||
@for (summary of listing.summary; track summary; let idx = $index; let last = $last) {
|
||||
<div class="text-900">{{summary}}</div>
|
||||
@if (!last) {
|
||||
<br/>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</li>
|
||||
} -->
|
||||
|
||||
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
||||
<div class="text-500 w-full md:w-2 font-medium">Description</div>
|
||||
<div class="text-900 w-full md:w-10 line-height-3">{{listing?.description}}</div>
|
||||
|
|
@ -63,47 +52,43 @@
|
|||
<div class="text-900 w-full md:w-10">{{listing.brokerLicencing}}</div>
|
||||
</li>
|
||||
}
|
||||
<!-- @if (listing && (listing.listingsCategory==='professionals_brokers')){
|
||||
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
||||
<div class="text-500 w-full md:w-2 font-medium">Located in</div>
|
||||
<div class="text-900 w-full md:w-10">{{selectOptions.getLocation(listing.location)}}</div>
|
||||
</li>
|
||||
<li class="flex align-items-center py-3 px-2 flex-wrap ">
|
||||
<div class="text-500 w-full md:w-2 font-medium">Address</div>
|
||||
<div class="text-900 w-full md:w-10">{{listing.address}}</div>
|
||||
</li>
|
||||
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
||||
<div class="text-500 w-full md:w-2 font-medium">EMail</div>
|
||||
<div class="text-900 w-full md:w-10">{{listing.email}}</div>
|
||||
</li>
|
||||
<li class="flex align-items-center py-3 px-2 flex-wrap ">
|
||||
<div class="text-500 w-full md:w-2 font-medium">Website</div>
|
||||
<div class="text-900 w-full md:w-10">{{listing.website}}</div>
|
||||
</li>
|
||||
<li class="flex align-items-center py-3 px-2 flex-wrap ">
|
||||
<div class="text-500 w-full md:w-2 font-medium">Category</div>
|
||||
<div class="text-900 w-full md:w-10">{{listing.category}}</div>
|
||||
</li>
|
||||
} -->
|
||||
@if (listing && (listing.listingsCategory==='commercialProperty')){
|
||||
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
||||
<div class="text-500 w-full md:w-2 font-medium">Property Category</div>
|
||||
<div class="text-900 w-full md:w-10">{{selectOptions.getCommercialProperty(listing.type)}}</div>
|
||||
</li>
|
||||
<li class="flex align-items-center py-3 px-2 flex-wrap ">
|
||||
<div class="text-500 w-full md:w-2 font-medium">Located in</div>
|
||||
<div class="text-900 w-full md:w-10">{{selectOptions.getState(listing.state)}}</div>
|
||||
</li>
|
||||
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
||||
<div class="text-500 w-full md:w-2 font-medium">City</div>
|
||||
<div class="text-900 w-full md:w-10">{{listing.city}}</div>
|
||||
</li>
|
||||
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
||||
<div class="text-500 w-full md:w-2 font-medium">EMail</div>
|
||||
<div class="text-900 w-full md:w-10">{{listing.email}}</div>
|
||||
<div class="text-500 w-full md:w-2 font-medium">Zip Code</div>
|
||||
<div class="text-900 w-full md:w-10">{{listing.zipCode}}</div>
|
||||
</li>
|
||||
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
||||
<div class="text-500 w-full md:w-2 font-medium">Website</div>
|
||||
<div class="text-900 w-full md:w-10">{{listing.website}}</div>
|
||||
<div class="text-500 w-full md:w-2 font-medium">County</div>
|
||||
<div class="text-900 w-full md:w-10">{{listing.county}}</div>
|
||||
</li>
|
||||
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
||||
<div class="text-500 w-full md:w-2 font-medium">Phone Number</div>
|
||||
<div class="text-900 w-full md:w-10">{{listing.phoneNumber}}</div>
|
||||
<div class="text-500 w-full md:w-2 font-medium">Asking Price:</div>
|
||||
<div class="text-900 w-full md:w-10">{{listing.price | currency}}</div>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
<p-galleria [value]="propertyImages" [showIndicators]="true" [showThumbnails]="false" [responsiveOptions]="responsiveOptions" [containerStyle]="{ 'max-width': '640px' }" [numVisible]="5">
|
||||
<ng-template pTemplate="item" let-item>
|
||||
<img src="{{environment.apiBaseUrl}}/property/{{listing.id}}/{{item.name}}" style="width: 100%;" />
|
||||
</ng-template>
|
||||
<!-- <ng-template pTemplate="thumbnail" let-item>
|
||||
<div class="grid grid-nogutter justify-content-center">
|
||||
<img src="{{environment.apiBaseUrl}}/property/{{listing.id}}/{{item.name}}" />
|
||||
</div>
|
||||
</ng-template> -->
|
||||
</p-galleria>
|
||||
@if(listing && user && (user.id===listing?.userId || isAdmin())){
|
||||
<button pButton pRipple label="Edit" icon="pi pi-file-edit" class="w-auto" [routerLink]="['/editListing',listing.id]"></button>
|
||||
}
|
||||
|
|
@ -141,6 +126,118 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="surface-section px-6 py-5">
|
||||
<div class="flex align-items-start flex-column lg:flex-row lg:justify-content-between">
|
||||
<div class="flex align-items-start flex-column md:flex-row">
|
||||
<img src="{{environment.apiBaseUrl}}/profile/{{user.id}}" class="mr-5 mb-3 lg:mb-0" style="width:90px;height:90px" />
|
||||
<div>
|
||||
<span class="text-900 font-medium text-3xl">{{user.firstname}} {{user.lastname}}</span>
|
||||
<i class="pi pi-star text-2xl ml-4 text-yellow-500"></i>
|
||||
<div class="flex align-items-center flex-wrap text-sm">
|
||||
<div class="mr-5 mt-3">
|
||||
<span class="font-medium text-500">Company</span>
|
||||
<div class="text-700 mt-2">{{user.companyName}}</div>
|
||||
</div>
|
||||
<div class="mr-5 mt-3">
|
||||
<span class="font-medium text-500">For Sale</span>
|
||||
<div class="text-700 mt-2">12</div>
|
||||
</div>
|
||||
<div class="mr-5 mt-3">
|
||||
<span class="font-medium text-500">Sold</span>
|
||||
<div class="text-700 mt-2">8</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<span class="font-medium text-500">Logo</span>
|
||||
<div class="mt-2"><img src="{{environment.apiBaseUrl}}/logo/{{user.id}}" class="mr-5 mb-3 lg:mb-0" style="width:100px;height:30px" /></div>
|
||||
<!-- <div class="text-700 mt-2">130</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="mt-3 lg:mt-0">
|
||||
<button pButton pRipple icon="pi pi-bookmark" class="p-button-rounded mr-2"></button>
|
||||
<button pButton pRipple icon="pi pi-heart" class="p-button-rounded p-button-success mr-2"></button>
|
||||
<button pButton pRipple icon="pi pi-list" class="p-button-rounded p-button-help"></button>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-6 py-5">
|
||||
<div class="surface-card p-4 shadow-2 border-round">
|
||||
<div class="font-medium text-3xl text-900 mb-3">Company Profile</div>
|
||||
<div class="text-500 mb-5">{{user.companyOverview}}</div>
|
||||
<ul class="list-none p-0 m-0 border-top-1 border-300">
|
||||
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
||||
<div class="text-500 w-full md:w-2 font-medium">Name</div>
|
||||
<div class="text-900 w-full md:w-10">{{user.firstname}} {{user.lastname}}</div>
|
||||
</li>
|
||||
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
||||
<div class="text-500 w-full md:w-2 font-medium">Description</div>
|
||||
<div class="text-900 w-full md:w-10 line-height-3">{{user.description}}</div>
|
||||
</li>
|
||||
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
||||
<div class="text-500 w-full md:w-2 font-medium">Services we offer</div>
|
||||
<div class="text-900 w-full md:w-10">{{user.offeredServices}}</div>
|
||||
</li>
|
||||
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
||||
<div class="text-500 w-full md:w-2 font-medium">Areas we serve</div>
|
||||
<div class="text-900 w-full md:w-10">
|
||||
@for (area of user.areasServed; track area) {
|
||||
<p-tag styleClass="mr-2" [value]="area" [rounded]="true"></p-tag>
|
||||
}
|
||||
<!-- <p-tag styleClass="mr-2" severity="success" value="Javascript" [rounded]="true"></p-tag>
|
||||
<p-tag styleClass="mr-2" severity="danger" value="Python" [rounded]="true"></p-tag>
|
||||
<p-tag severity="warning" value="SQL" [rounded]="true"></p-tag> -->
|
||||
</div>
|
||||
</li>
|
||||
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
||||
<div class="text-500 w-full md:w-2 font-medium">My Listings For Sale</div>
|
||||
<div class="text-900 w-full md:w-10">
|
||||
<div class="grid mt-0 mr-0">
|
||||
<div class="col-12 md:col-6">
|
||||
<div class="p-3 border-1 surface-border border-round surface-card">
|
||||
<div class="text-900 mb-2">
|
||||
<i class="pi pi-github mr-2"></i>
|
||||
<span class="font-medium">PrimeFaces</span>
|
||||
</div>
|
||||
<div class="text-700">Ultimate UI Component Suite for JavaServer Faces</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 md:col-6">
|
||||
<div class="p-3 border-1 surface-border border-round surface-card">
|
||||
<div class="text-900 mb-2">
|
||||
<i class="pi pi-github mr-2"></i>
|
||||
<span class="font-medium">PrimeNG</span>
|
||||
</div>
|
||||
<div class="text-700">The Most Complete Angular UI Component Library</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 md:col-6">
|
||||
<div class="p-3 border-1 surface-border border-round surface-card">
|
||||
<div class="text-900 mb-2">
|
||||
<i class="pi pi-github mr-2"></i>
|
||||
<span class="font-medium">PrimeReact</span>
|
||||
</div>
|
||||
<div class="text-700">Advanced UI Components for ReactJS</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 md:col-6">
|
||||
<div class="p-3 border-1 surface-border border-round surface-card">
|
||||
<div class="text-900 mb-2">
|
||||
<i class="pi pi-github mr-2"></i>
|
||||
<span class="font-medium">PrimeVue</span>
|
||||
</div>
|
||||
<div class="text-700">The Most Powerful Vue UI Component Library</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
@ -18,27 +18,46 @@ import { ListingsService } from '../../services/listings.service';
|
|||
import { UserService } from '../../services/user.service';
|
||||
import onChange from 'on-change';
|
||||
import { createGenericObject, getCriteriaStateObject, getSessionStorageHandler } from '../../utils/utils';
|
||||
import { ListingCriteria, ListingType, MailInfo, User } from '../../../../../common-models/src/main.model';
|
||||
import { ImageProperty, ListingCriteria, ListingType, MailInfo, User } from '../../../../../common-models/src/main.model';
|
||||
import { MailService } from '../../services/mail.service';
|
||||
import { MessageService } from 'primeng/api';
|
||||
import { SharedModule } from '../../shared/shared/shared.module';
|
||||
import { GalleriaModule } from 'primeng/galleria';
|
||||
import { environment } from '../../../environments/environment';
|
||||
@Component({
|
||||
selector: 'app-details',
|
||||
standalone: true,
|
||||
imports: [SharedModule],
|
||||
imports: [SharedModule,GalleriaModule],
|
||||
providers:[MessageService],
|
||||
templateUrl: './details.component.html',
|
||||
styleUrl: './details.component.scss'
|
||||
})
|
||||
export class DetailsComponent {
|
||||
// listings: Array<BusinessListing>;
|
||||
|
||||
responsiveOptions = [
|
||||
{
|
||||
breakpoint: '1199px',
|
||||
numVisible: 1,
|
||||
numScroll: 1
|
||||
},
|
||||
{
|
||||
breakpoint: '991px',
|
||||
numVisible: 2,
|
||||
numScroll: 1
|
||||
},
|
||||
{
|
||||
breakpoint: '767px',
|
||||
numVisible: 1,
|
||||
numScroll: 1
|
||||
}
|
||||
];
|
||||
private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;
|
||||
listing: ListingType;
|
||||
user:User;
|
||||
criteria:ListingCriteria
|
||||
mailinfo: MailInfo;
|
||||
|
||||
propertyImages:ImageProperty[]=[]
|
||||
environment=environment;
|
||||
constructor(private activatedRoute: ActivatedRoute,
|
||||
private listingsService:ListingsService,
|
||||
private router:Router,
|
||||
|
|
@ -55,6 +74,7 @@ export class DetailsComponent {
|
|||
this.user=user
|
||||
});
|
||||
this.listing=await lastValueFrom(this.listingsService.getListingById(this.id,this.criteria.listingsCategory));
|
||||
this.propertyImages=await this.listingsService.getPropertyImages(this.listing.id)
|
||||
}
|
||||
back(){
|
||||
this.router.navigate(['listings',this.criteria.listingsCategory])
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<div class="search">
|
||||
<div class="wrapper">
|
||||
<div class="grid p-4 align-items-center">
|
||||
@if (listingCategory==='business'){
|
||||
@if (category==='business'){
|
||||
<div class="col-2">
|
||||
<p-dropdown [options]="selectOptions.typesOfBusiness" [(ngModel)]="criteria.type" optionLabel="name"
|
||||
optionValue="value" [showClear]="true" placeholder="Categorie of Business"
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
offLabel="Real Estate included"></p-toggleButton>
|
||||
</div>
|
||||
}
|
||||
@if (listingCategory==='commercialProperty'){
|
||||
@if (category==='commercialProperty'){
|
||||
<div class="col-2">
|
||||
<p-dropdown [options]="states" [(ngModel)]="criteria.state" optionLabel="name" optionValue="value"
|
||||
[showClear]="true" placeholder="Location" [style]="{ width: '100%'}"></p-dropdown>
|
||||
|
|
@ -57,10 +57,9 @@
|
|||
<div class="wrapper">
|
||||
<div class="grid">
|
||||
@for (listing of filteredListings; track listing.id) {
|
||||
<div class="col-12 lg:col-3 p-3">
|
||||
<div *ngIf="listing.listingsCategory==='business'" class="col-12 lg:col-3 p-3">
|
||||
<div class="shadow-2 border-round surface-card mb-3 h-full flex-column justify-content-between flex">
|
||||
<div class="p-4 h-full flex flex-column">
|
||||
@if (listing.listingsCategory==='business'){
|
||||
<div class="flex align-items-center">
|
||||
<span [class]="selectOptions.getBgColorType(listing.type)"
|
||||
class="inline-flex border-circle align-items-center justify-content-center mr-3"
|
||||
|
|
@ -69,42 +68,83 @@
|
|||
</span>
|
||||
<span class="text-900 font-medium text-2xl">{{selectOptions.getBusiness(listing.type)}}</span>
|
||||
</div>
|
||||
}
|
||||
@if (listing.listingsCategory==='commercialProperty'){
|
||||
<!-- <div class="flex align-items-center">
|
||||
<span
|
||||
class="inline-flex border-circle align-items-center justify-content-center bg-green-100 mr-3"
|
||||
style="width:38px;height:38px">
|
||||
<i class="pi pi-globe text-xl text-green-600"></i>
|
||||
</span>
|
||||
<span class="text-900 font-medium text-2xl">Investment</span>
|
||||
</div> -->
|
||||
}
|
||||
<div class="text-900 my-3 text-xl font-medium">{{listing.title}}</div>
|
||||
@if (listing.listingsCategory==='business'){
|
||||
<p class="mt-0 mb-1 text-700 line-height-3">Asking price: {{listing.price | currency}}</p>
|
||||
<p class="mt-0 mb-1 text-700 line-height-3">Sales revenue: {{listing.salesRevenue | currency}}</p>
|
||||
<p class="mt-0 mb-1 text-700 line-height-3">Net profit: {{listing.cashFlow | currency}}</p>
|
||||
<p class="mt-0 mb-1 text-700 line-height-3">Location: {{selectOptions.getState(listing.state)}}</p>
|
||||
<p class="mt-0 mb-1 text-700 line-height-3">Established: {{listing.established}}</p>
|
||||
}
|
||||
@if (listing.listingsCategory==='commercialProperty'){
|
||||
<!-- <p class="mt-0 mb-1 text-700 line-height-3">Location: {{selectOptions.getState(listing.state)}}</p>
|
||||
<p class="mt-0 mb-1 text-700 line-height-3">EMail: {{listing.email}}</p>
|
||||
<p class="mt-0 mb-1 text-700 line-height-3">Website: {{listing.website}}</p>
|
||||
<p class="mt-0 mb-1 text-700 line-height-3">Phone Number: {{listing.phoneNumber}}</p> -->
|
||||
}
|
||||
<div class="mt-auto ml-auto">
|
||||
<img *ngIf="!listing.hideImage" src="{{environment.apiBaseUrl}}/profile/{{listing.userId}}" (error)="imageErrorHandler(listing)" class="rounded-image"/>
|
||||
<img *ngIf="!listing.hideImage" src="{{environment.apiBaseUrl}}/logo/{{listing.userId}}" (error)="imageErrorHandler(listing)" class="rounded-image"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-4 py-3 surface-100 text-right">
|
||||
<div class="px-4 py-3 surface-100 text-left">
|
||||
<button pButton pRipple icon="pi pi-arrow-right" iconPos="right" label="View Full Listing"
|
||||
class="p-button-rounded p-button-success" [routerLink]="['/details',listing.id]"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@for (listing of filteredListings; track listing.id) {
|
||||
<div *ngIf="listing.listingsCategory==='commercialProperty'" class="col-12 xl:col-4">
|
||||
<div class="surface-card p-2" style="border-radius: 10px">
|
||||
<article class="flex flex-column md:flex-row w-full gap-3 p-3 surface-card">
|
||||
<div class="relative">
|
||||
<img src="{{environment.apiBaseUrl}}/property/{{listing.id}}/1.avif" alt="Image" class="border-round w-full h-full md:w-12rem md:h-12rem">
|
||||
<p class="absolute px-2 py-1 border-round-lg text-sm font-normal text-white mt-0 mb-0" style="background-color: rgba(255, 255, 255, 0.3); backdrop-filter: blur(10px); top: 3%; left: 3%;">{{selectOptions.getState(listing.state)}}</p>
|
||||
</div>
|
||||
<div class="flex flex-column w-full gap-3">
|
||||
<div class="flex w-full justify-content-between align-items-center flex-wrap gap-3">
|
||||
<p class="font-semibold text-lg mt-0 mb-0">{{listing.title}}</p>
|
||||
<!-- <p-rating [ngModel]="val1" readonly="true" stars="5" [cancel]="false" ngClass="flex-shrink-0"></p-rating> -->
|
||||
</div>
|
||||
<p class="font-normal text-lg text-600 mt-0 mb-0">{{listing.city}}</p>
|
||||
<div class="flex flex-wrap justify-content-between xl:h-2rem mt-auto">
|
||||
<p class="text-base flex align-items-center text-900 mt-0 mb-1">
|
||||
<i class="pi pi-list mr-2"></i>
|
||||
<span class="font-medium">{{selectOptions.getCommercialProperty(listing.type)}}</span>
|
||||
</p>
|
||||
<!-- <p class="text-base flex align-items-center text-900 mt-0 mb-1">
|
||||
<i class="pi pi-check-circle mr-2"></i>
|
||||
<span class="font-medium">Verified</span>
|
||||
</p> -->
|
||||
</div>
|
||||
<p class="font-semibold text-3xl text-900 mt-0 mb-2">{{listing.price | currency}}</p>
|
||||
</div>
|
||||
</article>
|
||||
<div class="px-4 py-3 text-left">
|
||||
<button pButton pRipple icon="pi pi-arrow-right" iconPos="right" label="View Full Listing"
|
||||
class="p-button-rounded p-button-success" [routerLink]="['/details',listing.id]"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
}
|
||||
@for (user of users; track user.id) {
|
||||
<div class="col-12 lg:col-6 xl:col-4 p-4">
|
||||
<div class="surface-card shadow-2 p-2" style="border-radius: 10px">
|
||||
<div class="surface-card p-4 flex flex-column align-items-center md:flex-row md:align-items-stretch h-full" >
|
||||
<span>
|
||||
<img src="{{environment.apiBaseUrl}}/profile/{{user.id}}" class="w-5rem" />
|
||||
</span>
|
||||
<div class="flex flex-column align-items-center md:align-items-stretch ml-4 mt-4 md:mt-0">
|
||||
<p class="mt-0 mb-3 line-height-3 text-center md:text-left">{{user.description}}</p>
|
||||
<span class="text-900 font-medium mb-1 mt-auto">{{user.firstname}} {{user.lastname}}</span>
|
||||
<div class="text-600 text-sm">{{user.companyName}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-4 py-3 text-right flex justify-content-between align-items-center">
|
||||
<img *ngIf="user.hasCompanyLogo" src="{{environment.apiBaseUrl}}/logo/{{user.id}}" class="rounded-image"/>
|
||||
<button pButton pRipple icon="pi pi-arrow-right" iconPos="right" label="View Full profile"
|
||||
class="p-button-rounded p-button-success" [routerLink]="['/details',user.id]"></button>
|
||||
|
||||
</div>
|
||||
<!-- <div class="mt-auto ml-auto">
|
||||
<img *ngIf="user.hasCompanyLogo" src="{{environment.apiBaseUrl}}/logo/{{user.id}}" class="rounded-image"/>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="mb-2 surface-200 flex align-items-center justify-content-center">
|
||||
<div class="mx-1 text-color">Total number of Listings: {{totalRecords}}</div>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ import onChange from 'on-change';
|
|||
import { createGenericObject, getCriteriaStateObject, getSessionStorageHandler } from '../../utils/utils';
|
||||
import { InitEditableRow } from 'primeng/table';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { ListingCriteria, ListingType } from '../../../../../common-models/src/main.model';
|
||||
import { ListingCriteria, ListingType, User } from '../../../../../common-models/src/main.model';
|
||||
import { UserService } from '../../services/user.service';
|
||||
@Component({
|
||||
selector: 'app-listings',
|
||||
standalone: true,
|
||||
|
|
@ -27,10 +28,11 @@ import { ListingCriteria, ListingType } from '../../../../../common-models/src/m
|
|||
export class ListingsComponent {
|
||||
environment=environment;
|
||||
listings: Array<ListingType>;
|
||||
users: Array<User>
|
||||
filteredListings: Array<ListingType>;
|
||||
criteria:ListingCriteria;
|
||||
realEstateChecked: boolean;
|
||||
category: string;
|
||||
// category: string;
|
||||
maxPrice: string;
|
||||
minPrice: string;
|
||||
type:string;
|
||||
|
|
@ -40,9 +42,14 @@ export class ListingsComponent {
|
|||
first: number = 0;
|
||||
rows: number = 12;
|
||||
totalRecords:number = 0;
|
||||
public listingCategory: 'business' | 'commercialProperty' | undefined;
|
||||
public category: 'business' | 'commercialProperty' | 'professionals_brokers' | undefined;
|
||||
|
||||
constructor(public selectOptions: SelectOptionsService, private listingsService:ListingsService,private activatedRoute: ActivatedRoute, private router:Router, private cdRef:ChangeDetectorRef) {
|
||||
constructor(public selectOptions: SelectOptionsService,
|
||||
private listingsService:ListingsService,
|
||||
private userService:UserService,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private router:Router,
|
||||
private cdRef:ChangeDetectorRef) {
|
||||
this.criteria = onChange(getCriteriaStateObject(),getSessionStorageHandler);
|
||||
this.router.getCurrentNavigation()
|
||||
this.activatedRoute.snapshot
|
||||
|
|
@ -51,8 +58,8 @@ export class ListingsComponent {
|
|||
this.criteria = onChange(createGenericObject<ListingCriteria>(),getSessionStorageHandler)
|
||||
this.first=0;
|
||||
}
|
||||
this.listingCategory = (<any>params).type;
|
||||
this.criteria.listingsCategory=this.listingCategory;
|
||||
this.category = (<any>params).type;
|
||||
this.criteria.listingsCategory=this.category;
|
||||
this.init()
|
||||
})
|
||||
|
||||
|
|
@ -60,6 +67,8 @@ export class ListingsComponent {
|
|||
async ngOnInit(){
|
||||
}
|
||||
async init(){
|
||||
if (this.category==='business' || this.category==='commercialProperty'){
|
||||
this.users=[]
|
||||
this.listings=await this.listingsService.getListings(this.criteria);
|
||||
this.setStates();
|
||||
this.filteredListings=[...this.listings];
|
||||
|
|
@ -67,6 +76,14 @@ export class ListingsComponent {
|
|||
this.filteredListings=[...this.listings].splice(this.first,this.rows);
|
||||
this.cdRef.markForCheck();
|
||||
this.cdRef.detectChanges();
|
||||
} else {
|
||||
this.listings=[]
|
||||
this.filteredListings=[];
|
||||
this.users=await this.userService.search(this.criteria);
|
||||
this.cdRef.markForCheck();
|
||||
this.cdRef.detectChanges();
|
||||
}
|
||||
|
||||
}
|
||||
setStates(){
|
||||
this.statesSet=new Set();
|
||||
|
|
|
|||
|
|
@ -18,14 +18,26 @@
|
|||
<input id="state" type="text" [disabled]="true" pInputText [(ngModel)]="user.email">
|
||||
<p class="font-italic text-sm line-height-1">You can only modify your email by contacting us at emailchange@bizmatch.net</p>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<div class="grid">
|
||||
<div class="mb-4 col-12 md:col-6">
|
||||
<label for="firstname" class="block font-medium text-900 mb-2">First Name</label>
|
||||
<input id="firstname" type="text" pInputText [(ngModel)]="user.firstname">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<div class="mb-4 col-12 md:col-6">
|
||||
<label for="lastname" class="block font-medium text-900 mb-2">Last Name</label>
|
||||
<input id="lastname" type="text" pInputText [(ngModel)]="user.lastname">
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<div class="mb-4 col-12 md:col-6">
|
||||
<label for="firstname" class="block font-medium text-900 mb-2">Company Name</label>
|
||||
<input id="firstname" type="text" pInputText [(ngModel)]="user.companyName">
|
||||
</div>
|
||||
<div class="mb-4 col-12 md:col-6">
|
||||
<label for="lastname" class="block font-medium text-900 mb-2">Describe yourself</label>
|
||||
<input id="lastname" type="text" pInputText [(ngModel)]="user.description">
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<div class="mb-4 col-12 md:col-4">
|
||||
<label for="phoneNumber" class="block font-medium text-900 mb-2">Your Phone Number</label>
|
||||
|
|
@ -67,13 +79,6 @@
|
|||
<span class="text-xs"> (Add more licenses or remove existing ones.)</span>
|
||||
<!-- <button pButton pRipple label="Add Licence" class="w-auto" (click)="addLicence()"></button> -->
|
||||
</div>
|
||||
<!-- <div class="mb-4">
|
||||
<label for="state" class="block font-medium text-900 mb-2">New Password</label>
|
||||
<p class="font-italic text-sm line-height-1">If you would like to change the password type a new one. Otherwise leave this blank.</p>
|
||||
<input id="state" type="text" pInputText>
|
||||
<p class="font-italic text-sm line-height-1">Password repetition</p>
|
||||
<input id="state" type="text" pInputText>
|
||||
</div> -->
|
||||
|
||||
<div>
|
||||
<button pButton pRipple label="Update Profile" class="w-auto" (click)="updateProfile(user)"></button>
|
||||
|
|
@ -83,14 +88,14 @@
|
|||
<div class="flex flex-column align-items-center flex-or mb-8">
|
||||
<span class="font-medium text-900 mb-2">Company Logo</span>
|
||||
<span class="font-medium text-xs mb-2">(is shown in every offer)</span>
|
||||
<img [src]="companyLogoUrl" (error)="setImageToFallback($event)" class="rounded-image"/>
|
||||
<p-fileUpload mode="basic" chooseLabel="Upload" name="file" [url]="uploadUrl" accept="image/*" [maxFileSize]="maxFileSize" (onUpload)="onUploadCompanyLogo($event)" [auto]="true" styleClass="p-button-outlined p-button-plain p-button-rounded mt-4"></p-fileUpload>
|
||||
<img [src]="companyLogoUrl" class="rounded-logo"/>
|
||||
<p-fileUpload mode="basic" chooseLabel="Upload" name="file" [url]="uploadCompanyUrl" accept="image/*" [maxFileSize]="maxFileSize" (onUpload)="onUploadCompanyLogo($event)" [auto]="true" styleClass="p-button-outlined p-button-plain p-button-rounded mt-4"></p-fileUpload>
|
||||
</div>
|
||||
<p-divider></p-divider>
|
||||
<div class="flex flex-column align-items-center flex-or">
|
||||
<span class="font-medium text-900 mb-2">Your Profile Picture</span>
|
||||
<img [src]="profileUrl" (error)="setImageToFallback($event)" class="rounded-image"/>
|
||||
<p-fileUpload mode="basic" chooseLabel="Upload" name="file" [url]="uploadUrl" accept="image/*" [maxFileSize]="maxFileSize" (onUpload)="onUploadProfilePicture($event)" [auto]="true" styleClass="p-button-outlined p-button-plain p-button-rounded mt-4"></p-fileUpload>
|
||||
<img [src]="profileUrl" class="rounded-profile"/>
|
||||
<p-fileUpload mode="basic" chooseLabel="Upload" name="file" [url]="uploadProfileUrl" accept="image/*" [maxFileSize]="maxFileSize" (onUpload)="onUploadProfilePicture($event)" [auto]="true" styleClass="p-button-outlined p-button-plain p-button-rounded mt-4"></p-fileUpload>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
.rounded-image {
|
||||
.rounded-logo {
|
||||
border-radius: 6px;
|
||||
width: 120px;
|
||||
height: 30px;
|
||||
|
|
@ -6,7 +6,15 @@
|
|||
padding: 1px 1px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.rounded-profile {
|
||||
// @extend .rounded-logo;
|
||||
max-width: 100px;
|
||||
max-height: 120px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #6b7280;
|
||||
padding: 1px 1px;
|
||||
object-fit: contain;
|
||||
}
|
||||
.wfull{
|
||||
width: 100%;
|
||||
}
|
||||
|
|
@ -41,7 +41,8 @@ export class AccountComponent {
|
|||
user:User;
|
||||
subscriptions:Array<Subscription>;
|
||||
userSubscriptions:Array<Subscription>=[];
|
||||
uploadUrl:string;
|
||||
uploadProfileUrl:string;
|
||||
uploadCompanyUrl:string;
|
||||
maxFileSize=1000000;
|
||||
companyLogoUrl:string;
|
||||
profileUrl:string;
|
||||
|
|
@ -52,17 +53,18 @@ export class AccountComponent {
|
|||
public selectOptions:SelectOptionsService,
|
||||
private cdref:ChangeDetectorRef) {
|
||||
this.user=this.userService.getUser()
|
||||
|
||||
}
|
||||
async ngOnInit(){
|
||||
this.profileUrl = `${environment.apiBaseUrl}/profile/${this.user.id}`
|
||||
this.companyLogoUrl = `${environment.apiBaseUrl}/logo/${this.user.id}`
|
||||
|
||||
this.userSubscriptions=await lastValueFrom(this.subscriptionService.getAllSubscriptions());
|
||||
this.uploadUrl = `${environment.apiBaseUrl}/bizmatch/account/uploadPhoto/${this.user.id}`;
|
||||
this.uploadProfileUrl = `${environment.apiBaseUrl}/bizmatch/image/uploadProfile/${this.user.id}`;
|
||||
this.uploadCompanyUrl = `${environment.apiBaseUrl}/bizmatch/image/uploadCompanyLogo/${this.user.id}`;
|
||||
if (!this.user.licensedIn || this.user.licensedIn?.length===0){
|
||||
this.user.licensedIn = [{name:'',value:''}]
|
||||
}
|
||||
this.user=await this.userService.getById(this.user.id);
|
||||
this.profileUrl = this.user.hasProfile?`${environment.apiBaseUrl}/profile/${this.user.id}`:`/assets/images/placeholder.png`
|
||||
this.companyLogoUrl = this.user.hasCompanyLogo?`${environment.apiBaseUrl}/logo/${this.user.id}`:`/assets/images/placeholder.png`
|
||||
}
|
||||
printInvoice(invoice:Invoice){}
|
||||
|
||||
|
|
@ -74,7 +76,7 @@ export class AccountComponent {
|
|||
|
||||
onUploadCompanyLogo(event:any){
|
||||
const uniqueSuffix = '?_ts=' + new Date().getTime();
|
||||
this.companyLogoUrl = `${environment.apiBaseUrl}/company/${this.user.id}${uniqueSuffix}` //`http://IhrServer:Port/${newImagePath}${uniqueSuffix}`;
|
||||
this.companyLogoUrl = `${environment.apiBaseUrl}/logo/${this.user.id}${uniqueSuffix}` //`http://IhrServer:Port/${newImagePath}${uniqueSuffix}`;
|
||||
}
|
||||
onUploadProfilePicture(event:any){
|
||||
const uniqueSuffix = '?_ts=' + new Date().getTime();
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ export class EditListingComponent {
|
|||
this.listing.userId=this.user.id
|
||||
this.listing.listingsCategory='business';
|
||||
}
|
||||
this.uploadUrl = `${environment.apiBaseUrl}/bizmatch/${this.listing.listingsCategory}/uploadPropertyPicture/${this.listing.id}`;
|
||||
this.uploadUrl = `${environment.apiBaseUrl}/bizmatch/image/uploadPropertyPicture/${this.listing.id}`;
|
||||
this.propertyImages=await this.listingsService.getPropertyImages(this.listing.id)
|
||||
}
|
||||
async save(){
|
||||
|
|
|
|||
|
|
@ -31,6 +31,6 @@ export class ListingsService {
|
|||
await lastValueFrom(this.http.delete<ListingType>(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/${id}`));
|
||||
}
|
||||
async getPropertyImages(id:string):Promise<ImageProperty[]>{
|
||||
return await lastValueFrom(this.http.get<ImageProperty[]>(`${this.apiBaseUrl}/bizmatch/listings/commercialProperty/images/${id}`));
|
||||
return await lastValueFrom(this.http.get<ImageProperty[]>(`${this.apiBaseUrl}/bizmatch/image/${id}`));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, debounceTime, distinctUntilChanged, map, shareReplay } from 'rxjs';
|
||||
import { BehaviorSubject, Observable, debounceTime, distinctUntilChanged, map, shareReplay } from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
|
|
@ -7,6 +7,9 @@ import { BehaviorSubject, debounceTime, distinctUntilChanged, map, shareReplay }
|
|||
export class LoadingService {
|
||||
public loading$ = new BehaviorSubject<string[]>([]);
|
||||
|
||||
private loadingTextSubject = new BehaviorSubject<string | null>(null);
|
||||
loadingText$: Observable<string | null> = this.loadingTextSubject.asObservable();
|
||||
|
||||
public isLoading$ = this.loading$.asObservable().pipe(
|
||||
map((loading) => loading.length > 0),
|
||||
debounceTime(200),
|
||||
|
|
@ -14,15 +17,21 @@ export class LoadingService {
|
|||
shareReplay(1)
|
||||
);
|
||||
|
||||
public startLoading(type: string): void {
|
||||
public startLoading(type: string,request:string): void {
|
||||
if (!this.loading$.value.includes(type)) {
|
||||
this.loading$.next(this.loading$.value.concat(type));
|
||||
if (request.includes('uploadPropertyPicture')) {
|
||||
this.loadingTextSubject.next("Please wait - we're processing your image...");
|
||||
} else {
|
||||
this.loadingTextSubject.next(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public stopLoading(type: string): void {
|
||||
if (this.loading$.value.includes(type)) {
|
||||
this.loading$.next(this.loading$.value.filter((t) => t !== type));
|
||||
this.loadingTextSubject.next(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -42,7 +42,9 @@ export class SelectOptionsService {
|
|||
getBusiness(value:string):string{
|
||||
return this.typesOfBusiness.find(t=>t.value===value)?.name
|
||||
}
|
||||
|
||||
getCommercialProperty(value:string):string{
|
||||
return this.typesOfCommercialProperty.find(t=>t.value===value)?.name
|
||||
}
|
||||
getListingsCategory(value:string):string{
|
||||
return this.listingCategories.find(l=>l.value===value)?.name
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { jwtDecode } from 'jwt-decode';
|
|||
import { Observable, distinctUntilChanged, filter, from, lastValueFrom, map } from 'rxjs';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { KeycloakService } from './keycloak.service';
|
||||
import { JwtToken, User } from '../../../../common-models/src/main.model';
|
||||
import { JwtToken, ListingCriteria, User } from '../../../../common-models/src/main.model';
|
||||
import { environment } from '../../environments/environment';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
|
||||
|
|
@ -103,4 +103,7 @@ export class UserService {
|
|||
async getById(id:string):Promise<User>{
|
||||
return await lastValueFrom(this.http.get<User>(`${this.apiBaseUrl}/bizmatch/user/${id}`));
|
||||
}
|
||||
async search(criteria?:ListingCriteria){
|
||||
return await lastValueFrom(this.http.post<User[]>(`${this.apiBaseUrl}/bizmatch/user/search`,criteria));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,13 +32,11 @@ export interface Listing {
|
|||
}
|
||||
export interface BusinessListing extends Listing {
|
||||
listingsCategory: 'business'; //enum
|
||||
// summary: Array<string>;
|
||||
realEstateIncluded?: boolean;
|
||||
leasedLocation?:boolean;
|
||||
franchiseResale?:boolean;
|
||||
salesRevenue?: number;
|
||||
cashFlow?: number;
|
||||
// netProfit?: number;
|
||||
supportAndTraining?: string;
|
||||
employees?: number;
|
||||
established?: number;
|
||||
|
|
@ -49,24 +47,17 @@ export interface BusinessListing extends Listing {
|
|||
}
|
||||
export interface CommercialPropertyListing extends Listing {
|
||||
listingsCategory: 'commercialProperty'; //enum
|
||||
images:string[];
|
||||
zipCode:number;
|
||||
county:string
|
||||
email?: string;
|
||||
website?: string;
|
||||
phoneNumber?: string;
|
||||
hasImages:boolean;
|
||||
}
|
||||
export type ListingType =
|
||||
| BusinessListing
|
||||
| CommercialPropertyListing;
|
||||
// export interface ProfessionalsBrokersListing extends Listing {
|
||||
// listingsCategory: 'professionals_brokers'; //enum
|
||||
// summary: string;
|
||||
// address?: string;
|
||||
// email?: string;
|
||||
// website?: string;
|
||||
// category?: 'Professionals' | 'Broker';
|
||||
// }
|
||||
|
||||
export interface ListingCriteria {
|
||||
type:string,
|
||||
state:string,
|
||||
|
|
@ -82,11 +73,15 @@ export interface UserBase {
|
|||
lastname: string;
|
||||
email: string;
|
||||
phoneNumber?: string;
|
||||
description?:string;
|
||||
companyName?:string;
|
||||
companyOverview?:string;
|
||||
companyWebsite?:string;
|
||||
companyLocation?:string;
|
||||
offeredServices?:string;
|
||||
areasServed?:string;
|
||||
hasProfile?:boolean;
|
||||
hasCompanyLogo?:boolean;
|
||||
}
|
||||
export interface User extends UserBase {
|
||||
licensedIn?:KeyValue[];
|
||||
|
|
|
|||
Loading…
Reference in New Issue