feat: Initialize BizMatch application with core UI components, routing, listing pages, backend services, migration scripts, and vulnerability management.

This commit is contained in:
Timo
2026-01-03 23:05:38 +01:00
parent e32e43d17f
commit e3e726d8ca
42 changed files with 1478 additions and 1579 deletions

View File

@@ -31,8 +31,8 @@
class="text-primary-600 dark:text-primary-500 hover:underline">{{ listingUser.firstname }} {{
listingUser.lastname }}</a>
<img *ngIf="listing.imageName"
src="{{ env.imageBaseUrl }}/pictures/logo/{{ listing.imageName }}.avif?_ts={{ ts }}"
class="mr-5 lg:mb-0" style="max-height: 30px; max-width: 100px" width="100" height="30" />
ngSrc="{{ env.imageBaseUrl }}/pictures/logo/{{ listing.imageName }}.avif?_ts={{ ts }}"
class="mr-5 lg:mb-0" style="max-height: 30px; max-width: 100px" width="100" height="30" alt="Business logo for {{ listingUser.firstname }} {{ listingUser.lastname }}" />
</div>
</div>
</div>
@@ -135,6 +135,35 @@
}
</div>
<!-- FAQ Section for AEO (Answer Engine Optimization) -->
@if(businessFAQs && businessFAQs.length > 0) {
<div class="container mx-auto p-4 mt-8">
<div class="bg-white rounded-lg drop-shadow-custom-bg-mobile md:drop-shadow-custom-bg p-6">
<h2 class="text-2xl font-bold mb-6 text-gray-900">Frequently Asked Questions</h2>
<div class="space-y-4">
@for (faq of businessFAQs; track $index) {
<details class="group border border-gray-200 rounded-lg overflow-hidden hover:border-primary-300 transition-colors">
<summary class="flex items-center justify-between cursor-pointer p-4 bg-gray-50 hover:bg-gray-100 transition-colors">
<h3 class="text-lg font-semibold text-gray-900">{{ faq.question }}</h3>
<svg class="w-5 h-5 text-gray-600 group-open:rotate-180 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</summary>
<div class="p-4 bg-white border-t border-gray-200">
<p class="text-gray-700 leading-relaxed">{{ faq.answer }}</p>
</div>
</details>
}
</div>
<div class="mt-6 p-4 bg-primary-50 border-l-4 border-primary-500 rounded">
<p class="text-sm text-gray-700">
<strong class="text-primary-700">Have more questions?</strong> Contact the seller directly using the form above or reach out to our support team for assistance.
</p>
</div>
</div>
</div>
}
<!-- Related Listings Section for SEO Internal Linking -->
@if(relatedListings && relatedListings.length > 0) {
<div class="container mx-auto p-4 mt-8">

View File

@@ -1,4 +1,5 @@
import { ChangeDetectorRef, Component } from '@angular/core';
import { NgOptimizedImage } from '@angular/common';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { LeafletModule } from '@bluehalo/ngx-leaflet';
@@ -33,7 +34,7 @@ import { ShareButton } from 'ngx-sharebuttons/button';
@Component({
selector: 'app-details-business-listing',
standalone: true,
imports: [SharedModule, ValidatedInputComponent, ValidatedTextareaComponent, ValidatedNgSelectComponent, LeafletModule, BreadcrumbsComponent, ShareButton],
imports: [SharedModule, ValidatedInputComponent, ValidatedTextareaComponent, ValidatedNgSelectComponent, LeafletModule, BreadcrumbsComponent, ShareButton, NgOptimizedImage],
providers: [],
templateUrl: './details-business-listing.component.html',
styleUrl: '../details.scss',
@@ -70,6 +71,7 @@ export class DetailsBusinessListingComponent extends BaseDetailsComponent {
env = environment;
breadcrumbs: BreadcrumbItem[] = [];
relatedListings: BusinessListing[] = [];
businessFAQs: Array<{ question: string; answer: string }> = [];
constructor(
private activatedRoute: ActivatedRoute,
@@ -155,7 +157,13 @@ export class DetailsBusinessListingComponent extends BaseDetailsComponent {
{ name: 'Business Listings', url: '/businessListings' },
{ name: this.selectOptions.getBusiness(this.listing.type), url: `/business/${this.listing.slug || this.listing.id}` }
]);
this.seoService.injectMultipleSchemas([productSchema, breadcrumbSchema]);
// Generate FAQ for AEO (Answer Engine Optimization)
this.businessFAQs = this.generateBusinessFAQ();
const faqSchema = this.seoService.generateFAQPageSchema(this.businessFAQs);
// Inject all schemas including FAQ
this.seoService.injectMultipleSchemas([productSchema, breadcrumbSchema, faqSchema]);
// Generate breadcrumbs
this.breadcrumbs = [
@@ -193,6 +201,101 @@ export class DetailsBusinessListingComponent extends BaseDetailsComponent {
}
}
/**
* Generate dynamic FAQ based on business listing data fields
* Provides AEO (Answer Engine Optimization) content
*/
private generateBusinessFAQ(): Array<{ question: string; answer: string }> {
const faqs: Array<{ question: string; answer: string }> = [];
// FAQ 1: When was this business established?
if (this.listing.established) {
faqs.push({
question: 'When was this business established?',
answer: `This business was established ${this.listing.established} years ago${this.listing.established >= 10 ? ', demonstrating a proven track record and market stability' : ''}.`
});
}
// FAQ 2: What is the asking price?
if (this.listing.price) {
faqs.push({
question: 'What is the asking price for this business?',
answer: `The asking price for this business is $${this.listing.price.toLocaleString()}.${this.listing.salesRevenue ? ` With an annual revenue of $${this.listing.salesRevenue.toLocaleString()}, this represents a competitive valuation.` : ''}`
});
} else {
faqs.push({
question: 'What is the asking price for this business?',
answer: 'The asking price is available upon request. Please contact the seller for detailed pricing information.'
});
}
// FAQ 3: What is included in the sale?
const includedItems: string[] = [];
if (this.listing.realEstateIncluded) includedItems.push('real estate property');
if (this.listing.ffe) includedItems.push(`furniture, fixtures, and equipment valued at $${this.listing.ffe.toLocaleString()}`);
if (this.listing.inventory) includedItems.push(`inventory worth $${this.listing.inventory.toLocaleString()}`);
if (includedItems.length > 0) {
faqs.push({
question: 'What is included in the sale?',
answer: `The sale includes: ${includedItems.join(', ')}.${this.listing.leasedLocation ? ' The business operates from a leased location.' : ''}${this.listing.franchiseResale ? ' This is a franchise resale opportunity.' : ''}`
});
}
// FAQ 4: How many employees does the business have?
if (this.listing.employees) {
faqs.push({
question: 'How many employees does this business have?',
answer: `The business currently employs ${this.listing.employees} ${this.listing.employees === 1 ? 'person' : 'people'}.${this.listing.supportAndTraining ? ' The seller offers support and training to ensure smooth transition.' : ''}`
});
}
// FAQ 5: What is the annual revenue and cash flow?
if (this.listing.salesRevenue || this.listing.cashFlow) {
let answer = '';
if (this.listing.salesRevenue) {
answer += `The business generates an annual revenue of $${this.listing.salesRevenue.toLocaleString()}.`;
}
if (this.listing.cashFlow) {
answer += ` The annual cash flow is $${this.listing.cashFlow.toLocaleString()}.`;
}
faqs.push({
question: 'What is the financial performance of this business?',
answer: answer.trim()
});
}
// FAQ 6: Why is the business for sale?
if (this.listing.reasonForSale) {
faqs.push({
question: 'Why is this business for sale?',
answer: this.listing.reasonForSale
});
}
// FAQ 7: Where is the business located?
faqs.push({
question: 'Where is this business located?',
answer: `This ${this.selectOptions.getBusiness(this.listing.type)} business is located in ${this.listing.location.name || this.listing.location.county}, ${this.selectOptions.getState(this.listing.location.state)}.`
});
// FAQ 8: Is broker licensing required?
if (this.listing.brokerLicencing) {
faqs.push({
question: 'Is a broker license required for this business?',
answer: this.listing.brokerLicencing
});
}
// FAQ 9: What type of business is this?
faqs.push({
question: 'What type of business is this?',
answer: `This is a ${this.selectOptions.getBusiness(this.listing.type)} business${this.listing.established ? ` that has been operating for ${this.listing.established} years` : ''}.`
});
return faqs;
}
ngOnDestroy() {
this.validationMessagesService.clearMessages(); // Löschen Sie alle bestehenden Validierungsnachrichten
this.seoService.clearStructuredData(); // Clean up SEO structured data

View File

@@ -32,8 +32,8 @@
class="text-primary-600 dark:text-primary-500 hover:underline"> {{ detail.user.firstname }} {{
detail.user.lastname }} </a>
<img *ngIf="detail.user.hasCompanyLogo"
[src]="detail.imageBaseUrl + '/pictures/logo/' + detail.imagePath + '.avif?_ts=' + detail.ts"
class="mr-5 lg:mb-0" style="max-height: 30px; max-width: 100px" width="100" height="30" />
[ngSrc]="detail.imageBaseUrl + '/pictures/logo/' + detail.imagePath + '.avif?_ts=' + detail.ts"
class="mr-5 lg:mb-0" style="max-height: 30px; max-width: 100px" width="100" height="30" alt="Company logo for {{ detail.user.firstname }} {{ detail.user.lastname }}" />
</div>
</div>
</div>
@@ -149,6 +149,35 @@
}
</div>
<!-- FAQ Section for AEO (Answer Engine Optimization) -->
@if(propertyFAQs && propertyFAQs.length > 0) {
<div class="container mx-auto p-4 mt-8">
<div class="bg-white rounded-lg drop-shadow-custom-bg-mobile md:drop-shadow-custom-bg p-6">
<h2 class="text-2xl font-bold mb-6 text-gray-900">Frequently Asked Questions</h2>
<div class="space-y-4">
@for (faq of propertyFAQs; track $index) {
<details class="group border border-gray-200 rounded-lg overflow-hidden hover:border-primary-300 transition-colors">
<summary class="flex items-center justify-between cursor-pointer p-4 bg-gray-50 hover:bg-gray-100 transition-colors">
<h3 class="text-lg font-semibold text-gray-900">{{ faq.question }}</h3>
<svg class="w-5 h-5 text-gray-600 group-open:rotate-180 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</summary>
<div class="p-4 bg-white border-t border-gray-200">
<p class="text-gray-700 leading-relaxed">{{ faq.answer }}</p>
</div>
</details>
}
</div>
<div class="mt-6 p-4 bg-primary-50 border-l-4 border-primary-500 rounded">
<p class="text-sm text-gray-700">
<strong class="text-primary-700">Have more questions?</strong> Contact the seller directly using the form above or reach out to our support team for assistance.
</p>
</div>
</div>
</div>
}
<!-- Related Listings Section for SEO Internal Linking -->
@if(relatedListings && relatedListings.length > 0) {
<div class="container mx-auto p-4 mt-8">

View File

@@ -1,4 +1,5 @@
import { Component, NgZone } from '@angular/core';
import { NgOptimizedImage } from '@angular/common';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router';
import { LeafletModule } from '@bluehalo/ngx-leaflet';
@@ -33,7 +34,7 @@ import { ShareButton } from 'ngx-sharebuttons/button';
@Component({
selector: 'app-details-commercial-property-listing',
standalone: true,
imports: [SharedModule, ValidatedInputComponent, ValidatedTextareaComponent, ValidatedNgSelectComponent, GalleryModule, LeafletModule, BreadcrumbsComponent, ShareButton],
imports: [SharedModule, ValidatedInputComponent, ValidatedTextareaComponent, ValidatedNgSelectComponent, GalleryModule, LeafletModule, BreadcrumbsComponent, ShareButton, NgOptimizedImage],
providers: [],
templateUrl: './details-commercial-property-listing.component.html',
styleUrl: '../details.scss',
@@ -73,6 +74,7 @@ export class DetailsCommercialPropertyListingComponent extends BaseDetailsCompon
images: Array<ImageItem> = [];
relatedListings: CommercialPropertyListing[] = [];
breadcrumbs: BreadcrumbItem[] = [];
propertyFAQs: Array<{ question: string; answer: string }> = [];
constructor(
private activatedRoute: ActivatedRoute,
private listingsService: ListingsService,
@@ -174,7 +176,13 @@ export class DetailsCommercialPropertyListingComponent extends BaseDetailsCompon
{ name: 'Commercial Properties', url: '/commercialPropertyListings' },
{ name: propertyData.propertyType, url: `/details-commercial-property/${this.listing.id}` }
]);
this.seoService.injectMultipleSchemas([realEstateSchema, breadcrumbSchema]);
// Generate FAQ for AEO (Answer Engine Optimization)
this.propertyFAQs = this.generatePropertyFAQ();
const faqSchema = this.seoService.generateFAQPageSchema(this.propertyFAQs);
// Inject all schemas including FAQ
this.seoService.injectMultipleSchemas([realEstateSchema, breadcrumbSchema, faqSchema]);
// Generate breadcrumbs for navigation
this.breadcrumbs = [
@@ -213,6 +221,60 @@ export class DetailsCommercialPropertyListingComponent extends BaseDetailsCompon
}
}
/**
* Generate dynamic FAQ based on commercial property listing data
* Provides AEO (Answer Engine Optimization) content
*/
private generatePropertyFAQ(): Array<{ question: string; answer: string }> {
const faqs: Array<{ question: string; answer: string }> = [];
// FAQ 1: What type of property is this?
faqs.push({
question: 'What type of commercial property is this?',
answer: `This is a ${this.selectOptions.getCommercialProperty(this.listing.type)} property located in ${this.listing.location.name || this.listing.location.county}, ${this.selectOptions.getState(this.listing.location.state)}.`
});
// FAQ 2: What is the asking price?
if (this.listing.price) {
faqs.push({
question: 'What is the asking price for this property?',
answer: `The asking price for this commercial property is $${this.listing.price.toLocaleString()}.`
});
} else {
faqs.push({
question: 'What is the asking price for this property?',
answer: 'The asking price is available upon request. Please contact the seller for detailed pricing information.'
});
}
// FAQ 3: Where is the property located?
faqs.push({
question: 'Where is this commercial property located?',
answer: `The property is located in ${this.listing.location.name || this.listing.location.county}, ${this.selectOptions.getState(this.listing.location.state)}.${this.listing.location.street ? ' The exact address will be provided after initial contact.' : ''}`
});
// FAQ 4: How long has the property been listed?
const daysListed = this.getDaysListed();
faqs.push({
question: 'How long has this property been on the market?',
answer: `This property was listed on ${this.dateInserted()} and has been on the market for ${daysListed} ${daysListed === 1 ? 'day' : 'days'}.`
});
// FAQ 5: How can I schedule a viewing?
faqs.push({
question: 'How can I schedule a property viewing?',
answer: 'To schedule a viewing of this commercial property, please use the contact form above to get in touch with the listing agent. They will coordinate a convenient time for you to visit the property.'
});
// FAQ 6: What is the zoning for this property?
faqs.push({
question: 'What is this property suitable for?',
answer: `This ${this.selectOptions.getCommercialProperty(this.listing.type)} property is ideal for various commercial uses. Contact the seller for specific zoning information and permitted use details.`
});
return faqs;
}
ngOnDestroy() {
this.validationMessagesService.clearMessages(); // Löschen Sie alle bestehenden Validierungsnachrichten
this.seoService.clearStructuredData(); // Clean up SEO structured data

View File

@@ -11,9 +11,9 @@
<div class="flex items-center space-x-4">
<!-- <img src="https://placehold.co/80x80" alt="Profile picture of Avery Brown smiling" class="w-20 h-20 rounded-full" /> -->
@if(user.hasProfile){
<img src="{{ env.imageBaseUrl }}/pictures//profile/{{ emailToDirName(user.email) }}.avif?_ts={{ ts }}" class="w-20 h-20 rounded-full object-cover" width="80" height="80" />
<img ngSrc="{{ env.imageBaseUrl }}/pictures//profile/{{ emailToDirName(user.email) }}.avif?_ts={{ ts }}" class="w-20 h-20 rounded-full object-cover" width="80" height="80" priority alt="Profile picture of {{ user.firstname }} {{ user.lastname }}" />
} @else {
<img src="assets/images/person_placeholder.jpg" class="w-20 h-20 rounded-full" width="80" height="80" />
<img ngSrc="assets/images/person_placeholder.jpg" class="w-20 h-20 rounded-full" width="80" height="80" priority alt="Default profile picture" />
}
<div>
<h1 class="text-2xl font-bold flex items-center">
@@ -32,7 +32,7 @@
</p>
</div>
@if(user.hasCompanyLogo){
<img src="{{ env.imageBaseUrl }}/pictures/logo/{{ emailToDirName(user.email) }}.avif?_ts={{ ts }}" class="w-11 h-14" width="44" height="56" />
<img ngSrc="{{ env.imageBaseUrl }}/pictures/logo/{{ emailToDirName(user.email) }}.avif?_ts={{ ts }}" class="w-11 h-14" width="44" height="56" alt="Company logo of {{ user.companyName }}" />
}
<!-- <img src="https://placehold.co/45x60" class="w-11 h-14" /> -->
</div>
@@ -130,9 +130,9 @@
<div class="border rounded-lg p-4 hover:cursor-pointer" [routerLink]="['/commercial-property', listing.slug || listing.id]">
<div class="flex items-center space-x-4">
@if (listing.imageOrder?.length>0){
<img src="{{ env.imageBaseUrl }}/pictures/property/{{ listing.imagePath }}/{{ listing.serialId }}/{{ listing.imageOrder[0] }}?_ts={{ ts }}" class="w-12 h-12 object-cover rounded" />
<img ngSrc="{{ env.imageBaseUrl }}/pictures/property/{{ listing.imagePath }}/{{ listing.serialId }}/{{ listing.imageOrder[0] }}?_ts={{ ts }}" class="w-12 h-12 object-cover rounded" width="48" height="48" alt="Property image for {{ listing.title }}" />
} @else {
<img src="assets/images/placeholder_properties.jpg" class="w-12 h-12 object-cover rounded" />
<img ngSrc="assets/images/placeholder_properties.jpg" class="w-12 h-12 object-cover rounded" width="48" height="48" alt="Property placeholder image" />
}
<div>
<p class="font-medium">{{ selectOptions.getCommercialProperty(listing.type) }}</p>

View File

@@ -1,4 +1,5 @@
import { Component } from '@angular/core';
import { NgOptimizedImage } from '@angular/common';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router';
import { Observable } from 'rxjs';
@@ -18,7 +19,7 @@ import { formatPhoneNumber, map2User } from '../../../utils/utils';
@Component({
selector: 'app-details-user',
standalone: true,
imports: [SharedModule, BreadcrumbsComponent],
imports: [SharedModule, BreadcrumbsComponent, NgOptimizedImage],
templateUrl: './details-user.component.html',
styleUrl: '../details.scss',
})