Image Upload, spinner aktivirt, listings & details überarbeitet

This commit is contained in:
Andreas Knuth 2024-03-18 18:17:04 +01:00
parent 2b27ab8ba5
commit fd91adda57
31 changed files with 582 additions and 263 deletions

View File

@ -42,6 +42,7 @@
"redis-om": "^0.4.3", "redis-om": "^0.4.3",
"reflect-metadata": "^0.2.0", "reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"sharp": "^0.33.2",
"urlcat": "^3.1.0", "urlcat": "^3.1.0",
"winston": "^3.11.0" "winston": "^3.11.0"
}, },

View File

@ -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);
}
}

View File

@ -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 {}

View File

@ -10,8 +10,6 @@ import { SelectOptionsService } from './select-options/select-options.service.js
import { SubscriptionsController } from './subscriptions/subscriptions.controller.js'; import { SubscriptionsController } from './subscriptions/subscriptions.controller.js';
import { RedisModule } from './redis/redis.module.js'; import { RedisModule } from './redis/redis.module.js';
import { ListingsService } from './listings/listings.service.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 { ServeStaticModule } from '@nestjs/serve-static';
import path, { join } from 'path'; import path, { join } from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
@ -22,9 +20,9 @@ import { AuthModule } from './auth/auth.module.js';
import { GeoModule } from './geo/geo.module.js'; import { GeoModule } from './geo/geo.module.js';
import { UserModule } from './user/user.module.js'; import { UserModule } from './user/user.module.js';
import { ListingsModule } from './listings/listings.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 { SelectOptionsModule } from './select-options/select-options.module.js';
import { CommercialPropertyListingsController } from './listings/commercial-property-listings.controller.js'; import { CommercialPropertyListingsController } from './listings/commercial-property-listings.controller.js';
import { ImageModule } from './image/image.module.js';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
@ -52,9 +50,9 @@ const __dirname = path.dirname(__filename);
GeoModule, GeoModule,
UserModule, UserModule,
ListingsModule, ListingsModule,
AccountModule,
SelectOptionsModule, SelectOptionsModule,
RedisModule RedisModule,
ImageModule
], ],
controllers: [AppController, SubscriptionsController], controllers: [AppController, SubscriptionsController],
providers: [AppService, FileService], providers: [AppService, FileService],

View File

