feat: Initialize Angular SSR application with core pages, components, and server setup.

This commit is contained in:
Timo
2026-01-03 12:53:37 +01:00
parent 0ac17ef155
commit b52e47b653
28 changed files with 1115 additions and 461 deletions

View File

@@ -2,7 +2,6 @@ import { ChangeDetectorRef, Component } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { LeafletModule } from '@bluehalo/ngx-leaflet';
import { ShareButton } from 'ngx-sharebuttons/button';
import { lastValueFrom } from 'rxjs';
import { BusinessListing, EventTypeEnum, ShareByEMail, User } from '../../../../../../bizmatch-server/src/models/db.model';
import { KeycloakUser, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model';
@@ -25,15 +24,16 @@ import { UserService } from '../../../services/user.service';
import { SharedModule } from '../../../shared/shared/shared.module';
import { createMailInfo, map2User } from '../../../utils/utils';
// Import für Leaflet
// Benannte Importe für Leaflet
// Note: Leaflet requires browser environment - protected by isBrowser checks in base class
import { circle, Circle, Control, DomEvent, DomUtil, icon, Icon, latLng, LatLngBounds, Marker, polygon, Polygon, tileLayer } from 'leaflet';
import dayjs from 'dayjs';
import { AuthService } from '../../../services/auth.service';
import { BaseDetailsComponent } from '../base-details.component';
import { ShareButton } from 'ngx-sharebuttons/button';
@Component({
selector: 'app-details-business-listing',
standalone: true,
imports: [SharedModule, ValidatedInputComponent, ValidatedTextareaComponent, ShareButton, ValidatedNgSelectComponent, LeafletModule, BreadcrumbsComponent],
imports: [SharedModule, ValidatedInputComponent, ValidatedTextareaComponent, ValidatedNgSelectComponent, LeafletModule, BreadcrumbsComponent, ShareButton],
providers: [],
templateUrl: './details-business-listing.component.html',
styleUrl: '../details.scss',
@@ -231,28 +231,27 @@ export class DetailsBusinessListingComponent extends BaseDetailsComponent {
{ label: 'Category', value: this.selectOptions.getBusiness(this.listing.type) },
{
label: 'Located in',
value: `${this.listing.location.name ? this.listing.location.name : this.listing.location.county ? this.listing.location.county : ''} ${
this.listing.location.name || this.listing.location.county ? ', ' : ''
}${this.selectOptions.getState(this.listing.location.state)}`,
value: `${this.listing.location.name ? this.listing.location.name : this.listing.location.county ? this.listing.location.county : ''} ${this.listing.location.name || this.listing.location.county ? ', ' : ''
}${this.selectOptions.getState(this.listing.location.state)}`,
},
{ label: 'Asking Price', value: `${this.listing.price ? `$${this.listing.price.toLocaleString()}` : 'undisclosed '}` },
{ label: 'Sales revenue', value: `${this.listing.salesRevenue ? `$${this.listing.salesRevenue.toLocaleString()}` : 'undisclosed '}` },
{ label: 'Cash flow', value: `${this.listing.cashFlow ? `$${this.listing.cashFlow.toLocaleString()}` : 'undisclosed '}` },
...(this.listing.ffe
? [
{
label: 'Furniture, Fixtures / Equipment Value (FFE)',
value: `$${this.listing.ffe.toLocaleString()}`,
},
]
{
label: 'Furniture, Fixtures / Equipment Value (FFE)',
value: `$${this.listing.ffe.toLocaleString()}`,
},
]
: []),
...(this.listing.inventory
? [
{
label: 'Inventory at Cost Value',
value: `$${this.listing.inventory.toLocaleString()}`,
},
]
{
label: 'Inventory at Cost Value',
value: `$${this.listing.inventory.toLocaleString()}`,
},
]
: []),
{ label: 'Type of Real Estate', value: typeOfRealEstate },
{ label: 'Employees', value: this.listing.employees },
@@ -308,6 +307,26 @@ export class DetailsBusinessListingComponent extends BaseDetailsComponent {
createEvent(eventType: EventTypeEnum) {
this.auditService.createEvent(this.listing.id, eventType, this.user?.email);
}
shareToFacebook() {
const url = encodeURIComponent(window.location.href);
window.open(`https://www.facebook.com/sharer/sharer.php?u=${url}`, '_blank', 'width=600,height=400');
this.createEvent('facebook');
}
shareToTwitter() {
const url = encodeURIComponent(window.location.href);
const text = encodeURIComponent(this.listing?.title || 'Check out this business listing');
window.open(`https://twitter.com/intent/tweet?url=${url}&text=${text}`, '_blank', 'width=600,height=400');
this.createEvent('x');
}
shareToLinkedIn() {
const url = encodeURIComponent(window.location.href);
window.open(`https://www.linkedin.com/sharing/share-offsite/?url=${url}`, '_blank', 'width=600,height=400');
this.createEvent('linkedin');
}
getDaysListed() {
return dayjs().diff(this.listing.created, 'day');
}
@@ -330,7 +349,7 @@ export class DetailsBusinessListingComponent extends BaseDetailsComponent {
// Check if we have valid coordinates (null-safe check)
if (latitude !== null && latitude !== undefined &&
longitude !== null && longitude !== undefined) {
longitude !== null && longitude !== undefined) {
this.mapCenter = latLng(latitude, longitude);
@@ -340,23 +359,23 @@ export class DetailsBusinessListingComponent extends BaseDetailsComponent {
// Fetch city boundary from Nominatim API
this.geoService.getCityBoundary(cityName, state).subscribe({
next: (data) => {
if (data && data.length > 0 && data[0].geojson && data[0].geojson.type === 'Polygon') {
const coordinates = data[0].geojson.coordinates[0]; // Get outer boundary
next: (data) => {
if (data && data.length > 0 && data[0].geojson && data[0].geojson.type === 'Polygon') {
const coordinates = data[0].geojson.coordinates[0]; // Get outer boundary
// Convert GeoJSON coordinates [lon, lat] to Leaflet LatLng [lat, lon]
const latlngs = coordinates.map((coord: number[]) => latLng(coord[1], coord[0]));
// Convert GeoJSON coordinates [lon, lat] to Leaflet LatLng [lat, lon]
const latlngs = coordinates.map((coord: number[]) => latLng(coord[1], coord[0]));
// Create red outlined polygon for city boundary
const cityPolygon = polygon(latlngs, {
color: '#ef4444', // Red color (like Google Maps)
fillColor: '#ef4444',
fillOpacity: 0.1,
weight: 2
});
// Create red outlined polygon for city boundary
const cityPolygon = polygon(latlngs, {
color: '#ef4444', // Red color (like Google Maps)
fillColor: '#ef4444',
fillOpacity: 0.1,
weight: 2
});
// Add popup to polygon
cityPolygon.bindPopup(`
// Add popup to polygon
cityPolygon.bindPopup(`
<div style="padding: 8px;">
<strong>General Area:</strong><br/>
${cityName}, ${county ? county + ', ' : ''}${state}<br/>
@@ -364,74 +383,74 @@ export class DetailsBusinessListingComponent extends BaseDetailsComponent {
</div>
`);
this.mapLayers = [
tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; OpenStreetMap contributors',
}),
cityPolygon
];
this.mapLayers = [
tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; OpenStreetMap contributors',
}),
cityPolygon
];
// Fit map to polygon bounds
const bounds = cityPolygon.getBounds();
this.mapOptions = {
...this.mapOptions,
center: bounds.getCenter(),
zoom: this.mapZoom,
};
} else if (data && data.length > 0 && data[0].geojson && data[0].geojson.type === 'MultiPolygon') {
// Handle MultiPolygon case (cities with multiple areas)
const allPolygons: Polygon[] = [];
// Fit map to polygon bounds
const bounds = cityPolygon.getBounds();
this.mapOptions = {
...this.mapOptions,
center: bounds.getCenter(),
zoom: this.mapZoom,
};
} else if (data && data.length > 0 && data[0].geojson && data[0].geojson.type === 'MultiPolygon') {
// Handle MultiPolygon case (cities with multiple areas)
const allPolygons: Polygon[] = [];
data[0].geojson.coordinates.forEach((polygonCoords: number[][][]) => {
const latlngs = polygonCoords[0].map((coord: number[]) => latLng(coord[1], coord[0]));
const cityPolygon = polygon(latlngs, {
color: '#ef4444',
fillColor: '#ef4444',
fillOpacity: 0.1,
weight: 2
data[0].geojson.coordinates.forEach((polygonCoords: number[][][]) => {
const latlngs = polygonCoords[0].map((coord: number[]) => latLng(coord[1], coord[0]));
const cityPolygon = polygon(latlngs, {
color: '#ef4444',
fillColor: '#ef4444',
fillOpacity: 0.1,
weight: 2
});
allPolygons.push(cityPolygon);
});
allPolygons.push(cityPolygon);
});
// Add popup to first polygon
if (allPolygons.length > 0) {
allPolygons[0].bindPopup(`
// Add popup to first polygon
if (allPolygons.length > 0) {
allPolygons[0].bindPopup(`
<div style="padding: 8px;">
<strong>General Area:</strong><br/>
${cityName}, ${county ? county + ', ' : ''}${state}<br/>
<small style="color: #666;">City boundary shown for privacy.<br/>Exact location provided after contact.</small>
</div>
`);
}
}
this.mapLayers = [
tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; OpenStreetMap contributors',
}),
...allPolygons
];
this.mapLayers = [
tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; OpenStreetMap contributors',
}),
...allPolygons
];
// Calculate combined bounds
if (allPolygons.length > 0) {
const bounds = new LatLngBounds([]);
allPolygons.forEach(p => bounds.extend(p.getBounds()));
this.mapOptions = {
...this.mapOptions,
center: bounds.getCenter(),
zoom: this.mapZoom,
};
// Calculate combined bounds
if (allPolygons.length > 0) {
const bounds = new LatLngBounds([]);
allPolygons.forEach(p => bounds.extend(p.getBounds()));
this.mapOptions = {
...this.mapOptions,
center: bounds.getCenter(),
zoom: this.mapZoom,
};
}
} else {
// Fallback: Use circle if no polygon data available
this.useFallbackCircle(latitude, longitude, cityName, county, state);
}
} else {
// Fallback: Use circle if no polygon data available
},
error: (err) => {
console.error('Error fetching city boundary:', err);
// Fallback: Use circle on error
this.useFallbackCircle(latitude, longitude, cityName, county, state);
}
},
error: (err) => {
console.error('Error fetching city boundary:', err);
// Fallback: Use circle on error
this.useFallbackCircle(latitude, longitude, cityName, county, state);
}
});
});
}
// Case 2: Only state available (NEW) - show state-level circle
else if (state) {