broker direcrtory renewed, imageservice updated, demo data

This commit is contained in:
Andreas Knuth 2024-03-25 20:17:49 +01:00
parent 73ab12a694
commit 840d7a63b1
30 changed files with 616 additions and 212 deletions

View File

@ -30,19 +30,33 @@ export class FileService {
return this.subscriptions return this.subscriptions
} }
async storeProfilePicture(file: Express.Multer.File, userId: string) { async storeProfilePicture(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/profile/${userId}`, file.buffer); // await fs.outputFile(`./pictures/profile/${userId}`, file.buffer);
let quality = 50;
const output = await sharp(file.buffer)
.resize({ width: 300 })
.avif({ quality }) // Verwende AVIF
//.webp({ quality }) // Verwende Webp
.toBuffer();
await sharp(output).toFile(`./pictures/profile/${userId}.avif`);
} }
hasProfile(userId: string){ hasProfile(userId: string){
return fs.existsSync(`./pictures/profile/${userId}`) return fs.existsSync(`./pictures/profile/${userId}.avif`)
} }
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); let quality = 50;
const output = await sharp(file.buffer)
.resize({ width: 300 })
.avif({ quality }) // Verwende AVIF
//.webp({ quality }) // Verwende Webp
.toBuffer();
await sharp(output).toFile(`./pictures/logo/${userId}.avif`); // Ersetze Dateierweiterung
// await fs.outputFile(`./pictures/logo/${userId}`, file.buffer);
} }
hasCompanyLogo(userId: string){ hasCompanyLogo(userId: string){
return fs.existsSync(`./pictures/logo/${userId}`) return fs.existsSync(`./pictures/logo/${userId}.avif`)
} }
async getPropertyImages(listingId: string): Promise<ImageProperty[]> { async getPropertyImages(listingId: string): Promise<ImageProperty[]> {
@ -103,13 +117,28 @@ export class FileService {
//.webp({ quality }) // Verwende Webp //.webp({ quality }) // Verwende Webp
.toBuffer(); .toBuffer();
if (output.byteLength > maxSize) { // if (output.byteLength > maxSize) {
quality -= 5; // Justiere Qualität in feineren Schritten // quality -= 5; // Justiere Qualität in feineren Schritten
} // }
// } while (output.byteLength > maxSize && quality > 0); // } while (output.byteLength > maxSize && quality > 0);
await sharp(output).toFile(`${directory}/${imageName}.avif`); // Ersetze Dateierweiterung await sharp(output).toFile(`${directory}/${imageName}.avif`); // Ersetze Dateierweiterung
let timeTaken = Date.now() - start; let timeTaken = Date.now() - start;
this.logger.info(`Quality: ${quality} - Time: ${timeTaken} milliseconds`) this.logger.info(`Quality: ${quality} - Time: ${timeTaken} milliseconds`)
} }
getProfileImagesForUsers(userids:string){
const ids = userids.split(',');
let result = {};
for (const id of ids){
result = {...result,[id]:fs.existsSync(`./pictures/profile/${id}.avif`)}
}
return result;
}
getCompanyLogosForUsers(userids:string){
const ids = userids.split(',');
let result = {};
for (const id of ids){
result = {...result,[id]:fs.existsSync(`./pictures/logo/${id}.avif`)}
}
return result;
}
} }

View File

@ -23,13 +23,22 @@ export class ImageController {
@Post('uploadProfile/:id') @Post('uploadProfile/:id')
@UseInterceptors(FileInterceptor('file'),) @UseInterceptors(FileInterceptor('file'),)
uploadProfile(@UploadedFile() file: Express.Multer.File,@Param('id') id:string) { async uploadProfile(@UploadedFile() file: Express.Multer.File,@Param('id') id:string) {
this.fileService.storeProfilePicture(file,id); await this.fileService.storeProfilePicture(file,id);
} }
@Post('uploadCompanyLogo/:id') @Post('uploadCompanyLogo/:id')
@UseInterceptors(FileInterceptor('file'),) @UseInterceptors(FileInterceptor('file'),)
uploadCompanyLogo(@UploadedFile() file: Express.Multer.File,@Param('id') id:string) { async uploadCompanyLogo(@UploadedFile() file: Express.Multer.File,@Param('id') id:string) {
this.fileService.storeCompanyLogo(file,id); await this.fileService.storeCompanyLogo(file,id);
}
@Get('profileImages/:userids')
async getProfileImagesForUsers(@Param('userids') userids:string): Promise<any> {
return await this.fileService.getProfileImagesForUsers(userids);
}
@Get('companyLogos/:userids')
async getCompanyLogosForUsers(@Param('userids') userids:string): Promise<any> {
return await this.fileService.getCompanyLogosForUsers(userids);
} }
} }

View File

@ -7,22 +7,30 @@ import { UserEntity } from 'src/models/server.model.js';
@Controller('user') @Controller('user')
export class UserController { export class UserController {
constructor(private userService:UserService,@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger){} constructor(private userService: UserService, @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {}
@Get(':id') @Get(':id')
findById(@Param('id') id:string): any { findById(@Param('id') id: string): any {
return this.userService.getUserById(id); this.logger.info(`Searching for user with ID: ${id}`);
} const user = this.userService.getUserById(id);
this.logger.info(`Found user: ${JSON.stringify(user)}`);
return user;
}
@Post() @Post()
save(@Body() user: any):Promise<UserEntity>{ save(@Body() user: any): Promise<UserEntity> {
this.logger.info(`User persisted: `); this.logger.info(`Saving user: ${JSON.stringify(user)}`);
return this.userService.saveUser(user); const savedUser = this.userService.saveUser(user);
} this.logger.info(`User persisted: ${JSON.stringify(savedUser)}`);
return savedUser;
}
@Post('search') @Post('search')
find(@Body() criteria: any): any { find(@Body() criteria: any): any {
return this.userService.findUser(criteria); this.logger.info(`Searching for users with criteria: ${JSON.stringify(criteria)}`);
} const foundUsers = this.userService.findUser(criteria);
this.logger.info(`Found users: ${JSON.stringify(foundUsers)}`);
return foundUsers;
}
} }

View File

@ -19,7 +19,7 @@ export class UserService {
companyWebsite:{ type: 'string' }, companyWebsite:{ type: 'string' },
companyLocation:{ type: 'string' }, companyLocation:{ type: 'string' },
offeredServices:{ type: 'string' }, offeredServices:{ type: 'string' },
areasServed:{ type: 'string' }, areasServed:{ type: 'string[]' },
names:{ type: 'string[]', path:'$.licensedIn.name' }, names:{ type: 'string[]', path:'$.licensedIn.name' },
values:{ type: 'string[]', path:'$.licensedIn.value' } values:{ type: 'string[]', path:'$.licensedIn.value' }
}, { }, {

View File

@ -31,7 +31,8 @@
"src/assets" "src/assets"
], ],
"styles": [ "styles": [
"src/styles.scss" "src/styles.scss",
"node_modules/quill/dist/quill.snow.css"
] ]
}, },
"configurations": { "configurations": {

View File

@ -39,6 +39,7 @@
"primeflex": "^3.3.1", "primeflex": "^3.3.1",
"primeicons": "^6.0.1", "primeicons": "^6.0.1",
"primeng": "^17.10.0", "primeng": "^17.10.0",
"quill": "^1.3.7",
"rxjs": "~7.8.1", "rxjs": "~7.8.1",
"tslib": "^2.3.0", "tslib": "^2.3.0",
"urlcat": "^3.1.0", "urlcat": "^3.1.0",

View File

@ -42,7 +42,7 @@ export const routes: Routes = [
component: DetailsUserComponent, component: DetailsUserComponent,
}, },
{ {
path: 'account', path: 'account/:id',
component: AccountComponent, component: AccountComponent,
canActivate: [authGuard], canActivate: [authGuard],
}, },

View File

@ -4,7 +4,6 @@
<div class="col-12 md:col-3 md:mb-0 mb-3"> <div class="col-12 md:col-3 md:mb-0 mb-3">
<img src="assets/images/header-logo.png" alt="footer sections" height="30" class="mr-3"> <img src="assets/images/header-logo.png" alt="footer sections" height="30" class="mr-3">
<div class="text-500">© 2024 Bizmatch All rights reserved.</div> <div class="text-500">© 2024 Bizmatch All rights reserved.</div>
<!-- <div class="text-gray-300 font-bold text-5xl">Bastion</div> -->
</div> </div>
<div class="col-12 md:col-3"> <div class="col-12 md:col-3">
<div class="text-black mb-4 flex flex-wrap" style="max-width: 290px">BizMatch, Inc., 1001 Blucher Street, Corpus Christi, Texas 78401</div> <div class="text-black mb-4 flex flex-wrap" style="max-width: 290px">BizMatch, Inc., 1001 Blucher Street, Corpus Christi, Texas 78401</div>
@ -19,7 +18,7 @@
<div class="col-12 md:col-3 text-500"> <div class="col-12 md:col-3 text-500">
<div class="text-black font-bold line-height-3 mb-3">Actions</div> <div class="text-black font-bold line-height-3 mb-3">Actions</div>
<a *ngIf="!userService.isLoggedIn()" (click)="login()" class="text-500 line-height-3 block cursor-pointer mb-2 no-underline">Login</a> <a *ngIf="!userService.isLoggedIn()" (click)="login()" class="text-500 line-height-3 block cursor-pointer mb-2 no-underline">Login</a>
<a *ngIf="userService.isLoggedIn()" [routerLink]="['/account']" class="text-500 line-height-3 block cursor-pointer mb-2 no-underline">Account</a> <a *ngIf="userService.isLoggedIn()" [routerLink]="['/account',userService.getUser()?.id]" class="text-500 line-height-3 block cursor-pointer mb-2 no-underline">Account</a>
<a *ngIf="userService.isLoggedIn()" class="text-500 line-height-3 block cursor-pointer mb-2 no-underline" (click)="userService.logout()">Log Out</a> <a *ngIf="userService.isLoggedIn()" class="text-500 line-height-3 block cursor-pointer mb-2 no-underline" (click)="userService.logout()">Log Out</a>
</div> </div>
</div> </div>

View File

@ -21,8 +21,8 @@ import { User } from '../../../../../common-models/src/main.model';
}) })
export class HeaderComponent { export class HeaderComponent {
public buildVersion = environment.buildVersion; public buildVersion = environment.buildVersion;
user:User;
user$:Observable<User> user$:Observable<User>
user:User;
public tabItems: MenuItem[]; public tabItems: MenuItem[];
public menuItems: MenuItem[]; public menuItems: MenuItem[];
activeItem activeItem
@ -33,6 +33,60 @@ export class HeaderComponent {
ngOnInit(){ ngOnInit(){
this.user$=this.userService.getUserObservable(); this.user$=this.userService.getUserObservable();
this.user$.subscribe(u=>{
this.user=u;
this.menuItems = [
{
label: 'User Actions',
icon: 'fas fa-cog',
items: [
{
label: 'Account',
icon: 'pi pi-user',
routerLink: `/account/${this.user.id}`,
visible: this.isUserLogedIn()
},
{
label: 'Create Listing',
icon: 'pi pi-plus-circle',
routerLink: "/createListing",
visible: this.isUserLogedIn()
},
{
label: 'My Listings',
icon: 'pi pi-list',
routerLink:"/myListings",
visible: this.isUserLogedIn()
},
{
label: 'My Favorites',
icon: 'pi pi-star',
routerLink:"/myFavorites",
visible: this.isUserLogedIn()
},
{
label: 'EMail Us',
icon: 'fa-regular fa-envelope',
routerLink:"/emailUs",
visible: this.isUserLogedIn()
},
{
label: 'Logout',
icon: 'fa-solid fa-right-from-bracket',
routerLink:"/logout",
visible: this.isUserLogedIn()
},
{
label: 'Login',
icon: 'fa-solid fa-right-from-bracket',
//routerLink:"/account",
command: () => this.login(),
visible: !this.isUserLogedIn()
},
]
}
]
});
this.tabItems = [ this.tabItems = [
{ {
label: 'Businesses for Sale', label: 'Businesses for Sale',
@ -50,63 +104,13 @@ export class HeaderComponent {
fragment:'' fragment:''
} }
]; ];
this.menuItems = [
{
label: 'User Actions',
icon: 'fas fa-cog',
items: [
{
label: 'Account',
icon: 'pi pi-user',
routerLink: '/account',
visible: this.isUserLoogedIn()
},
{
label: 'Create Listing',
icon: 'pi pi-plus-circle',
routerLink: "/createListing",
visible: this.isUserLoogedIn()
},
{
label: 'My Listings',
icon: 'pi pi-list',
routerLink:"/myListings",
visible: this.isUserLoogedIn()
},
{
label: 'My Favorites',
icon: 'pi pi-star',
routerLink:"/myFavorites",
visible: this.isUserLoogedIn()
},
{
label: 'EMail Us',
icon: 'fa-regular fa-envelope',
routerLink:"/emailUs",
visible: this.isUserLoogedIn()
},
{
label: 'Logout',
icon: 'fa-solid fa-right-from-bracket',
routerLink:"/logout",
visible: this.isUserLoogedIn()
},
{
label: 'Login',
icon: 'fa-solid fa-right-from-bracket',
//routerLink:"/account",
command: () => this.login(),
visible: !this.isUserLoogedIn()
},
]
}
]
this.activeItem=this.tabItems[0]; this.activeItem=this.tabItems[0];
} }
navigateWithState(dest: string, state: any) { navigateWithState(dest: string, state: any) {
this.router.navigate([dest], { state: state }); this.router.navigate([dest], { state: state });
} }
isUserLoogedIn(){ isUserLogedIn(){
return this.userService?.isLoggedIn(); return this.userService?.isLoggedIn();
} }
login(){ login(){

View File

@ -8,8 +8,12 @@
<div class="surface-section px-6 pt-5"> <div class="surface-section px-6 pt-5">
<div class="flex align-items-start flex-column lg:flex-row lg:justify-content-between"> <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"> <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" @if(user.hasProfile){
style="width:90px;height:90px" /> <img src="{{environment.apiBaseUrl}}/profile/{{user.id}}.avif" class="mr-5 mb-3 lg:mb-0"
style="width:90px" />
} @else {
<img src="assets/images/person_placeholder.jpg" class="mr-5 mb-3 lg:mb-0" style="width:90px" />
}
<div> <div>
<span class="text-900 font-medium text-3xl">{{user.firstname}} {{user.lastname}}</span> <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> <i class="pi pi-star text-2xl ml-4 text-yellow-500"></i>
@ -28,8 +32,14 @@
</div> </div>
<div class="flex align-items-center mt-3"> <div class="flex align-items-center mt-3">
<!-- <span class="font-medium text-500">Logo</span> --> <!-- <span class="font-medium text-500">Logo</span> -->
<div ><img src="{{environment.apiBaseUrl}}/logo/{{user.id}}" <div >
class="mr-5 lg:mb-0" style="width:100px;height:30px" /></div> @if(user.hasCompanyLogo){
<img src="{{environment.apiBaseUrl}}/logo/{{user.id}}.avif"
class="mr-5 lg:mb-0" style="height:60px;max-width:100px" />
}
<!-- <img *ngIf="!user.hasCompanyLogo" src="assets/images/placeholder.png"
class="mr-5 lg:mb-0" style="height:60px;max-width:100px" /> -->
</div>
<!-- <div class="text-700 mt-2">130</div> --> <!-- <div class="text-700 mt-2">130</div> -->
</div> </div>
</div> </div>
@ -37,18 +47,13 @@
</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>
<p class="mt-2 text-700 line-height-3 text-l font-semibold">{{user.description}}</p> <p class="mt-2 text-700 line-height-3 text-l font-semibold">{{user.description}}</p>
</div> </div>
<div class="px-6 py-5"> <div class="px-6 py-5">
<div class="surface-card p-4 shadow-2 border-round"> <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="font-medium text-3xl text-900 mb-3">Company Profile</div>
<div class="text-500 mb-5">{{user.companyOverview}}</div> <div class="text-500 mb-5" [innerHTML]="companyOverview"></div>
<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">
<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">Name</div> <div class="text-500 w-full md:w-2 font-medium">Name</div>
@ -67,14 +72,10 @@
<div class="text-900 w-full md:w-10 line-height-3">{{user.companyLocation}}</div> <div class="text-900 w-full md:w-10 line-height-3">{{user.companyLocation}}</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">Company Overview</div> <div class="text-500 w-full md:w-2 font-medium">Services we offer</div>
<div class="text-900 w-full md:w-10 line-height-3">{{user.companyOverview}}</div> <div class="text-900 w-full md:w-10" [innerHTML]="offeredServices"></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">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 surface-ground">
<div class="text-500 w-full md:w-2 font-medium">Areas we serve</div> <div class="text-500 w-full md:w-2 font-medium">Areas we serve</div>
<div class="text-900 w-full md:w-10"> <div class="text-900 w-full md:w-10">
@for (area of user.areasServed; track area) { @for (area of user.areasServed; track area) {
@ -123,7 +124,7 @@
</div> </div>
@if( user?.id===(user$| async)?.id || isAdmin()){ @if( user?.id===(user$| async)?.id || isAdmin()){
<button pButton pRipple label="Edit" icon="pi pi-file-edit" class="w-auto" <button pButton pRipple label="Edit" icon="pi pi-file-edit" class="w-auto"
[routerLink]="['/account']"></button> [routerLink]="['/account',user.id]"></button>
} }
</div> </div>

View File

@ -0,0 +1,3 @@
:host ::ng-deep p {
margin:3px 0;
}

View File

@ -9,6 +9,8 @@ import { UserService } from '../../../services/user.service';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { ListingsService } from '../../../services/listings.service'; import { ListingsService } from '../../../services/listings.service';
import { SelectOptionsService } from '../../../services/select-options.service'; import { SelectOptionsService } from '../../../services/select-options.service';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { ImageService } from '../../../services/image.service';
@Component({ @Component({
selector: 'app-details-user', selector: 'app-details-user',
@ -25,18 +27,25 @@ export class DetailsUserComponent {
environment = environment; environment = environment;
criteria:ListingCriteria; criteria:ListingCriteria;
userListings:BusinessListing[] userListings:BusinessListing[]
companyOverview:SafeHtml;
offeredServices:SafeHtml;
constructor(private activatedRoute: ActivatedRoute, constructor(private activatedRoute: ActivatedRoute,
private router: Router, private router: Router,
private userService: UserService, private userService: UserService,
private listingsService:ListingsService, private listingsService:ListingsService,
private messageService: MessageService, private messageService: MessageService,
public selectOptions: SelectOptionsService) { public selectOptions: SelectOptionsService,
private sanitizer: DomSanitizer,
private imageService:ImageService) {
} }
async ngOnInit() { async ngOnInit() {
this.user = await this.userService.getById(this.id); this.user = await this.userService.getById(this.id);
this.userListings = await this.listingsService.getListingByUserId(this.id); this.userListings = await this.listingsService.getListingByUserId(this.id);
this.user$ = this.userService.getUserObservable(); this.user$ = this.userService.getUserObservable();
this.companyOverview=this.sanitizer.bypassSecurityTrustHtml(this.user.companyOverview);
this.offeredServices=this.sanitizer.bypassSecurityTrustHtml(this.user.offeredServices);
} }
back() { back() {
this.router.navigate(['listings', this.criteria.listingsCategory]) this.router.navigate(['listings', this.criteria.listingsCategory])

View File

@ -5,31 +5,13 @@
<div <div
class="align-items-center flex-grow-1 justify-content-between hidden lg:flex absolute lg:static w-full left-0 top-100 px-6 lg:px-0 shadow-2 lg:shadow-none z-2"> class="align-items-center flex-grow-1 justify-content-between hidden lg:flex absolute lg:static w-full left-0 top-100 px-6 lg:px-0 shadow-2 lg:shadow-none z-2">
<section></section> <section></section>
<!-- <ul
class="list-none p-0 m-0 flex lg:align-items-center text-blue-900 select-none flex-column lg:flex-row cursor-pointer">
<li>
<a pRipple
class="flex px-0 lg:px-5 py-3 hover:text-blue-600 font-medium transition-colors transition-duration-150">
<span>Corporate</span>
</a>
</li>
<li>
<a pRipple
class="flex px-0 lg:px-5 py-3 hover:text-blue-600 font-medium transition-colors transition-duration-150">
<span>Resources</span>
</a>
</li>
<li>
<a pRipple
class="flex px-0 lg:px-5 py-3 hover:text-blue-600 font-medium transition-colors transition-duration-150">
<span>Pricing</span>
</a>
</li>
</ul> -->
<div <div
class="flex justify-content-between lg:block border-top-1 lg:border-top-none border-gray-800 py-3 lg:py-0 mt-3 lg:mt-0"> class="flex justify-content-between lg:block border-top-1 lg:border-top-none border-gray-800 py-3 lg:py-0 mt-3 lg:mt-0">
<!-- <p-button label="Account" class="ml-3 font-bold" [outlined]="true" severity="secondary" [routerLink]="['/account']"></p-button> --> @if(userService.isLoggedIn()){
<p-button label="Account" class="ml-3 font-bold" [outlined]="true" severity="secondary" (click)="account()"></p-button> <p-button label="Account" class="ml-3 font-bold" [outlined]="true" severity="secondary" [routerLink]="['/account',(user$|async).id]"></p-button>
} @else {
<p-button label="Log In" class="ml-3 font-bold" [outlined]="true" severity="secondary" (click)="login()"></p-button>
}
</div> </div>
</div> </div>
</div> </div>

View File

@ -12,7 +12,8 @@ import { SelectOptionsService } from '../../services/select-options.service';
import { UserService } from '../../services/user.service'; import { UserService } from '../../services/user.service';
import onChange from 'on-change'; import onChange from 'on-change';
import { getCriteriaStateObject, getSessionStorageHandler } from '../../utils/utils'; import { getCriteriaStateObject, getSessionStorageHandler } from '../../utils/utils';
import { ListingCriteria } from '../../../../../common-models/src/main.model'; import { ListingCriteria, User } from '../../../../../common-models/src/main.model';
import { Observable } from 'rxjs';
@Component({ @Component({
selector: 'app-home', selector: 'app-home',
standalone: true, standalone: true,
@ -26,20 +27,20 @@ export class HomeComponent {
maxPrice:string; maxPrice:string;
minPrice:string; minPrice:string;
criteria:ListingCriteria criteria:ListingCriteria
user$:Observable<User>
public constructor(private router: Router,private activatedRoute: ActivatedRoute, public selectOptions:SelectOptionsService, public userService:UserService) { public constructor(private router: Router,private activatedRoute: ActivatedRoute, public selectOptions:SelectOptionsService, public userService:UserService) {
this.criteria = onChange(getCriteriaStateObject(),getSessionStorageHandler); this.criteria = onChange(getCriteriaStateObject(),getSessionStorageHandler);
} }
ngOnInit(){ ngOnInit(){
this.user$=this.userService.getUserObservable();
} }
search(){ search(){
this.router.navigate([`listings/${this.activeTabAction}`]) this.router.navigate([`listings/${this.activeTabAction}`])
} }
account(){
setTimeout(()=>{ login(){
this.router.navigate([`account`]) this.userService.login(window.location.href);
},10); }
}
} }

View File

@ -121,11 +121,15 @@
} }
@for (user of users; track user.id) { @for (user of users; track user.id) {
<div class="col-12 lg:col-6 xl:col-4 p-4"> <div class="col-12 lg:col-6 xl:col-4 p-4 flex flex-column flex-grow-1">
<div class="surface-card shadow-2 p-2" style="border-radius: 10px"> <div class="surface-card shadow-2 p-2 flex flex-column flex-grow-1 justify-content-between" 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" > <div class="surface-card p-4 flex flex-column align-items-center md:flex-row md:align-items-stretch h-full" >
<span> <span>
<img src="{{environment.apiBaseUrl}}/profile/{{user.id}}" class="w-5rem" /> @if(user.hasProfile){
<img src="{{environment.apiBaseUrl}}/profile/{{user.id}}.avif" class="w-5rem" />
} @else {
<img src="assets/images/person_placeholder.jpg" class="w-5rem" />
}
</span> </span>
<div class="flex flex-column align-items-center md:align-items-stretch ml-4 mt-4 md:mt-0"> <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> <p class="mt-0 mb-3 line-height-3 text-center md:text-left">{{user.description}}</p>
@ -134,7 +138,11 @@
</div> </div>
</div> </div>
<div class="px-4 py-3 text-right flex justify-content-between align-items-center"> <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"/> @if(user.hasCompanyLogo){
<img src="{{environment.apiBaseUrl}}/logo/{{user.id}}.avif" class="rounded-image"/>
} @else {
<img src="assets/images/placeholder.png" class="rounded-image"/>
}
<button pButton pRipple icon="pi pi-arrow-right" iconPos="right" label="View Full profile" <button pButton pRipple icon="pi pi-arrow-right" iconPos="right" label="View Full profile"
class="p-button-rounded p-button-success" [routerLink]="['/details-user',user.id]"></button> class="p-button-rounded p-button-success" [routerLink]="['/details-user',user.id]"></button>

View File

@ -14,8 +14,9 @@
} }
.rounded-image { .rounded-image {
border-radius: 6px; border-radius: 6px;
width: 100px; // width: 100px;
height: 25px; max-width: 100px;
height: 45px;
border: 1px solid rgba(0,0,0,0.2); border: 1px solid rgba(0,0,0,0.2);
padding: 1px 1px; padding: 1px 1px;
object-fit: contain; object-fit: contain;

View File

@ -18,6 +18,7 @@ import { InitEditableRow } from 'primeng/table';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
import { ListingCriteria, ListingType, User } from '../../../../../common-models/src/main.model'; import { ListingCriteria, ListingType, User } from '../../../../../common-models/src/main.model';
import { UserService } from '../../services/user.service'; import { UserService } from '../../services/user.service';
import { ImageService } from '../../services/image.service';
@Component({ @Component({
selector: 'app-listings', selector: 'app-listings',
standalone: true, standalone: true,
@ -49,7 +50,8 @@ export class ListingsComponent {
private userService:UserService, private userService:UserService,
private activatedRoute: ActivatedRoute, private activatedRoute: ActivatedRoute,
private router:Router, private router:Router,
private cdRef:ChangeDetectorRef) { private cdRef:ChangeDetectorRef,
private imageService:ImageService) {
this.criteria = onChange(getCriteriaStateObject(),getSessionStorageHandler); this.criteria = onChange(getCriteriaStateObject(),getSessionStorageHandler);
this.router.getCurrentNavigation() this.router.getCurrentNavigation()
this.activatedRoute.snapshot this.activatedRoute.snapshot
@ -80,6 +82,12 @@ export class ListingsComponent {
this.listings=[] this.listings=[]
this.filteredListings=[]; this.filteredListings=[];
this.users=await this.userService.search(this.criteria); this.users=await this.userService.search(this.criteria);
const profiles = await this.imageService.getProfileImagesForUsers(this.users.map(u=>u.id));
const logos = await this.imageService.getCompanyLogosForUsers(this.users.map(u=>u.id));
this.users.forEach(u=>{
u.hasProfile=profiles[u.id]
u.hasCompanyLogo=logos[u.id]
})
this.cdRef.markForCheck(); this.cdRef.markForCheck();
this.cdRef.detectChanges(); this.cdRef.detectChanges();
} }

View File

@ -1,4 +1,3 @@
<div class="surface-ground px-4 py-8 md:px-6 lg:px-8"> <div class="surface-ground px-4 py-8 md:px-6 lg:px-8">
<div class="p-fluid flex flex-column lg:flex-row"> <div class="p-fluid flex flex-column lg:flex-row">
<menu-account></menu-account> <menu-account></menu-account>
@ -16,7 +15,8 @@
<div class="mb-4"> <div class="mb-4">
<label for="state" class="block font-medium text-900 mb-2">E-mail (required)</label> <label for="state" class="block font-medium text-900 mb-2">E-mail (required)</label>
<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="grid"> <div class="grid">
<div class="mb-4 col-12 md:col-6"> <div class="mb-4 col-12 md:col-6">
@ -48,60 +48,109 @@
<input id="companyWebsite" type="text" pInputText [(ngModel)]="user.companyWebsite"> <input id="companyWebsite" type="text" pInputText [(ngModel)]="user.companyWebsite">
</div> </div>
<div class="mb-4 col-12 md:col-4"> <div class="mb-4 col-12 md:col-4">
<label for="companyLocation" class="block font-medium text-900 mb-2">Company Location</label> <label for="companyLocation" class="block font-medium text-900 mb-2">Company
<p-autoComplete [(ngModel)]="user.companyLocation" [suggestions]="suggestions" (completeMethod)="search($event)"></p-autoComplete> Location</label>
<p-autoComplete [(ngModel)]="user.companyLocation" [suggestions]="suggestions"
(completeMethod)="search($event)"></p-autoComplete>
</div> </div>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label for="companyOverview" class="block font-medium text-900 mb-2">Company Overview</label> <label for="companyOverview" class="block font-medium text-900 mb-2">Company Overview</label>
<textarea id="companyOverview" type="text" pInputTextarea rows="5" [autoResize]="true" [(ngModel)]="user.companyOverview"></textarea> <p-editor [(ngModel)]="user.companyOverview" [style]="{ height: '320px' }">
<ng-template pTemplate="header">
<span class="ql-formats">
<button type="button" class="ql-bold" aria-label="Bold"></button>
<button type="button" class="ql-italic" aria-label="Italic"></button>
<button type="button" class="ql-underline" aria-label="Underline"></button>
<button value="ordered" aria-label="Ordered List" type="button"
class="ql-list"></button>
<button value="bullet" aria-label="Unordered List" type="button"
class="ql-list"></button>
</span>
</ng-template>
</p-editor>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label for="areasServed" class="block font-medium text-900 mb-2">Areas We Serve</label> <label for="companyOverview" class="block font-medium text-900 mb-2">Services We offer</label>
<textarea id="areasServed" type="text" pInputTextarea rows="5" [autoResize]="true" [(ngModel)]="user.areasServed"></textarea> <p-editor [(ngModel)]="user.offeredServices" [style]="{ height: '320px' }">
<ng-template pTemplate="header">
<span class="ql-formats">
<button type="button" class="ql-bold" aria-label="Bold"></button>
<button type="button" class="ql-italic" aria-label="Italic"></button>
<button type="button" class="ql-underline" aria-label="Underline"></button>
<button value="ordered" aria-label="Ordered List" type="button"
class="ql-list"></button>
<button value="bullet" aria-label="Unordered List" type="button"
class="ql-list"></button>
</span>
</ng-template>
</p-editor>
</div> </div>
<div >
<div class="mb-4">
<label for="areasServed" class="block font-medium text-900 mb-2">Areas We Serve</label>
<textarea id="areasServed" type="text" pInputTextarea rows="5" [autoResize]="true"
[(ngModel)]="user.areasServed"></textarea>
</div>
<div>
<label for="companyOverview" class="block font-medium text-900 mb-2">Licensed In</label> <label for="companyOverview" class="block font-medium text-900 mb-2">Licensed In</label>
@for (licensedIn of user.licensedIn; track licensedIn.value){ @for (licensedIn of user.licensedIn; track licensedIn.value){
<div class="grid"> <div class="grid">
<div class="flex col-12 md:col-6"> <div class="flex col-12 md:col-6">
<p-dropdown id="states" [options]="selectOptions?.states" [(ngModel)]="licensedIn.name" optionLabel="name" optionValue="value" [showClear]="true" placeholder="State" [ngStyle]="{ width: '100%'}"></p-dropdown> <p-dropdown id="states" [options]="selectOptions?.states" [(ngModel)]="licensedIn.name"
optionLabel="name" optionValue="value" [showClear]="true" placeholder="State"
[ngStyle]="{ width: '100%'}"></p-dropdown>
</div> </div>
<div class="flex col-12 md:col-6"> <div class="flex col-12 md:col-6">
<input id="companyWebsite" type="text" pInputText [(ngModel)]="licensedIn.value" placeholder="Licence Number"> <input id="companyWebsite" type="text" pInputText [(ngModel)]="licensedIn.value"
placeholder="Licence Number">
</div> </div>
</div> </div>
} }
</div> </div>
<div class="field mb-5 col-12 md:col-6 flex align-items-center"> <div class="field mb-5 col-12 md:col-6 flex align-items-center">
<p-button class="mr-1" icon="pi pi-plus" severity="success" (click)="addLicence()"></p-button> <p-button class="mr-1" icon="pi pi-plus" severity="success" (click)="addLicence()"></p-button>
<p-button icon="pi pi-minus" severity="danger" (click)="removeLicence()" [disabled]="user.licensedIn?.length<2"></p-button> <p-button icon="pi pi-minus" severity="danger" (click)="removeLicence()"
[disabled]="user.licensedIn?.length<2"></p-button>
<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> <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>
</div> </div>
</div> </div>
<div> <div>
<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" class="rounded-logo"/> @if(user.hasCompanyLogo){
<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> <img src="{{environment.apiBaseUrl}}/logo/{{user.id}}.avif" class="rounded-profile" />
} @else {
<img src="assets/images/placeholder.png" class="rounded-profile" />
}
<p-fileUpload #companyUpload mode="basic" chooseLabel="Upload" name="file" [customUpload]="true"
accept="image/*" [maxFileSize]="maxFileSize" (onSelect)="select($event,'company')"
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" class="rounded-profile"/> @if(user.hasProfile){
<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> <img src="{{environment.apiBaseUrl}}/profile/{{user.id}}.avif" class="rounded-profile" />
} @else {
<img src="assets/images/person_placeholder.jpg" class="rounded-profile" />
}
<p-fileUpload #profileUpload mode="basic" chooseLabel="Upload" name="file" [customUpload]="true"
accept="image/*" [maxFileSize]="maxFileSize" (onSelect)="select($event,'profile')"
styleClass="p-button-outlined p-button-plain p-button-rounded mt-4"></p-fileUpload>
</div> </div>
</div> </div>
</div> </div>
<div class="text-900 font-semibold text-lg mt-3">Membership Level</div> <div class="text-900 font-semibold text-lg mt-3">Membership Level</div>
<p-divider></p-divider> <p-divider></p-divider>
<p-table [value]="userSubscriptions" [tableStyle]="{ 'min-width': '50rem' }" dataKey="id"> <p-table [value]="userSubscriptions" [tableStyle]="{ 'min-width': '50rem' }" dataKey="id">
<ng-template pTemplate="header"> <ng-template pTemplate="header">
<tr> <tr>
<th style="width: 5rem"></th> <th style="width: 5rem"></th>
@ -116,7 +165,9 @@
<ng-template pTemplate="body" let-subscription let-expanded="expanded"> <ng-template pTemplate="body" let-subscription let-expanded="expanded">
<tr> <tr>
<td> <td>
<button type="button" pButton pRipple [pRowToggler]="subscription" class="p-button-text p-button-rounded p-button-plain" [icon]="expanded ? 'pi pi-chevron-down' : 'pi pi-chevron-right'"></button> <button type="button" pButton pRipple [pRowToggler]="subscription"
class="p-button-text p-button-rounded p-button-plain"
[icon]="expanded ? 'pi pi-chevron-down' : 'pi pi-chevron-right'"></button>
</td> </td>
<td>{{ subscription.id }}</td> <td>{{ subscription.id }}</td>
<td>{{ subscription.level }}</td> <td>{{ subscription.level }}</td>
@ -132,31 +183,51 @@
<div class="p-3"> <div class="p-3">
<p-table [value]="subscription.invoices" dataKey="id"> <p-table [value]="subscription.invoices" dataKey="id">
<ng-template pTemplate="header"> <ng-template pTemplate="header">
<tr> <tr>
<th style="width: 5rem"></th> <th style="width: 5rem"></th>
<th>ID</th> <th>ID</th>
<th>Date</th> <th>Date</th>
<th>Price</th> <th>Price</th>
</tr> </tr>
</ng-template> </ng-template>
<ng-template pTemplate="body" let-invoice> <ng-template pTemplate="body" let-invoice>
<tr> <tr>
<td> <td>
<button pButton pRipple icon="pi pi-print" class="p-button-rounded p-button-success mr-2" (click)="printInvoice(invoice)"></button> <button pButton pRipple icon="pi pi-print" class="p-button-rounded p-button-success mr-2"
</td> (click)="printInvoice(invoice)"></button>
<td>{{ invoice.id }}</td>
<td>{{ invoice.date | date}}</td>
<td>{{ invoice.price | currency}}</td>
<td></td>
<td></td>
</tr>
</ng-template>
</p-table>
</div>
</td> </td>
<td>{{ invoice.id }}</td>
<td>{{ invoice.date | date}}</td>
<td>{{ invoice.price | currency}}</td>
<td></td>
<td></td>
</tr> </tr>
</ng-template> </ng-template>
</p-table> </p-table>
</div> </div>
</td>
</tr>
</ng-template>
</p-table>
</div> </div>
</div> </div>
</div>
<p-dialog header="Edit Image" [visible]="imageUrl" [modal]="true" [style]="{ width: '50vw' }" [draggable]="false" [resizable]="false">
<!-- <app-cropper #cropper [imageUrl]="imageUrl"></app-cropper> -->
<angular-cropper #cropper [imageUrl]="imageUrl" [cropperOptions]="config"></angular-cropper>
<ng-template pTemplate="footer" let-config="config">
<div class="flex justify-content-between">
@if(type==='company'){
<div>
<p-selectButton [options]="stateOptions" [ngModel]="value" (ngModelChange)="changeAspectRation($event)" optionLabel="label" optionValue="value"></p-selectButton>
</div>
} @else {
<div></div>
}
<div>
<p-button icon="pi" (click)="cancelUpload()" label="Cancel" [outlined]="true"></p-button>
<p-button icon="pi pi-check" (click)="sendImage()" label="Finish" pAutoFocus [autofocus]="true"></p-button>
</div>
</div>
</ng-template>
</p-dialog>

View File

@ -1,4 +1,4 @@
import { ChangeDetectorRef, Component } from '@angular/core'; import { ChangeDetectorRef, Component, ViewChild } from '@angular/core';
import { ButtonModule } from 'primeng/button'; import { ButtonModule } from 'primeng/button';
import { CheckboxModule } from 'primeng/checkbox'; import { CheckboxModule } from 'primeng/checkbox';
import { InputTextModule } from 'primeng/inputtext'; import { InputTextModule } from 'primeng/inputtext';
@ -15,56 +15,72 @@ import { ChipModule } from 'primeng/chip';
import { MenuAccountComponent } from '../../menu-account/menu-account.component'; import { MenuAccountComponent } from '../../menu-account/menu-account.component';
import { DividerModule } from 'primeng/divider'; import { DividerModule } from 'primeng/divider';
import { TableModule } from 'primeng/table'; import { TableModule } from 'primeng/table';
import { HttpClient } from '@angular/common/http'; import { HttpClient, HttpEventType } from '@angular/common/http';
import { UserService } from '../../../services/user.service'; import { UserService } from '../../../services/user.service';
import { SharedModule } from '../../../shared/shared/shared.module'; import { SharedModule } from '../../../shared/shared/shared.module';
import { SubscriptionsService } from '../../../services/subscriptions.service'; import { SubscriptionsService } from '../../../services/subscriptions.service';
import { lastValueFrom } from 'rxjs'; import { lastValueFrom } from 'rxjs';
import { MessageService } from 'primeng/api'; import { MessageService } from 'primeng/api';
import { environment } from '../../../../environments/environment'; import { environment } from '../../../../environments/environment';
import { FileUploadModule } from 'primeng/fileupload'; import { FileUpload, FileUploadModule } from 'primeng/fileupload';
import { AutoCompleteCompleteEvent, Invoice, Subscription, User } from '../../../../../../common-models/src/main.model'; import { AutoCompleteCompleteEvent, Invoice, KeyValue, KeyValueRatio, Subscription, User } from '../../../../../../common-models/src/main.model';
import { GeoService } from '../../../services/geo.service'; import { GeoService } from '../../../services/geo.service';
import { ChangeDetectionStrategy } from '@angular/compiler'; import { ChangeDetectionStrategy } from '@angular/compiler';
import { EditorModule } from 'primeng/editor';
import { LoadingService } from '../../../services/loading.service';
import { AngularCropperjsModule, CropperComponent } from 'angular-cropperjs';
import { ImageService } from '../../../services/image.service';
import { DialogModule } from 'primeng/dialog';
import { SelectButtonModule } from 'primeng/selectbutton';
@Component({ @Component({
selector: 'app-account', selector: 'app-account',
standalone: true, standalone: true,
// imports: [CommonModule, StyleClassModule, MenuAccountComponent, DividerModule,ButtonModule, TableModule, InputTextModule, DropdownModule, FormsModule, ChipModule,InputTextareaModule ], imports: [SharedModule,FileUploadModule,EditorModule,AngularCropperjsModule,DialogModule,SelectButtonModule],
imports: [SharedModule,FileUploadModule],
providers:[MessageService], providers:[MessageService],
templateUrl: './account.component.html', templateUrl: './account.component.html',
styleUrl: './account.component.scss' styleUrl: './account.component.scss'
}) })
export class AccountComponent { export class AccountComponent {
@ViewChild(CropperComponent) public angularCropper: CropperComponent;
@ViewChild('companyUpload') public companyUpload: FileUpload;
@ViewChild('profileUpload') public profileUpload: FileUpload;
private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;
user:User; user:User;
subscriptions:Array<Subscription>; subscriptions:Array<Subscription>;
userSubscriptions:Array<Subscription>=[]; userSubscriptions:Array<Subscription>=[];
uploadProfileUrl:string;
uploadCompanyUrl:string;
maxFileSize=1000000; maxFileSize=1000000;
companyLogoUrl:string; companyLogoUrl:string;
profileUrl:string; profileUrl:string;
imageUrl;
type:'company'|'profile'
stateOptions:KeyValueRatio[]=[
{label:'16/9',value:16/9},
{label:'1/1',value:1},
{label:'2/3',value:2/3},
]
value:number = this.stateOptions[0].value;
config={aspectRatio: this.value}
environment=environment
constructor(public userService: UserService, constructor(public userService: UserService,
private subscriptionService: SubscriptionsService, private subscriptionService: SubscriptionsService,
private messageService: MessageService, private messageService: MessageService,
private geoService:GeoService, private geoService:GeoService,
public selectOptions:SelectOptionsService, public selectOptions:SelectOptionsService,
private cdref:ChangeDetectorRef) { private cdref:ChangeDetectorRef,
this.user=this.userService.getUser() private activatedRoute: ActivatedRoute,
private loadingService:LoadingService,
private imageUploadService: ImageService) {
} }
async ngOnInit(){ async ngOnInit(){
this.user=await this.userService.getById(this.id);
this.userSubscriptions=await lastValueFrom(this.subscriptionService.getAllSubscriptions()); this.userSubscriptions=await lastValueFrom(this.subscriptionService.getAllSubscriptions());
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.profileUrl = this.user.hasProfile?`${environment.apiBaseUrl}/profile/${this.user.id}.avif`:`/assets/images/placeholder.png`
this.companyLogoUrl = this.user.hasCompanyLogo?`${environment.apiBaseUrl}/logo/${this.user.id}`:`/assets/images/placeholder.png` this.companyLogoUrl = this.user.hasCompanyLogo?`${environment.apiBaseUrl}/logo/${this.user.id}.avif`:`/assets/images/placeholder.png`
} }
printInvoice(invoice:Invoice){} printInvoice(invoice:Invoice){}
@ -99,4 +115,60 @@ export class AccountComponent {
removeLicence(){ removeLicence(){
this.user.licensedIn.splice(this.user.licensedIn.length-2,1); this.user.licensedIn.splice(this.user.licensedIn.length-2,1);
} }
select(event:any,type:'company'|'profile'){
this.imageUrl = URL.createObjectURL(event.files[0]);
this.type=type
this.config={aspectRatio: type==='company'?this.stateOptions[0].value:this.stateOptions[2].value}
}
sendImage(){
this.imageUrl=null
this.loadingService.startLoading('uploadImage');
this.angularCropper.cropper.getCroppedCanvas().toBlob(async(blob) => {
if (this.type==='company'){
this.imageUploadService.uploadCompanyLogo(blob,this.user.id).subscribe(async(event) => {
if (event.type === HttpEventType.UploadProgress) {
// Berechne und zeige den Fortschritt basierend auf event.loaded und event.total
const progress = event.total ? event.loaded / event.total : 0;
console.log(`Upload-Fortschritt: ${progress * 100}%`);
// Hier könntest du beispielsweise eine Fortschrittsanzeige aktualisieren
} else if (event.type === HttpEventType.Response) {
console.log('Upload abgeschlossen', event.body);
this.companyUpload.clear();
this.loadingService.stopLoading('uploadImage');
this.companyLogoUrl=`${environment.apiBaseUrl}/logo/${this.user.id}.avif?_ts=${new Date().getTime()}`
}
}, error => console.error('Fehler beim Upload:', error));
} else {
this.imageUploadService.uploadProfileImage(blob,this.user.id).subscribe(async(event) => {
if (event.type === HttpEventType.UploadProgress) {
// Berechne und zeige den Fortschritt basierend auf event.loaded und event.total
const progress = event.total ? event.loaded / event.total : 0;
console.log(`Upload-Fortschritt: ${progress * 100}%`);
// Hier könntest du beispielsweise eine Fortschrittsanzeige aktualisieren
} else if (event.type === HttpEventType.Response) {
console.log('Upload abgeschlossen', event.body);
this.profileUpload.clear();
this.loadingService.stopLoading('uploadImage');
this.profileUrl=`${environment.apiBaseUrl}/profile/${this.user.id}.avif?_ts=${new Date().getTime()}`
}
}, error => console.error('Fehler beim Upload:', error));
}
// this.fileUpload.upload();
}, 'image/png');
}
cancelUpload(){
this.imageUrl=null
if (this.type==='company'){
this.companyUpload.clear();
} else {
this.profileUpload.clear();
}
}
changeAspectRation(ratio:number){
this.config={aspectRatio: ratio}
this.angularCropper.cropper.setAspectRatio(ratio);
}
} }

View File

@ -134,7 +134,7 @@ export class EditListingComponent {
this.loadingService.startLoading('uploadImage'); this.loadingService.startLoading('uploadImage');
this.angularCropper.cropper.getCroppedCanvas().toBlob(async(blob) => { this.angularCropper.cropper.getCroppedCanvas().toBlob(async(blob) => {
this.imageUploadService.uploadImage(blob).subscribe(async(event) => { this.imageUploadService.uploadPropertyImage(blob,this.listing.id).subscribe(async(event) => {
if (event.type === HttpEventType.UploadProgress) { if (event.type === HttpEventType.UploadProgress) {
// Berechne und zeige den Fortschritt basierend auf event.loaded und event.total // Berechne und zeige den Fortschritt basierend auf event.loaded und event.total
const progress = event.total ? event.loaded / event.total : 0; const progress = event.total ? event.loaded / event.total : 0;

View File

@ -1,23 +1,44 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { environment } from '../../environments/environment';
import { lastValueFrom } from 'rxjs';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class ImageService { export class ImageService {
private uploadUrl = 'http://localhost:3000/bizmatch/image/uploadPropertyPicture/1a4b800e-793c-4c47-b987-7bf634060a4e'; private apiBaseUrl = environment.apiBaseUrl;
constructor(private http: HttpClient) { } constructor(private http: HttpClient) { }
uploadImage(imageBlob: Blob) { uploadPropertyImage(imageBlob: Blob,listingId:string) {
const uploadUrl = `${this.apiBaseUrl}/bizmatch/image/uploadPropertyPicture/${listingId}`;
return this.uploadImage(imageBlob,uploadUrl);
}
uploadCompanyLogo(imageBlob: Blob,userId:string) {
const uploadUrl = `${this.apiBaseUrl}/bizmatch/image/uploadCompanyLogo/${userId}`;
return this.uploadImage(imageBlob,uploadUrl);
}
uploadProfileImage(imageBlob: Blob,userId:string) {
const uploadUrl = `${this.apiBaseUrl}/bizmatch/image/uploadProfile/${userId}`;
return this.uploadImage(imageBlob,uploadUrl);
}
uploadImage(imageBlob: Blob,uploadUrl:string) {
const formData = new FormData(); const formData = new FormData();
formData.append('file', imageBlob, 'image.png'); formData.append('file', imageBlob, 'image.png');
return this.http.post(this.uploadUrl, formData,{ return this.http.post(uploadUrl, formData,{
// headers: this.headers, // headers: this.headers,
reportProgress: true, reportProgress: true,
observe: 'events', observe: 'events',
}); });
} }
async getProfileImagesForUsers(userids:string[]){
return await lastValueFrom(this.http.get<[]>(`${this.apiBaseUrl}/bizmatch/image/profileImages/${userids.join(',')}`));
}
async getCompanyLogosForUsers(userids:string[]){
return await lastValueFrom(this.http.get<[]>(`${this.apiBaseUrl}/bizmatch/image/companyLogos/${userids.join(',')}`));
}
} }

View File

@ -20,7 +20,7 @@ export class LoadingService {
public startLoading(type: string,request?: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 (type==='uploadImage' || request?.includes('uploadPropertyPicture')) { if (type==='uploadImage' || request?.includes('uploadPropertyPicture')|| request?.includes('uploadProfile')|| request?.includes('uploadCompanyLogo')) {
this.loadingTextSubject.next("Please wait - we're processing your image..."); this.loadingTextSubject.next("Please wait - we're processing your image...");
} else { } else {
this.loadingTextSubject.next(null); this.loadingTextSubject.next(null);

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

@ -0,0 +1,3 @@
[ZoneTransfer]
LastWriterPackageFamilyName=Microsoft.Windows.Photos_8wekyb3d8bbwe
ZoneId=3

View File

@ -2,6 +2,10 @@ export interface KeyValue {
name: string; name: string;
value: string; value: string;
} }
export interface KeyValueRatio {
label: string;
value: number;
}
export interface KeyValueStyle { export interface KeyValueStyle {
name: string; name: string;
value: string; value: string;

13
crawler/importUser.ts Normal file
View File

@ -0,0 +1,13 @@
import fs from 'fs-extra';
(async () => {
const listings = await fs.readJson('./users.json');
//listings.forEach(element => {
for (const listing of listings) {
const response = await fetch('http://localhost:3000/bizmatch/user', {
method: 'POST',
body: JSON.stringify(listing),
headers: { 'Content-Type': 'application/json' },
});
}
})();

View File

@ -106,7 +106,7 @@ async function extractListingData(page: Page): Promise<BusinessListing | null> {
employees: content[labels.employeesLabel], employees: content[labels.employeesLabel],
reasonForSale: content[labels.reasonLabel], reasonForSale: content[labels.reasonLabel],
internals: '', internals: '',
} as BusinessListing; } as any;
return listing; return listing;
} catch (error) { } catch (error) {
console.log(`Fehler bei ${title}`); console.log(`Fehler bei ${title}`);

View File

@ -21,9 +21,9 @@ import { BusinessListing } from "../common-models/src/main.model"
}) })
const listings:Array<BusinessListing> = await response.json(); const listings:Array<BusinessListing> = await response.json();
for (const listing of listings) { for (const listing of listings) {
const option = selectOptions.locations.find(l=>l.name.toLowerCase()===listing.location.toLowerCase()); const option = selectOptions.locations.find(l=>l.name.toLowerCase()===listing.state.toLowerCase());
if (option){ if (option){
listing.location=option.value listing.state=option.value
} }
const response = await fetch(`http://localhost:3000/bizmatch/listings/${listing.id}`, { const response = await fetch(`http://localhost:3000/bizmatch/listings/${listing.id}`, {
method: 'PUT', method: 'PUT',

156
crawler/users.json Normal file
View File

@ -0,0 +1,156 @@
[
{
"id": "8a2b1c5d-7e6f-4g3h-9i1j-2k3l4m5n6o7p",
"firstname": "Sarah",
"lastname": "Thompson",
"email": "sarah.thompson@businessbrokers.com",
"licensedIn": [
{
"name": "California",
"value": "123456"
},
{
"name": "Nevada",
"value": "789012"
}
],
"phoneNumber": "(310) 555-1234",
"companyLocation": "Los Angeles - CA",
"hasCompanyLogo": true,
"hasProfile": true,
"companyName": "Business Brokers Inc.",
"companyWebsite": "https://www.businessbrokers.com",
"description": "Experienced business brokers helping clients buy and sell businesses in California and Nevada.",
"companyOverview": "<p>Business Brokers Inc. is a team of experienced professionals dedicated to helping clients navigate the complex process of buying and selling businesses. With a strong focus on client satisfaction and a deep understanding of the local markets in California and Nevada, we strive to deliver exceptional results for our clients.</p>",
"areasServed": [
"Los Angeles County, CA",
"Orange County, CA",
"San Diego County, CA",
"Clark County, NV",
"Washoe County, NV"
],
"offeredServices": "<h2>Services Offered</h2><ul><li>Business valuation</li><li>Market analysis</li><li>Buyer and seller representation</li><li>Due diligence assistance</li><li>Negotiations and closing support</li></ul>"
},
{
"id": "4q5r6s7t-8u9v-0w1x-2y3z-4a5b6c7d8e9f",
"firstname": "Michael",
"lastname": "Johnson",
"email": "michael@bizbrokers.net",
"licensedIn": [
{
"name": "Florida",
"value": "654321"
}
],
"phoneNumber": "(407) 555-9876",
"companyLocation": "Orlando - FL",
"hasCompanyLogo": false,
"hasProfile": true,
"companyName": "BizBrokers",
"companyWebsite": "https://www.bizbrokers.net",
"description": "Helping business owners in Florida sell their businesses and assisting buyers in finding the perfect opportunity.",
"companyOverview": "<p>At BizBrokers, we understand the unique challenges of buying and selling businesses in the Florida market. Our team of experienced brokers is committed to providing personalized service and expert guidance to ensure a smooth and successful transaction for our clients.</p>",
"areasServed": [
"Orange County, FL",
"Seminole County, FL",
"Osceola County, FL",
"Brevard County, FL",
"Volusia County, FL"
],
"offeredServices": "<h2>Services Offered</h2><ul><li>Business listings</li><li>Buyer search and qualification</li><li>Due diligence support</li><li>Financing assistance</li><li>Closing coordination</li></ul>"
},
{
"id": "1g2h3i4j-5k6l-7m8n-9o0p-1q2r3s4t5u6v",
"firstname": "Emily",
"lastname": "Davis",
"email": "emily.davis@bizsaleexperts.com",
"licensedIn": [
{
"name": "New York",
"value": "987654"
},
{
"name": "New Jersey",
"value": "210987"
}
],
"phoneNumber": "(212) 555-4321",
"companyLocation": "New York City - NY",
"hasCompanyLogo": true,
"hasProfile": true,
"companyName": "Business Sale Experts",
"companyWebsite": "https://www.bizsaleexperts.com",
"description": "Experienced business brokers specializing in the sale of businesses in the New York and New Jersey area.",
"companyOverview": "<p>Business Sale Experts is a leading business brokerage firm serving the New York and New Jersey markets. With a team of seasoned professionals and a proven track record of success, we are dedicated to helping our clients achieve their goals in the sale or acquisition of businesses.</p>",
"areasServed": [
"New York County, NY",
"Kings County, NY",
"Queens County, NY",
"Bronx County, NY",
"Hudson County, NJ",
"Bergen County, NJ",
"Essex County, NJ"
],
"offeredServices": "<h2>Services Offered</h2><ul><li>Business valuation</li><li>Marketing and advertising</li><li>Buyer screening and qualification</li><li>Negotiations and deal structuring</li><li>Due diligence coordination</li><li>Closing support</li></ul>"
},
{
"id": "7w8x9y0z-1a2b-3c4d-5e6f-7g8h9i0j1k2l",
"firstname": "David",
"lastname": "Wilson",
"email": "david.wilson@bizacquisitions.com",
"licensedIn": [
{
"name": "Texas",
"value": "543210"
}
],
"phoneNumber": "(713) 555-8765",
"companyLocation": "Houston - TX",
"hasCompanyLogo": true,
"hasProfile": true,
"companyName": "Business Acquisitions Ltd.",
"companyWebsite": "https://www.bizacquisitions.com",
"description": "Helping business owners in Texas sell their businesses and assisting buyers in finding the perfect acquisition opportunity.",
"companyOverview": "<p>Business Acquisitions Ltd. is a leading business brokerage firm serving the Texas market. Our team of experienced brokers is dedicated to providing expert guidance and personalized service to help our clients achieve their goals in the sale or acquisition of businesses.</p>",
"areasServed": [
"Harris County, TX",
"Dallas County, TX",
"Tarrant County, TX",
"Bexar County, TX",
"Travis County, TX"
],
"offeredServices": "<h2>Services Offered</h2><ul><li>Business valuation</li><li>Market analysis</li><li>Buyer search and qualification</li><li>Due diligence coordination</li><li>Financing assistance</li><li>Closing support</li></ul>"
},
{
"id": "3m4n5o6p-7q8r-9s0t-1u2v-3w4x5y6z7a8b",
"firstname": "Jessica",
"lastname": "Brown",
"email": "jessica@bizbrokerexperts.com",
"licensedIn": [
{
"name": "Illinois",
"value": "876543"
},
{
"name": "Wisconsin",
"value": "109876"
}
],
"phoneNumber": "(312) 555-2109",
"companyLocation": "Chicago - IL",
"hasCompanyLogo": true,
"hasProfile": true,
"companyName": "Business Broker Experts",
"companyWebsite": "https://www.bizbrokerexperts.com",
"description": "Experienced business brokers helping clients buy and sell businesses in Illinois and Wisconsin.",
"companyOverview": "<p>Business Broker Experts is a leading business brokerage firm serving the Illinois and Wisconsin markets. With a team of seasoned professionals and a commitment to client satisfaction, we strive to deliver exceptional results for our clients in the sale or acquisition of businesses.</p>",
"areasServed": [
"Cook County, IL",
"DuPage County, IL",
"Lake County, IL",
"Milwaukee County, WI",
"Dane County, WI"
],
"offeredServices": "<h2>Services Offered</h2><ul><li>Business valuation</li><li>Market analysis</li><li>Buyer and seller representation</li><li>Due diligence assistance</li><li>Negotiations and closing support</li></ul>"
}
]