@ -1,18 +1,20 @@
import { Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { fstat, readFileSync } from 'fs'; import { fstat, readFileSync } from 'fs';
import { join } from 'path'; import { join } from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import path from 'path'; import path from 'path';
import fs from 'fs-extra'; 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 __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
@Injectable() @Injectable()
export class FileService { export class FileService {
private subscriptions: any; private subscriptions: any;
constructor() { constructor(@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {
this.loadSubscriptions(); this.loadSubscriptions();
fs.ensureDirSync(`./pictures`); fs.ensureDirSync(`./pictures`);
fs.ensureDirSync(`./pictures/profile`); fs.ensureDirSync(`./pictures/profile`);
@ -31,10 +33,18 @@ export class FileService {
const suffix = file.mimetype.includes('png') ? 'png' : 'jpg' const suffix = file.mimetype.includes('png') ? 'png' : 'jpg'
await fs.outputFile(`./pictures/profile/${userId}`, file.buffer); await fs.outputFile(`./pictures/profile/${userId}`, file.buffer);
} }
hasProfile(userId: string){
return fs.existsSync(`./pictures/profile/${userId}`)
}
async storeCompanyLogo(file: Express.Multer.File, userId: string) { async storeCompanyLogo(file: Express.Multer.File, userId: string) {
const suffix = file.mimetype.includes('png') ? 'png' : 'jpg' const suffix = file.mimetype.includes('png') ? 'png' : 'jpg'
await fs.outputFile(`./pictures/logo/${userId}`, file.buffer); await fs.outputFile(`./pictures/logo/${userId}`, file.buffer);
} }
hasCompanyLogo(userId: string){
return fs.existsSync(`./pictures/logo/${userId}`)
}
async getPropertyImages(listingId: string): Promise<ImageProperty[]> { async getPropertyImages(listingId: string): Promise<ImageProperty[]> {
const result: ImageProperty[] = [] const result: ImageProperty[] = []
const directory = `./pictures/property/${listingId}` const directory = `./pictures/property/${listingId}`
@ -48,14 +58,24 @@ export class FileService {
} else { } else {
return [] return []
} }
}
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) { async storePropertyPicture(file: Express.Multer.File, listingId: string) {
const suffix = file.mimetype.includes('png') ? 'png' : 'jpg' const suffix = file.mimetype.includes('png') ? 'png' : 'jpg'
const directory = `./pictures/property/${listingId}` const directory = `./pictures/property/${listingId}`
fs.ensureDirSync(`${directory}`); fs.ensureDirSync(`${directory}`);
const imageName = await this.getNextImageName(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) { async getNextImageName(directory) {
try { try {
@ -72,4 +92,24 @@ export class FileService {
return null; 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
}
} }

View File

@ -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);
}
}

View File

@ -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 {}

View File

@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
@Injectable() @Injectable()
export class AccountService {} export class ImageService {}

View File

@ -11,10 +11,6 @@ export class CommercialPropertyListingsController {
constructor(private readonly listingsService:ListingsService,private fileService:FileService,@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) { 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') @Get(':id')
findById(@Param('id') id:string): any { findById(@Param('id') id:string): any {
@ -43,14 +39,5 @@ export class CommercialPropertyListingsController {
this.listingsService.deleteCommercialPropertyListing(id) 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);
}
} }

View File

@ -95,8 +95,6 @@ export class ListingsService {
async getAllCommercialListings(start?: number, end?: number) { async getAllCommercialListings(start?: number, end?: number) {
return await this.commercialPropertyListingRepository.search().return.all() return await this.commercialPropertyListingRepository.search().return.all()
} }
async findBusinessListings(criteria:ListingCriteria): Promise<any> { async findBusinessListings(criteria:ListingCriteria): Promise<any> {
let listings = await this.getAllBusinessListings(); let listings = await this.getAllBusinessListings();
return this.find(criteria,listings); return this.find(criteria,listings);

View File

@ -20,9 +20,9 @@ export class UserController {
return this.userService.saveUser(user); return this.userService.saveUser(user);
} }
// @Put() @Post('search')
// update(@Body() user: any):Promise<UserEntity>{ find(@Body() criteria: any): any {
// this.logger.info(`update User`); return this.userService.findUser(criteria);
// return this.userService.saveUser(user); }
// }
} }

View File

@ -2,11 +2,12 @@ import { Module } from '@nestjs/common';
import { UserController } from './user.controller.js'; import { UserController } from './user.controller.js';
import { UserService } from './user.service.js'; import { UserService } from './user.service.js';
import { RedisModule } from '../redis/redis.module.js'; import { RedisModule } from '../redis/redis.module.js';
import { FileService } from '../file/file.service.js';
@Module({ @Module({
imports: [RedisModule], imports: [RedisModule],
controllers: [UserController], controllers: [UserController],
providers: [UserService] providers: [UserService,FileService]
}) })
export class UserModule { export class UserModule {
} }

View File

@ -4,6 +4,7 @@ import { Entity, Repository, Schema } from 'redis-om';
import { ListingCriteria, User } from '../models/main.model.js'; import { ListingCriteria, User } from '../models/main.model.js';
import { REDIS_CLIENT } from '../redis/redis.module.js'; import { REDIS_CLIENT } from '../redis/redis.module.js';
import { UserEntity } from '../models/server.model.js'; import { UserEntity } from '../models/server.model.js';
import { FileService } from '../file/file.service.js';
@Injectable() @Injectable()
export class UserService { export class UserService {
@ -24,12 +25,15 @@ export class UserService {
}, { }, {
dataStructure: 'JSON' 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 = new Repository(this.userSchema, redis)
this.userRepository.createIndex(); this.userRepository.createIndex();
} }
async getUserById( id:string){ 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>{ async saveUser(user:any):Promise<UserEntity>{
return await this.userRepository.save(user.id,user) as UserEntity return await this.userRepository.save(user.id,user) as UserEntity

View File

@ -4,11 +4,20 @@
<header></header> <header></header>
} }
<router-outlet></router-outlet> <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> </div>
<footer></footer> <footer></footer>
</div> </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>
}

View File

@ -8,3 +8,57 @@
/* Optional: Padding für den Inhalt, um sicherzustellen, dass er nicht direkt am Footer klebt */ /* Optional: Padding für den Inhalt, um sicherzustellen, dass er nicht direkt am Footer klebt */
// padding-bottom: 20px; // 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 */
// }

View File

@ -39,17 +39,6 @@ export class AppComponent {
// Hier haben Sie Zugriff auf den aktuellen Route-Pfad // Hier haben Sie Zugriff auf den aktuellen Route-Pfad
this.actualRoute=currentRoute.snapshot.url[0].path 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(){ ngOnInit(){
this.user = this.userService.getUser(); this.user = this.userService.getUser();

View File

@ -4,11 +4,12 @@ import { provideRouter } from '@angular/router';
import { routes } from './app.routes'; import { routes } from './app.routes';
import { provideClientHydration } from '@angular/platform-browser'; import { provideClientHydration } from '@angular/platform-browser';
import { provideAnimations } from '@angular/platform-browser/animations'; 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 { environment } from '../environments/environment';
import { SelectOptionsService } from './services/select-options.service'; import { SelectOptionsService } from './services/select-options.service';
import { KeycloakService } from './services/keycloak.service'; import { KeycloakService } from './services/keycloak.service';
import { UserService } from './services/user.service'; import { UserService } from './services/user.service';
import { LoadingInterceptor } from './interceptors/loading.interceptor';
// provideClientHydration() // provideClientHydration()
export const appConfig: ApplicationConfig = { export const appConfig: ApplicationConfig = {
providers: [ providers: [
@ -26,6 +27,11 @@ export const appConfig: ApplicationConfig = {
multi: true, multi: true,
deps: [SelectOptionsService], deps: [SelectOptionsService],
}, },
{
provide:HTTP_INTERCEPTORS,
useClass:LoadingInterceptor,
multi:true
},
provideRouter(routes),provideAnimations(), provideRouter(routes),provideAnimations(),
// {provide: LOCALE_ID, useValue: 'en-US' } // {provide: LOCALE_ID, useValue: 'en-US' }
] ]

View File

@ -1,21 +1,39 @@
import { HttpInterceptorFn } from '@angular/common/http'; import { HttpEvent, HttpHandler, HttpInterceptor, HttpInterceptorFn, HttpRequest } from '@angular/common/http';
import { inject } from '@angular/core'; import { Injectable, inject } from '@angular/core';
import { tap } from 'rxjs'; import { Observable, tap } from 'rxjs';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { LoadingService } from '../services/loading.service'; import { LoadingService } from '../services/loading.service';
export const loadingInterceptor: HttpInterceptorFn = (req, next) => { // export const loadingInterceptor: HttpInterceptorFn = (req, next) => {
const loadingService = inject(LoadingService); // 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()}`; const requestId = `HTTP-${v4()}`;
this.loadingService.startLoading(requestId,request.url);
loadingService.startLoading(requestId); // return next.handle(request);
return next.handle(request).pipe(
return next(req).pipe(
tap({ tap({
finalize: () => loadingService.stopLoading(requestId), finalize: () => this.loadingService.stopLoading(requestId), // Stoppt den Ladevorgang, wenn die Anfrage abgeschlossen ist
error: () => loadingService.stopLoading(requestId), // Beachte, dass 'error' und 'complete' hier entfernt wurden, da 'finalize' in allen Fällen aufgerufen wird,
complete: () => loadingService.stopLoading(requestId), // egal ob die Anfrage erfolgreich war, einen Fehler geworfen hat oder abgeschlossen wurde.
}) })
); );
}; }
}

View File

@ -7,22 +7,11 @@
<p-button icon="pi pi-times" [rounded]="true" severity="danger" (click)="back()"></p-button> <p-button icon="pi pi-times" [rounded]="true" severity="danger" (click)="back()"></p-button>
</div> </div>
<!-- <div class="text-500 mb-5">Egestas sed tempus urna et pharetra pharetra massa massa ultricies.</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="grid">
<div class="col-12 md:col-6"> <div class="col-12 md:col-6">
<ul class="list-none p-0 m-0 border-top-1 border-300"> <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"> <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-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> <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> <div class="text-900 w-full md:w-10">{{listing.brokerLicencing}}</div>
</li> </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')){ @if (listing && (listing.listingsCategory==='commercialProperty')){
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground"> <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-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> <div class="text-900 w-full md:w-10">{{selectOptions.getState(listing.state)}}</div>
</li> </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"> <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-500 w-full md:w-2 font-medium">Zip Code</div>
<div class="text-900 w-full md:w-10">{{listing.email}}</div> <div class="text-900 w-full md:w-10">{{listing.zipCode}}</div>
</li> </li>
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground"> <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-500 w-full md:w-2 font-medium">County</div>
<div class="text-900 w-full md:w-10">{{listing.website}}</div> <div class="text-900 w-full md:w-10">{{listing.county}}</div>
</li> </li>
<li class="flex align-items-center py-3 px-2 flex-wrap"> <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-500 w-full md:w-2 font-medium">Asking Price:</div>
<div class="text-900 w-full md:w-10">{{listing.phoneNumber}}</div> <div class="text-900 w-full md:w-10">{{listing.price | currency}}</div>
</li> </li>
} }
</ul> </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())){ @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> <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> </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>
</div> </div>

View File

@ -18,27 +18,46 @@ import { ListingsService } from '../../services/listings.service';
import { UserService } from '../../services/user.service'; import { UserService } from '../../services/user.service';
import onChange from 'on-change'; import onChange from 'on-change';
import { createGenericObject, getCriteriaStateObject, getSessionStorageHandler } from '../../utils/utils'; 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 { MailService } from '../../services/mail.service';
import { MessageService } from 'primeng/api'; import { MessageService } from 'primeng/api';
import { SharedModule } from '../../shared/shared/shared.module'; import { SharedModule } from '../../shared/shared/shared.module';
import { GalleriaModule } from 'primeng/galleria';
import { environment } from '../../../environments/environment';
@Component({ @Component({
selector: 'app-details', selector: 'app-details',
standalone: true, standalone: true,
imports: [SharedModule], imports: [SharedModule,GalleriaModule],
providers:[MessageService], providers:[MessageService],
templateUrl: './details.component.html', templateUrl: './details.component.html',
styleUrl: './details.component.scss' styleUrl: './details.component.scss'
}) })
export class DetailsComponent { export class DetailsComponent {
// listings: Array<BusinessListing>; // 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; private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;
listing: ListingType; listing: ListingType;
user:User; user:User;
criteria:ListingCriteria criteria:ListingCriteria
mailinfo: MailInfo; mailinfo: MailInfo;
propertyImages:ImageProperty[]=[]
environment=environment;
constructor(private activatedRoute: ActivatedRoute, constructor(private activatedRoute: ActivatedRoute,
private listingsService:ListingsService, private listingsService:ListingsService,
private router:Router, private router:Router,
@ -55,6 +74,7 @@ export class DetailsComponent {
this.user=user this.user=user
}); });
this.listing=await lastValueFrom(this.listingsService.getListingById(this.id,this.criteria.listingsCategory)); this.listing=await lastValueFrom(this.listingsService.getListingById(this.id,this.criteria.listingsCategory));
this.propertyImages=await this.listingsService.getPropertyImages(this.listing.id)
} }
back(){ back(){
this.router.navigate(['listings',this.criteria.listingsCategory]) this.router.navigate(['listings',this.criteria.listingsCategory])

View File

@ -3,7 +3,7 @@
<div class="search"> <div class="search">
<div class="wrapper"> <div class="wrapper">
<div class="grid p-4 align-items-center"> <div class="grid p-4 align-items-center">
@if (listingCategory==='business'){ @if (category==='business'){
<div class="col-2"> <div class="col-2">
<p-dropdown [options]="selectOptions.typesOfBusiness" [(ngModel)]="criteria.type" optionLabel="name" <p-dropdown [options]="selectOptions.typesOfBusiness" [(ngModel)]="criteria.type" optionLabel="name"
optionValue="value" [showClear]="true" placeholder="Categorie of Business" optionValue="value" [showClear]="true" placeholder="Categorie of Business"
@ -29,7 +29,7 @@
offLabel="Real Estate included"></p-toggleButton> offLabel="Real Estate included"></p-toggleButton>
</div> </div>
} }
@if (listingCategory==='commercialProperty'){ @if (category==='commercialProperty'){
<div class="col-2"> <div class="col-2">
<p-dropdown [options]="states" [(ngModel)]="criteria.state" optionLabel="name" optionValue="value" <p-dropdown [options]="states" [(ngModel)]="criteria.state" optionLabel="name" optionValue="value"
[showClear]="true" placeholder="Location" [style]="{ width: '100%'}"></p-dropdown> [showClear]="true" placeholder="Location" [style]="{ width: '100%'}"></p-dropdown>
@ -57,10 +57,9 @@
<div class="wrapper"> <div class="wrapper">
<div class="grid"> <div class="grid">
@for (listing of filteredListings; track listing.id) { @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="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"> <div class="p-4 h-full flex flex-column">
@if (listing.listingsCategory==='business'){
<div class="flex align-items-center"> <div class="flex align-items-center">
<span [class]="selectOptions.getBgColorType(listing.type)" <span [class]="selectOptions.getBgColorType(listing.type)"
class="inline-flex border-circle align-items-center justify-content-center mr-3" class="inline-flex border-circle align-items-center justify-content-center mr-3"
@ -69,42 +68,83 @@
</span> </span>
<span class="text-900 font-medium text-2xl">{{selectOptions.getBusiness(listing.type)}}</span> <span class="text-900 font-medium text-2xl">{{selectOptions.getBusiness(listing.type)}}</span>
</div> </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> <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">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">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">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">Location: {{selectOptions.getState(listing.state)}}</p>
<p class="mt-0 mb-1 text-700 line-height-3">Established: {{listing.established}}</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"> <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> </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" <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> class="p-button-rounded p-button-success" [routerLink]="['/details',listing.id]"></button>
</div> </div>
</div> </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>
<div class="mb-2 surface-200 flex align-items-center justify-content-center"> <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> <div class="mx-1 text-color">Total number of Listings: {{totalRecords}}</div>

View File

@ -16,7 +16,8 @@ import onChange from 'on-change';
import { createGenericObject, getCriteriaStateObject, getSessionStorageHandler } from '../../utils/utils'; import { createGenericObject, getCriteriaStateObject, getSessionStorageHandler } from '../../utils/utils';
import { InitEditableRow } from 'primeng/table'; import { InitEditableRow } from 'primeng/table';
import { environment } from '../../../environments/environment'; 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({ @Component({
selector: 'app-listings', selector: 'app-listings',
standalone: true, standalone: true,
@ -27,10 +28,11 @@ import { ListingCriteria, ListingType } from '../../../../../common-models/src/m
export class ListingsComponent { export class ListingsComponent {
environment=environment; environment=environment;
listings: Array<ListingType>; listings: Array<ListingType>;
users: Array<User>
filteredListings: Array<ListingType>; filteredListings: Array<ListingType>;
criteria:ListingCriteria; criteria:ListingCriteria;
realEstateChecked: boolean; realEstateChecked: boolean;
category: string; // category: string;
maxPrice: string; maxPrice: string;
minPrice: string; minPrice: string;
type:string; type:string;
@ -40,9 +42,14 @@ export class ListingsComponent {
first: number = 0; first: number = 0;
rows: number = 12; rows: number = 12;
totalRecords:number = 0; 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.criteria = onChange(getCriteriaStateObject(),getSessionStorageHandler);
this.router.getCurrentNavigation() this.router.getCurrentNavigation()
this.activatedRoute.snapshot this.activatedRoute.snapshot
@ -51,8 +58,8 @@ export class ListingsComponent {
this.criteria = onChange(createGenericObject<ListingCriteria>(),getSessionStorageHandler) this.criteria = onChange(createGenericObject<ListingCriteria>(),getSessionStorageHandler)
this.first=0; this.first=0;
} }
this.listingCategory = (<any>params).type; this.category = (<any>params).type;
this.criteria.listingsCategory=this.listingCategory; this.criteria.listingsCategory=this.category;
this.init() this.init()
}) })
@ -60,6 +67,8 @@ export class ListingsComponent {
async ngOnInit(){ async ngOnInit(){
} }
async init(){ async init(){
if (this.category==='business' || this.category==='commercialProperty'){
this.users=[]
this.listings=await this.listingsService.getListings(this.criteria); this.listings=await this.listingsService.getListings(this.criteria);
this.setStates(); this.setStates();
this.filteredListings=[...this.listings]; this.filteredListings=[...this.listings];
@ -67,6 +76,14 @@ export class ListingsComponent {
this.filteredListings=[...this.listings].splice(this.first,this.rows); this.filteredListings=[...this.listings].splice(this.first,this.rows);
this.cdRef.markForCheck(); this.cdRef.markForCheck();
this.cdRef.detectChanges(); this.cdRef.detectChanges();
} else {
this.listings=[]
this.filteredListings=[];
this.users=await this.userService.search(this.criteria);
this.cdRef.markForCheck();
this.cdRef.detectChanges();
}
} }
setStates(){ setStates(){
this.statesSet=new Set(); this.statesSet=new Set();

View File

@ -18,14 +18,26 @@
<input id="state" type="text" [disabled]="true" pInputText [(ngModel)]="user.email"> <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&#64;bizmatch.net</p> <p class="font-italic text-sm line-height-1">You can only modify your email by contacting us at emailchange&#64;bizmatch.net</p>
</div> </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> <label for="firstname" class="block font-medium text-900 mb-2">First Name</label>
<input id="firstname" type="text" pInputText [(ngModel)]="user.firstname"> <input id="firstname" type="text" pInputText [(ngModel)]="user.firstname">
</div> </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> <label for="lastname" class="block font-medium text-900 mb-2">Last Name</label>
<input id="lastname" type="text" pInputText [(ngModel)]="user.lastname"> <input id="lastname" type="text" pInputText [(ngModel)]="user.lastname">
</div> </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="grid">
<div class="mb-4 col-12 md:col-4"> <div class="mb-4 col-12 md:col-4">
<label for="phoneNumber" class="block font-medium text-900 mb-2">Your Phone Number</label> <label for="phoneNumber" class="block font-medium text-900 mb-2">Your Phone Number</label>
@ -67,13 +79,6 @@
<span class="text-xs">&nbsp;(Add more licenses or remove existing ones.)</span> <span class="text-xs">&nbsp;(Add more licenses or remove existing ones.)</span>
<!-- <button pButton pRipple label="Add Licence" class="w-auto" (click)="addLicence()"></button> --> <!-- <button pButton pRipple label="Add Licence" class="w-auto" (click)="addLicence()"></button> -->
</div> </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> <div>
<button pButton pRipple label="Update Profile" class="w-auto" (click)="updateProfile(user)"></button> <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"> <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-900 mb-2">Company Logo</span>
<span class="font-medium text-xs mb-2">(is shown in every offer)</span> <span class="font-medium text-xs mb-2">(is shown in every offer)</span>
<img [src]="companyLogoUrl" (error)="setImageToFallback($event)" class="rounded-image"/> <img [src]="companyLogoUrl" class="rounded-logo"/>
<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> <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> </div>
<p-divider></p-divider> <p-divider></p-divider>
<div class="flex flex-column align-items-center flex-or"> <div class="flex flex-column align-items-center flex-or">
<span class="font-medium text-900 mb-2">Your Profile Picture</span> <span class="font-medium text-900 mb-2">Your Profile Picture</span>
<img [src]="profileUrl" (error)="setImageToFallback($event)" class="rounded-image"/> <img [src]="profileUrl" class="rounded-profile"/>
<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> <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> </div>
</div> </div>

View File

@ -1,4 +1,4 @@
.rounded-image { .rounded-logo {
border-radius: 6px; border-radius: 6px;
width: 120px; width: 120px;
height: 30px; height: 30px;
@ -6,7 +6,15 @@
padding: 1px 1px; padding: 1px 1px;
object-fit: contain; 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{ .wfull{
width: 100%; width: 100%;
} }

View File

@ -41,7 +41,8 @@ export class AccountComponent {
user:User; user:User;
subscriptions:Array<Subscription>; subscriptions:Array<Subscription>;
userSubscriptions:Array<Subscription>=[]; userSubscriptions:Array<Subscription>=[];
uploadUrl:string; uploadProfileUrl:string;
uploadCompanyUrl:string;
maxFileSize=1000000; maxFileSize=1000000;
companyLogoUrl:string; companyLogoUrl:string;
profileUrl:string; profileUrl:string;
@ -52,17 +53,18 @@ export class AccountComponent {
public selectOptions:SelectOptionsService, public selectOptions:SelectOptionsService,
private cdref:ChangeDetectorRef) { private cdref:ChangeDetectorRef) {
this.user=this.userService.getUser() this.user=this.userService.getUser()
} }
async ngOnInit(){ 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.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){ if (!this.user.licensedIn || this.user.licensedIn?.length===0){
this.user.licensedIn = [{name:'',value:''}] this.user.licensedIn = [{name:'',value:''}]
} }
this.user=await this.userService.getById(this.user.id); 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){} printInvoice(invoice:Invoice){}
@ -74,7 +76,7 @@ export class AccountComponent {
onUploadCompanyLogo(event:any){ onUploadCompanyLogo(event:any){
const uniqueSuffix = '?_ts=' + new Date().getTime(); 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){ onUploadProfilePicture(event:any){
const uniqueSuffix = '?_ts=' + new Date().getTime(); const uniqueSuffix = '?_ts=' + new Date().getTime();

View File

@ -99,7 +99,7 @@ export class EditListingComponent {
this.listing.userId=this.user.id this.listing.userId=this.user.id
this.listing.listingsCategory='business'; 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) this.propertyImages=await this.listingsService.getPropertyImages(this.listing.id)
} }
async save(){ async save(){

View File

@ -31,6 +31,6 @@ export class ListingsService {
await lastValueFrom(this.http.delete<ListingType>(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/${id}`)); await lastValueFrom(this.http.delete<ListingType>(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/${id}`));
} }
async getPropertyImages(id:string):Promise<ImageProperty[]>{ 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}`));
} }
} }

View File

@ -1,5 +1,5 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { BehaviorSubject, debounceTime, distinctUntilChanged, map, shareReplay } from 'rxjs'; import { BehaviorSubject, Observable, debounceTime, distinctUntilChanged, map, shareReplay } from 'rxjs';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@ -7,6 +7,9 @@ import { BehaviorSubject, debounceTime, distinctUntilChanged, map, shareReplay }
export class LoadingService { export class LoadingService {
public loading$ = new BehaviorSubject<string[]>([]); 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( public isLoading$ = this.loading$.asObservable().pipe(
map((loading) => loading.length > 0), map((loading) => loading.length > 0),
debounceTime(200), debounceTime(200),
@ -14,15 +17,21 @@ export class LoadingService {
shareReplay(1) shareReplay(1)
); );
public startLoading(type: string): void { public startLoading(type: string,request:string): void {
if (!this.loading$.value.includes(type)) { if (!this.loading$.value.includes(type)) {
this.loading$.next(this.loading$.value.concat(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 { public stopLoading(type: string): void {
if (this.loading$.value.includes(type)) { if (this.loading$.value.includes(type)) {
this.loading$.next(this.loading$.value.filter((t) => t !== type)); this.loading$.next(this.loading$.value.filter((t) => t !== type));
this.loadingTextSubject.next(null);
} }
} }
} }

View File

@ -42,7 +42,9 @@ export class SelectOptionsService {
getBusiness(value:string):string{ getBusiness(value:string):string{
return this.typesOfBusiness.find(t=>t.value===value)?.name 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{ getListingsCategory(value:string):string{
return this.listingCategories.find(l=>l.value===value)?.name return this.listingCategories.find(l=>l.value===value)?.name
} }

View File

@ -4,7 +4,7 @@ import { jwtDecode } from 'jwt-decode';
import { Observable, distinctUntilChanged, filter, from, lastValueFrom, map } from 'rxjs'; import { Observable, distinctUntilChanged, filter, from, lastValueFrom, map } from 'rxjs';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { KeycloakService } from './keycloak.service'; 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 { environment } from '../../environments/environment';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
@ -103,4 +103,7 @@ export class UserService {
async getById(id:string):Promise<User>{ async getById(id:string):Promise<User>{
return await lastValueFrom(this.http.get<User>(`${this.apiBaseUrl}/bizmatch/user/${id}`)); 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));
}
} }

View File

@ -32,13 +32,11 @@ export interface Listing {
} }
export interface BusinessListing extends Listing { export interface BusinessListing extends Listing {
listingsCategory: 'business'; //enum listingsCategory: 'business'; //enum
// summary: Array<string>;
realEstateIncluded?: boolean; realEstateIncluded?: boolean;
leasedLocation?:boolean; leasedLocation?:boolean;
franchiseResale?:boolean; franchiseResale?:boolean;
salesRevenue?: number; salesRevenue?: number;
cashFlow?: number; cashFlow?: number;
// netProfit?: number;
supportAndTraining?: string; supportAndTraining?: string;
employees?: number; employees?: number;
established?: number; established?: number;
@ -49,24 +47,17 @@ export interface BusinessListing extends Listing {
} }
export interface CommercialPropertyListing extends Listing { export interface CommercialPropertyListing extends Listing {
listingsCategory: 'commercialProperty'; //enum listingsCategory: 'commercialProperty'; //enum
images:string[];
zipCode:number; zipCode:number;
county:string county:string
email?: string; email?: string;
website?: string; website?: string;
phoneNumber?: string; phoneNumber?: string;
hasImages:boolean;
} }
export type ListingType = export type ListingType =
| BusinessListing | BusinessListing
| CommercialPropertyListing; | CommercialPropertyListing;
// export interface ProfessionalsBrokersListing extends Listing {
// listingsCategory: 'professionals_brokers'; //enum
// summary: string;
// address?: string;
// email?: string;
// website?: string;
// category?: 'Professionals' | 'Broker';
// }
export interface ListingCriteria { export interface ListingCriteria {
type:string, type:string,
state:string, state:string,
@ -82,11 +73,15 @@ export interface UserBase {
lastname: string; lastname: string;
email: string; email: string;
phoneNumber?: string; phoneNumber?: string;
description?:string;
companyName?:string;
companyOverview?:string; companyOverview?:string;
companyWebsite?:string; companyWebsite?:string;
companyLocation?:string; companyLocation?:string;
offeredServices?:string; offeredServices?:string;
areasServed?:string; areasServed?:string;
hasProfile?:boolean;
hasCompanyLogo?:boolean;
} }
export interface User extends UserBase { export interface User extends UserBase {
licensedIn?:KeyValue[]; licensedIn?:KeyValue[];