feat: Initialize Angular SSR application with core pages, components, and server setup.
This commit is contained in:
parent
0ac17ef155
commit
b52e47b653
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(npm install)",
|
||||
"Bash(docker ps:*)",
|
||||
"Bash(docker cp:*)",
|
||||
"Bash(docker exec:*)",
|
||||
"Bash(find:*)",
|
||||
"Bash(docker restart:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -31,20 +31,35 @@ export class BusinessListingService {
|
|||
const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city.name);
|
||||
whereConditions.push(sql`${getDistanceQuery(businesses_json, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`);
|
||||
}
|
||||
if (criteria.types && criteria.types.length > 0) {
|
||||
whereConditions.push(inArray(sql`${businesses_json.data}->>'type'`, criteria.types));
|
||||
if (criteria.types && Array.isArray(criteria.types) && criteria.types.length > 0) {
|
||||
const validTypes = criteria.types.filter(t => t !== null && t !== undefined && t !== '');
|
||||
if (validTypes.length > 0) {
|
||||
whereConditions.push(inArray(sql`${businesses_json.data}->>'type'`, validTypes));
|
||||
}
|
||||
}
|
||||
|
||||
if (criteria.state) {
|
||||
whereConditions.push(sql`(${businesses_json.data}->'location'->>'state') = ${criteria.state}`);
|
||||
}
|
||||
|
||||
if (criteria.minPrice) {
|
||||
whereConditions.push(gte(sql`(${businesses_json.data}->>'price')::double precision`, criteria.minPrice));
|
||||
if (criteria.minPrice !== undefined && criteria.minPrice !== null) {
|
||||
whereConditions.push(
|
||||
and(
|
||||
sql`(${businesses_json.data}->>'price') IS NOT NULL`,
|
||||
sql`(${businesses_json.data}->>'price') != ''`,
|
||||
gte(sql`REPLACE(${businesses_json.data}->>'price', ',', '')::double precision`, criteria.minPrice)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (criteria.maxPrice) {
|
||||
whereConditions.push(lte(sql`(${businesses_json.data}->>'price')::double precision`, criteria.maxPrice));
|
||||
if (criteria.maxPrice !== undefined && criteria.maxPrice !== null) {
|
||||
whereConditions.push(
|
||||
and(
|
||||
sql`(${businesses_json.data}->>'price') IS NOT NULL`,
|
||||
sql`(${businesses_json.data}->>'price') != ''`,
|
||||
lte(sql`REPLACE(${businesses_json.data}->>'price', ',', '')::double precision`, criteria.maxPrice)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (criteria.minRevenue) {
|
||||
|
|
@ -87,8 +102,14 @@ export class BusinessListingService {
|
|||
whereConditions.push(eq(sql`(${businesses_json.data}->>'franchiseResale')::boolean`, criteria.franchiseResale));
|
||||
}
|
||||
|
||||
if (criteria.title) {
|
||||
whereConditions.push(sql`(${businesses_json.data}->>'title') ILIKE ${`%${criteria.title}%`} OR (${businesses_json.data}->>'description') ILIKE ${`%${criteria.title}%`}`);
|
||||
if (criteria.title && criteria.title.trim() !== '') {
|
||||
const searchTerm = `%${criteria.title.trim()}%`;
|
||||
whereConditions.push(
|
||||
or(
|
||||
sql`(${businesses_json.data}->>'title') ILIKE ${searchTerm}`,
|
||||
sql`(${businesses_json.data}->>'description') ILIKE ${searchTerm}`
|
||||
)
|
||||
);
|
||||
}
|
||||
if (criteria.brokerName) {
|
||||
const { firstname, lastname } = splitName(criteria.brokerName);
|
||||
|
|
@ -122,9 +143,16 @@ export class BusinessListingService {
|
|||
|
||||
const whereConditions = this.getWhereConditions(criteria, user);
|
||||
|
||||
// Uncomment for debugging filter issues:
|
||||
// this.logger.info('Filter Criteria:', { criteria });
|
||||
// this.logger.info('Where Conditions Count:', { count: whereConditions.length });
|
||||
|
||||
if (whereConditions.length > 0) {
|
||||
const whereClause = and(...whereConditions);
|
||||
query.where(whereClause);
|
||||
|
||||
// Uncomment for debugging SQL queries:
|
||||
// this.logger.info('Generated SQL:', { sql: query.toSQL() });
|
||||
}
|
||||
|
||||
// Sortierung
|
||||
|
|
|
|||
|
|
@ -48,4 +48,15 @@ export class SitemapController {
|
|||
async getCommercialSitemap(@Param('page', ParseIntPipe) page: number): Promise<string> {
|
||||
return await this.sitemapService.generateCommercialSitemap(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broker profiles sitemap (paginated)
|
||||
* Route: /sitemap/brokers-1.xml, /sitemap/brokers-2.xml, etc.
|
||||
*/
|
||||
@Get('sitemap/brokers-:page.xml')
|
||||
@Header('Content-Type', 'application/xml')
|
||||
@Header('Cache-Control', 'public, max-age=3600')
|
||||
async getBrokerSitemap(@Param('page', ParseIntPipe) page: number): Promise<string> {
|
||||
return await this.sitemapService.generateBrokerSitemap(page);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,26 +32,36 @@ export class SitemapService {
|
|||
|
||||
// Add static pages sitemap
|
||||
sitemaps.push({
|
||||
loc: `${this.baseUrl}/sitemap/static.xml`,
|
||||
loc: `${this.baseUrl}/bizmatch/sitemap/static.xml`,
|
||||
lastmod: this.formatDate(new Date()),
|
||||
});
|
||||
|
||||
// Count business listings
|
||||
const businessCount = await this.getBusinessListingsCount();
|
||||
const businessPages = Math.ceil(businessCount / this.URLS_PER_SITEMAP);
|
||||
const businessPages = Math.ceil(businessCount / this.URLS_PER_SITEMAP) || 1;
|
||||
for (let page = 1; page <= businessPages; page++) {
|
||||
sitemaps.push({
|
||||
loc: `${this.baseUrl}/sitemap/business-${page}.xml`,
|
||||
loc: `${this.baseUrl}/bizmatch/sitemap/business-${page}.xml`,
|
||||
lastmod: this.formatDate(new Date()),
|
||||
});
|
||||
}
|
||||
|
||||
// Count commercial property listings
|
||||
const commercialCount = await this.getCommercialPropertiesCount();
|
||||
const commercialPages = Math.ceil(commercialCount / this.URLS_PER_SITEMAP);
|
||||
const commercialPages = Math.ceil(commercialCount / this.URLS_PER_SITEMAP) || 1;
|
||||
for (let page = 1; page <= commercialPages; page++) {
|
||||
sitemaps.push({
|
||||
loc: `${this.baseUrl}/sitemap/commercial-${page}.xml`,
|
||||
loc: `${this.baseUrl}/bizmatch/sitemap/commercial-${page}.xml`,
|
||||
lastmod: this.formatDate(new Date()),
|
||||
});
|
||||
}
|
||||
|
||||
// Count broker profiles
|
||||
const brokerCount = await this.getBrokerProfilesCount();
|
||||
const brokerPages = Math.ceil(brokerCount / this.URLS_PER_SITEMAP) || 1;
|
||||
for (let page = 1; page <= brokerPages; page++) {
|
||||
sitemaps.push({
|
||||
loc: `${this.baseUrl}/bizmatch/sitemap/brokers-${page}.xml`,
|
||||
lastmod: this.formatDate(new Date()),
|
||||
});
|
||||
}
|
||||
|
|
@ -289,4 +299,64 @@ ${sitemapElements}
|
|||
const d = typeof date === 'string' ? new Date(date) : date;
|
||||
return d.toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate broker profiles sitemap (paginated)
|
||||
*/
|
||||
async generateBrokerSitemap(page: number): Promise<string> {
|
||||
const offset = (page - 1) * this.URLS_PER_SITEMAP;
|
||||
const urls = await this.getBrokerProfileUrls(offset, this.URLS_PER_SITEMAP);
|
||||
return this.buildXmlSitemap(urls);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count broker profiles (professionals with showInDirectory=true)
|
||||
*/
|
||||
private async getBrokerProfilesCount(): Promise<number> {
|
||||
try {
|
||||
const result = await this.db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(schema.users_json)
|
||||
.where(sql`
|
||||
(${schema.users_json.data}->>'customerType') = 'professional'
|
||||
AND (${schema.users_json.data}->>'showInDirectory')::boolean IS TRUE
|
||||
`);
|
||||
|
||||
return Number(result[0]?.count || 0);
|
||||
} catch (error) {
|
||||
console.error('Error counting broker profiles:', error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get broker profile URLs from database (paginated)
|
||||
*/
|
||||
private async getBrokerProfileUrls(offset: number, limit: number): Promise<SitemapUrl[]> {
|
||||
try {
|
||||
const brokers = await this.db
|
||||
.select({
|
||||
email: schema.users_json.email,
|
||||
updated: sql<Date>`(${schema.users_json.data}->>'updated')::timestamptz`,
|
||||
created: sql<Date>`(${schema.users_json.data}->>'created')::timestamptz`,
|
||||
})
|
||||
.from(schema.users_json)
|
||||
.where(sql`
|
||||
(${schema.users_json.data}->>'customerType') = 'professional'
|
||||
AND (${schema.users_json.data}->>'showInDirectory')::boolean IS TRUE
|
||||
`)
|
||||
.limit(limit)
|
||||
.offset(offset);
|
||||
|
||||
return brokers.map(broker => ({
|
||||
loc: `${this.baseUrl}/details-user/${encodeURIComponent(broker.email)}`,
|
||||
lastmod: this.formatDate(broker.updated || broker.created),
|
||||
changefreq: 'weekly' as const,
|
||||
priority: 0.7,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Error fetching broker profiles for sitemap:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,11 @@
|
|||
"outputPath": "dist/bizmatch",
|
||||
"index": "src/index.html",
|
||||
"browser": "src/main.ts",
|
||||
"server": "src/main.server.ts",
|
||||
"prerender": false,
|
||||
"ssr": {
|
||||
"entry": "server.ts"
|
||||
},
|
||||
"polyfills": [
|
||||
"zone.js"
|
||||
],
|
||||
|
|
@ -33,6 +38,7 @@
|
|||
},
|
||||
"src/favicon.ico",
|
||||
"src/assets",
|
||||
"src/robots.txt",
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "node_modules/leaflet/dist/images",
|
||||
|
|
@ -65,7 +71,8 @@
|
|||
"development": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true
|
||||
"sourceMap": true,
|
||||
"ssr": false
|
||||
},
|
||||
"dev": {
|
||||
"fileReplacements": [
|
||||
|
|
|
|||
|
|
@ -8,9 +8,13 @@
|
|||
"build": "node version.js && ng build",
|
||||
"build.dev": "node version.js && ng build --configuration dev --output-hashing=all",
|
||||
"build.prod": "node version.js && ng build --configuration prod --output-hashing=all",
|
||||
"build:ssr": "node version.js && ng build --configuration prod",
|
||||
"build:ssr:dev": "node version.js && ng build --configuration dev",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test",
|
||||
"serve:ssr:bizmatch": "node dist/bizmatch/server/server.mjs"
|
||||
"serve:ssr": "node dist/bizmatch/server/server.mjs",
|
||||
"serve:ssr:bizmatch": "node dist/bizmatch/server/server.mjs",
|
||||
"dev:ssr": "NODE_OPTIONS='--import ./ssr-dom-preload.mjs' ng serve"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
// IMPORTANT: DOM polyfill must be imported FIRST, before any browser-dependent libraries
|
||||
import './src/ssr-dom-polyfill';
|
||||
|
||||
import { APP_BASE_HREF } from '@angular/common';
|
||||
import { CommonEngine } from '@angular/ssr';
|
||||
import express from 'express';
|
||||
|
|
@ -44,7 +47,7 @@ export function app(): express.Express {
|
|||
}
|
||||
|
||||
function run(): void {
|
||||
const port = process.env['PORT'] || 4000;
|
||||
const port = process.env['PORT'] || 4200;
|
||||
|
||||
// Start up the Node server
|
||||
const server = app();
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<div class="flex flex-col lg:flex-row items-center mb-4 lg:mb-0">
|
||||
<!-- <img src="assets/images/header-logo.png" alt="BizMatch Logo" class="h-8 mb-2 lg:mb-0 lg:mr-4" /> -->
|
||||
<a routerLink="/home" class="flex items-center space-x-3 rtl:space-x-reverse">
|
||||
<img src="assets/images/header-logo.png" class="h-8 mb-2 lg:mb-0 lg:mr-4" width="120" height="32" />
|
||||
<img src="assets/images/header-logo.png" class="h-8 mb-2 lg:mb-0 lg:mr-4" alt="BizMatch Logo" />
|
||||
</a>
|
||||
<p class="text-sm text-neutral-600 text-center lg:text-left">© {{ currentYear }} Bizmatch All rights reserved.</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,226 +1,210 @@
|
|||
<nav class="bg-white border-neutral-200 dark:bg-neutral-900 print:hidden">
|
||||
<div class="max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-4">
|
||||
<a routerLink="/home" class="flex items-center space-x-3 rtl:space-x-reverse">
|
||||
<img src="assets/images/header-logo.png" class="h-10 w-auto" alt="BizMatch - Business Marketplace for Buying and Selling Businesses" />
|
||||
<img src="assets/images/header-logo.png" class="h-10 w-auto"
|
||||
alt="BizMatch - Business Marketplace for Buying and Selling Businesses" />
|
||||
</a>
|
||||
<div class="flex items-center md:order-2 space-x-3 rtl:space-x-reverse">
|
||||
<!-- Filter button -->
|
||||
@if(isFilterUrl()){
|
||||
|
||||
<div class="relative">
|
||||
<button
|
||||
type="button"
|
||||
id="sortDropdownButton"
|
||||
<button type="button" id="sortDropdownButton"
|
||||
class="max-sm:hidden px-4 py-2 text-sm font-medium bg-white border border-neutral-200 rounded-lg hover:bg-neutral-100 hover:text-primary-600 dark:bg-neutral-800 dark:text-neutral-400 dark:border-neutral-600 dark:hover:text-white dark:hover:bg-neutral-700"
|
||||
(click)="toggleSortDropdown()"
|
||||
[ngClass]="{ 'text-primary-500': selectOptions.getSortByOption(sortBy) !== 'Sort', 'text-neutral-900': selectOptions.getSortByOption(sortBy) === 'Sort' }"
|
||||
>
|
||||
[ngClass]="{ 'text-primary-500': selectOptions.getSortByOption(sortBy) !== 'Sort', 'text-neutral-900': selectOptions.getSortByOption(sortBy) === 'Sort' }">
|
||||
<i class="fas fa-sort mr-2"></i>{{ selectOptions.getSortByOption(sortBy) }}
|
||||
</button>
|
||||
|
||||
<!-- Sort options dropdown -->
|
||||
<div *ngIf="sortDropdownVisible" class="absolute right-0 z-50 w-48 md:mt-2 max-md:mt-20 max-md:mr-[-2.5rem] bg-white border border-neutral-200 rounded-lg drop-shadow-custom-bg dark:bg-neutral-800 dark:border-neutral-600">
|
||||
<div *ngIf="sortDropdownVisible"
|
||||
class="absolute right-0 z-50 w-48 md:mt-2 max-md:mt-20 max-md:mr-[-2.5rem] bg-white border border-neutral-200 rounded-lg drop-shadow-custom-bg dark:bg-neutral-800 dark:border-neutral-600">
|
||||
<ul class="py-1 text-sm text-neutral-700 dark:text-neutral-200">
|
||||
@for(item of sortByOptions; track item){
|
||||
<li (click)="sortByFct(item.value)" class="block px-4 py-2 hover:bg-neutral-100 dark:hover:bg-neutral-700 cursor-pointer">{{ item.selectName ? item.selectName : item.name }}</li>
|
||||
<li (click)="sortByFct(item.value)"
|
||||
class="block px-4 py-2 hover:bg-neutral-100 dark:hover:bg-neutral-700 cursor-pointer">{{ item.selectName ?
|
||||
item.selectName : item.name }}</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<button
|
||||
type="button"
|
||||
<button type="button"
|
||||
class="flex text-sm bg-neutral-400 rounded-full md:me-0 focus:ring-4 focus:ring-neutral-300 dark:focus:ring-neutral-600"
|
||||
id="user-menu-button"
|
||||
aria-expanded="false"
|
||||
[attr.data-dropdown-toggle]="user ? 'user-login' : 'user-unknown'"
|
||||
data-dropdown-placement="bottom"
|
||||
>
|
||||
id="user-menu-button" aria-expanded="false" [attr.data-dropdown-toggle]="user ? 'user-login' : 'user-unknown'"
|
||||
data-dropdown-placement="bottom">
|
||||
<span class="sr-only">Open user menu</span>
|
||||
@if(isProfessional || (authService.isAdmin() | async) && user?.hasProfile){
|
||||
<img class="w-8 h-8 rounded-full object-cover" src="{{ profileUrl }}" alt="{{ user?.firstname }} {{ user?.lastname }} profile photo" width="32" height="32" />
|
||||
<img class="w-8 h-8 rounded-full object-cover" src="{{ profileUrl }}"
|
||||
alt="{{ user?.firstname }} {{ user?.lastname }} profile photo" width="32" height="32" />
|
||||
} @else {
|
||||
<i class="flex justify-center items-center text-stone-50 w-8 h-8 rounded-full fa-solid fa-bars"></i>
|
||||
}
|
||||
</button>
|
||||
<!-- Dropdown menu -->
|
||||
@if(user){
|
||||
<div class="z-50 hidden my-4 text-base list-none bg-white divide-y divide-neutral-100 rounded-lg shadow dark:bg-neutral-700 dark:divide-neutral-600" id="user-login">
|
||||
<div
|
||||
class="z-50 hidden my-4 text-base list-none bg-white divide-y divide-neutral-100 rounded-lg shadow dark:bg-neutral-700 dark:divide-neutral-600"
|
||||
id="user-login">
|
||||
<div class="px-4 py-3">
|
||||
<span class="block text-sm text-neutral-900 dark:text-white">Welcome, {{ user.firstname }} </span>
|
||||
<span class="block text-sm text-neutral-500 truncate dark:text-neutral-400">{{ user.email }}</span>
|
||||
</div>
|
||||
<ul class="py-2" aria-labelledby="user-menu-button">
|
||||
<li>
|
||||
<a routerLink="/account" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-neutral-700 hover:bg-neutral-100 dark:hover:bg-neutral-600 dark:text-neutral-200 dark:hover:text-white">Account</a>
|
||||
<a routerLink="/account" (click)="closeDropdown()"
|
||||
class="block px-4 py-2 text-sm text-neutral-700 hover:bg-neutral-100 dark:hover:bg-neutral-600 dark:text-neutral-200 dark:hover:text-white">Account</a>
|
||||
</li>
|
||||
@if((user.customerType==='professional' && user.customerSubType==='broker') || user.customerType==='seller' || (authService.isAdmin() | async)){
|
||||
@if((user.customerType==='professional' && user.customerSubType==='broker') || user.customerType==='seller' ||
|
||||
(authService.isAdmin() | async)){
|
||||
<li>
|
||||
@if(user.customerType==='professional'){
|
||||
<a routerLink="/createBusinessListing" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-neutral-700 hover:bg-neutral-100 dark:hover:bg-neutral-600 dark:text-neutral-200 dark:hover:text-white"
|
||||
>Create Listing</a
|
||||
>
|
||||
<a routerLink="/createBusinessListing" (click)="closeDropdown()"
|
||||
class="block px-4 py-2 text-sm text-neutral-700 hover:bg-neutral-100 dark:hover:bg-neutral-600 dark:text-neutral-200 dark:hover:text-white">Create
|
||||
Listing</a>
|
||||
}@else {
|
||||
<a routerLink="/createCommercialPropertyListing" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-neutral-700 hover:bg-neutral-100 dark:hover:bg-neutral-600 dark:text-neutral-200 dark:hover:text-white"
|
||||
>Create Listing</a
|
||||
>
|
||||
<a routerLink="/createCommercialPropertyListing" (click)="closeDropdown()"
|
||||
class="block px-4 py-2 text-sm text-neutral-700 hover:bg-neutral-100 dark:hover:bg-neutral-600 dark:text-neutral-200 dark:hover:text-white">Create
|
||||
Listing</a>
|
||||
}
|
||||
</li>
|
||||
<li>
|
||||
<a routerLink="/myListings" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-neutral-700 hover:bg-neutral-100 dark:hover:bg-neutral-600 dark:text-neutral-200 dark:hover:text-white">My Listings</a>
|
||||
<a routerLink="/myListings" (click)="closeDropdown()"
|
||||
class="block px-4 py-2 text-sm text-neutral-700 hover:bg-neutral-100 dark:hover:bg-neutral-600 dark:text-neutral-200 dark:hover:text-white">My
|
||||
Listings</a>
|
||||
</li>
|
||||
}
|
||||
<li>
|
||||
<a routerLink="/myFavorites" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-neutral-700 hover:bg-neutral-100 dark:hover:bg-neutral-600 dark:text-neutral-200 dark:hover:text-white">My Favorites</a>
|
||||
<a routerLink="/myFavorites" (click)="closeDropdown()"
|
||||
class="block px-4 py-2 text-sm text-neutral-700 hover:bg-neutral-100 dark:hover:bg-neutral-600 dark:text-neutral-200 dark:hover:text-white">My
|
||||
Favorites</a>
|
||||
</li>
|
||||
<li>
|
||||
<a routerLink="/emailUs" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-neutral-700 hover:bg-neutral-100 dark:hover:bg-neutral-600 dark:text-neutral-200 dark:hover:text-white">EMail Us</a>
|
||||
<a routerLink="/emailUs" (click)="closeDropdown()"
|
||||
class="block px-4 py-2 text-sm text-neutral-700 hover:bg-neutral-100 dark:hover:bg-neutral-600 dark:text-neutral-200 dark:hover:text-white">EMail
|
||||
Us</a>
|
||||
</li>
|
||||
<li>
|
||||
<a routerLink="/logout" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-neutral-700 hover:bg-neutral-100 dark:hover:bg-neutral-600 dark:text-neutral-200 dark:hover:text-white">Logout</a>
|
||||
<a routerLink="/logout" (click)="closeDropdown()"
|
||||
class="block px-4 py-2 text-sm text-neutral-700 hover:bg-neutral-100 dark:hover:bg-neutral-600 dark:text-neutral-200 dark:hover:text-white">Logout</a>
|
||||
</li>
|
||||
</ul>
|
||||
@if(authService.isAdmin() | async){
|
||||
<ul class="py-2">
|
||||
<li>
|
||||
<a routerLink="admin/users" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-neutral-700 hover:bg-neutral-100 dark:hover:bg-neutral-600 dark:text-neutral-200 dark:hover:text-white">Users (Admin)</a>
|
||||
<a routerLink="admin/users" (click)="closeDropdown()"
|
||||
class="block px-4 py-2 text-sm text-neutral-700 hover:bg-neutral-100 dark:hover:bg-neutral-600 dark:text-neutral-200 dark:hover:text-white">Users
|
||||
(Admin)</a>
|
||||
</li>
|
||||
</ul>
|
||||
}
|
||||
<ul class="py-2 md:hidden">
|
||||
<li>
|
||||
<a
|
||||
routerLink="/businessListings"
|
||||
<a routerLink="/businessListings"
|
||||
[ngClass]="{ 'text-primary-600': isActive('/businessListings'), 'text-neutral-700': !isActive('/businessListings') }"
|
||||
class="block px-4 py-2 text-sm font-semibold"
|
||||
(click)="closeMenusAndSetCriteria('businessListings')"
|
||||
>Businesses</a
|
||||
>
|
||||
(click)="closeMenusAndSetCriteria('businessListings')">Businesses</a>
|
||||
</li>
|
||||
@if ((numberOfCommercial$ | async) > 0) {
|
||||
<li>
|
||||
<a
|
||||
routerLink="/commercialPropertyListings"
|
||||
<a routerLink="/commercialPropertyListings"
|
||||
[ngClass]="{ 'text-primary-600': isActive('/commercialPropertyListings'), 'text-neutral-700': !isActive('/commercialPropertyListings') }"
|
||||
class="block px-4 py-2 text-sm font-semibold"
|
||||
(click)="closeMenusAndSetCriteria('commercialPropertyListings')"
|
||||
>Properties</a
|
||||
>
|
||||
</li>
|
||||
} @if ((numberOfBroker$ | async) > 0) {
|
||||
<li>
|
||||
<a
|
||||
routerLink="/brokerListings"
|
||||
[ngClass]="{ 'text-primary-600': isActive('/brokerListings'), 'text-neutral-700': !isActive('/brokerListings') }"
|
||||
class="block px-4 py-2 text-sm font-semibold"
|
||||
(click)="closeMenusAndSetCriteria('brokerListings')"
|
||||
>Professionals</a
|
||||
>
|
||||
(click)="closeMenusAndSetCriteria('commercialPropertyListings')">Properties</a>
|
||||
</li>
|
||||
}
|
||||
<li>
|
||||
<a routerLink="/brokerListings"
|
||||
[ngClass]="{ 'text-primary-600': isActive('/brokerListings'), 'text-neutral-700': !isActive('/brokerListings') }"
|
||||
class="block px-4 py-2 text-sm font-semibold"
|
||||
(click)="closeMenusAndSetCriteria('brokerListings')">Professionals</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="z-50 hidden my-4 text-base list-none bg-white divide-y divide-neutral-100 rounded-lg shadow dark:bg-neutral-700 dark:divide-neutral-600" id="user-unknown">
|
||||
<div
|
||||
class="z-50 hidden my-4 text-base list-none bg-white divide-y divide-neutral-100 rounded-lg shadow dark:bg-neutral-700 dark:divide-neutral-600"
|
||||
id="user-unknown">
|
||||
<ul class="py-2" aria-labelledby="user-menu-button">
|
||||
<li>
|
||||
<a routerLink="/login" [queryParams]="{ mode: 'login' }" class="block px-4 py-2 text-sm text-neutral-700 hover:bg-neutral-100 dark:hover:bg-neutral-600 dark:text-neutral-200 dark:hover:text-white">Log In</a>
|
||||
<a routerLink="/login" [queryParams]="{ mode: 'login' }"
|
||||
class="block px-4 py-2 text-sm text-neutral-700 hover:bg-neutral-100 dark:hover:bg-neutral-600 dark:text-neutral-200 dark:hover:text-white">Log
|
||||
In</a>
|
||||
</li>
|
||||
<li>
|
||||
<a routerLink="/login" [queryParams]="{ mode: 'register' }" class="block px-4 py-2 text-sm text-neutral-700 hover:bg-neutral-100 dark:hover:bg-neutral-600 dark:text-neutral-200 dark:hover:text-white">Sign Up</a>
|
||||
<a routerLink="/login" [queryParams]="{ mode: 'register' }"
|
||||
class="block px-4 py-2 text-sm text-neutral-700 hover:bg-neutral-100 dark:hover:bg-neutral-600 dark:text-neutral-200 dark:hover:text-white">Sign
|
||||
Up</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="py-2 md:hidden">
|
||||
<li>
|
||||
<a
|
||||
routerLink="/businessListings"
|
||||
<a routerLink="/businessListings"
|
||||
[ngClass]="{ 'text-primary-600': isActive('/businessListings'), 'text-neutral-700': !isActive('/businessListings') }"
|
||||
class="block px-4 py-2 text-sm font-bold"
|
||||
(click)="closeMenusAndSetCriteria('businessListings')"
|
||||
>Businesses</a
|
||||
>
|
||||
(click)="closeMenusAndSetCriteria('businessListings')">Businesses</a>
|
||||
</li>
|
||||
@if ((numberOfCommercial$ | async) > 0) {
|
||||
<li>
|
||||
<a
|
||||
routerLink="/commercialPropertyListings"
|
||||
<a routerLink="/commercialPropertyListings"
|
||||
[ngClass]="{ 'text-primary-600': isActive('/commercialPropertyListings'), 'text-neutral-700': !isActive('/commercialPropertyListings') }"
|
||||
class="block px-4 py-2 text-sm font-bold"
|
||||
(click)="closeMenusAndSetCriteria('commercialPropertyListings')"
|
||||
>Properties</a
|
||||
>
|
||||
</li>
|
||||
} @if ((numberOfBroker$ | async) > 0) {
|
||||
<li>
|
||||
<a
|
||||
routerLink="/brokerListings"
|
||||
[ngClass]="{ 'text-primary-600': isActive('/brokerListings'), 'text-neutral-700': !isActive('/brokerListings') }"
|
||||
class="block px-4 py-2 text-sm font-bold"
|
||||
(click)="closeMenusAndSetCriteria('brokerListings')"
|
||||
>Professionals</a
|
||||
>
|
||||
(click)="closeMenusAndSetCriteria('commercialPropertyListings')">Properties</a>
|
||||
</li>
|
||||
}
|
||||
<li>
|
||||
<a routerLink="/brokerListings"
|
||||
[ngClass]="{ 'text-primary-600': isActive('/brokerListings'), 'text-neutral-700': !isActive('/brokerListings') }"
|
||||
class="block px-4 py-2 text-sm font-bold"
|
||||
(click)="closeMenusAndSetCriteria('brokerListings')">Professionals</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="items-center justify-between hidden w-full md:flex md:w-auto md:order-1" id="navbar-user">
|
||||
<ul
|
||||
class="flex flex-col font-medium p-4 md:p-0 mt-4 border border-neutral-100 rounded-lg bg-neutral-50 md:space-x-8 rtl:space-x-reverse md:flex-row md:mt-0 md:border-0 md:bg-white dark:bg-neutral-800 md:dark:bg-neutral-900 dark:border-neutral-700"
|
||||
>
|
||||
class="flex flex-col font-medium p-4 md:p-0 mt-4 border border-neutral-100 rounded-lg bg-neutral-50 md:space-x-8 rtl:space-x-reverse md:flex-row md:mt-0 md:border-0 md:bg-white dark:bg-neutral-800 md:dark:bg-neutral-900 dark:border-neutral-700">
|
||||
<li>
|
||||
<a
|
||||
routerLinkActive="active-link"
|
||||
routerLink="/businessListings"
|
||||
<a routerLinkActive="active-link" routerLink="/businessListings"
|
||||
[ngClass]="{ 'bg-primary-600 text-white md:text-primary-600 md:bg-transparent md:dark:text-primary-500': isActive('/businessListings') }"
|
||||
class="block py-2 px-3 rounded hover:bg-neutral-100 md:hover:bg-transparent md:hover:text-primary-600 md:p-0 dark:text-white md:dark:hover:text-primary-500 dark:hover:bg-neutral-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-neutral-700 inline-flex items-center"
|
||||
aria-current="page"
|
||||
(click)="closeMenusAndSetCriteria('businessListings')"
|
||||
>
|
||||
<img src="assets/images/business_logo.png" alt="Business" class="w-5 h-5 mr-2 object-contain" width="20" height="20" />
|
||||
aria-current="page" (click)="closeMenusAndSetCriteria('businessListings')">
|
||||
<img src="assets/images/business_logo.png" alt="Business" class="w-5 h-5 mr-2 object-contain" width="20"
|
||||
height="20" />
|
||||
<span>Businesses</span>
|
||||
</a>
|
||||
</li>
|
||||
@if ((numberOfCommercial$ | async) > 0) {
|
||||
<li>
|
||||
<a
|
||||
routerLinkActive="active-link"
|
||||
routerLink="/commercialPropertyListings"
|
||||
<a routerLinkActive="active-link" routerLink="/commercialPropertyListings"
|
||||
[ngClass]="{ 'bg-primary-600 text-white md:text-primary-600 md:bg-transparent md:dark:text-primary-500': isActive('/commercialPropertyListings') }"
|
||||
class="block py-2 px-3 rounded hover:bg-neutral-100 md:hover:bg-transparent md:hover:text-primary-600 md:p-0 dark:text-white md:dark:hover:text-primary-500 dark:hover:bg-neutral-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-neutral-700 inline-flex items-center"
|
||||
(click)="closeMenusAndSetCriteria('commercialPropertyListings')"
|
||||
>
|
||||
<img src="assets/images/properties_logo.png" alt="Properties" class="w-5 h-5 mr-2 object-contain" width="20" height="20" />
|
||||
(click)="closeMenusAndSetCriteria('commercialPropertyListings')">
|
||||
<img src="assets/images/properties_logo.png" alt="Properties" class="w-5 h-5 mr-2 object-contain" width="20"
|
||||
height="20" />
|
||||
<span>Properties</span>
|
||||
</a>
|
||||
</li>
|
||||
} @if ((numberOfBroker$ | async) > 0) {
|
||||
}
|
||||
<li>
|
||||
<a
|
||||
routerLinkActive="active-link"
|
||||
routerLink="/brokerListings"
|
||||
<a routerLinkActive="active-link" routerLink="/brokerListings"
|
||||
[ngClass]="{ 'bg-primary-600 text-white md:text-primary-600 md:bg-transparent md:dark:text-primary-500': isActive('/brokerListings') }"
|
||||
class="inline-flex items-center py-2 px-3 rounded hover:bg-neutral-100 md:hover:bg-transparent md:hover:text-primary-600 md:p-0 dark:text-white md:dark:hover:text-primary-500 dark:hover:bg-neutral-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-neutral-700"
|
||||
(click)="closeMenusAndSetCriteria('brokerListings')"
|
||||
>
|
||||
<img src="assets/images/icon_professionals.png" alt="Professionals" class="w-5 h-5 mr-2 object-contain bg-transparent" style="mix-blend-mode: darken;" />
|
||||
(click)="closeMenusAndSetCriteria('brokerListings')">
|
||||
<img src="assets/images/icon_professionals.png" alt="Professionals"
|
||||
class="w-5 h-5 mr-2 object-contain bg-transparent" style="mix-blend-mode: darken;" />
|
||||
<span>Professionals</span>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Mobile filter button -->
|
||||
<div class="md:hidden flex justify-center pb-4">
|
||||
<button
|
||||
(click)="toggleSortDropdown()"
|
||||
type="button"
|
||||
id="sortDropdownMobileButton"
|
||||
<button (click)="toggleSortDropdown()" type="button" id="sortDropdownMobileButton"
|
||||
class="mx-4 w-1/2 px-4 py-2 text-sm font-medium bg-white border border-neutral-200 rounded-lg hover:bg-neutral-100 hover:text-primary-600 focus:ring-2 focus:ring-primary-600 focus:text-primary-600 dark:bg-neutral-800 dark:text-neutral-400 dark:border-neutral-600 dark:hover:text-white dark:hover:bg-neutral-700"
|
||||
[ngClass]="{ 'text-primary-500': selectOptions.getSortByOption(sortBy) !== 'Sort', 'text-neutral-900': selectOptions.getSortByOption(sortBy) === 'Sort' }"
|
||||
>
|
||||
[ngClass]="{ 'text-primary-500': selectOptions.getSortByOption(sortBy) !== 'Sort', 'text-neutral-900': selectOptions.getSortByOption(sortBy) === 'Sort' }">
|
||||
<i class="fas fa-sort mr-2"></i>{{ selectOptions.getSortByOption(sortBy) }}
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@
|
|||
<!-- <fa-icon [icon]="isLoginMode ? 'fas fas-user-plus' : 'userplus'" class="mr-2"></fa-icon> -->
|
||||
<i *ngIf="isLoginMode" class="fa-solid fa-user-plus mr-2"></i>
|
||||
<i *ngIf="!isLoginMode" class="fa-solid fa-arrow-right mr-2"></i>
|
||||
{{ isLoginMode ? 'Sign in with Email' : 'Register' }}
|
||||
{{ isLoginMode ? 'Sign in with Email' : 'Sign Up' }}
|
||||
</button>
|
||||
|
||||
<!-- Trennlinie -->
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { Component, inject, PLATFORM_ID } from '@angular/core';
|
||||
import { isPlatformBrowser } from '@angular/common';
|
||||
import { Control, DomEvent, DomUtil, icon, Icon, latLng, Layer, Map, MapOptions, Marker, tileLayer } from 'leaflet';
|
||||
import { BusinessListing, CommercialPropertyListing } from '../../../../../bizmatch-server/src/models/db.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-base-details',
|
||||
template: ``,
|
||||
|
|
@ -12,9 +14,15 @@ export abstract class BaseDetailsComponent {
|
|||
mapOptions: MapOptions;
|
||||
mapLayers: Layer[] = [];
|
||||
mapCenter: any;
|
||||
mapZoom: number = 13; // Standardzoomlevel
|
||||
mapZoom: number = 13;
|
||||
protected listing: BusinessListing | CommercialPropertyListing;
|
||||
protected isBrowser: boolean;
|
||||
private platformId = inject(PLATFORM_ID);
|
||||
|
||||
constructor() {
|
||||
this.isBrowser = isPlatformBrowser(this.platformId);
|
||||
// Only initialize mapOptions in browser context
|
||||
if (this.isBrowser) {
|
||||
this.mapOptions = {
|
||||
layers: [
|
||||
tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
|
|
@ -22,10 +30,16 @@ export abstract class BaseDetailsComponent {
|
|||
}),
|
||||
],
|
||||
zoom: this.mapZoom,
|
||||
center: latLng(0, 0), // Platzhalter, wird später gesetzt
|
||||
center: latLng(0, 0),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
protected configureMap() {
|
||||
if (!this.isBrowser) {
|
||||
return; // Skip on server
|
||||
}
|
||||
|
||||
const latitude = this.listing.location.latitude;
|
||||
const longitude = this.listing.location.longitude;
|
||||
|
||||
|
|
@ -33,7 +47,6 @@ export abstract class BaseDetailsComponent {
|
|||
longitude !== null && longitude !== undefined) {
|
||||
this.mapCenter = latLng(latitude, longitude);
|
||||
|
||||
// Build address string from available location data
|
||||
const addressParts = [];
|
||||
if (this.listing.location.housenumber) addressParts.push(this.listing.location.housenumber);
|
||||
if (this.listing.location.street) addressParts.push(this.listing.location.street);
|
||||
|
|
@ -53,7 +66,6 @@ export abstract class BaseDetailsComponent {
|
|||
}),
|
||||
});
|
||||
|
||||
// Add popup to marker with address
|
||||
if (fullAddress) {
|
||||
marker.bindPopup(`
|
||||
<div style="padding: 8px;">
|
||||
|
|
@ -76,8 +88,12 @@ export abstract class BaseDetailsComponent {
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
onMapReady(map: Map) {
|
||||
// Build comprehensive address for the control
|
||||
if (!this.isBrowser) {
|
||||
return;
|
||||
}
|
||||
|
||||
const addressParts = [];
|
||||
if (this.listing.location.housenumber) addressParts.push(this.listing.location.housenumber);
|
||||
if (this.listing.location.street) addressParts.push(this.listing.location.street);
|
||||
|
|
@ -99,10 +115,8 @@ export abstract class BaseDetailsComponent {
|
|||
</div>
|
||||
`;
|
||||
|
||||
// Verhindere, dass die Karte durch das Klicken des Links bewegt wird
|
||||
DomEvent.disableClickPropagation(container);
|
||||
|
||||
// Füge einen Event Listener für den Link hinzu
|
||||
const link = container.querySelector('#view-full-map') as HTMLElement;
|
||||
if (link) {
|
||||
DomEvent.on(link, 'click', (e: Event) => {
|
||||
|
|
@ -117,12 +131,20 @@ export abstract class BaseDetailsComponent {
|
|||
addressControl.addTo(map);
|
||||
}
|
||||
}
|
||||
|
||||
openFullMap() {
|
||||
if (!this.isBrowser) {
|
||||
return;
|
||||
}
|
||||
|
||||
const latitude = this.listing.location.latitude;
|
||||
const longitude = this.listing.location.longitude;
|
||||
const address = `${this.listing.location.housenumber} ${this.listing.location.street}, ${this.listing.location.name ? this.listing.location.name : this.listing.location.county}, ${this.listing.location.state}`;
|
||||
const url = `https://www.openstreetmap.org/?mlat=${latitude}&mlon=${longitude}#map=15/${latitude}/${longitude}`;
|
||||
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,10 +5,8 @@
|
|||
}
|
||||
|
||||
<div class="bg-white rounded-lg drop-shadow-custom-bg-mobile md:drop-shadow-custom-bg overflow-hidden relative">
|
||||
<button
|
||||
(click)="historyService.goBack()"
|
||||
class="absolute top-4 right-4 bg-red-500 text-white rounded-full w-8 h-8 flex items-center justify-center hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-opacity-50 print:hidden"
|
||||
>
|
||||
<button (click)="historyService.goBack()"
|
||||
class="absolute top-4 right-4 bg-red-500 text-white rounded-full w-8 h-8 flex items-center justify-center hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-opacity-50 print:hidden">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
@if(listing){
|
||||
|
|
@ -19,30 +17,38 @@
|
|||
<p class="mb-4" [innerHTML]="description"></p>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div *ngFor="let detail of listingDetails; let i = index" class="flex flex-col sm:flex-row" [ngClass]="{ 'bg-neutral-100': i % 2 === 0 }">
|
||||
<div *ngFor="let detail of listingDetails; let i = index" class="flex flex-col sm:flex-row"
|
||||
[ngClass]="{ 'bg-neutral-100': i % 2 === 0 }">
|
||||
<div class="w-full sm:w-1/3 font-semibold p-2">{{ detail.label }}</div>
|
||||
|
||||
<div class="w-full sm:w-2/3 p-2" *ngIf="!detail.isHtml && !detail.isListingBy">{{ detail.value }}</div>
|
||||
|
||||
<div class="w-full sm:w-2/3 p-2 flex space-x-2" [innerHTML]="detail.value" *ngIf="detail.isHtml && !detail.isListingBy"></div>
|
||||
<div class="w-full sm:w-2/3 p-2 flex space-x-2" [innerHTML]="detail.value"
|
||||
*ngIf="detail.isHtml && !detail.isListingBy"></div>
|
||||
|
||||
<div class="w-full sm:w-2/3 p-2 flex space-x-2" *ngIf="detail.isListingBy && listingUser">
|
||||
<a routerLink="/details-user/{{ listingUser.id }}" 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" />
|
||||
<a routerLink="/details-user/{{ listingUser.id }}"
|
||||
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" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="py-4 print:hidden">
|
||||
@if(listing && listingUser && (listingUser?.email===user?.email || (authService.isAdmin() | async))){
|
||||
<div class="inline">
|
||||
<button class="share share-edit text-white font-bold text-xs py-1.5 px-2 inline-flex items-center" [routerLink]="['/editBusinessListing', listing.id]">
|
||||
<button class="share share-edit text-white font-bold text-xs py-1.5 px-2 inline-flex items-center"
|
||||
[routerLink]="['/editBusinessListing', listing.id]">
|
||||
<i class="fa-regular fa-pen-to-square"></i>
|
||||
<span class="ml-2">Edit</span>
|
||||
</button>
|
||||
</div>
|
||||
} @if(user){
|
||||
<div class="inline">
|
||||
<button class="share share-save text-white font-bold text-xs py-1.5 px-2 inline-flex items-center" (click)="save()" [disabled]="listing.favoritesForUser.includes(user.email)">
|
||||
<button class="share share-save text-white font-bold text-xs py-1.5 px-2 inline-flex items-center"
|
||||
(click)="save()" [disabled]="listing.favoritesForUser.includes(user.email)">
|
||||
<i class="fa-regular fa-heart"></i>
|
||||
@if(listing.favoritesForUser.includes(user.email)){
|
||||
<span class="ml-2">Saved ...</span>
|
||||
|
|
@ -55,21 +61,46 @@
|
|||
<share-button button="print" showText="true" (click)="createEvent('print')"></share-button>
|
||||
<!-- <share-button button="email" showText="true"></share-button> -->
|
||||
<div class="inline">
|
||||
<button class="share share-email text-white font-bold text-xs py-1.5 px-2 inline-flex items-center" (click)="showShareByEMail()">
|
||||
<button class="share share-email text-white font-bold text-xs py-1.5 px-2 inline-flex items-center"
|
||||
(click)="showShareByEMail()">
|
||||
<i class="fa-solid fa-envelope"></i>
|
||||
<span class="ml-2">Email</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<share-button button="facebook" showText="true" (click)="createEvent('facebook')"></share-button>
|
||||
<share-button button="x" showText="true" (click)="createEvent('x')"></share-button>
|
||||
<share-button button="linkedin" showText="true" (click)="createEvent('linkedin')"></share-button>
|
||||
<div class="inline">
|
||||
<button type="button"
|
||||
class="share share-facebook text-white font-bold text-xs py-1.5 px-2 inline-flex items-center"
|
||||
(click)="shareToFacebook()">
|
||||
<i class="fab fa-facebook"></i>
|
||||
<span class="ml-2">Facebook</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="inline">
|
||||
<button type="button"
|
||||
class="share share-twitter text-white font-bold text-xs py-1.5 px-2 inline-flex items-center"
|
||||
(click)="shareToTwitter()">
|
||||
<i class="fab fa-x-twitter"></i>
|
||||
<span class="ml-2">X</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="inline">
|
||||
<button type="button"
|
||||
class="share share-linkedin text-white font-bold text-xs py-1.5 px-2 inline-flex items-center"
|
||||
(click)="shareToLinkedIn()">
|
||||
<i class="fab fa-linkedin"></i>
|
||||
<span class="ml-2">LinkedIn</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Karte hinzufügen, wenn Straße vorhanden ist -->
|
||||
<div *ngIf="listing.location.latitude && listing.location.longitude" class="mt-6">
|
||||
<h2 class="text-xl font-semibold mb-2">Location Map</h2>
|
||||
<!-- <div style="height: 300px" leaflet [leafletOptions]="mapOptions" [leafletLayers]="mapLayers" [leafletCenter]="mapCenter" [leafletZoom]="mapZoom"></div> -->
|
||||
<div style="height: 400px" leaflet [leafletOptions]="mapOptions" [leafletLayers]="mapLayers" [leafletCenter]="mapCenter" [leafletZoom]="mapZoom" (leafletMapReady)="onMapReady($event)"></div>
|
||||
<div style="height: 400px" leaflet [leafletOptions]="mapOptions" [leafletLayers]="mapLayers"
|
||||
[leafletCenter]="mapCenter" [leafletZoom]="mapZoom" (leafletMapReady)="onMapReady($event)"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -80,19 +111,24 @@
|
|||
<form class="space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<app-validated-input label="Your Name" name="name" [(ngModel)]="mailinfo.sender.name"></app-validated-input>
|
||||
<app-validated-input label="Your Email" name="email" [(ngModel)]="mailinfo.sender.email" kind="email"></app-validated-input>
|
||||
<app-validated-input label="Your Email" name="email" [(ngModel)]="mailinfo.sender.email"
|
||||
kind="email"></app-validated-input>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<app-validated-input label="Phone Number" name="phoneNumber" [(ngModel)]="mailinfo.sender.phoneNumber" mask="(000) 000-0000"></app-validated-input>
|
||||
<app-validated-input label="Phone Number" name="phoneNumber" [(ngModel)]="mailinfo.sender.phoneNumber"
|
||||
mask="(000) 000-0000"></app-validated-input>
|
||||
<!-- <app-validated-input label="Country/State" name="state" [(ngModel)]="mailinfo.sender.state"></app-validated-input> -->
|
||||
<app-validated-ng-select label="State" name="state" [(ngModel)]="mailinfo.sender.state" [items]="selectOptions?.states"></app-validated-ng-select>
|
||||
<app-validated-ng-select label="State" name="state" [(ngModel)]="mailinfo.sender.state"
|
||||
[items]="selectOptions?.states"></app-validated-ng-select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<app-validated-textarea label="Questions/Comments" name="comments" [(ngModel)]="mailinfo.sender.comments"></app-validated-textarea>
|
||||
<app-validated-textarea label="Questions/Comments" name="comments"
|
||||
[(ngModel)]="mailinfo.sender.comments"></app-validated-textarea>
|
||||
</div>
|
||||
<button (click)="mail()" class="w-full sm:w-auto px-4 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-opacity-50">Submit</button>
|
||||
<button (click)="mail()"
|
||||
class="w-full sm:w-auto px-4 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-opacity-50">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -107,13 +143,17 @@
|
|||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
@for (related of relatedListings; track related.id) {
|
||||
<a [routerLink]="['/business', related.slug || related.id]" class="block group">
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden hover:shadow-xl transition-all duration-300 hover:scale-[1.02]">
|
||||
<div
|
||||
class="bg-white rounded-lg border border-gray-200 overflow-hidden hover:shadow-xl transition-all duration-300 hover:scale-[1.02]">
|
||||
<div class="p-4">
|
||||
<div class="flex items-center mb-3">
|
||||
<i [class]="selectOptions.getIconAndTextColorType(related.type)" class="mr-2 text-lg"></i>
|
||||
<span [class]="selectOptions.getTextColorType(related.type)" class="font-semibold text-sm">{{ selectOptions.getBusiness(related.type) }}</span>
|
||||
<span [class]="selectOptions.getTextColorType(related.type)" class="font-semibold text-sm">{{
|
||||
selectOptions.getBusiness(related.type) }}</span>
|
||||
</div>
|
||||
<h3 class="text-lg font-bold mb-2 text-gray-900 group-hover:text-primary-600 transition-colors line-clamp-2">{{ related.title }}</h3>
|
||||
<h3
|
||||
class="text-lg font-bold mb-2 text-gray-900 group-hover:text-primary-600 transition-colors line-clamp-2">
|
||||
{{ related.title }}</h3>
|
||||
<div class="space-y-1 text-sm text-gray-600">
|
||||
<div class="flex justify-between">
|
||||
<span class="font-medium">Price:</span>
|
||||
|
|
@ -127,11 +167,14 @@
|
|||
}
|
||||
<div class="flex justify-between">
|
||||
<span class="font-medium">Location:</span>
|
||||
<span>{{ related.location.name || related.location.county }}, {{ selectOptions.getState(related.location.state) }}</span>
|
||||
<span>{{ related.location.name || related.location.county }}, {{
|
||||
selectOptions.getState(related.location.state) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<span class="inline-block bg-primary-100 text-primary-800 text-xs font-medium px-2.5 py-0.5 rounded">View Details →</span>
|
||||
<span
|
||||
class="inline-block bg-primary-100 text-primary-800 text-xs font-medium px-2.5 py-0.5 rounded">View
|
||||
Details →</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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,8 +231,7 @@ 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 ? ', ' : ''
|
||||
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 '}` },
|
||||
|
|
@ -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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,10 +6,8 @@
|
|||
@if(listing){
|
||||
<div class="p-6 relative">
|
||||
<h1 class="text-3xl font-bold mb-4">{{ listing?.title }}</h1>
|
||||
<button
|
||||
(click)="historyService.goBack()"
|
||||
class="print:hidden absolute top-4 right-4 bg-red-500 text-white rounded-full w-8 h-8 flex items-center justify-center hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-opacity-50"
|
||||
>
|
||||
<button (click)="historyService.goBack()"
|
||||
class="print:hidden absolute top-4 right-4 bg-red-500 text-white rounded-full w-8 h-8 flex items-center justify-center hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-opacity-50">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
<div class="flex flex-col lg:flex-row">
|
||||
|
|
@ -17,33 +15,41 @@
|
|||
<p class="mb-4" [innerHTML]="description"></p>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div *ngFor="let detail of propertyDetails; let i = index" class="flex flex-col sm:flex-row" [ngClass]="{ 'bg-neutral-100': i % 2 === 0 }">
|
||||
<div *ngFor="let detail of propertyDetails; let i = index" class="flex flex-col sm:flex-row"
|
||||
[ngClass]="{ 'bg-neutral-100': i % 2 === 0 }">
|
||||
<div class="w-full sm:w-1/3 font-semibold p-2">{{ detail.label }}</div>
|
||||
|
||||
<!-- Standard Text -->
|
||||
<div class="w-full sm:w-2/3 p-2" *ngIf="!detail.isHtml && !detail.isListingBy">{{ detail.value }}</div>
|
||||
|
||||
<!-- HTML Content (nicht für RouterLink) -->
|
||||
<div class="w-full sm:w-2/3 p-2 flex space-x-2" [innerHTML]="detail.value" *ngIf="detail.isHtml && !detail.isListingBy"></div>
|
||||
<div class="w-full sm:w-2/3 p-2 flex space-x-2" [innerHTML]="detail.value"
|
||||
*ngIf="detail.isHtml && !detail.isListingBy"></div>
|
||||
|
||||
<!-- Speziell für Listing By mit RouterLink -->
|
||||
<div class="w-full sm:w-2/3 p-2 flex space-x-2" *ngIf="detail.isListingBy && detail.user">
|
||||
<a [routerLink]="['/details-user', detail.user.id]" 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" />
|
||||
<a [routerLink]="['/details-user', detail.user.id]"
|
||||
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" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="py-4 print:hidden">
|
||||
@if(listing && listingUser && (listingUser?.email===user?.email || (authService.isAdmin() | async))){
|
||||
<div class="inline">
|
||||
<button class="share share-edit text-white font-bold text-xs py-1.5 px-2 inline-flex items-center" [routerLink]="['/editCommercialPropertyListing', listing.id]">
|
||||
<button class="share share-edit text-white font-bold text-xs py-1.5 px-2 inline-flex items-center"
|
||||
[routerLink]="['/editCommercialPropertyListing', listing.id]">
|
||||
<i class="fa-regular fa-pen-to-square"></i>
|
||||
<span class="ml-2">Edit</span>
|
||||
</button>
|
||||
</div>
|
||||
} @if(user){
|
||||
<div class="inline">
|
||||
<button class="share share-save text-white font-bold text-xs py-1.5 px-2 inline-flex items-center" (click)="save()" [disabled]="listing.favoritesForUser.includes(user.email)">
|
||||
<button class="share share-save text-white font-bold text-xs py-1.5 px-2 inline-flex items-center"
|
||||
(click)="save()" [disabled]="listing.favoritesForUser.includes(user.email)">
|
||||
<i class="fa-regular fa-heart"></i>
|
||||
@if(listing.favoritesForUser.includes(user.email)){
|
||||
<span class="ml-2">Saved ...</span>
|
||||
|
|
@ -56,21 +62,46 @@
|
|||
<share-button button="print" showText="true" (click)="createEvent('print')"></share-button>
|
||||
<!-- <share-button button="email" showText="true"></share-button> -->
|
||||
<div class="inline">
|
||||
<button class="share share-email text-white font-bold text-xs py-1.5 px-2 inline-flex items-center" (click)="showShareByEMail()">
|
||||
<button class="share share-email text-white font-bold text-xs py-1.5 px-2 inline-flex items-center"
|
||||
(click)="showShareByEMail()">
|
||||
<i class="fa-solid fa-envelope"></i>
|
||||
<span class="ml-2">Email</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<share-button button="facebook" showText="true" (click)="createEvent('facebook')"></share-button>
|
||||
<share-button button="x" showText="true" (click)="createEvent('x')"></share-button>
|
||||
<share-button button="linkedin" showText="true" (click)="createEvent('linkedin')"></share-button>
|
||||
<div class="inline">
|
||||
<button type="button"
|
||||
class="share share-facebook text-white font-bold text-xs py-1.5 px-2 inline-flex items-center"
|
||||
(click)="shareToFacebook()">
|
||||
<i class="fab fa-facebook"></i>
|
||||
<span class="ml-2">Facebook</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="inline">
|
||||
<button type="button"
|
||||
class="share share-twitter text-white font-bold text-xs py-1.5 px-2 inline-flex items-center"
|
||||
(click)="shareToTwitter()">
|
||||
<i class="fab fa-x-twitter"></i>
|
||||
<span class="ml-2">X</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="inline">
|
||||
<button type="button"
|
||||
class="share share-linkedin text-white font-bold text-xs py-1.5 px-2 inline-flex items-center"
|
||||
(click)="shareToLinkedIn()">
|
||||
<i class="fab fa-linkedin"></i>
|
||||
<span class="ml-2">LinkedIn</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Karte hinzufügen, wenn Straße vorhanden ist -->
|
||||
<div *ngIf="listing.location.latitude && listing.location.longitude" class="mt-6">
|
||||
<h2 class="text-lg font-semibold mb-2">Location Map</h2>
|
||||
<!-- <div style="height: 300px" leaflet [leafletOptions]="mapOptions" [leafletLayers]="mapLayers" [leafletCenter]="mapCenter" [leafletZoom]="mapZoom"></div> -->
|
||||
<div style="height: 400px" leaflet [leafletOptions]="mapOptions" [leafletLayers]="mapLayers" [leafletCenter]="mapCenter" [leafletZoom]="mapZoom" (leafletMapReady)="onMapReady($event)"></div>
|
||||
<div style="height: 400px" leaflet [leafletOptions]="mapOptions" [leafletLayers]="mapLayers"
|
||||
[leafletCenter]="mapCenter" [leafletZoom]="mapZoom" (leafletMapReady)="onMapReady($event)"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -89,20 +120,26 @@
|
|||
<p class="text-sm text-neutral-600 mb-4">Please include your contact info below</p>
|
||||
<form class="space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<app-validated-input label="Your Name" name="name" [(ngModel)]="mailinfo.sender.name"></app-validated-input>
|
||||
<app-validated-input label="Your Email" name="email" [(ngModel)]="mailinfo.sender.email" kind="email"></app-validated-input>
|
||||
<app-validated-input label="Your Name" name="name"
|
||||
[(ngModel)]="mailinfo.sender.name"></app-validated-input>
|
||||
<app-validated-input label="Your Email" name="email" [(ngModel)]="mailinfo.sender.email"
|
||||
kind="email"></app-validated-input>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<app-validated-input label="Phone Number" name="phoneNumber" [(ngModel)]="mailinfo.sender.phoneNumber" mask="(000) 000-0000"></app-validated-input>
|
||||
<app-validated-input label="Phone Number" name="phoneNumber" [(ngModel)]="mailinfo.sender.phoneNumber"
|
||||
mask="(000) 000-0000"></app-validated-input>
|
||||
<!-- <app-validated-input label="Country/State" name="state" [(ngModel)]="mailinfo.sender.state"></app-validated-input> -->
|
||||
<app-validated-ng-select label="State" name="state" [(ngModel)]="mailinfo.sender.state" [items]="selectOptions?.states"></app-validated-ng-select>
|
||||
<app-validated-ng-select label="State" name="state" [(ngModel)]="mailinfo.sender.state"
|
||||
[items]="selectOptions?.states"></app-validated-ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<app-validated-textarea label="Questions/Comments" name="comments" [(ngModel)]="mailinfo.sender.comments"></app-validated-textarea>
|
||||
<app-validated-textarea label="Questions/Comments" name="comments"
|
||||
[(ngModel)]="mailinfo.sender.comments"></app-validated-textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<button (click)="mail()" class="bg-primary-500 text-white px-4 py-2 rounded hover:bg-primary-600">Submit</button>
|
||||
<button (click)="mail()"
|
||||
class="bg-primary-500 text-white px-4 py-2 rounded hover:bg-primary-600">Submit</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -120,13 +157,17 @@
|
|||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
@for (related of relatedListings; track related.id) {
|
||||
<a [routerLink]="['/commercial-property', related.slug || related.id]" class="block group">
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden hover:shadow-xl transition-all duration-300 hover:scale-[1.02]">
|
||||
<div
|
||||
class="bg-white rounded-lg border border-gray-200 overflow-hidden hover:shadow-xl transition-all duration-300 hover:scale-[1.02]">
|
||||
<div class="p-4">
|
||||
<div class="flex items-center mb-3">
|
||||
<i [class]="selectOptions.getIconAndTextColorType(related.type)" class="mr-2 text-lg"></i>
|
||||
<span [class]="selectOptions.getTextColorType(related.type)" class="font-semibold text-sm">{{ selectOptions.getCommercialProperty(related.type) }}</span>
|
||||
<span [class]="selectOptions.getTextColorType(related.type)" class="font-semibold text-sm">{{
|
||||
selectOptions.getCommercialProperty(related.type) }}</span>
|
||||
</div>
|
||||
<h3 class="text-lg font-bold mb-2 text-gray-900 group-hover:text-primary-600 transition-colors line-clamp-2">{{ related.title }}</h3>
|
||||
<h3
|
||||
class="text-lg font-bold mb-2 text-gray-900 group-hover:text-primary-600 transition-colors line-clamp-2">
|
||||
{{ related.title }}</h3>
|
||||
<div class="space-y-1 text-sm text-gray-600">
|
||||
<div class="flex justify-between">
|
||||
<span class="font-medium">Price:</span>
|
||||
|
|
@ -134,11 +175,14 @@
|
|||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="font-medium">Location:</span>
|
||||
<span>{{ related.location.name || related.location.county }}, {{ selectOptions.getState(related.location.state) }}</span>
|
||||
<span>{{ related.location.name || related.location.county }}, {{
|
||||
selectOptions.getState(related.location.state) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<span class="inline-block bg-primary-100 text-primary-800 text-xs font-medium px-2.5 py-0.5 rounded">View Details →</span>
|
||||
<span
|
||||
class="inline-block bg-primary-100 text-primary-800 text-xs font-medium px-2.5 py-0.5 rounded">View
|
||||
Details →</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { LeafletModule } from '@bluehalo/ngx-leaflet';
|
|||
import { faTimes } from '@fortawesome/free-solid-svg-icons';
|
||||
import dayjs from 'dayjs';
|
||||
import { GalleryModule, ImageItem } from 'ng-gallery';
|
||||
import { ShareButton } from 'ngx-sharebuttons/button';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
import { CommercialPropertyListing, EventTypeEnum, ShareByEMail, User } from '../../../../../../bizmatch-server/src/models/db.model';
|
||||
import { CommercialPropertyListingCriteria, ErrorResponse, KeycloakUser, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model';
|
||||
|
|
@ -29,11 +28,12 @@ import { SharedModule } from '../../../shared/shared/shared.module';
|
|||
import { createMailInfo, map2User } from '../../../utils/utils';
|
||||
import { BaseDetailsComponent } from '../base-details.component';
|
||||
import { BreadcrumbItem, BreadcrumbsComponent } from '../../../components/breadcrumbs/breadcrumbs.component';
|
||||
import { ShareButton } from 'ngx-sharebuttons/button';
|
||||
|
||||
@Component({
|
||||
selector: 'app-details-commercial-property-listing',
|
||||
standalone: true,
|
||||
imports: [SharedModule, ValidatedInputComponent, ValidatedTextareaComponent, ShareButton, ValidatedNgSelectComponent, GalleryModule, LeafletModule, BreadcrumbsComponent],
|
||||
imports: [SharedModule, ValidatedInputComponent, ValidatedTextareaComponent, ValidatedNgSelectComponent, GalleryModule, LeafletModule, BreadcrumbsComponent, ShareButton],
|
||||
providers: [],
|
||||
templateUrl: './details-commercial-property-listing.component.html',
|
||||
styleUrl: '../details.scss',
|
||||
|
|
@ -282,6 +282,26 @@ export class DetailsCommercialPropertyListingComponent extends BaseDetailsCompon
|
|||
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 commercial property');
|
||||
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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ button.share {
|
|||
margin-right: 4px;
|
||||
margin-left: 2px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
i {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
|
@ -71,6 +72,15 @@ button.share {
|
|||
.share-email {
|
||||
background-color: #ff961c;
|
||||
}
|
||||
.share-facebook {
|
||||
background-color: #1877f2;
|
||||
}
|
||||
.share-twitter {
|
||||
background-color: #000000;
|
||||
}
|
||||
.share-linkedin {
|
||||
background-color: #0a66c2;
|
||||
}
|
||||
:host ::ng-deep .ng-select-container {
|
||||
height: 42px !important;
|
||||
border-radius: 0.5rem;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
} @else {
|
||||
<!-- <a routerLink="/pricing" class="text-neutral-800">Pricing</a> -->
|
||||
<a routerLink="/login" [queryParams]="{ mode: 'login' }" class="text-primary-600 border border-primary-600 px-3 py-2 rounded">Log In</a>
|
||||
<a routerLink="/login" [queryParams]="{ mode: 'register' }" class="text-white bg-primary-600 px-4 py-2 rounded">Register</a>
|
||||
<a routerLink="/login" [queryParams]="{ mode: 'register' }" class="text-white bg-primary-600 px-4 py-2 rounded">Sign Up</a>
|
||||
<!-- <a routerLink="/login" class="text-primary-500 hover:underline">Login/Register</a> -->
|
||||
}
|
||||
</div>
|
||||
|
|
@ -24,7 +24,7 @@
|
|||
<a routerLink="/account" class="text-white text-xl py-2">Account</a>
|
||||
} @else {
|
||||
<a routerLink="/login" [queryParams]="{ mode: 'login' }" class="text-white text-xl py-2">Log In</a>
|
||||
<a routerLink="/login" [queryParams]="{ mode: 'register' }" class="text-white text-xl py-2">Register</a>
|
||||
<a routerLink="/login" [queryParams]="{ mode: 'register' }" class="text-white text-xl py-2">Sign Up</a>
|
||||
}
|
||||
<button (click)="toggleMenu()" class="text-white mt-4">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
|
|
@ -191,65 +191,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Trust & Social Proof Section -->
|
||||
<div class="w-full px-4 mt-8">
|
||||
<div class="trust-section-container bg-white rounded-xl py-10 px-6 md:px-10 border border-neutral-200">
|
||||
<div class="max-w-6xl mx-auto">
|
||||
<h2 class="text-2xl md:text-3xl font-semibold text-center text-neutral-800 mb-2">Trusted by Thousands</h2>
|
||||
<p class="text-center text-neutral-500 mb-10 text-base">Join thousands of successful buyers and sellers on BizMatch</p>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 md:gap-8">
|
||||
<!-- Trust Badge 1 -->
|
||||
<div class="trust-badge text-center">
|
||||
<div class="trust-icon inline-flex items-center justify-center w-12 h-12 bg-neutral-100 text-neutral-600 rounded-full mb-3">
|
||||
<i class="fas fa-shield-alt text-lg"></i>
|
||||
</div>
|
||||
<h3 class="text-base font-semibold text-neutral-800 mb-1">Verified Listings</h3>
|
||||
<p class="text-sm text-neutral-500">All business listings are verified and reviewed by our team</p>
|
||||
</div>
|
||||
|
||||
<!-- Trust Badge 2 -->
|
||||
<div class="trust-badge text-center">
|
||||
<div class="trust-icon inline-flex items-center justify-center w-12 h-12 bg-neutral-100 text-neutral-600 rounded-full mb-3">
|
||||
<i class="fas fa-users text-lg"></i>
|
||||
</div>
|
||||
<h3 class="text-base font-semibold text-neutral-800 mb-1">Expert Support</h3>
|
||||
<p class="text-sm text-neutral-500">Connect with licensed business brokers and advisors</p>
|
||||
</div>
|
||||
|
||||
<!-- Trust Badge 3 -->
|
||||
<div class="trust-badge text-center">
|
||||
<div class="trust-icon inline-flex items-center justify-center w-12 h-12 bg-neutral-100 text-neutral-600 rounded-full mb-3">
|
||||
<i class="fas fa-lock text-lg"></i>
|
||||
</div>
|
||||
<h3 class="text-base font-semibold text-neutral-800 mb-1">Secure Platform</h3>
|
||||
<p class="text-sm text-neutral-500">Your information is protected with enterprise-grade security</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Row -->
|
||||
<div class="stats-section grid grid-cols-2 md:grid-cols-4 gap-4 md:gap-6 mt-10 pt-6 border-t border-neutral-100">
|
||||
<div class="text-center">
|
||||
<div class="stat-number text-2xl md:text-3xl font-semibold text-neutral-700 mb-1">{{ activeListingsCount | number:'1.0-0' }}+</div>
|
||||
<div class="text-xs md:text-sm text-neutral-500">Active Listings</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="stat-number text-2xl md:text-3xl font-semibold text-neutral-700 mb-1">{{ successfulSalesCount | number:'1.0-0' }}+</div>
|
||||
<div class="text-xs md:text-sm text-neutral-500">Successful Sales</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="stat-number text-2xl md:text-3xl font-semibold text-neutral-700 mb-1">{{ brokersCount | number:'1.0-0' }}+</div>
|
||||
<div class="text-xs md:text-sm text-neutral-500">Business Brokers</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="stat-number text-2xl md:text-3xl font-semibold text-neutral-700 mb-1">24/7</div>
|
||||
<div class="text-xs md:text-sm text-neutral-500">Platform Access</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FAQ Section for SEO/AEO -->
|
||||
<div class="w-full px-4 mt-12 max-w-4xl mx-auto">
|
||||
<app-faq [faqItems]="faqItems"></app-faq>
|
||||
|
|
|
|||
|
|
@ -59,12 +59,6 @@ export class HomeComponent {
|
|||
showInput: boolean = true;
|
||||
tooltipTargetBeta = 'tooltipTargetBeta';
|
||||
|
||||
// Counter animation
|
||||
activeListingsCount = 0;
|
||||
successfulSalesCount = 0;
|
||||
brokersCount = 0;
|
||||
hasAnimated = false;
|
||||
|
||||
// FAQ data optimized for AEO (Answer Engine Optimization) and Featured Snippets
|
||||
faqItems: FAQItem[] = [
|
||||
{
|
||||
|
|
@ -200,47 +194,6 @@ export class HomeComponent {
|
|||
this.user = map2User(token);
|
||||
this.loadCities();
|
||||
this.setTotalNumberOfResults();
|
||||
|
||||
// Setup intersection observer for counter animation
|
||||
this.setupCounterAnimation();
|
||||
}
|
||||
|
||||
setupCounterAnimation() {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting && !this.hasAnimated) {
|
||||
this.hasAnimated = true;
|
||||
this.animateCounter('activeListingsCount', 1000, 2000);
|
||||
this.animateCounter('successfulSalesCount', 500, 2000);
|
||||
this.animateCounter('brokersCount', 50, 2000);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: 0.3 }
|
||||
);
|
||||
|
||||
// Wait for the element to be available
|
||||
setTimeout(() => {
|
||||
const statsElement = document.querySelector('.stats-section');
|
||||
if (statsElement) {
|
||||
observer.observe(statsElement);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
animateCounter(property: 'activeListingsCount' | 'successfulSalesCount' | 'brokersCount', target: number, duration: number) {
|
||||
const start = 0;
|
||||
const increment = target / (duration / 16); // 60fps
|
||||
const step = () => {
|
||||
this[property] += increment;
|
||||
if (this[property] < target) {
|
||||
requestAnimationFrame(step);
|
||||
} else {
|
||||
this[property] = target;
|
||||
}
|
||||
};
|
||||
requestAnimationFrame(step);
|
||||
}
|
||||
|
||||
changeTab(tabname: 'business' | 'commercialProperty' | 'broker') {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
<!-- SEO-optimized heading -->
|
||||
<div class="mb-6">
|
||||
<h1 class="text-3xl md:text-4xl font-bold text-neutral-900 mb-2">Business Professionals Directory</h1>
|
||||
<h1 class="text-3xl md:text-4xl font-bold text-neutral-900 mb-2">Professional Business Brokers & Advisors</h1>
|
||||
<p class="text-lg text-neutral-600">Connect with licensed business brokers, CPAs, attorneys, and other professionals across the United States.</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@
|
|||
<!-- SEO-optimized heading -->
|
||||
<div class="mb-6">
|
||||
<h1 class="text-3xl md:text-4xl font-bold text-neutral-900 mb-2">Businesses for Sale</h1>
|
||||
<p class="text-lg text-neutral-600">Discover profitable business opportunities across the United States. Browse verified listings from business owners and brokers.</p>
|
||||
<p class="text-lg text-neutral-600">Discover profitable business opportunities across the United States. Browse
|
||||
verified listings from business owners and brokers.</p>
|
||||
</div>
|
||||
|
||||
<!-- Loading Skeleton -->
|
||||
|
|
@ -55,39 +56,42 @@
|
|||
<h2 class="text-2xl font-semibold text-neutral-800 mb-4">Available Business Listings</h2>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
@for (listing of listings; track listing.id) {
|
||||
<div class="bg-white rounded-lg drop-shadow-custom-bg-mobile md:drop-shadow-custom-bg overflow-hidden hover:shadow-2xl transition-all duration-300 hover:scale-[1.02] group">
|
||||
<div
|
||||
class="bg-white rounded-lg drop-shadow-custom-bg-mobile md:drop-shadow-custom-bg overflow-hidden hover:shadow-2xl transition-all duration-300 hover:scale-[1.02] group">
|
||||
<div class="p-6 flex flex-col h-full relative z-[0]">
|
||||
<!-- Quick Actions Overlay -->
|
||||
<div class="absolute top-4 right-4 flex gap-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 z-20">
|
||||
<div
|
||||
class="absolute top-4 right-4 flex gap-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 z-20">
|
||||
@if(user) {
|
||||
<button
|
||||
class="bg-white rounded-full p-2 shadow-lg transition-colors"
|
||||
<button class="bg-white rounded-full p-2 shadow-lg transition-colors"
|
||||
[class.bg-red-50]="isFavorite(listing)"
|
||||
[title]="isFavorite(listing) ? 'Remove from favorites' : 'Save to favorites'"
|
||||
(click)="toggleFavorite($event, listing)">
|
||||
<i [class]="isFavorite(listing) ? 'fas fa-heart text-red-500' : 'far fa-heart text-red-500 hover:scale-110 transition-transform'"></i>
|
||||
<i
|
||||
[class]="isFavorite(listing) ? 'fas fa-heart text-red-500' : 'far fa-heart text-red-500 hover:scale-110 transition-transform'"></i>
|
||||
</button>
|
||||
}
|
||||
<button
|
||||
class="bg-white rounded-full p-2 shadow-lg hover:bg-blue-50 transition-colors"
|
||||
title="Share listing"
|
||||
(click)="$event.stopPropagation()">
|
||||
<button type="button" class="bg-white rounded-full p-2 shadow-lg hover:bg-blue-50 transition-colors"
|
||||
title="Share listing" (click)="shareListing($event, listing)">
|
||||
<i class="fas fa-share-alt text-blue-500 hover:scale-110 transition-transform"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center mb-4">
|
||||
<i [class]="selectOptions.getIconAndTextColorType(listing.type)" class="mr-2 text-xl"></i>
|
||||
<span [class]="selectOptions.getTextColorType(listing.type)" class="font-bold text-lg">{{ selectOptions.getBusiness(listing.type) }}</span>
|
||||
<span [class]="selectOptions.getTextColorType(listing.type)" class="font-bold text-lg">{{
|
||||
selectOptions.getBusiness(listing.type) }}</span>
|
||||
</div>
|
||||
<h2 class="text-xl font-semibold mb-4">
|
||||
{{ listing.title }}
|
||||
@if(listing.draft) {
|
||||
<span class="bg-amber-100 text-amber-800 border border-amber-300 text-sm font-medium me-2 ml-2 px-2.5 py-0.5 rounded">Draft</span>
|
||||
<span
|
||||
class="bg-amber-100 text-amber-800 border border-amber-300 text-sm font-medium me-2 ml-2 px-2.5 py-0.5 rounded">Draft</span>
|
||||
}
|
||||
</h2>
|
||||
<div class="flex justify-between">
|
||||
<span class="w-fit inline-flex items-center justify-center px-2 py-1 mb-4 text-xs font-bold leading-none bg-neutral-200 text-neutral-700 rounded-full">
|
||||
<span
|
||||
class="w-fit inline-flex items-center justify-center px-2 py-1 mb-4 text-xs font-bold leading-none bg-neutral-200 text-neutral-700 rounded-full">
|
||||
{{ selectOptions.getState(listing.location.state) }}
|
||||
</span>
|
||||
|
||||
|
|
@ -97,8 +101,7 @@
|
|||
[ngClass]="{
|
||||
'bg-emerald-100 text-emerald-800 border-emerald-300': badge === 'NEW',
|
||||
'bg-teal-100 text-teal-800 border-teal-300': badge === 'UPDATED'
|
||||
}"
|
||||
>
|
||||
}">
|
||||
{{ badge }}
|
||||
</span>
|
||||
}
|
||||
|
|
@ -112,28 +115,28 @@
|
|||
</p>
|
||||
<p class="text-sm text-neutral-600 mb-2">
|
||||
<strong>Sales revenue:</strong>
|
||||
{{ listing?.salesRevenue != null ? (listing.salesRevenue | currency : 'USD' : 'symbol' : '1.0-0') : 'undisclosed' }}
|
||||
{{ listing?.salesRevenue != null ? (listing.salesRevenue | currency : 'USD' : 'symbol' : '1.0-0') :
|
||||
'undisclosed' }}
|
||||
</p>
|
||||
<p class="text-sm text-neutral-600 mb-2">
|
||||
<strong>Net profit:</strong>
|
||||
{{ listing?.cashFlow != null ? (listing.cashFlow | currency : 'USD' : 'symbol' : '1.0-0') : 'undisclosed' }}
|
||||
{{ listing?.cashFlow != null ? (listing.cashFlow | currency : 'USD' : 'symbol' : '1.0-0') : 'undisclosed'
|
||||
}}
|
||||
</p>
|
||||
<p class="text-sm text-neutral-600 mb-2">
|
||||
<strong>Location:</strong> {{ listing.location.name ? listing.location.name : listing.location.county ? listing.location.county : this.selectOptions.getState(listing.location.state) }}
|
||||
<strong>Location:</strong> {{ listing.location.name ? listing.location.name : listing.location.county ?
|
||||
listing.location.county : this.selectOptions.getState(listing.location.state) }}
|
||||
</p>
|
||||
<p class="text-sm text-neutral-600 mb-4"><strong>Years established:</strong> {{ listing.established }}</p>
|
||||
@if(listing.imageName) {
|
||||
<img [appLazyLoad]="env.imageBaseUrl + '/pictures/logo/' + listing.imageName + '.avif?_ts=' + ts"
|
||||
[alt]="altText.generateListingCardLogoAlt(listing)"
|
||||
class="absolute bottom-[80px] right-[20px] h-[45px] w-auto"
|
||||
width="100"
|
||||
height="45" />
|
||||
class="absolute bottom-[80px] right-[20px] h-[45px] w-auto" width="100" height="45" />
|
||||
}
|
||||
<div class="flex-grow"></div>
|
||||
<button
|
||||
class="bg-success-600 text-white px-5 py-3 rounded-full w-full flex items-center justify-center mt-4 transition-all duration-200 hover:bg-success-700 hover:shadow-lg group/btn"
|
||||
[routerLink]="['/business', listing.slug || listing.id]"
|
||||
>
|
||||
[routerLink]="['/business', listing.slug || listing.id]">
|
||||
<span class="font-semibold">View Opportunity</span>
|
||||
<i class="fas fa-arrow-right ml-2 group-hover/btn:translate-x-1 transition-transform duration-200"></i>
|
||||
</button>
|
||||
|
|
@ -144,39 +147,33 @@
|
|||
} @else if (listings?.length === 0) {
|
||||
<div class="w-full flex items-center flex-wrap justify-center gap-10 py-12">
|
||||
<div class="grid gap-6 max-w-2xl w-full">
|
||||
<svg class="mx-auto" xmlns="http://www.w3.org/2000/svg" width="154" height="161" viewBox="0 0 154 161" fill="none">
|
||||
<svg class="mx-auto" xmlns="http://www.w3.org/2000/svg" width="154" height="161" viewBox="0 0 154 161"
|
||||
fill="none">
|
||||
<path
|
||||
d="M0.0616455 84.4268C0.0616455 42.0213 34.435 7.83765 76.6507 7.83765C118.803 7.83765 153.224 42.0055 153.224 84.4268C153.224 102.42 147.026 118.974 136.622 132.034C122.282 150.138 100.367 161 76.6507 161C52.7759 161 30.9882 150.059 16.6633 132.034C6.25961 118.974 0.0616455 102.42 0.0616455 84.4268Z"
|
||||
fill="#EEF2FF"
|
||||
/>
|
||||
fill="#EEF2FF" />
|
||||
<path
|
||||
d="M96.8189 0.632498L96.8189 0.632384L96.8083 0.630954C96.2034 0.549581 95.5931 0.5 94.9787 0.5H29.338C22.7112 0.5 17.3394 5.84455 17.3394 12.4473V142.715C17.3394 149.318 22.7112 154.662 29.338 154.662H123.948C130.591 154.662 135.946 149.317 135.946 142.715V38.9309C135.946 38.0244 135.847 37.1334 135.648 36.2586L135.648 36.2584C135.117 33.9309 133.874 31.7686 132.066 30.1333C132.066 30.1331 132.065 30.1329 132.065 30.1327L103.068 3.65203C103.068 3.6519 103.067 3.65177 103.067 3.65164C101.311 2.03526 99.1396 0.995552 96.8189 0.632498Z"
|
||||
fill="white"
|
||||
stroke="#E5E7EB"
|
||||
/>
|
||||
fill="white" stroke="#E5E7EB" />
|
||||
<ellipse cx="80.0618" cy="81" rx="28.0342" ry="28.0342" fill="#EEF2FF" />
|
||||
<path
|
||||
d="M99.2393 61.3061L99.2391 61.3058C88.498 50.5808 71.1092 50.5804 60.3835 61.3061C49.6423 72.0316 49.6422 89.4361 60.3832 100.162C71.109 110.903 88.4982 110.903 99.2393 100.162C109.965 89.4363 109.965 72.0317 99.2393 61.3061ZM105.863 54.6832C120.249 69.0695 120.249 92.3985 105.863 106.785C91.4605 121.171 68.1468 121.171 53.7446 106.785C39.3582 92.3987 39.3582 69.0693 53.7446 54.683C68.1468 40.2965 91.4605 40.2966 105.863 54.6832Z"
|
||||
stroke="#E5E7EB"
|
||||
/>
|
||||
<path d="M110.782 119.267L102.016 110.492C104.888 108.267 107.476 105.651 109.564 102.955L118.329 111.729L110.782 119.267Z" stroke="#E5E7EB" />
|
||||
stroke="#E5E7EB" />
|
||||
<path
|
||||
d="M110.782 119.267L102.016 110.492C104.888 108.267 107.476 105.651 109.564 102.955L118.329 111.729L110.782 119.267Z"
|
||||
stroke="#E5E7EB" />
|
||||
<path
|
||||
d="M139.122 125.781L139.122 125.78L123.313 109.988C123.313 109.987 123.313 109.987 123.312 109.986C121.996 108.653 119.849 108.657 118.521 109.985L118.871 110.335L118.521 109.985L109.047 119.459C107.731 120.775 107.735 122.918 109.044 124.247L109.047 124.249L124.858 140.06C128.789 143.992 135.191 143.992 139.122 140.06C143.069 136.113 143.069 129.728 139.122 125.781Z"
|
||||
fill="#A5B4FC"
|
||||
stroke="#818CF8"
|
||||
/>
|
||||
fill="#A5B4FC" stroke="#818CF8" />
|
||||
<path
|
||||
d="M83.185 87.2285C82.5387 87.2285 82.0027 86.6926 82.0027 86.0305C82.0027 86.0305 82.0027 83.3821 77.9987 83.3821C77.9987 83.3821 77.9987 86.0305 77.9987 86.0305C77.9987 86.6926 77.4627 87.2285 76.8006 87.2285C76.1543 87.2285 75.6183 86.6926 75.6183 86.0305C75.6183 80.2294 84.3831 80.2451 84.3831 86.0305C84.3831 86.6926 83.8471 87.2285 83.185 87.2285Z"
|
||||
fill="#4F46E5"
|
||||
/>
|
||||
fill="#4F46E5" />
|
||||
<path
|
||||
d="M93.3528 77.0926H88.403C87.7409 77.0926 87.2049 76.5567 87.2049 75.8946C87.2049 75.2483 87.7409 74.7123 88.403 74.7123H93.3528C94.0149 74.7123 94.5509 75.2483 94.5509 75.8946C94.5509 76.5567 94.0149 77.0926 93.3528 77.0926Z"
|
||||
fill="#4F46E5"
|
||||
/>
|
||||
fill="#4F46E5" />
|
||||
<path
|
||||
d="M71.5987 77.0925H66.6488C65.9867 77.0925 65.4507 76.5565 65.4507 75.8945C65.4507 75.2481 65.9867 74.7122 66.6488 74.7122H71.5987C72.245 74.7122 72.781 75.2481 72.781 75.8945C72.781 76.5565 72.245 77.0925 71.5987 77.0925Z"
|
||||
fill="#4F46E5"
|
||||
/>
|
||||
fill="#4F46E5" />
|
||||
<rect x="38.3522" y="21.5128" width="41.0256" height="2.73504" rx="1.36752" fill="#4F46E5" />
|
||||
<rect x="38.3522" y="133.65" width="54.7009" height="5.47009" rx="2.73504" fill="#A5B4FC" />
|
||||
<rect x="38.3522" y="29.7179" width="13.6752" height="2.73504" rx="1.36752" fill="#4F46E5" />
|
||||
|
|
@ -186,14 +183,17 @@
|
|||
</svg>
|
||||
<div class="text-center">
|
||||
<h2 class="text-black text-2xl font-semibold leading-loose pb-2">No listings found</h2>
|
||||
<p class="text-neutral-600 text-base font-normal leading-relaxed pb-6">We couldn't find any businesses matching your criteria.<br />Try adjusting your filters or explore popular categories below.</p>
|
||||
<p class="text-neutral-600 text-base font-normal leading-relaxed pb-6">We couldn't find any businesses
|
||||
matching your criteria.<br />Try adjusting your filters or explore popular categories below.</p>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex flex-col sm:flex-row gap-3 justify-center mb-8">
|
||||
<button (click)="clearAllFilters()" class="px-6 py-3 rounded-full bg-primary-600 text-white text-sm font-semibold hover:bg-primary-700 transition-colors">
|
||||
<button (click)="clearAllFilters()"
|
||||
class="px-6 py-3 rounded-full bg-primary-600 text-white text-sm font-semibold hover:bg-primary-700 transition-colors">
|
||||
<i class="fas fa-redo mr-2"></i>Clear All Filters
|
||||
</button>
|
||||
<button [routerLink]="['/home']" class="px-6 py-3 rounded-full border-2 border-neutral-300 text-neutral-700 text-sm font-semibold hover:border-primary-600 hover:text-primary-600 transition-colors">
|
||||
<button [routerLink]="['/home']"
|
||||
class="px-6 py-3 rounded-full border-2 border-neutral-300 text-neutral-700 text-sm font-semibold hover:border-primary-600 hover:text-primary-600 transition-colors">
|
||||
<i class="fas fa-home mr-2"></i>Back to Home
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -204,22 +204,28 @@
|
|||
<i class="fas fa-fire text-orange-500 mr-2"></i>Popular Categories
|
||||
</h3>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<button (click)="filterByCategory('foodAndRestaurant')" class="px-4 py-2 bg-white rounded-lg border border-neutral-200 hover:border-primary-500 hover:bg-primary-50 text-sm text-neutral-700 hover:text-primary-600 transition-all">
|
||||
<button (click)="filterByCategory('foodAndRestaurant')"
|
||||
class="px-4 py-2 bg-white rounded-lg border border-neutral-200 hover:border-primary-500 hover:bg-primary-50 text-sm text-neutral-700 hover:text-primary-600 transition-all">
|
||||
<i class="fas fa-utensils mr-2"></i>Restaurants
|
||||
</button>
|
||||
<button (click)="filterByCategory('retail')" class="px-4 py-2 bg-white rounded-lg border border-neutral-200 hover:border-primary-500 hover:bg-primary-50 text-sm text-neutral-700 hover:text-primary-600 transition-all">
|
||||
<button (click)="filterByCategory('retail')"
|
||||
class="px-4 py-2 bg-white rounded-lg border border-neutral-200 hover:border-primary-500 hover:bg-primary-50 text-sm text-neutral-700 hover:text-primary-600 transition-all">
|
||||
<i class="fas fa-store mr-2"></i>Retail
|
||||
</button>
|
||||
<button (click)="filterByCategory('realEstate')" class="px-4 py-2 bg-white rounded-lg border border-neutral-200 hover:border-primary-500 hover:bg-primary-50 text-sm text-neutral-700 hover:text-primary-600 transition-all">
|
||||
<button (click)="filterByCategory('realEstate')"
|
||||
class="px-4 py-2 bg-white rounded-lg border border-neutral-200 hover:border-primary-500 hover:bg-primary-50 text-sm text-neutral-700 hover:text-primary-600 transition-all">
|
||||
<i class="fas fa-building mr-2"></i>Real Estate
|
||||
</button>
|
||||
<button (click)="filterByCategory('service')" class="px-4 py-2 bg-white rounded-lg border border-neutral-200 hover:border-primary-500 hover:bg-primary-50 text-sm text-neutral-700 hover:text-primary-600 transition-all">
|
||||
<button (click)="filterByCategory('service')"
|
||||
class="px-4 py-2 bg-white rounded-lg border border-neutral-200 hover:border-primary-500 hover:bg-primary-50 text-sm text-neutral-700 hover:text-primary-600 transition-all">
|
||||
<i class="fas fa-cut mr-2"></i>Services
|
||||
</button>
|
||||
<button (click)="filterByCategory('franchise')" class="px-4 py-2 bg-white rounded-lg border border-neutral-200 hover:border-primary-500 hover:bg-primary-50 text-sm text-neutral-700 hover:text-primary-600 transition-all">
|
||||
<button (click)="filterByCategory('franchise')"
|
||||
class="px-4 py-2 bg-white rounded-lg border border-neutral-200 hover:border-primary-500 hover:bg-primary-50 text-sm text-neutral-700 hover:text-primary-600 transition-all">
|
||||
<i class="fas fa-handshake mr-2"></i>Franchise
|
||||
</button>
|
||||
<button (click)="filterByCategory('professional')" class="px-4 py-2 bg-white rounded-lg border border-neutral-200 hover:border-primary-500 hover:bg-primary-50 text-sm text-neutral-700 hover:text-primary-600 transition-all">
|
||||
<button (click)="filterByCategory('professional')"
|
||||
class="px-4 py-2 bg-white rounded-lg border border-neutral-200 hover:border-primary-500 hover:bg-primary-50 text-sm text-neutral-700 hover:text-primary-600 transition-all">
|
||||
<i class="fas fa-briefcase mr-2"></i>Professional
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -247,5 +253,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Filter Button for Mobile -->
|
||||
<button (click)="openFilterModal()" class="md:hidden fixed bottom-4 right-4 bg-primary-500 text-white p-3 rounded-full shadow-lg z-20"><i class="fas fa-filter"></i> Filter</button>
|
||||
<button (click)="openFilterModal()"
|
||||
class="md:hidden fixed bottom-4 right-4 bg-primary-500 text-white p-3 rounded-full shadow-lg z-20"><i
|
||||
class="fas fa-filter"></i> Filter</button>
|
||||
</div>
|
||||
|
|
@ -259,6 +259,47 @@ export class BusinessListingsComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Share a listing - opens native share dialog or copies to clipboard
|
||||
*/
|
||||
async shareListing(event: Event, listing: BusinessListing): Promise<void> {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
const url = `${window.location.origin}/business/${listing.slug || listing.id}`;
|
||||
const title = listing.title || 'Business Listing';
|
||||
|
||||
// Try native share API first (works on mobile and some desktop browsers)
|
||||
if (navigator.share) {
|
||||
try {
|
||||
await navigator.share({
|
||||
title: title,
|
||||
text: `Check out this business: ${title}`,
|
||||
url: url,
|
||||
});
|
||||
} catch (err) {
|
||||
// User cancelled or share failed - fall back to clipboard
|
||||
this.copyToClipboard(url);
|
||||
}
|
||||
} else {
|
||||
// Fallback: open Facebook share dialog
|
||||
const encodedUrl = encodeURIComponent(url);
|
||||
window.open(`https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`, '_blank', 'width=600,height=400');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy URL to clipboard and show feedback
|
||||
*/
|
||||
private copyToClipboard(url: string): void {
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
// Could add a toast notification here
|
||||
console.log('Link copied to clipboard!');
|
||||
}).catch(err => {
|
||||
console.error('Failed to copy link:', err);
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Build information, automatically generated by `the_build_script` :zwinkern:
|
||||
const build = {
|
||||
timestamp: "GER: 01.12.2025 20:23 | TX: 12/01/2025 1:23 PM"
|
||||
timestamp: "GER: 02.01.2026 23:17 | TX: 01/02/2026 4:17 PM"
|
||||
};
|
||||
|
||||
export default build;
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
export const hostname = window.location.hostname;
|
||||
// SSR-safe: check if window exists (it doesn't on server-side)
|
||||
const hostname = typeof window !== 'undefined' ? window.location.hostname : 'localhost';
|
||||
export const environment_base = {
|
||||
// apiBaseUrl: 'http://localhost:3000',
|
||||
apiBaseUrl: `http://${hostname}:4200`,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
// IMPORTANT: DOM polyfill must be imported FIRST, before any browser-dependent libraries
|
||||
import './ssr-dom-polyfill';
|
||||
|
||||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { AppComponent } from './app/app.component';
|
||||
import { config } from './app/app.config.server';
|
||||
|
|
|
|||
|
|
@ -1,27 +1,140 @@
|
|||
# robots.txt for BizMatch
|
||||
# robots.txt for BizMatch - Business Marketplace
|
||||
# https://biz-match.com
|
||||
# Last updated: 2026-01-02
|
||||
|
||||
# ===========================================
|
||||
# Default rules for all crawlers
|
||||
# ===========================================
|
||||
User-agent: *
|
||||
|
||||
# Allow all public pages
|
||||
Allow: /
|
||||
Allow: /home
|
||||
Allow: /listings
|
||||
Allow: /listings-2
|
||||
Allow: /listings-3
|
||||
Allow: /listings-4
|
||||
Allow: /details-business-listing/
|
||||
Allow: /details-commercial-property/
|
||||
Allow: /businessListings
|
||||
Allow: /commercialPropertyListings
|
||||
Allow: /brokerListings
|
||||
Allow: /business/*
|
||||
Allow: /commercial-property/*
|
||||
Allow: /details-user/*
|
||||
Allow: /terms-of-use
|
||||
Allow: /privacy-statement
|
||||
|
||||
# Disallow private/admin areas
|
||||
Disallow: /admin/
|
||||
Disallow: /profile/
|
||||
Disallow: /dashboard/
|
||||
Disallow: /favorites/
|
||||
Disallow: /settings/
|
||||
Disallow: /account
|
||||
Disallow: /myListings
|
||||
Disallow: /myFavorites
|
||||
Disallow: /createBusinessListing
|
||||
Disallow: /createCommercialPropertyListing
|
||||
Disallow: /editBusinessListing/*
|
||||
Disallow: /editCommercialPropertyListing/*
|
||||
Disallow: /login
|
||||
Disallow: /logout
|
||||
Disallow: /register
|
||||
Disallow: /emailUs
|
||||
|
||||
# Allow common crawlers
|
||||
# Disallow duplicate content / API routes
|
||||
Disallow: /api/
|
||||
Disallow: /bizmatch/
|
||||
|
||||
# Disallow search result pages with parameters (to avoid duplicate content)
|
||||
Disallow: /*?*sortBy=
|
||||
Disallow: /*?*page=
|
||||
Disallow: /*?*start=
|
||||
|
||||
# ===========================================
|
||||
# Google-specific rules
|
||||
# ===========================================
|
||||
User-agent: Googlebot
|
||||
Allow: /
|
||||
Crawl-delay: 1
|
||||
|
||||
# Allow Google to index images
|
||||
User-agent: Googlebot-Image
|
||||
Allow: /assets/
|
||||
Disallow: /assets/leaflet/
|
||||
|
||||
# ===========================================
|
||||
# Bing-specific rules
|
||||
# ===========================================
|
||||
User-agent: Bingbot
|
||||
Allow: /
|
||||
Crawl-delay: 2
|
||||
|
||||
# Sitemap location (served from backend API)
|
||||
Sitemap: https://biz-match.com/bizmatch/sitemap/sitemap.xml
|
||||
# ===========================================
|
||||
# Other major search engines
|
||||
# ===========================================
|
||||
User-agent: DuckDuckBot
|
||||
Allow: /
|
||||
Crawl-delay: 2
|
||||
|
||||
User-agent: Slurp
|
||||
Allow: /
|
||||
Crawl-delay: 2
|
||||
|
||||
User-agent: Yandex
|
||||
Allow: /
|
||||
Crawl-delay: 5
|
||||
|
||||
User-agent: Baiduspider
|
||||
Allow: /
|
||||
Crawl-delay: 5
|
||||
|
||||
# ===========================================
|
||||
# AI/LLM Crawlers (Answer Engine Optimization)
|
||||
# ===========================================
|
||||
User-agent: GPTBot
|
||||
Allow: /
|
||||
Allow: /businessListings
|
||||
Allow: /business/*
|
||||
Disallow: /admin/
|
||||
Disallow: /account
|
||||
|
||||
User-agent: ChatGPT-User
|
||||
Allow: /
|
||||
|
||||
User-agent: Claude-Web
|
||||
Allow: /
|
||||
|
||||
User-agent: Anthropic-AI
|
||||
Allow: /
|
||||
|
||||
User-agent: PerplexityBot
|
||||
Allow: /
|
||||
|
||||
User-agent: Cohere-ai
|
||||
Allow: /
|
||||
|
||||
# ===========================================
|
||||
# Block unwanted bots
|
||||
# ===========================================
|
||||
User-agent: AhrefsBot
|
||||
Disallow: /
|
||||
|
||||
User-agent: SemrushBot
|
||||
Disallow: /
|
||||
|
||||
User-agent: MJ12bot
|
||||
Disallow: /
|
||||
|
||||
User-agent: DotBot
|
||||
Disallow: /
|
||||
|
||||
User-agent: BLEXBot
|
||||
Disallow: /
|
||||
|
||||
# ===========================================
|
||||
# Sitemap locations
|
||||
# ===========================================
|
||||
# Main sitemap index (dynamically generated, contains all sub-sitemaps)
|
||||
Sitemap: https://biz-match.com/bizmatch/sitemap.xml
|
||||
|
||||
# Individual sitemaps (auto-listed in sitemap index)
|
||||
# - https://biz-match.com/bizmatch/sitemap/static.xml
|
||||
# - https://biz-match.com/bizmatch/sitemap/business-1.xml
|
||||
# - https://biz-match.com/bizmatch/sitemap/commercial-1.xml
|
||||
|
||||
# ===========================================
|
||||
# Host directive (for Yandex)
|
||||
# ===========================================
|
||||
Host: https://biz-match.com
|
||||
|
|
|
|||
|
|
@ -0,0 +1,163 @@
|
|||
/**
|
||||
* DOM Polyfills for Server-Side Rendering
|
||||
*
|
||||
* This file must be imported BEFORE any browser-only libraries like Leaflet.
|
||||
* It provides minimal stubs for browser globals that are required during module loading.
|
||||
*/
|
||||
|
||||
// Create a minimal screen mock
|
||||
const screenMock = {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
availWidth: 1920,
|
||||
availHeight: 1080,
|
||||
colorDepth: 24,
|
||||
pixelDepth: 24,
|
||||
deviceXDPI: 96,
|
||||
deviceYDPI: 96,
|
||||
logicalXDPI: 96,
|
||||
logicalYDPI: 96,
|
||||
};
|
||||
|
||||
// Create a minimal document mock
|
||||
const documentMock = {
|
||||
createElement: (tag: string) => ({
|
||||
style: {},
|
||||
setAttribute: () => { },
|
||||
getAttribute: () => null,
|
||||
appendChild: () => { },
|
||||
removeChild: () => { },
|
||||
classList: {
|
||||
add: () => { },
|
||||
remove: () => { },
|
||||
contains: () => false,
|
||||
},
|
||||
tagName: tag.toUpperCase(),
|
||||
}),
|
||||
createElementNS: (ns: string, tag: string) => ({
|
||||
style: {},
|
||||
setAttribute: () => { },
|
||||
getAttribute: () => null,
|
||||
appendChild: () => { },
|
||||
getBBox: () => ({ x: 0, y: 0, width: 0, height: 0 }),
|
||||
tagName: tag.toUpperCase(),
|
||||
}),
|
||||
createTextNode: () => ({}),
|
||||
head: { appendChild: () => { }, removeChild: () => { } },
|
||||
body: { appendChild: () => { }, removeChild: () => { } },
|
||||
documentElement: {
|
||||
style: {},
|
||||
clientWidth: 1920,
|
||||
clientHeight: 1080,
|
||||
},
|
||||
addEventListener: () => { },
|
||||
removeEventListener: () => { },
|
||||
querySelector: () => null,
|
||||
querySelectorAll: () => [],
|
||||
getElementById: () => null,
|
||||
getElementsByTagName: () => [],
|
||||
getElementsByClassName: () => [],
|
||||
};
|
||||
|
||||
// Create a minimal window mock for libraries that check for window existence during load
|
||||
const windowMock = {
|
||||
requestAnimationFrame: (callback: FrameRequestCallback) => setTimeout(callback, 16),
|
||||
cancelAnimationFrame: (id: number) => clearTimeout(id),
|
||||
addEventListener: () => { },
|
||||
removeEventListener: () => { },
|
||||
getComputedStyle: () => ({
|
||||
getPropertyValue: () => '',
|
||||
}),
|
||||
matchMedia: () => ({
|
||||
matches: false,
|
||||
addListener: () => { },
|
||||
removeListener: () => { },
|
||||
addEventListener: () => { },
|
||||
removeEventListener: () => { },
|
||||
}),
|
||||
document: documentMock,
|
||||
screen: screenMock,
|
||||
devicePixelRatio: 1,
|
||||
navigator: {
|
||||
userAgent: 'node',
|
||||
platform: 'server',
|
||||
language: 'en',
|
||||
languages: ['en'],
|
||||
onLine: true,
|
||||
geolocation: null,
|
||||
},
|
||||
location: {
|
||||
hostname: 'localhost',
|
||||
href: 'http://localhost',
|
||||
protocol: 'http:',
|
||||
pathname: '/',
|
||||
search: '',
|
||||
hash: '',
|
||||
host: 'localhost',
|
||||
origin: 'http://localhost',
|
||||
},
|
||||
history: {
|
||||
pushState: () => { },
|
||||
replaceState: () => { },
|
||||
back: () => { },
|
||||
forward: () => { },
|
||||
go: () => { },
|
||||
length: 0,
|
||||
},
|
||||
localStorage: {
|
||||
getItem: () => null,
|
||||
setItem: () => { },
|
||||
removeItem: () => { },
|
||||
clear: () => { },
|
||||
},
|
||||
sessionStorage: {
|
||||
getItem: () => null,
|
||||
setItem: () => { },
|
||||
removeItem: () => { },
|
||||
clear: () => { },
|
||||
},
|
||||
setTimeout,
|
||||
clearTimeout,
|
||||
setInterval,
|
||||
clearInterval,
|
||||
innerWidth: 1920,
|
||||
innerHeight: 1080,
|
||||
outerWidth: 1920,
|
||||
outerHeight: 1080,
|
||||
scrollX: 0,
|
||||
scrollY: 0,
|
||||
pageXOffset: 0,
|
||||
pageYOffset: 0,
|
||||
scrollTo: () => { },
|
||||
scroll: () => { },
|
||||
Image: class Image { },
|
||||
HTMLElement: class HTMLElement { },
|
||||
SVGElement: class SVGElement { },
|
||||
};
|
||||
|
||||
// Only set globals if they don't exist (i.e., we're in Node.js)
|
||||
if (typeof window === 'undefined') {
|
||||
(global as any).window = windowMock;
|
||||
}
|
||||
|
||||
if (typeof document === 'undefined') {
|
||||
(global as any).document = documentMock;
|
||||
}
|
||||
|
||||
if (typeof navigator === 'undefined') {
|
||||
(global as any).navigator = windowMock.navigator;
|
||||
}
|
||||
|
||||
if (typeof screen === 'undefined') {
|
||||
(global as any).screen = screenMock;
|
||||
}
|
||||
|
||||
if (typeof HTMLElement === 'undefined') {
|
||||
(global as any).HTMLElement = windowMock.HTMLElement;
|
||||
}
|
||||
|
||||
if (typeof SVGElement === 'undefined') {
|
||||
(global as any).SVGElement = windowMock.SVGElement;
|
||||
}
|
||||
|
||||
export { };
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
/**
|
||||
* Node.js Preload Script for SSR Development
|
||||
*
|
||||
* This script creates DOM global mocks BEFORE any modules are loaded.
|
||||
* It only applies in the main thread - NOT in worker threads (sass, esbuild).
|
||||
*/
|
||||
|
||||
import { isMainThread } from 'node:worker_threads';
|
||||
|
||||
// Only apply polyfills in the main thread, not in workers
|
||||
if (!isMainThread) {
|
||||
// Skip polyfills in worker threads to avoid breaking sass/esbuild
|
||||
// console.log('[SSR] Skipping polyfills in worker thread');
|
||||
} else {
|
||||
// Create screen mock
|
||||
const screenMock = {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
availWidth: 1920,
|
||||
availHeight: 1080,
|
||||
colorDepth: 24,
|
||||
pixelDepth: 24,
|
||||
deviceXDPI: 96,
|
||||
deviceYDPI: 96,
|
||||
logicalXDPI: 96,
|
||||
logicalYDPI: 96,
|
||||
};
|
||||
|
||||
// Create document mock
|
||||
const documentMock = {
|
||||
createElement: (tag) => ({
|
||||
style: {},
|
||||
setAttribute: () => { },
|
||||
getAttribute: () => null,
|
||||
appendChild: () => { },
|
||||
removeChild: () => { },
|
||||
classList: { add: () => { }, remove: () => { }, contains: () => false },
|
||||
tagName: tag?.toUpperCase() || 'DIV',
|
||||
}),
|
||||
createElementNS: (ns, tag) => ({
|
||||
style: {},
|
||||
setAttribute: () => { },
|
||||
getAttribute: () => null,
|
||||
appendChild: () => { },
|
||||
getBBox: () => ({ x: 0, y: 0, width: 0, height: 0 }),
|
||||
tagName: tag?.toUpperCase() || 'SVG',
|
||||
}),
|
||||
createTextNode: () => ({}),
|
||||
head: { appendChild: () => { }, removeChild: () => { } },
|
||||
body: { appendChild: () => { }, removeChild: () => { } },
|
||||
documentElement: {
|
||||
style: {},
|
||||
clientWidth: 1920,
|
||||
clientHeight: 1080,
|
||||
querySelector: () => null,
|
||||
querySelectorAll: () => [],
|
||||
getAttribute: () => null,
|
||||
setAttribute: () => { },
|
||||
},
|
||||
addEventListener: () => { },
|
||||
removeEventListener: () => { },
|
||||
querySelector: () => null,
|
||||
querySelectorAll: () => [],
|
||||
getElementById: () => null,
|
||||
getElementsByTagName: () => [],
|
||||
getElementsByClassName: () => [],
|
||||
};
|
||||
|
||||
// Create window mock
|
||||
const windowMock = {
|
||||
requestAnimationFrame: (callback) => setTimeout(callback, 16),
|
||||
cancelAnimationFrame: (id) => clearTimeout(id),
|
||||
addEventListener: () => { },
|
||||
removeEventListener: () => { },
|
||||
getComputedStyle: () => ({ getPropertyValue: () => '' }),
|
||||
matchMedia: () => ({
|
||||
matches: false,
|
||||
addListener: () => { },
|
||||
removeListener: () => { },
|
||||
addEventListener: () => { },
|
||||
removeEventListener: () => { },
|
||||
}),
|
||||
document: documentMock,
|
||||
screen: screenMock,
|
||||
devicePixelRatio: 1,
|
||||
navigator: {
|
||||
userAgent: 'node',
|
||||
platform: 'server',
|
||||
language: 'en',
|
||||
languages: ['en'],
|
||||
onLine: true,
|
||||
geolocation: null,
|
||||
},
|
||||
location: {
|
||||
hostname: 'localhost',
|
||||
href: 'http://localhost',
|
||||
protocol: 'http:',
|
||||
pathname: '/',
|
||||
search: '',
|
||||
hash: '',
|
||||
host: 'localhost',
|
||||
origin: 'http://localhost',
|
||||
},
|
||||
history: {
|
||||
pushState: () => { },
|
||||
replaceState: () => { },
|
||||
back: () => { },
|
||||
forward: () => { },
|
||||
go: () => { },
|
||||
length: 0,
|
||||
},
|
||||
localStorage: {
|
||||
getItem: () => null,
|
||||
setItem: () => { },
|
||||
removeItem: () => { },
|
||||
clear: () => { },
|
||||
},
|
||||
sessionStorage: {
|
||||
getItem: () => null,
|
||||
setItem: () => { },
|
||||
removeItem: () => { },
|
||||
clear: () => { },
|
||||
},
|
||||
setTimeout,
|
||||
clearTimeout,
|
||||
setInterval,
|
||||
clearInterval,
|
||||
innerWidth: 1920,
|
||||
innerHeight: 1080,
|
||||
outerWidth: 1920,
|
||||
outerHeight: 1080,
|
||||
scrollX: 0,
|
||||
scrollY: 0,
|
||||
pageXOffset: 0,
|
||||
pageYOffset: 0,
|
||||
scrollTo: () => { },
|
||||
scroll: () => { },
|
||||
Image: class Image { },
|
||||
HTMLElement: class HTMLElement { },
|
||||
SVGElement: class SVGElement { },
|
||||
};
|
||||
|
||||
// Set globals
|
||||
globalThis.window = windowMock;
|
||||
globalThis.document = documentMock;
|
||||
globalThis.navigator = windowMock.navigator;
|
||||
globalThis.screen = screenMock;
|
||||
globalThis.HTMLElement = windowMock.HTMLElement;
|
||||
globalThis.SVGElement = windowMock.SVGElement;
|
||||
globalThis.localStorage = windowMock.localStorage;
|
||||
globalThis.sessionStorage = windowMock.sessionStorage;
|
||||
|
||||
console.log('[SSR] DOM polyfills loaded');
|
||||
}
|
||||
Loading…
Reference in New Issue