Compare commits
No commits in common. "master" and "seo-meta-fixes" have entirely different histories.
master
...
seo-meta-f
|
|
@ -1,6 +0,0 @@
|
|||
node_modules
|
||||
.git
|
||||
.idea
|
||||
.vscode
|
||||
dist
|
||||
coverage
|
||||
|
|
@ -1,25 +1,19 @@
|
|||
# --- STAGE 1: Build ---
|
||||
FROM node:22-alpine AS builder
|
||||
# Build Stage
|
||||
FROM node:18-alpine AS build
|
||||
|
||||
WORKDIR /app
|
||||
# HIER KEIN NODE_ENV=production setzen! Wir brauchen devDependencies zum Bauen.
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
RUN npm install
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# --- STAGE 2: Runtime ---
|
||||
FROM node:22-alpine
|
||||
# Runtime Stage
|
||||
FROM node:18-alpine
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/dist /app/dist
|
||||
COPY --from=build /app/package*.json /app/
|
||||
|
||||
# HIER ist es richtig!
|
||||
ENV NODE_ENV=production
|
||||
RUN npm install --production
|
||||
|
||||
COPY --from=builder /app/dist /app/dist
|
||||
COPY --from=builder /app/package*.json /app/
|
||||
|
||||
# Installiert nur "dependencies" (Nest core, TypeORM, Helmet, Sharp etc.)
|
||||
# "devDependencies" (TypeScript, Jest, ESLint) werden weggelassen.
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
# WICHTIG: Pfad prüfen (siehe Punkt 2 unten)
|
||||
CMD ["node", "dist/src/main.js"]
|
||||
CMD ["node", "dist/main.js"]
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
services:
|
||||
app:
|
||||
image: node:22-alpine
|
||||
container_name: bizmatch-app
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- ./:/app
|
||||
- node_modules:/app/node_modules
|
||||
ports:
|
||||
- '3001:3001'
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- DATABASE_URL
|
||||
command: sh -c "if [ ! -f node_modules/.installed ]; then npm ci && touch node_modules/.installed; fi && npm run build && node dist/src/main.js"
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- postgres
|
||||
networks:
|
||||
- bizmatch
|
||||
|
||||
postgres:
|
||||
container_name: bizmatchdb
|
||||
image: postgres:17-alpine
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- bizmatch-db-data:/var/lib/postgresql/data
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
ports:
|
||||
- '5434:5432'
|
||||
networks:
|
||||
- bizmatch
|
||||
|
||||
volumes:
|
||||
bizmatch-db-data:
|
||||
driver: local
|
||||
node_modules:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
bizmatch:
|
||||
external: true
|
||||
|
|
@ -47,7 +47,6 @@
|
|||
"fs-extra": "^11.2.0",
|
||||
"groq-sdk": "^0.5.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"helmet": "^8.1.0",
|
||||
"nest-winston": "^1.9.4",
|
||||
"nestjs-cls": "^5.4.0",
|
||||
"nodemailer": "^7.0.12",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { LoggerService } from '@nestjs/common';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import express from 'express';
|
||||
import helmet from 'helmet';
|
||||
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
|
|
@ -23,37 +22,6 @@ async function bootstrap() {
|
|||
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
||||
allowedHeaders: 'Content-Type, Accept, Authorization, x-hide-loading',
|
||||
});
|
||||
|
||||
// Security Headers with helmet
|
||||
app.use(
|
||||
helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'", "https://fonts.googleapis.com"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
|
||||
imgSrc: ["'self'", "data:", "https:", "blob:"],
|
||||
connectSrc: ["'self'", "https://api.bizmatch.net", "https://*.firebaseapp.com", "https://*.googleapis.com"],
|
||||
fontSrc: ["'self'", "https://fonts.gstatic.com", "data:"],
|
||||
objectSrc: ["'none'"],
|
||||
mediaSrc: ["'self'"],
|
||||
frameSrc: ["'self'"],
|
||||
},
|
||||
},
|
||||
crossOriginEmbedderPolicy: false, // Disable for now to avoid breaking existing functionality
|
||||
hsts: {
|
||||
maxAge: 31536000,
|
||||
includeSubDomains: true,
|
||||
preload: true,
|
||||
},
|
||||
frameguard: {
|
||||
action: 'sameorigin', // Allow same-origin framing
|
||||
},
|
||||
crossOriginOpenerPolicy: { policy: 'same-origin-allow-popups' }, // Allow popups for OAuth
|
||||
crossOriginResourcePolicy: { policy: 'cross-origin' }, // Allow cross-origin resources
|
||||
}),
|
||||
);
|
||||
|
||||
await app.listen(process.env.PORT || 3001);
|
||||
}
|
||||
bootstrap();
|
||||
|
|
|
|||
|
|
@ -1,41 +1,13 @@
|
|||
# STAGE 1: Build
|
||||
FROM node:22-alpine AS builder
|
||||
|
||||
# Wir erstellen ein Arbeitsverzeichnis, das eine Ebene über dem Projekt liegt
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# 1. Wir kopieren die Backend-Models an die Stelle, wo Angular sie erwartet
|
||||
# Deine Pfade suchen nach ../bizmatch-server, also legen wir es daneben.
|
||||
COPY bizmatch-server/src/models ./bizmatch-server/src/models
|
||||
|
||||
# 2. Jetzt kümmern wir uns um das Frontend
|
||||
# Wir kopieren erst die package Files für besseres Caching
|
||||
COPY bizmatch/package*.json ./bizmatch/
|
||||
|
||||
# Wechseln in den Frontend Ordner zum Installieren
|
||||
WORKDIR /usr/src/app/bizmatch
|
||||
RUN npm ci
|
||||
|
||||
# 3. Den Rest des Frontends kopieren
|
||||
COPY bizmatch/ .
|
||||
|
||||
# 4. Bauen
|
||||
RUN npm run build:ssr
|
||||
|
||||
# --- STAGE 2: Runtime ---
|
||||
FROM node:22-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=4000
|
||||
|
||||
# Kopiere das Ergebnis aus dem Builder (Pfad beachten!)
|
||||
COPY --from=builder /usr/src/app/bizmatch/dist /app/dist
|
||||
COPY --from=builder /usr/src/app/bizmatch/package*.json /app/
|
||||
# GANZEN dist-Ordner kopieren, nicht nur bizmatch
|
||||
COPY dist ./dist
|
||||
COPY package*.json ./
|
||||
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
EXPOSE 4000
|
||||
EXPOSE 4200
|
||||
|
||||
CMD ["node", "dist/bizmatch/server/server.mjs"]
|
||||
|
|
|
|||
|
|
@ -69,8 +69,8 @@
|
|||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "30kb",
|
||||
"maximumError": "30kb"
|
||||
"maximumWarning": "2kb",
|
||||
"maximumError": "4kb"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
|
|
@ -101,8 +101,7 @@
|
|||
],
|
||||
"optimization": true,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true,
|
||||
"outputHashing": "all"
|
||||
"sourceMap": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
services:
|
||||
bizmatch-ssr:
|
||||
build: .
|
||||
image: bizmatch-ssr
|
||||
container_name: bizmatch-ssr
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- '4200:4200'
|
||||
environment:
|
||||
NODE_ENV: DEVELOPMENT
|
||||
|
|
@ -1 +0,0 @@
|
|||
google-site-verification: googleccd5315437d68a49.html
|
||||
|
|
@ -26,74 +26,6 @@ export async function app(): Promise<express.Express> {
|
|||
server.set('view engine', 'html');
|
||||
server.set('views', browserDistFolder);
|
||||
|
||||
// Sitemap XML endpoints - MUST be before static files middleware
|
||||
server.get('/sitemap.xml', async (req, res) => {
|
||||
try {
|
||||
const sitemapIndexXml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<sitemap>
|
||||
<loc>https://www.bizmatch.net/sitemap-static.xml</loc>
|
||||
<lastmod>${new Date().toISOString().split('T')[0]}</lastmod>
|
||||
</sitemap>
|
||||
</sitemapindex>`;
|
||||
|
||||
res.header('Content-Type', 'application/xml; charset=utf-8');
|
||||
res.send(sitemapIndexXml);
|
||||
} catch (error) {
|
||||
console.error('[SSR] Error generating sitemap index:', error);
|
||||
res.status(500).send('Error generating sitemap');
|
||||
}
|
||||
});
|
||||
|
||||
server.get('/sitemap-static.xml', async (req, res) => {
|
||||
try {
|
||||
const sitemapXml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://www.bizmatch.net/</loc>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.bizmatch.net/home</loc>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.bizmatch.net/businessListings</loc>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>0.9</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.bizmatch.net/commercialPropertyListings</loc>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>0.9</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.bizmatch.net/brokerListings</loc>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>0.9</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.bizmatch.net/terms-of-use</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.5</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.bizmatch.net/privacy-statement</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.5</priority>
|
||||
</url>
|
||||
</urlset>`;
|
||||
|
||||
res.header('Content-Type', 'application/xml; charset=utf-8');
|
||||
res.send(sitemapXml);
|
||||
} catch (error) {
|
||||
console.error('[SSR] Error generating static sitemap:', error);
|
||||
res.status(500).send('Error generating sitemap');
|
||||
}
|
||||
});
|
||||
|
||||
// Example Express Rest API endpoints
|
||||
// server.get('/api/**', (req, res) => { });
|
||||
// Serve static files from /browser
|
||||
|
|
@ -104,8 +36,6 @@ export async function app(): Promise<express.Express> {
|
|||
// All regular routes use the Angular engine
|
||||
server.get('*', async (req, res, next) => {
|
||||
console.log(`[SSR] Handling request: ${req.method} ${req.url}`);
|
||||
// Cache SSR-rendered pages at CDN level
|
||||
res.setHeader('Cache-Control', 'public, s-maxage=300, stale-while-revalidate=600');
|
||||
try {
|
||||
const response = await angularApp.handle(req);
|
||||
if (response) {
|
||||
|
|
|
|||
|
|
@ -1,24 +1,8 @@
|
|||
import { RenderMode, ServerRoute } from '@angular/ssr';
|
||||
|
||||
export const serverRoutes: ServerRoute[] = [
|
||||
{ path: 'home', renderMode: RenderMode.Server }, // Das hatten wir vorhin gefixt
|
||||
|
||||
// WICHTIG: Alle geschützten Routen nur im Browser rendern!
|
||||
// Damit überspringt der Server den AuthGuard Check komplett und schickt
|
||||
// nur eine leere Hülle (index.html), die der Browser dann füllt.
|
||||
{ path: 'account', renderMode: RenderMode.Client },
|
||||
{ path: 'account/**', renderMode: RenderMode.Client },
|
||||
{ path: 'myListings', renderMode: RenderMode.Client },
|
||||
{ path: 'myFavorites', renderMode: RenderMode.Client },
|
||||
{ path: 'createBusinessListing', renderMode: RenderMode.Client },
|
||||
{ path: 'createCommercialPropertyListing', renderMode: RenderMode.Client },
|
||||
{ path: 'editBusinessListing/**', renderMode: RenderMode.Client },
|
||||
{ path: 'editCommercialPropertyListing/**', renderMode: RenderMode.Client },
|
||||
|
||||
// Statische Seiten
|
||||
{ path: 'terms-of-use', renderMode: RenderMode.Prerender },
|
||||
{ path: 'privacy-statement', renderMode: RenderMode.Prerender },
|
||||
|
||||
// Fallback
|
||||
{ path: '**', renderMode: RenderMode.Server }
|
||||
{
|
||||
path: '**',
|
||||
renderMode: RenderMode.Server
|
||||
}
|
||||
];
|
||||
|
|
|
|||
|
|
@ -11,14 +11,19 @@ import { LoginRegisterComponent } from './components/login-register/login-regist
|
|||
import { AuthGuard } from './guards/auth.guard';
|
||||
import { ListingCategoryGuard } from './guards/listing-category.guard';
|
||||
|
||||
// Public pages - HomeComponent stays eagerly loaded as landing page
|
||||
// Public pages (eagerly loaded - high traffic)
|
||||
import { DetailsBusinessListingComponent } from './pages/details/details-business-listing/details-business-listing.component';
|
||||
import { DetailsCommercialPropertyListingComponent } from './pages/details/details-commercial-property-listing/details-commercial-property-listing.component';
|
||||
import { DetailsUserComponent } from './pages/details/details-user/details-user.component';
|
||||
import { HomeComponent } from './pages/home/home.component';
|
||||
import { BrokerListingsComponent } from './pages/listings/broker-listings/broker-listings.component';
|
||||
import { BusinessListingsComponent } from './pages/listings/business-listings/business-listings.component';
|
||||
import { CommercialPropertyListingsComponent } from './pages/listings/commercial-property-listings/commercial-property-listings.component';
|
||||
import { SuccessComponent } from './pages/success/success.component';
|
||||
import { TermsOfUseComponent } from './pages/legal/terms-of-use.component';
|
||||
import { PrivacyStatementComponent } from './pages/legal/privacy-statement.component';
|
||||
|
||||
// Note: All listing and details components are now lazy-loaded for better initial bundle size
|
||||
|
||||
// Note: Account, Edit, Admin, Favorites, MyListing, and EmailUs components are now lazy-loaded below
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
|
|
@ -27,17 +32,17 @@ export const routes: Routes = [
|
|||
},
|
||||
{
|
||||
path: 'businessListings',
|
||||
loadComponent: () => import('./pages/listings/business-listings/business-listings.component').then(m => m.BusinessListingsComponent),
|
||||
component: BusinessListingsComponent,
|
||||
runGuardsAndResolvers: 'always',
|
||||
},
|
||||
{
|
||||
path: 'commercialPropertyListings',
|
||||
loadComponent: () => import('./pages/listings/commercial-property-listings/commercial-property-listings.component').then(m => m.CommercialPropertyListingsComponent),
|
||||
component: CommercialPropertyListingsComponent,
|
||||
runGuardsAndResolvers: 'always',
|
||||
},
|
||||
{
|
||||
path: 'brokerListings',
|
||||
loadComponent: () => import('./pages/listings/broker-listings/broker-listings.component').then(m => m.BrokerListingsComponent),
|
||||
component: BrokerListingsComponent,
|
||||
runGuardsAndResolvers: 'always',
|
||||
},
|
||||
{
|
||||
|
|
@ -48,11 +53,11 @@ export const routes: Routes = [
|
|||
// Listings Details - New SEO-friendly slug-based URLs
|
||||
{
|
||||
path: 'business/:slug',
|
||||
loadComponent: () => import('./pages/details/details-business-listing/details-business-listing.component').then(m => m.DetailsBusinessListingComponent),
|
||||
component: DetailsBusinessListingComponent,
|
||||
},
|
||||
{
|
||||
path: 'commercial-property/:slug',
|
||||
loadComponent: () => import('./pages/details/details-commercial-property-listing/details-commercial-property-listing.component').then(m => m.DetailsCommercialPropertyListingComponent),
|
||||
component: DetailsCommercialPropertyListingComponent,
|
||||
},
|
||||
// Backward compatibility redirects for old UUID-based URLs
|
||||
{
|
||||
|
|
@ -90,7 +95,7 @@ export const routes: Routes = [
|
|||
// User Details
|
||||
{
|
||||
path: 'details-user/:id',
|
||||
loadComponent: () => import('./pages/details/details-user/details-user.component').then(m => m.DetailsUserComponent),
|
||||
component: DetailsUserComponent,
|
||||
},
|
||||
// #########
|
||||
// User edit (lazy-loaded)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import { ConfirmationService } from './confirmation.service';
|
|||
<button
|
||||
(click)="confirmationService.reject()"
|
||||
type="button"
|
||||
class="absolute top-3 end-2.5 text-gray-600 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
class="absolute top-3 end-2.5 text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
>
|
||||
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
|
||||
|
|
@ -21,11 +21,11 @@ import { ConfirmationService } from './confirmation.service';
|
|||
<span class="sr-only">Close modal</span>
|
||||
</button>
|
||||
<div class="p-4 md:p-5 text-center">
|
||||
<svg class="mx-auto mb-4 text-gray-600 w-12 h-12 dark:text-gray-200" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
|
||||
<svg class="mx-auto mb-4 text-gray-400 w-12 h-12 dark:text-gray-200" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 11V6m0 8h.01M19 10a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
||||
</svg>
|
||||
@let confirmation = (confirmationService.confirmation$ | async);
|
||||
<h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-600">{{ confirmation?.message }}</h3>
|
||||
<h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">{{ confirmation?.message }}</h3>
|
||||
@if(confirmation?.buttons==='both'){
|
||||
<button
|
||||
(click)="confirmationService.accept()"
|
||||
|
|
@ -37,7 +37,7 @@ import { ConfirmationService } from './confirmation.service';
|
|||
<button
|
||||
(click)="confirmationService.reject()"
|
||||
type="button"
|
||||
class="py-2.5 px-5 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-600 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
|
||||
class="py-2.5 px-5 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
|
||||
>
|
||||
No, cancel
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
<button
|
||||
(click)="eMailService.reject()"
|
||||
type="button"
|
||||
class="end-2.5 text-gray-600 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
class="end-2.5 text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
>
|
||||
<svg class="w-3 h-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
|
||||
|
|
|
|||
|
|
@ -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" alt="BizMatch Logo" 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>
|
||||
|
|
@ -18,13 +18,14 @@
|
|||
|
||||
<div class="flex flex-col lg:flex-row items-center order-2 lg:order-3">
|
||||
<div class="mb-4 lg:mb-0 lg:mr-6 text-center lg:text-right">
|
||||
<p class="text-sm text-neutral-600 mb-1 lg:mb-2">Contact: BizMatch, Inc.</p>
|
||||
<p class="text-sm text-neutral-600 mb-1 lg:mb-2">BizMatch, Inc., 1001 Blucher Street, Corpus</p>
|
||||
<p class="text-sm text-neutral-600">Christi, Texas 78401</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-4 lg:mb-0 flex flex-col items-center lg:items-end">
|
||||
<a href="tel:+1-800-840-6025" class="text-sm text-neutral-600 mb-1 lg:mb-2 hover:text-primary-600 w-full"> <i
|
||||
<a class="text-sm text-neutral-600 mb-1 lg:mb-2 hover:text-primary-600 w-full"> <i
|
||||
class="fas fa-phone-alt mr-2"></i>1-800-840-6025 </a>
|
||||
<a href="mailto:info@bizmatch.net" class="text-sm text-neutral-600 hover:text-primary-600"> <i
|
||||
<a class="text-sm text-neutral-600 hover:text-primary-600"> <i
|
||||
class="fas fa-envelope mr-2"></i>info@bizmatch.net </a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@
|
|||
<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" width="150" height="40" />
|
||||
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" aria-label="Sort listings" aria-haspopup="listbox"
|
||||
<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' }">
|
||||
|
|
@ -192,7 +192,7 @@
|
|||
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;" width="20" height="20" />
|
||||
class="w-5 h-5 mr-2 object-contain bg-transparent" style="mix-blend-mode: darken;" />
|
||||
<span>Professionals</span>
|
||||
</a>
|
||||
</li>
|
||||
|
|
@ -201,7 +201,7 @@
|
|||
</div>
|
||||
<!-- Mobile filter button -->
|
||||
<div class="md:hidden flex justify-center pb-4">
|
||||
<button (click)="toggleSortDropdown()" type="button" id="sortDropdownMobileButton" aria-label="Sort listings" aria-haspopup="listbox"
|
||||
<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' }">
|
||||
<i class="fas fa-sort mr-2"></i>{{ selectOptions.getSortByOption(sortBy) }}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { CommonModule, isPlatformBrowser } from '@angular/common';
|
|||
import { Component, HostListener, OnDestroy, OnInit, AfterViewInit, PLATFORM_ID, inject } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { NavigationEnd, Router, RouterModule } from '@angular/router';
|
||||
import { APP_ICONS } from '../../utils/fontawesome-icons';
|
||||
import { faUserGear } from '@fortawesome/free-solid-svg-icons';
|
||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||
import { Collapse, Dropdown, initFlowbite } from 'flowbite';
|
||||
import { filter, Observable, Subject, takeUntil } from 'rxjs';
|
||||
|
|
@ -35,7 +35,7 @@ export class HeaderComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||
keycloakUser: KeycloakUser;
|
||||
user: User;
|
||||
activeItem;
|
||||
faUserGear = APP_ICONS.faUserGear;
|
||||
faUserGear = faUserGear;
|
||||
profileUrl: string;
|
||||
env = environment;
|
||||
private filterDropdown: Dropdown | null = null;
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@
|
|||
placeholder="Please enter E-Mail Address"
|
||||
class="w-full px-3 py-2 pl-10 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
<fa-icon [icon]="envelope" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-600"></fa-icon>
|
||||
<fa-icon [icon]="envelope" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></fa-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -50,7 +50,7 @@
|
|||
placeholder="Please enter Password"
|
||||
class="w-full px-3 py-2 pl-10 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
<fa-icon [icon]="lock" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-600"></fa-icon>
|
||||
<fa-icon [icon]="lock" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></fa-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -65,7 +65,7 @@
|
|||
placeholder="Repeat Password"
|
||||
class="w-full px-3 py-2 pl-10 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
<fa-icon [icon]="lock" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-600"></fa-icon>
|
||||
<fa-icon [icon]="lock" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></fa-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { Component } from '@angular/core';
|
|||
import { FormsModule } from '@angular/forms';
|
||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||
import { APP_ICONS } from '../../utils/fontawesome-icons';
|
||||
import { faArrowRight, faEnvelope, faLock, faUserPlus } from '@fortawesome/free-solid-svg-icons';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
import { LoadingService } from '../../services/loading.service';
|
||||
@Component({
|
||||
|
|
@ -18,10 +18,10 @@ export class LoginRegisterComponent {
|
|||
confirmPassword: string = '';
|
||||
isLoginMode: boolean = true; // true: Login, false: Registration
|
||||
errorMessage: string = '';
|
||||
envelope = APP_ICONS.faEnvelope;
|
||||
lock = APP_ICONS.faLock;
|
||||
arrowRight = APP_ICONS.faArrowRight;
|
||||
userplus = APP_ICONS.faUserPlus;
|
||||
envelope = faEnvelope;
|
||||
lock = faLock;
|
||||
arrowRight = faArrowRight;
|
||||
userplus = faUserPlus;
|
||||
constructor(private authService: AuthService, private route: ActivatedRoute, private router: Router, private loadingService: LoadingService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
<div class="p-6 space-y-6">
|
||||
<div class="flex space-x-4 mb-4">
|
||||
<button class="text-primary-600 font-medium border-b-2 border-primary-600 pb-2">Filter ({{ numberOfResults$ | async }})</button>
|
||||
<button type="button" aria-label="Clear all filters" data-tooltip-target="tooltip-light" class="flex self-center ml-2 cursor-pointer text-primary-500 p-1" (click)="clearFilter()"><i class="fa-solid fa-trash-can"></i></button>
|
||||
<i data-tooltip-target="tooltip-light" class="fa-solid fa-trash-can flex self-center ml-2 hover:cursor-pointer text-primary-500" (click)="clearFilter()"></i>
|
||||
<div id="tooltip-light" role="tooltip" class="absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-neutral-900 bg-white border border-neutral-200 rounded-lg shadow-sm opacity-0 tooltip">
|
||||
Clear all Filter
|
||||
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||
|
|
@ -25,22 +25,22 @@
|
|||
<!-- Display active filters as tags -->
|
||||
<div class="flex flex-wrap gap-2 mb-4" *ngIf="hasActiveFilters()">
|
||||
<span *ngIf="criteria.state" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
State: {{ criteria.state }} <button (click)="removeFilter('state')" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]" aria-label="Remove filter">×</button>
|
||||
State: {{ criteria.state }} <button (click)="removeFilter('state')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.city" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
City: {{ criteria.city.name }} <button (click)="removeFilter('city')" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]" aria-label="Remove filter">×</button>
|
||||
City: {{ criteria.city.name }} <button (click)="removeFilter('city')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.types?.length" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Types: {{ criteria.types.join(', ') }} <button (click)="removeFilter('types')" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]" aria-label="Remove filter">×</button>
|
||||
Types: {{ criteria.types.join(', ') }} <button (click)="removeFilter('types')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.brokerName" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Professional Name: {{ criteria.brokerName }} <button (click)="removeFilter('brokerName')" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]" aria-label="Remove filter">×</button>
|
||||
Professional Name: {{ criteria.brokerName }} <button (click)="removeFilter('brokerName')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.companyName" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Company: {{ criteria.companyName }} <button (click)="removeFilter('companyName')" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]" aria-label="Remove filter">×</button>
|
||||
Company: {{ criteria.companyName }} <button (click)="removeFilter('companyName')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.counties?.length" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Areas Served: {{ criteria.counties.join(', ') }} <button (click)="removeFilter('counties')" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]" aria-label="Remove filter">×</button>
|
||||
Areas Served: {{ criteria.counties.join(', ') }} <button (click)="removeFilter('counties')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
</div>
|
||||
@if(criteria.criteriaType==='brokerListings') {
|
||||
|
|
@ -48,7 +48,7 @@
|
|||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="state" class="block mb-2 text-sm font-medium text-neutral-900">Location - State</label>
|
||||
<ng-select id="state-select" aria-label="Location - State" class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state"></ng-select>
|
||||
<ng-select class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state"></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<app-validated-city label="Location - City" name="city" [ngModel]="criteria.city" (ngModelChange)="setCity($event)" labelClasses="text-neutral-900 font-medium" [state]="criteria.state"></app-validated-city>
|
||||
|
|
@ -141,7 +141,7 @@
|
|||
<div *ngIf="!isModal" class="space-y-6 pb-10">
|
||||
<div class="flex space-x-4 mb-4">
|
||||
<h3 class="text-xl font-semibold text-neutral-900">Filter ({{ numberOfResults$ | async }})</h3>
|
||||
<button type="button" aria-label="Clear all filters" data-tooltip-target="tooltip-light" class="flex self-center ml-2 cursor-pointer text-primary-500 p-1" (click)="clearFilter()"><i class="fa-solid fa-trash-can"></i></button>
|
||||
<i data-tooltip-target="tooltip-light" class="fa-solid fa-trash-can flex self-center ml-2 hover:cursor-pointer text-primary-500" (click)="clearFilter()"></i>
|
||||
<div id="tooltip-light" role="tooltip" class="absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-neutral-900 bg-white border border-neutral-200 rounded-lg shadow-sm opacity-0 tooltip">
|
||||
Clear all Filter
|
||||
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||
|
|
@ -150,29 +150,29 @@
|
|||
<!-- Display active filters as tags -->
|
||||
<div class="flex flex-wrap gap-2" *ngIf="hasActiveFilters()">
|
||||
<span *ngIf="criteria.state" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
State: {{ criteria.state }} <button (click)="removeFilter('state')" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]" aria-label="Remove filter">×</button>
|
||||
State: {{ criteria.state }} <button (click)="removeFilter('state')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.city" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
City: {{ criteria.city.name }} <button (click)="removeFilter('city')" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]" aria-label="Remove filter">×</button>
|
||||
City: {{ criteria.city.name }} <button (click)="removeFilter('city')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.types?.length" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Types: {{ criteria.types.join(', ') }} <button (click)="removeFilter('types')" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]" aria-label="Remove filter">×</button>
|
||||
Types: {{ criteria.types.join(', ') }} <button (click)="removeFilter('types')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.brokerName" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Professional Name: {{ criteria.brokerName }} <button (click)="removeFilter('brokerName')" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]" aria-label="Remove filter">×</button>
|
||||
Professional Name: {{ criteria.brokerName }} <button (click)="removeFilter('brokerName')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.companyName" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Company: {{ criteria.companyName }} <button (click)="removeFilter('companyName')" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]" aria-label="Remove filter">×</button>
|
||||
Company: {{ criteria.companyName }} <button (click)="removeFilter('companyName')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.counties?.length" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Areas Served: {{ criteria.counties.join(', ') }} <button (click)="removeFilter('counties')" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]" aria-label="Remove filter">×</button>
|
||||
Areas Served: {{ criteria.counties.join(', ') }} <button (click)="removeFilter('counties')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
</div>
|
||||
@if(criteria.criteriaType==='brokerListings') {
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="state" class="block mb-2 text-sm font-medium text-neutral-900">Location - State</label>
|
||||
<ng-select id="state-select" aria-label="Location - State" class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state"></ng-select>
|
||||
<ng-select class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state"></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<app-validated-city label="Location - City" name="city" [ngModel]="criteria.city" (ngModelChange)="setCity($event)" labelClasses="text-neutral-900 font-medium" [state]="criteria.state"></app-validated-city>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
<div class="p-6 space-y-6">
|
||||
<div class="flex space-x-4 mb-4">
|
||||
<button class="text-primary-600 font-medium border-b-2 border-primary-600 pb-2">Filter ({{ numberOfResults$ | async }})</button>
|
||||
<button type="button" aria-label="Clear all filters" data-tooltip-target="tooltip-light" class="flex self-center ml-2 cursor-pointer text-primary-500 p-1" (click)="clearFilter()"><i class="fa-solid fa-trash-can"></i></button>
|
||||
<i data-tooltip-target="tooltip-light" class="fa-solid fa-trash-can flex self-center ml-2 hover:cursor-pointer text-primary-500" (click)="clearFilter()"></i>
|
||||
<div id="tooltip-light" role="tooltip" class="absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-neutral-900 bg-white border border-neutral-200 rounded-lg shadow-sm opacity-0 tooltip">
|
||||
Clear all Filter
|
||||
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||
|
|
@ -25,22 +25,22 @@
|
|||
<!-- Display active filters as tags -->
|
||||
<div class="flex flex-wrap gap-2 mb-4" *ngIf="hasActiveFilters()">
|
||||
<span *ngIf="criteria.state" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
State: {{ criteria.state }} <button (click)="removeFilter('state')" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]" aria-label="Remove filter">×</button>
|
||||
State: {{ criteria.state }} <button (click)="removeFilter('state')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.city" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
City: {{ criteria.city.name }} <button (click)="removeFilter('city')" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]" aria-label="Remove filter">×</button>
|
||||
City: {{ criteria.city.name }} <button (click)="removeFilter('city')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.minPrice || criteria.maxPrice" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Price: {{ criteria.minPrice || 'Any' }} - {{ criteria.maxPrice || 'Any' }} <button (click)="removeFilter('price')" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]" aria-label="Remove filter">×</button>
|
||||
Price: {{ criteria.minPrice || 'Any' }} - {{ criteria.maxPrice || 'Any' }} <button (click)="removeFilter('price')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.types.length" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Categories: {{ criteria.types.join(', ') }} <button (click)="removeFilter('types')" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]" aria-label="Remove filter">×</button>
|
||||
Categories: {{ criteria.types.join(', ') }} <button (click)="removeFilter('types')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.title" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Title: {{ criteria.title }} <button (click)="removeFilter('title')" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]" aria-label="Remove filter">×</button>
|
||||
Title: {{ criteria.title }} <button (click)="removeFilter('title')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.brokerName" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Broker: {{ criteria.brokerName }} <button (click)="removeFilter('brokerName')" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]" aria-label="Remove filter">×</button>
|
||||
Broker: {{ criteria.brokerName }} <button (click)="removeFilter('brokerName')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
</div>
|
||||
@if(criteria.criteriaType==='commercialPropertyListings') {
|
||||
|
|
@ -48,7 +48,7 @@
|
|||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="state" class="block mb-2 text-sm font-medium text-neutral-900">Location - State</label>
|
||||
<ng-select id="state-select" aria-label="Location - State" class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state"></ng-select>
|
||||
<ng-select class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state"></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<app-validated-city label="Location - City" name="city" [ngModel]="criteria.city" (ngModelChange)="setCity($event)" labelClasses="text-neutral-900 font-medium" [state]="criteria.state"></app-validated-city>
|
||||
|
|
@ -137,7 +137,7 @@
|
|||
<div *ngIf="!isModal" class="space-y-6 pb-10">
|
||||
<div class="flex space-x-4 mb-4">
|
||||
<h3 class="text-xl font-semibold text-neutral-900">Filter ({{ numberOfResults$ | async }})</h3>
|
||||
<button type="button" aria-label="Clear all filters" data-tooltip-target="tooltip-light" class="flex self-center ml-2 cursor-pointer text-primary-500 p-1" (click)="clearFilter()"><i class="fa-solid fa-trash-can"></i></button>
|
||||
<i data-tooltip-target="tooltip-light" class="fa-solid fa-trash-can flex self-center ml-2 hover:cursor-pointer text-primary-500" (click)="clearFilter()"></i>
|
||||
<div id="tooltip-light" role="tooltip" class="absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-neutral-900 bg-white border border-neutral-200 rounded-lg shadow-sm opacity-0 tooltip">
|
||||
Clear all Filter
|
||||
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||
|
|
@ -146,29 +146,29 @@
|
|||
<!-- Display active filters as tags -->
|
||||
<div class="flex flex-wrap gap-2" *ngIf="hasActiveFilters()">
|
||||
<span *ngIf="criteria.state" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
State: {{ criteria.state }} <button (click)="removeFilter('state')" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]" aria-label="Remove filter">×</button>
|
||||
State: {{ criteria.state }} <button (click)="removeFilter('state')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.city" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
City: {{ criteria.city.name }} <button (click)="removeFilter('city')" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]" aria-label="Remove filter">×</button>
|
||||
City: {{ criteria.city.name }} <button (click)="removeFilter('city')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.minPrice || criteria.maxPrice" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Price: {{ criteria.minPrice || 'Any' }} - {{ criteria.maxPrice || 'Any' }} <button (click)="removeFilter('price')" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]" aria-label="Remove filter">×</button>
|
||||
Price: {{ criteria.minPrice || 'Any' }} - {{ criteria.maxPrice || 'Any' }} <button (click)="removeFilter('price')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.types.length" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Categories: {{ criteria.types.join(', ') }} <button (click)="removeFilter('types')" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]" aria-label="Remove filter">×</button>
|
||||
Categories: {{ criteria.types.join(', ') }} <button (click)="removeFilter('types')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.title" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Title: {{ criteria.title }} <button (click)="removeFilter('title')" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]" aria-label="Remove filter">×</button>
|
||||
Title: {{ criteria.title }} <button (click)="removeFilter('title')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.brokerName" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Broker: {{ criteria.brokerName }} <button (click)="removeFilter('brokerName')" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]" aria-label="Remove filter">×</button>
|
||||
Broker: {{ criteria.brokerName }} <button (click)="removeFilter('brokerName')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
</div>
|
||||
@if(criteria.criteriaType==='commercialPropertyListings') {
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="state" class="block mb-2 text-sm font-medium text-neutral-900">Location - State</label>
|
||||
<ng-select id="state-select" aria-label="Location - State" class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state"></ng-select>
|
||||
<ng-select class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state"></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<app-validated-city label="Location - City" name="city" [ngModel]="criteria.city" (ngModelChange)="setCity($event)" labelClasses="text-neutral-900 font-medium" [state]="criteria.state"></app-validated-city>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
<div class="p-6 space-y-6">
|
||||
<div class="flex space-x-4 mb-4">
|
||||
<button class="text-primary-600 font-medium border-b-2 border-primary-600 pb-2">Filter ({{ numberOfResults$ | async }})</button>
|
||||
<button type="button" aria-label="Clear all filters" data-tooltip-target="tooltip-light" class="flex self-center ml-2 cursor-pointer text-primary-500 p-1" (click)="clearFilter()"><i class="fa-solid fa-trash-can"></i></button>
|
||||
<i data-tooltip-target="tooltip-light" class="fa-solid fa-trash-can flex self-center ml-2 hover:cursor-pointer text-primary-500" (click)="clearFilter()"></i>
|
||||
<div id="tooltip-light" role="tooltip" class="absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-neutral-900 bg-white border border-neutral-200 rounded-lg shadow-sm opacity-0 tooltip">
|
||||
Clear all Filter
|
||||
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||
|
|
@ -25,44 +25,44 @@
|
|||
<!-- Display active filters as tags -->
|
||||
<div class="flex flex-wrap gap-2 mb-4" *ngIf="hasActiveFilters()">
|
||||
<span *ngIf="criteria.state" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
State: {{ criteria.state }} <button (click)="removeFilter('state')" aria-label="Remove state filter" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]">×</button>
|
||||
State: {{ criteria.state }} <button (click)="removeFilter('state')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.city" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
City: {{ criteria.city.name }} <button (click)="removeFilter('city')" aria-label="Remove city filter" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]">×</button>
|
||||
City: {{ criteria.city.name }} <button (click)="removeFilter('city')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.minPrice || criteria.maxPrice" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Price: {{ criteria.minPrice || 'Any' }} - {{ criteria.maxPrice || 'Any' }} <button (click)="removeFilter('price')" aria-label="Remove price filter" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]">×</button>
|
||||
Price: {{ criteria.minPrice || 'Any' }} - {{ criteria.maxPrice || 'Any' }} <button (click)="removeFilter('price')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.minRevenue || criteria.maxRevenue" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Revenue: {{ criteria.minRevenue || 'Any' }} - {{ criteria.maxRevenue || 'Any' }} <button (click)="removeFilter('revenue')" aria-label="Remove revenue filter" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]">×</button>
|
||||
Revenue: {{ criteria.minRevenue || 'Any' }} - {{ criteria.maxRevenue || 'Any' }} <button (click)="removeFilter('revenue')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.minCashFlow || criteria.maxCashFlow" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Cashflow: {{ criteria.minCashFlow || 'Any' }} - {{ criteria.maxCashFlow || 'Any' }} <button (click)="removeFilter('cashflow')" aria-label="Remove cashflow filter" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]">×</button>
|
||||
Cashflow: {{ criteria.minCashFlow || 'Any' }} - {{ criteria.maxCashFlow || 'Any' }} <button (click)="removeFilter('cashflow')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.title" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Title: {{ criteria.title }} <button (click)="removeFilter('title')" aria-label="Remove title filter" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]">×</button>
|
||||
Title: {{ criteria.title }} <button (click)="removeFilter('title')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.types.length" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Categories: {{ criteria.types.join(', ') }} <button (click)="removeFilter('types')" aria-label="Remove categories filter" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]">×</button>
|
||||
Categories: {{ criteria.types.join(', ') }} <button (click)="removeFilter('types')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="selectedPropertyType" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Property Type: {{ getSelectedPropertyTypeName() }} <button (click)="removeFilter('propertyType')" aria-label="Remove property type filter" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]">×</button>
|
||||
Property Type: {{ getSelectedPropertyTypeName() }} <button (click)="removeFilter('propertyType')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.minNumberEmployees || criteria.maxNumberEmployees" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Employees: {{ criteria.minNumberEmployees || 'Any' }} - {{ criteria.maxNumberEmployees || 'Any' }} <button (click)="removeFilter('employees')" aria-label="Remove employees filter" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]">×</button>
|
||||
Employees: {{ criteria.minNumberEmployees || 'Any' }} - {{ criteria.maxNumberEmployees || 'Any' }} <button (click)="removeFilter('employees')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.establishedMin" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Established: {{ criteria.establishedMin || 'Any' }} <button (click)="removeFilter('established')" aria-label="Remove established filter" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]">×</button>
|
||||
Established: {{ criteria.establishedMin || 'Any' }} <button (click)="removeFilter('established')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.brokerName" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Broker: {{ criteria.brokerName }} <button (click)="removeFilter('brokerName')" aria-label="Remove broker filter" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]">×</button>
|
||||
Broker: {{ criteria.brokerName }} <button (click)="removeFilter('brokerName')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-6">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="state-select" class="block mb-2 text-sm font-medium text-neutral-900">Location - State</label>
|
||||
<ng-select id="state-select" class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state" aria-label="Location - State"></ng-select>
|
||||
<label for="state" class="block mb-2 text-sm font-medium text-neutral-900">Location - State</label>
|
||||
<ng-select class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state"></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<app-validated-city label="Location - City" name="city" [ngModel]="criteria.city" (ngModelChange)="setCity($event)" labelClasses="text-neutral-900 font-medium" [state]="criteria.state"></app-validated-city>
|
||||
|
|
@ -148,7 +148,6 @@
|
|||
[multiple]="true"
|
||||
[closeOnSelect]="true"
|
||||
placeholder="Select categories"
|
||||
aria-label="Category"
|
||||
></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -161,7 +160,6 @@
|
|||
[ngModel]="selectedPropertyType"
|
||||
(ngModelChange)="onPropertyTypeChange($event)"
|
||||
placeholder="Select property type"
|
||||
aria-label="Type of Property"
|
||||
></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -222,7 +220,7 @@
|
|||
<div *ngIf="!isModal" class="space-y-6">
|
||||
<div class="flex space-x-4 mb-4">
|
||||
<h3 class="text-xl font-semibold text-neutral-900">Filter ({{ numberOfResults$ | async }})</h3>
|
||||
<button type="button" aria-label="Clear all filters" data-tooltip-target="tooltip-light" class="flex self-center ml-2 cursor-pointer text-primary-500 p-1" (click)="clearFilter()"><i class="fa-solid fa-trash-can"></i></button>
|
||||
<i data-tooltip-target="tooltip-light" class="fa-solid fa-trash-can flex self-center ml-2 hover:cursor-pointer text-primary-500" (click)="clearFilter()"></i>
|
||||
<div id="tooltip-light" role="tooltip" class="absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-neutral-900 bg-white border border-neutral-200 rounded-lg shadow-sm opacity-0 tooltip">
|
||||
Clear all Filter
|
||||
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||
|
|
@ -231,44 +229,44 @@
|
|||
<!-- Display active filters as tags -->
|
||||
<div class="flex flex-wrap gap-2" *ngIf="hasActiveFilters()">
|
||||
<span *ngIf="criteria.state" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
State: {{ criteria.state }} <button (click)="removeFilter('state')" aria-label="Remove state filter" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]">×</button>
|
||||
State: {{ criteria.state }} <button (click)="removeFilter('state')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.city" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
City: {{ criteria.city.name }} <button (click)="removeFilter('city')" aria-label="Remove city filter" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]">×</button>
|
||||
City: {{ criteria.city.name }} <button (click)="removeFilter('city')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.minPrice || criteria.maxPrice" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Price: {{ criteria.minPrice || 'Any' }} - {{ criteria.maxPrice || 'Any' }} <button (click)="removeFilter('price')" aria-label="Remove price filter" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]">×</button>
|
||||
Price: {{ criteria.minPrice || 'Any' }} - {{ criteria.maxPrice || 'Any' }} <button (click)="removeFilter('price')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.minRevenue || criteria.maxRevenue" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Revenue: {{ criteria.minRevenue || 'Any' }} - {{ criteria.maxRevenue || 'Any' }} <button (click)="removeFilter('revenue')" aria-label="Remove revenue filter" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]">×</button>
|
||||
Revenue: {{ criteria.minRevenue || 'Any' }} - {{ criteria.maxRevenue || 'Any' }} <button (click)="removeFilter('revenue')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.minCashFlow || criteria.maxCashFlow" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Cashflow: {{ criteria.minCashFlow || 'Any' }} - {{ criteria.maxCashFlow || 'Any' }} <button (click)="removeFilter('cashflow')" aria-label="Remove cashflow filter" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]">×</button>
|
||||
Cashflow: {{ criteria.minCashFlow || 'Any' }} - {{ criteria.maxCashFlow || 'Any' }} <button (click)="removeFilter('cashflow')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.title" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Title: {{ criteria.title }} <button (click)="removeFilter('title')" aria-label="Remove title filter" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]">×</button>
|
||||
Title: {{ criteria.title }} <button (click)="removeFilter('title')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.types.length" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Categories: {{ criteria.types.join(', ') }} <button (click)="removeFilter('types')" aria-label="Remove categories filter" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]">×</button>
|
||||
Categories: {{ criteria.types.join(', ') }} <button (click)="removeFilter('types')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="selectedPropertyType" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Property Type: {{ getSelectedPropertyTypeName() }} <button (click)="removeFilter('propertyType')" aria-label="Remove property type filter" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]">×</button>
|
||||
Property Type: {{ getSelectedPropertyTypeName() }} <button (click)="removeFilter('propertyType')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.minNumberEmployees || criteria.maxNumberEmployees" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Employees: {{ criteria.minNumberEmployees || 'Any' }} - {{ criteria.maxNumberEmployees || 'Any' }} <button (click)="removeFilter('employees')" aria-label="Remove employees filter" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]">×</button>
|
||||
Employees: {{ criteria.minNumberEmployees || 'Any' }} - {{ criteria.maxNumberEmployees || 'Any' }} <button (click)="removeFilter('employees')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.establishedMin" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Years established: {{ criteria.establishedMin || 'Any' }} <button (click)="removeFilter('established')" aria-label="Remove established filter" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]">×</button>
|
||||
Years established: {{ criteria.establishedMin || 'Any' }} <button (click)="removeFilter('established')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.brokerName" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Broker: {{ criteria.brokerName }} <button (click)="removeFilter('brokerName')" aria-label="Remove broker filter" class="ml-1 text-red-500 hover:text-red-700 min-w-[24px] min-h-[24px]">×</button>
|
||||
Broker: {{ criteria.brokerName }} <button (click)="removeFilter('brokerName')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
</div>
|
||||
@if(criteria.criteriaType==='businessListings') {
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="state" class="block mb-2 text-sm font-medium text-neutral-900">Location - State</label>
|
||||
<ng-select id="state-select" aria-label="Location - State" class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state"></ng-select>
|
||||
<ng-select class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state"></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<app-validated-city label="Location - City" name="city" [ngModel]="criteria.city" (ngModelChange)="setCity($event)" labelClasses="text-neutral-900 font-medium" [state]="criteria.state"></app-validated-city>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { Component, forwardRef, ViewEncapsulation } from '@angular/core';
|
||||
import { Component, forwardRef } from '@angular/core';
|
||||
import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import { QuillModule } from 'ngx-quill';
|
||||
import { BaseInputComponent } from '../base-input/base-input.component';
|
||||
|
|
@ -9,11 +9,9 @@ import { ValidationMessagesService } from '../validation-messages.service';
|
|||
@Component({
|
||||
selector: 'app-validated-quill',
|
||||
templateUrl: './validated-quill.component.html',
|
||||
styleUrls: ['../../../../node_modules/quill/dist/quill.snow.css'],
|
||||
styles: [`quill-editor {
|
||||
styles: `quill-editor {
|
||||
width: 100%;
|
||||
}`],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
}`,
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, QuillModule, TooltipComponent],
|
||||
providers: [
|
||||
|
|
|
|||
|
|
@ -1,27 +1,15 @@
|
|||
import { Injectable, PLATFORM_ID, Inject } from '@angular/core';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CanActivate, Router } from '@angular/router';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
import { isPlatformBrowser } from '@angular/common';
|
||||
|
||||
import { createLogger } from '../utils/utils';
|
||||
const logger = createLogger('AuthGuard');
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AuthGuard implements CanActivate {
|
||||
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private router: Router,
|
||||
@Inject(PLATFORM_ID) private platformId: Object
|
||||
) {}
|
||||
constructor(private authService: AuthService, private router: Router) {}
|
||||
|
||||
async canActivate(): Promise<boolean> {
|
||||
// 1. SSR CHECK: Wenn wir auf dem Server sind, immer erlauben!
|
||||
// Der Server soll nicht redirecten, sondern einfach das HTML rendern.
|
||||
if (!isPlatformBrowser(this.platformId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. CLIENT CHECK: Das läuft nur im Browser
|
||||
const token = await this.authService.getToken();
|
||||
if (token) {
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -5,8 +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()" aria-label="Go back"
|
||||
class="absolute top-4 right-4 bg-red-500 text-white rounded-full w-10 h-10 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){
|
||||
|
|
|
|||
|
|
@ -31,27 +31,13 @@ import dayjs from 'dayjs';
|
|||
import { AuthService } from '../../../services/auth.service';
|
||||
import { BaseDetailsComponent } from '../base-details.component';
|
||||
import { ShareButton } from 'ngx-sharebuttons/button';
|
||||
import { provideShareButtonsOptions, SharerMethods, withConfig } from 'ngx-sharebuttons';
|
||||
import { shareIcons } from 'ngx-sharebuttons/icons';
|
||||
@Component({
|
||||
selector: 'app-details-business-listing',
|
||||
standalone: true,
|
||||
imports: [SharedModule, ValidatedInputComponent, ValidatedTextareaComponent, ValidatedNgSelectComponent, LeafletModule, BreadcrumbsComponent, ShareButton, NgOptimizedImage],
|
||||
providers: [
|
||||
provideShareButtonsOptions(
|
||||
shareIcons(),
|
||||
withConfig({
|
||||
debug: false,
|
||||
sharerMethod: SharerMethods.Anchor,
|
||||
}),
|
||||
),
|
||||
],
|
||||
providers: [],
|
||||
templateUrl: './details-business-listing.component.html',
|
||||
styleUrls: [
|
||||
'../details.scss',
|
||||
'../../../../../node_modules/leaflet/dist/leaflet.css',
|
||||
'../../../../../node_modules/ngx-sharebuttons/themes/default.scss'
|
||||
],
|
||||
styleUrl: '../details.scss',
|
||||
})
|
||||
export class DetailsBusinessListingComponent extends BaseDetailsComponent {
|
||||
// listings: Array<BusinessListing>;
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@
|
|||
@if(listing){
|
||||
<div class="p-6 relative">
|
||||
<h1 class="text-3xl font-bold mb-4 break-words">{{ listing?.title }}</h1>
|
||||
<button (click)="historyService.goBack()" aria-label="Go back"
|
||||
class="print:hidden absolute top-4 right-4 bg-red-500 text-white rounded-full w-10 h-10 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">
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ import { NgOptimizedImage } from '@angular/common';
|
|||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { LeafletModule } from '@bluehalo/ngx-leaflet';
|
||||
import { APP_ICONS } from '../../../utils/fontawesome-icons';
|
||||
import { faTimes } from '@fortawesome/free-solid-svg-icons';
|
||||
import dayjs from 'dayjs';
|
||||
import { GALLERY_CONFIG, GalleryConfig, GalleryModule, ImageItem } from 'ng-gallery';
|
||||
import { GalleryModule, ImageItem } from 'ng-gallery';
|
||||
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';
|
||||
|
|
@ -30,35 +30,14 @@ 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';
|
||||
import { provideShareButtonsOptions, SharerMethods, withConfig } from 'ngx-sharebuttons';
|
||||
import { shareIcons } from 'ngx-sharebuttons/icons';
|
||||
|
||||
@Component({
|
||||
selector: 'app-details-commercial-property-listing',
|
||||
standalone: true,
|
||||
imports: [SharedModule, ValidatedInputComponent, ValidatedTextareaComponent, ValidatedNgSelectComponent, GalleryModule, LeafletModule, BreadcrumbsComponent, ShareButton, NgOptimizedImage],
|
||||
providers: [
|
||||
provideShareButtonsOptions(
|
||||
shareIcons(),
|
||||
withConfig({
|
||||
debug: false,
|
||||
sharerMethod: SharerMethods.Anchor,
|
||||
}),
|
||||
),
|
||||
{
|
||||
provide: GALLERY_CONFIG,
|
||||
useValue: {
|
||||
autoHeight: true,
|
||||
imageSize: 'cover',
|
||||
} as GalleryConfig,
|
||||
},
|
||||
],
|
||||
providers: [],
|
||||
templateUrl: './details-commercial-property-listing.component.html',
|
||||
styleUrls: [
|
||||
'../details.scss',
|
||||
'../../../../../node_modules/leaflet/dist/leaflet.css',
|
||||
'../../../../../node_modules/ngx-sharebuttons/themes/default.scss'
|
||||
],
|
||||
styleUrl: '../details.scss',
|
||||
})
|
||||
export class DetailsCommercialPropertyListingComponent extends BaseDetailsComponent {
|
||||
responsiveOptions = [
|
||||
|
|
@ -90,7 +69,7 @@ export class DetailsCommercialPropertyListingComponent extends BaseDetailsCompon
|
|||
ts = new Date().getTime();
|
||||
env = environment;
|
||||
errorResponse: ErrorResponse;
|
||||
faTimes = APP_ICONS.faTimes;
|
||||
faTimes = faTimes;
|
||||
propertyDetails = [];
|
||||
images: Array<ImageItem> = [];
|
||||
relatedListings: CommercialPropertyListing[] = [];
|
||||
|
|
|
|||
|
|
@ -42,8 +42,8 @@
|
|||
}
|
||||
<!-- <img src="https://placehold.co/45x60" class="w-11 h-14" /> -->
|
||||
</div>
|
||||
<button (click)="historyService.goBack()" aria-label="Go back"
|
||||
class="absolute top-4 right-4 bg-red-500 text-white rounded-full w-10 h-10 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="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>
|
||||
|
|
|
|||
|
|
@ -19,27 +19,13 @@ import { UserService } from '../../../services/user.service';
|
|||
import { SharedModule } from '../../../shared/shared/shared.module';
|
||||
import { formatPhoneNumber, map2User } from '../../../utils/utils';
|
||||
import { ShareButton } from 'ngx-sharebuttons/button';
|
||||
import { provideShareButtonsOptions, SharerMethods, withConfig } from 'ngx-sharebuttons';
|
||||
import { shareIcons } from 'ngx-sharebuttons/icons';
|
||||
|
||||
@Component({
|
||||
selector: 'app-details-user',
|
||||
standalone: true,
|
||||
imports: [SharedModule, BreadcrumbsComponent, NgOptimizedImage, ShareButton],
|
||||
providers: [
|
||||
provideShareButtonsOptions(
|
||||
shareIcons(),
|
||||
withConfig({
|
||||
debug: false,
|
||||
sharerMethod: SharerMethods.Anchor,
|
||||
}),
|
||||
),
|
||||
],
|
||||
templateUrl: './details-user.component.html',
|
||||
styleUrls: [
|
||||
'../details.scss',
|
||||
'../../../../../node_modules/ngx-sharebuttons/themes/default.scss'
|
||||
],
|
||||
styleUrl: '../details.scss',
|
||||
})
|
||||
export class DetailsUserComponent {
|
||||
private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<header class="w-full flex justify-between items-center p-4 bg-white top-0 z-10 h-16 md:h-20">
|
||||
<img src="/assets/images/header-logo.png" alt="BizMatch - Business & Property Marketplace" class="h-8 md:h-10 w-auto" width="120" height="40" />
|
||||
<img src="/assets/images/header-logo.png" alt="Logo" class="h-8 md:h-10 w-auto" />
|
||||
<div class="hidden md:flex items-center space-x-4">
|
||||
@if(user){
|
||||
<a routerLink="/account" class="text-primary-600 border border-primary-600 px-3 py-2 rounded">Account</a>
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
</div>
|
||||
<button
|
||||
(click)="toggleMenu()"
|
||||
class="md:hidden text-neutral-700 p-2 min-w-[44px] min-h-[44px] flex items-center justify-center"
|
||||
class="md:hidden text-neutral-600"
|
||||
aria-label="Open navigation menu"
|
||||
[attr.aria-expanded]="isMenuOpen"
|
||||
>
|
||||
|
|
@ -88,8 +88,8 @@
|
|||
<!-- Restliche Anpassungen (Innenabstände, Button-Paddings etc.) bleiben wie im vorherigen Schritt -->
|
||||
<div class="search-form-container bg-white bg-opacity-80 pb-4 md:pb-6 pt-2 px-3 sm:px-4 md:px-6 rounded-lg shadow-lg w-full" [ngClass]="{ 'pt-6': aiSearch }">
|
||||
@if(!aiSearch){
|
||||
<div class="text-sm lg:text-base mb-1 text-center text-neutral-700 border-neutral-200 dark:text-neutral-300 dark:border-neutral-700 flex justify-between">
|
||||
<ul class="flex flex-wrap -mb-px w-full" role="tablist" aria-label="Search categories">
|
||||
<div class="text-sm lg:text-base mb-1 text-center text-neutral-500 border-neutral-200 dark:text-neutral-400 dark:border-neutral-700 flex justify-between">
|
||||
<ul class="flex flex-wrap -mb-px w-full" role="tablist">
|
||||
<li class="w-[33%]" role="presentation">
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -99,11 +99,9 @@
|
|||
[ngClass]="
|
||||
activeTabAction === 'business'
|
||||
? ['text-primary-600', 'border-primary-600', 'active', 'dark:text-primary-500', 'dark:border-primary-500']
|
||||
: ['border-transparent', 'text-neutral-700', 'hover:text-neutral-900', 'hover:border-neutral-400', 'dark:hover:text-neutral-300']
|
||||
: ['border-transparent', 'hover:text-neutral-600', 'hover:border-neutral-300', 'dark:hover:text-neutral-300']
|
||||
"
|
||||
class="tab-link w-full hover:cursor-pointer inline-flex items-center justify-center px-3 py-3 md:p-4 border-b-2 rounded-t-lg bg-transparent min-h-[44px]"
|
||||
[attr.aria-controls]="'tabpanel-search'"
|
||||
id="tab-business"
|
||||
class="tab-link w-full hover:cursor-pointer inline-flex items-center justify-center px-3 py-3 md:p-4 border-b-2 rounded-t-lg bg-transparent"
|
||||
>
|
||||
<img src="/assets/images/business_logo.png" alt="" aria-hidden="true" class="tab-icon w-6 h-6 md:w-7 md:h-7 mr-1 md:mr-2 object-contain" width="28" height="28" />
|
||||
<span>Businesses</span>
|
||||
|
|
@ -119,11 +117,9 @@
|
|||
[ngClass]="
|
||||
activeTabAction === 'commercialProperty'
|
||||
? ['text-primary-600', 'border-primary-600', 'active', 'dark:text-primary-500', 'dark:border-primary-500']
|
||||
: ['border-transparent', 'text-neutral-700', 'hover:text-neutral-900', 'hover:border-neutral-400', 'dark:hover:text-neutral-300']
|
||||
: ['border-transparent', 'hover:text-neutral-600', 'hover:border-neutral-300', 'dark:hover:text-neutral-300']
|
||||
"
|
||||
class="tab-link w-full hover:cursor-pointer inline-flex items-center justify-center px-3 py-3 md:p-4 border-b-2 rounded-t-lg bg-transparent min-h-[44px]"
|
||||
[attr.aria-controls]="'tabpanel-search'"
|
||||
id="tab-properties"
|
||||
class="tab-link w-full hover:cursor-pointer inline-flex items-center justify-center px-3 py-3 md:p-4 border-b-2 rounded-t-lg bg-transparent"
|
||||
>
|
||||
<img src="/assets/images/properties_logo.png" alt="" aria-hidden="true" class="tab-icon w-6 h-6 md:w-7 md:h-7 mr-1 md:mr-2 object-contain" width="28" height="28" />
|
||||
<span>Properties</span>
|
||||
|
|
@ -139,11 +135,9 @@
|
|||
[ngClass]="
|
||||
activeTabAction === 'broker'
|
||||
? ['text-primary-600', 'border-primary-600', 'active', 'dark:text-primary-500', 'dark:border-primary-500']
|
||||
: ['border-transparent', 'text-neutral-700', 'hover:text-neutral-900', 'hover:border-neutral-400', 'dark:hover:text-neutral-300']
|
||||
: ['border-transparent', 'hover:text-neutral-600', 'hover:border-neutral-300', 'dark:hover:text-neutral-300']
|
||||
"
|
||||
class="tab-link w-full hover:cursor-pointer inline-flex items-center justify-center px-3 py-3 md:p-4 border-b-2 rounded-t-lg bg-transparent min-h-[44px]"
|
||||
[attr.aria-controls]="'tabpanel-search'"
|
||||
id="tab-professionals"
|
||||
class="tab-link w-full hover:cursor-pointer inline-flex items-center justify-center px-3 py-3 md:p-4 border-b-2 rounded-t-lg bg-transparent"
|
||||
>
|
||||
<img
|
||||
src="/assets/images/icon_professionals.png"
|
||||
|
|
@ -159,7 +153,7 @@
|
|||
</ul>
|
||||
</div>
|
||||
} @if(criteria && !aiSearch){
|
||||
<div id="tabpanel-search" role="tabpanel" aria-labelledby="tab-business" class="w-full max-w-3xl mx-auto bg-white rounded-lg flex flex-col md:flex-row md:border md:border-neutral-300">
|
||||
<div class="w-full max-w-3xl mx-auto bg-white rounded-lg flex flex-col md:flex-row md:border md:border-neutral-300">
|
||||
<div class="md:flex-none md:w-48 flex-1 md:border-r border-neutral-300 overflow-hidden mb-2 md:mb-0">
|
||||
<div class="relative max-sm:border border-neutral-300 rounded-md">
|
||||
<label for="type-filter" class="sr-only">Filter by type</label>
|
||||
|
|
@ -201,11 +195,7 @@
|
|||
groupBy="type"
|
||||
labelForId="location-search"
|
||||
aria-label="Search by city or state"
|
||||
[inputAttrs]="{'aria-describedby': 'location-search-hint'}"
|
||||
>
|
||||
<ng-template ng-footer-tmp>
|
||||
<span id="location-search-hint" class="sr-only">Enter at least 2 characters to search for a city or state</span>
|
||||
</ng-template>
|
||||
@for (city of cities$ | async; track city.id) { @let state = city.type==='city'?city.content.state:''; @let separator = city.type==='city'?' - ':'';
|
||||
<ng-option [value]="city">{{ city.content.name }}{{ separator }}{{ state }}</ng-option>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
|
||||
select:not([size]) {
|
||||
background-image: unset;
|
||||
}
|
||||
|
||||
[type='text'],
|
||||
[type='email'],
|
||||
[type='url'],
|
||||
|
|
@ -19,51 +19,39 @@ textarea,
|
|||
select {
|
||||
border: unset;
|
||||
}
|
||||
|
||||
.toggle-checkbox:checked {
|
||||
right: 0;
|
||||
border-color: rgb(125 211 252);
|
||||
}
|
||||
|
||||
.toggle-checkbox:checked + .toggle-label {
|
||||
background-color: rgb(125 211 252);
|
||||
}
|
||||
|
||||
:host ::ng-deep .ng-select.ng-select-single .ng-select-container {
|
||||
min-height: 52px;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
|
||||
.ng-value-container .ng-input {
|
||||
top: 12px;
|
||||
}
|
||||
|
||||
span.ng-arrow-wrapper {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
color: #000;
|
||||
/* Standard-Textfarbe für das Dropdown */
|
||||
color: #000; /* Standard-Textfarbe für das Dropdown */
|
||||
// background-color: #fff; /* Hintergrundfarbe für das Dropdown */
|
||||
}
|
||||
|
||||
select option {
|
||||
color: #000;
|
||||
/* Textfarbe für Dropdown-Optionen */
|
||||
color: #000; /* Textfarbe für Dropdown-Optionen */
|
||||
}
|
||||
|
||||
select.placeholder-selected {
|
||||
color: #6b7280;
|
||||
/* gray-500 - besserer Kontrast für WCAG AA */
|
||||
color: #999; /* Farbe für den Platzhalter */
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
color: #555;
|
||||
/* Dunkleres Grau */
|
||||
opacity: 1;
|
||||
/* Stellt sicher, dass die Deckkraft 100% ist */
|
||||
color: #555; /* Dunkleres Grau */
|
||||
opacity: 1; /* Stellt sicher, dass die Deckkraft 100% ist */
|
||||
}
|
||||
|
||||
/* Stellt sicher, dass die Optionen im Dropdown immer schwarz sind */
|
||||
|
|
@ -71,14 +59,10 @@ select:focus option,
|
|||
select:hover option {
|
||||
color: #000 !important;
|
||||
}
|
||||
|
||||
input[type='text'][name='aiSearchText'] {
|
||||
padding: 14px;
|
||||
/* Innerer Abstand */
|
||||
font-size: 16px;
|
||||
/* Schriftgröße anpassen */
|
||||
box-sizing: border-box;
|
||||
/* Padding und Border in die Höhe und Breite einrechnen */
|
||||
padding: 14px; /* Innerer Abstand */
|
||||
font-size: 16px; /* Schriftgröße anpassen */
|
||||
box-sizing: border-box; /* Padding und Border in die Höhe und Breite einrechnen */
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
|
|
@ -161,7 +145,6 @@ select,
|
|||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
|
|
@ -229,7 +212,6 @@ header {
|
|||
transition: all 0.2s ease-in-out;
|
||||
|
||||
&.text-blue-600.border.border-blue-600 {
|
||||
|
||||
// Log In button
|
||||
&:hover {
|
||||
background-color: rgba(37, 99, 235, 0.05);
|
||||
|
|
@ -242,7 +224,6 @@ header {
|
|||
}
|
||||
|
||||
&.bg-blue-600 {
|
||||
|
||||
// Register button
|
||||
&:hover {
|
||||
background-color: rgb(29, 78, 216);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { NgSelectModule } from '@ng-select/ng-select';
|
|||
import { UntilDestroy } from '@ngneat/until-destroy';
|
||||
import { catchError, concat, distinctUntilChanged, Observable, of, Subject, switchMap, tap } from 'rxjs';
|
||||
import { BusinessListingCriteria, CityAndStateResult, CommercialPropertyListingCriteria, GeoResult, KeycloakUser, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model';
|
||||
import { FAQItem } from '../../components/faq/faq.component';
|
||||
import { FaqComponent, FAQItem } from '../../components/faq/faq.component';
|
||||
import { ModalService } from '../../components/search-modal/modal.service';
|
||||
import { TooltipComponent } from '../../components/tooltip/tooltip.component';
|
||||
import { AiService } from '../../services/ai.service';
|
||||
|
|
@ -24,7 +24,7 @@ import { map2User } from '../../utils/utils';
|
|||
@Component({
|
||||
selector: 'app-home',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, RouterModule, NgSelectModule],
|
||||
imports: [CommonModule, FormsModule, RouterModule, NgSelectModule, FaqComponent],
|
||||
templateUrl: './home.component.html',
|
||||
styleUrl: './home.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
|
|
|||
|
|
@ -13,14 +13,14 @@
|
|||
</div>
|
||||
|
||||
<!-- SEO-optimized heading -->
|
||||
<!-- <div class="mb-6">
|
||||
<div class="mb-6">
|
||||
<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 class="mt-4 text-base text-neutral-700 max-w-4xl">
|
||||
<p>BizMatch connects business buyers and sellers with experienced professionals. Find qualified business brokers to help with your business sale or acquisition. Our platform features verified professionals including business brokers, M&A advisors, CPAs, and attorneys specializing in business transactions across the United States. Whether you're looking to buy or sell a business, our network of professionals can guide you through the process.</p>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<!-- Mobile Filter Button -->
|
||||
<div class="md:hidden mb-4">
|
||||
|
|
@ -44,14 +44,14 @@
|
|||
@if(currentUser) {
|
||||
<button type="button" class="bg-white rounded-full p-2 shadow-lg transition-colors"
|
||||
[class.bg-red-50]="isFavorite(user)"
|
||||
[attr.aria-label]="isFavorite(user) ? 'Remove from favorites' : 'Save to favorites'"
|
||||
[title]="isFavorite(user) ? 'Remove from favorites' : 'Save to favorites'"
|
||||
(click)="toggleFavorite($event, user)">
|
||||
<i
|
||||
[class]="isFavorite(user) ? 'fas fa-heart text-red-500' : 'far fa-heart text-red-500 hover:scale-110 transition-transform'"></i>
|
||||
</button>
|
||||
}
|
||||
<button type="button" class="bg-white rounded-full p-2 shadow-lg hover:bg-blue-50 transition-colors"
|
||||
aria-label="Share professional" (click)="shareProfessional($event, user)">
|
||||
title="Share professional" (click)="shareProfessional($event, user)">
|
||||
<i class="fas fa-share-alt text-blue-500 hover:scale-110 transition-transform"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,10 +15,10 @@
|
|||
<!-- SEO-optimized heading -->
|
||||
<div class="mb-6">
|
||||
<h1 class="text-3xl md:text-4xl font-bold text-neutral-900 mb-2">Businesses for Sale - Find Your Next Business Opportunity</h1>
|
||||
<p class="text-lg text-neutral-600">Discover profitable business opportunities across the State of Texas. Browse
|
||||
<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 class="mt-4 text-base text-neutral-700 max-w-4xl">
|
||||
<p>BizMatch features a curated selection of businesses for sale across diverse industries and price ranges. Browse opportunities in sectors like restaurants, retail, franchises, services, e-commerce, and manufacturing. Each listing includes financial details, years established, location information, and seller contact details. Our marketplace connects business buyers with sellers and brokers nationwide, making it easy to find your next business opportunity.</p>
|
||||
<p>BizMatch features thousands of businesses for sale across all industries and price ranges. Browse restaurants, retail stores, franchises, service businesses, e-commerce operations, and manufacturing companies. Each listing includes financial details, years established, location information, and seller contact details. Our marketplace connects business buyers with sellers and brokers nationwide, making it easy to find your next business opportunity.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -68,14 +68,14 @@
|
|||
@if(user) {
|
||||
<button class="bg-white rounded-full p-2 shadow-lg transition-colors"
|
||||
[class.bg-red-50]="isFavorite(listing)"
|
||||
[attr.aria-label]="isFavorite(listing) ? 'Remove from favorites' : 'Save to favorites'"
|
||||
[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>
|
||||
</button>
|
||||
}
|
||||
<button type="button" class="bg-white rounded-full p-2 shadow-lg hover:bg-blue-50 transition-colors"
|
||||
aria-label="Share listing" (click)="shareListing($event, listing)">
|
||||
title="Share listing" (click)="shareListing($event, listing)">
|
||||
<i class="fas fa-share-alt text-blue-500 hover:scale-110 transition-transform"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,9 +15,9 @@
|
|||
<!-- SEO-optimized heading -->
|
||||
<div class="mb-6">
|
||||
<h1 class="text-3xl md:text-4xl font-bold text-neutral-900 mb-2">Commercial Properties for Sale</h1>
|
||||
<p class="text-lg text-neutral-600">Discover selected investment properties and commercial spaces. Connect with verified sellers and brokers to find your next asset.</p>
|
||||
<p class="text-lg text-neutral-600">Find office buildings, retail spaces, warehouses, and industrial properties across the United States. Investment opportunities from verified sellers and commercial real estate brokers.</p>
|
||||
<div class="mt-4 text-base text-neutral-700 max-w-4xl">
|
||||
<p>BizMatch presents commercial real estate opportunities for sale or lease. View investment properties with detailed information on square footage, zoning, pricing, and location. Our platform connects property buyers and investors directly with sellers and commercial real estate brokers, focusing on transparent and valuable transactions.</p>
|
||||
<p>BizMatch showcases commercial real estate listings including office buildings, retail spaces, warehouses, and industrial properties for sale or lease. Browse investment properties across the United States with detailed information on square footage, zoning, pricing, and location. Our platform connects property buyers and investors with sellers and commercial real estate brokers. Find shopping centers, medical buildings, land parcels, and mixed-use developments in your target market.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -33,13 +33,13 @@
|
|||
class="bg-white rounded-full p-2 shadow-lg transition-colors"
|
||||
[class.bg-red-50]="isFavorite(listing)"
|
||||
[class.opacity-100]="isFavorite(listing)"
|
||||
[attr.aria-label]="isFavorite(listing) ? 'Remove from favorites' : 'Save to favorites'"
|
||||
[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>
|
||||
</button>
|
||||
}
|
||||
<button type="button" class="bg-white rounded-full p-2 shadow-lg hover:bg-blue-50 transition-colors"
|
||||
aria-label="Share property" (click)="shareProperty($event, listing)">
|
||||
title="Share property" (click)="shareProperty($event, listing)">
|
||||
<i class="fas fa-share-alt text-blue-500 hover:scale-110 transition-transform"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -18,16 +18,16 @@
|
|||
<div class="w-20 h-20 w-full rounded-md flex items-center justify-center relative">
|
||||
@if(user?.hasCompanyLogo){
|
||||
<img src="{{ companyLogoUrl }}" alt="Company logo" class="max-w-full max-h-full" />
|
||||
<button type="button" aria-label="Delete company logo"
|
||||
class="absolute top-[-0.5rem] right-[0rem] bg-white rounded-full p-2 drop-shadow-custom-bg cursor-pointer min-w-[32px] min-h-[32px] flex items-center justify-center"
|
||||
<div
|
||||
class="absolute top-[-0.5rem] right-[0rem] bg-white rounded-full p-1 drop-shadow-custom-bg hover:cursor-pointer"
|
||||
(click)="deleteConfirm('logo')">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"
|
||||
class="w-4 h-4 text-gray-600">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
} @else {
|
||||
<img src="/assets/images/placeholder.png" class="max-w-full max-h-full" alt="" width="80" height="80" />
|
||||
<img src="/assets/images/placeholder.png" class="max-w-full max-h-full" />
|
||||
}
|
||||
</div>
|
||||
<button type="button"
|
||||
|
|
@ -41,16 +41,16 @@
|
|||
<div class="w-20 h-20 w-full rounded-md flex items-center justify-center relative">
|
||||
@if(user?.hasProfile){
|
||||
<img src="{{ profileUrl }}" alt="Profile picture" class="max-w-full max-h-full" />
|
||||
<button type="button" aria-label="Delete profile picture"
|
||||
class="absolute top-[-0.5rem] right-[0rem] bg-white rounded-full p-2 drop-shadow-custom-bg cursor-pointer min-w-[32px] min-h-[32px] flex items-center justify-center"
|
||||
<div
|
||||
class="absolute top-[-0.5rem] right-[0rem] bg-white rounded-full p-1 drop-shadow-custom-bg hover:cursor-pointer"
|
||||
(click)="deleteConfirm('profile')">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"
|
||||
class="w-4 h-4 text-gray-600">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
} @else {
|
||||
<img src="/assets/images/placeholder.png" class="max-w-full max-h-full" alt="" width="80" height="80" />
|
||||
<img src="/assets/images/placeholder.png" class="max-w-full max-h-full" />
|
||||
}
|
||||
</div>
|
||||
<button type="button"
|
||||
|
|
@ -129,16 +129,23 @@
|
|||
mask="(000) 000-0000"></app-validated-input>
|
||||
<app-validated-input label="Company Website" name="companyWebsite"
|
||||
[(ngModel)]="user.companyWebsite"></app-validated-input>
|
||||
<!-- <app-validated-input label="Company Location" name="companyLocation" [(ngModel)]="user.companyLocation"></app-validated-input> -->
|
||||
<!-- <app-validated-city label="Company Location" name="location" [(ngModel)]="user.location"></app-validated-city> -->
|
||||
<app-validated-location label="Company Location" name="location"
|
||||
[(ngModel)]="user.location"></app-validated-location>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- <div>
|
||||
<label for="companyOverview" class="block text-sm font-medium text-gray-700">Company Overview</label>
|
||||
<quill-editor [(ngModel)]="user.companyOverview" name="companyOverview" [modules]="quillModules"></quill-editor>
|
||||
</div> -->
|
||||
<div>
|
||||
<app-validated-quill label="Company Overview" name="companyOverview"
|
||||
[(ngModel)]="user.companyOverview"></app-validated-quill>
|
||||
</div>
|
||||
<div>
|
||||
<!-- <label for="offeredServices" class="block text-sm font-medium text-gray-700">Services We Offer</label>
|
||||
<quill-editor [(ngModel)]="user.offeredServices" name="offeredServices" [modules]="quillModules"></quill-editor> -->
|
||||
<app-validated-quill label="Services We Offer" name="offeredServices"
|
||||
[(ngModel)]="user.offeredServices"></app-validated-quill>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { DatePipe, TitleCasePipe } from '@angular/common';
|
||||
import { ChangeDetectorRef, Component } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { APP_ICONS } from '../../../utils/fontawesome-icons';
|
||||
import { faTrash } from '@fortawesome/free-solid-svg-icons';
|
||||
import { NgSelectModule } from '@ng-select/ng-select';
|
||||
import { QuillModule } from 'ngx-quill';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
|
|
@ -45,15 +45,9 @@ import { TOOLBAR_OPTIONS } from '../../utils/defaults';
|
|||
ValidatedCountyComponent,
|
||||
ValidatedLocationComponent,
|
||||
],
|
||||
providers: [
|
||||
TitleCasePipe,
|
||||
DatePipe
|
||||
],
|
||||
providers: [TitleCasePipe, DatePipe],
|
||||
templateUrl: './account.component.html',
|
||||
styleUrls: [
|
||||
'./account.component.scss',
|
||||
'../../../../../node_modules/quill/dist/quill.snow.css'
|
||||
],
|
||||
styleUrl: './account.component.scss',
|
||||
})
|
||||
export class AccountComponent {
|
||||
id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;
|
||||
|
|
@ -64,7 +58,10 @@ export class AccountComponent {
|
|||
environment = environment;
|
||||
editorModules = TOOLBAR_OPTIONS;
|
||||
env = environment;
|
||||
faTrash = APP_ICONS.faTrash;
|
||||
faTrash = faTrash;
|
||||
quillModules = {
|
||||
toolbar: [['bold', 'italic', 'underline', 'strike'], [{ list: 'ordered' }, { list: 'bullet' }], [{ header: [1, 2, 3, 4, 5, 6, false] }], [{ color: [] }, { background: [] }], ['clean']],
|
||||
};
|
||||
uploadParams: UploadParams;
|
||||
validationMessages: ValidationMessage[] = [];
|
||||
customerTypeOptions: Array<{ value: string; label: string }> = [];
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { SelectOptionsService } from '../../../services/select-options.service';
|
|||
import { map2User, routeListingWithState } from '../../../utils/utils';
|
||||
|
||||
import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||
import { APP_ICONS } from '../../../utils/fontawesome-icons';
|
||||
import { faTrash } from '@fortawesome/free-solid-svg-icons';
|
||||
import { QuillModule } from 'ngx-quill';
|
||||
|
||||
import { NgSelectModule } from '@ng-select/ng-select';
|
||||
|
|
@ -47,11 +47,9 @@ import { TOOLBAR_OPTIONS } from '../../utils/defaults';
|
|||
ValidatedTextareaComponent,
|
||||
ValidatedLocationComponent,
|
||||
],
|
||||
providers: [],
|
||||
templateUrl: './edit-business-listing.component.html',
|
||||
styleUrls: [
|
||||
'./edit-business-listing.component.scss',
|
||||
'../../../../../node_modules/quill/dist/quill.snow.css'
|
||||
],
|
||||
styleUrl: './edit-business-listing.component.scss',
|
||||
})
|
||||
export class EditBusinessListingComponent {
|
||||
listingsCategory = 'business';
|
||||
|
|
@ -66,7 +64,7 @@ export class EditBusinessListingComponent {
|
|||
config = { aspectRatio: 16 / 9 };
|
||||
editorModules = TOOLBAR_OPTIONS;
|
||||
draggedImage: ImageProperty;
|
||||
faTrash = APP_ICONS.faTrash;
|
||||
faTrash = faTrash;
|
||||
data: CommercialPropertyListing;
|
||||
typesOfBusiness = [];
|
||||
quillModules = {
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@
|
|||
(click)="uploadPropertyPicture()"
|
||||
class="flex items-center justify-center px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 bg-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||
>
|
||||
<svg class="mr-2 h-5 w-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg class="mr-2 h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
|
||||
</svg>
|
||||
Upload
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { map2User, routeListingWithState } from '../../../utils/utils';
|
|||
|
||||
import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||
import { ViewportRuler } from '@angular/cdk/scrolling';
|
||||
import { APP_ICONS } from '../../../utils/fontawesome-icons';
|
||||
import { faTrash } from '@fortawesome/free-solid-svg-icons';
|
||||
import { NgSelectModule } from '@ng-select/ng-select';
|
||||
import { NgxCurrencyDirective } from 'ngx-currency';
|
||||
import { ImageCropperComponent } from 'ngx-image-cropper';
|
||||
|
|
@ -53,11 +53,9 @@ import { TOOLBAR_OPTIONS } from '../../utils/defaults';
|
|||
ValidatedLocationComponent,
|
||||
ImageCropAndUploadComponent,
|
||||
],
|
||||
providers: [],
|
||||
templateUrl: './edit-commercial-property-listing.component.html',
|
||||
styleUrls: [
|
||||
'./edit-commercial-property-listing.component.scss',
|
||||
'../../../../../node_modules/quill/dist/quill.snow.css'
|
||||
],
|
||||
styleUrl: './edit-commercial-property-listing.component.scss',
|
||||
})
|
||||
export class EditCommercialPropertyListingComponent {
|
||||
@ViewChild('fileInput') fileInput!: ElementRef<HTMLInputElement>;
|
||||
|
|
@ -93,7 +91,7 @@ export class EditCommercialPropertyListingComponent {
|
|||
editorModules = TOOLBAR_OPTIONS;
|
||||
|
||||
draggedImage: ImageProperty;
|
||||
faTrash = APP_ICONS.faTrash;
|
||||
faTrash = faTrash;
|
||||
suggestions: string[] | undefined;
|
||||
data: BusinessListing;
|
||||
userId: string;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Injectable, inject, PLATFORM_ID, Renderer2, RendererFactory2 } from '@angular/core';
|
||||
import { isPlatformBrowser, DOCUMENT } from '@angular/common';
|
||||
import { Injectable, inject, PLATFORM_ID } from '@angular/core';
|
||||
import { isPlatformBrowser } from '@angular/common';
|
||||
import { Meta, Title } from '@angular/platform-browser';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
|
|
@ -22,17 +22,11 @@ export class SeoService {
|
|||
private router = inject(Router);
|
||||
private platformId = inject(PLATFORM_ID);
|
||||
private isBrowser = isPlatformBrowser(this.platformId);
|
||||
private document = inject(DOCUMENT);
|
||||
private renderer: Renderer2;
|
||||
|
||||
private readonly defaultImage = 'https://www.bizmatch.net/assets/images/bizmatch-og-image.jpg';
|
||||
private readonly siteName = 'BizMatch';
|
||||
private readonly baseUrl = 'https://www.bizmatch.net';
|
||||
|
||||
constructor(rendererFactory: RendererFactory2) {
|
||||
this.renderer = rendererFactory.createRenderer(null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the base URL for SEO purposes
|
||||
*/
|
||||
|
|
@ -115,18 +109,20 @@ export class SeoService {
|
|||
}
|
||||
|
||||
/**
|
||||
* Update canonical URL (SSR-compatible using Renderer2)
|
||||
* Update canonical URL
|
||||
*/
|
||||
private updateCanonicalUrl(url: string): void {
|
||||
let link: HTMLLinkElement | null = this.document.querySelector('link[rel="canonical"]');
|
||||
if (!this.isBrowser) return;
|
||||
|
||||
let link: HTMLLinkElement | null = document.querySelector('link[rel="canonical"]');
|
||||
|
||||
if (link) {
|
||||
this.renderer.setAttribute(link, 'href', url);
|
||||
link.setAttribute('href', url);
|
||||
} else {
|
||||
link = this.renderer.createElement('link');
|
||||
this.renderer.setAttribute(link, 'rel', 'canonical');
|
||||
this.renderer.setAttribute(link, 'href', url);
|
||||
this.renderer.appendChild(this.document.head, link);
|
||||
link = document.createElement('link');
|
||||
link.setAttribute('rel', 'canonical');
|
||||
link.setAttribute('href', url);
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -273,40 +269,32 @@ export class SeoService {
|
|||
}
|
||||
|
||||
/**
|
||||
* Inject JSON-LD structured data into page (SSR-compatible using Renderer2)
|
||||
* Inject JSON-LD structured data into page
|
||||
*/
|
||||
injectStructuredData(schema: object): void {
|
||||
// Clear existing schema scripts with the same type
|
||||
this.removeAllSchemas();
|
||||
if (!this.isBrowser) return;
|
||||
|
||||
// Create new script element using Renderer2 (works in both SSR and browser)
|
||||
const script = this.renderer.createElement('script');
|
||||
this.renderer.setAttribute(script, 'type', 'application/ld+json');
|
||||
this.renderer.setAttribute(script, 'data-schema', 'true');
|
||||
// Remove existing schema script
|
||||
const existingScript = document.querySelector('script[type="application/ld+json"]');
|
||||
if (existingScript) {
|
||||
existingScript.remove();
|
||||
}
|
||||
|
||||
// Create text node with schema JSON
|
||||
const schemaText = this.renderer.createText(JSON.stringify(schema));
|
||||
this.renderer.appendChild(script, schemaText);
|
||||
|
||||
// Append to document head
|
||||
this.renderer.appendChild(this.document.head, script);
|
||||
// Add new schema script
|
||||
const script = document.createElement('script');
|
||||
script.type = 'application/ld+json';
|
||||
script.text = JSON.stringify(schema);
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all schema scripts (internal helper, SSR-compatible)
|
||||
*/
|
||||
private removeAllSchemas(): void {
|
||||
const existingScripts = this.document.querySelectorAll('script[data-schema="true"]');
|
||||
existingScripts.forEach(script => {
|
||||
this.renderer.removeChild(this.document.head, script);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all structured data (SSR-compatible)
|
||||
* Clear all structured data
|
||||
*/
|
||||
clearStructuredData(): void {
|
||||
this.removeAllSchemas();
|
||||
if (!this.isBrowser) return;
|
||||
|
||||
const scripts = document.querySelectorAll('script[type="application/ld+json"]');
|
||||
scripts.forEach(script => script.remove());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -528,21 +516,20 @@ export class SeoService {
|
|||
}
|
||||
|
||||
/**
|
||||
* Inject multiple structured data schemas (SSR-compatible using Renderer2)
|
||||
* Inject multiple structured data schemas
|
||||
*/
|
||||
injectMultipleSchemas(schemas: object[]): void {
|
||||
// Clear existing schema scripts
|
||||
this.removeAllSchemas();
|
||||
if (!this.isBrowser) return;
|
||||
|
||||
// Add new schema scripts using Renderer2
|
||||
// Remove existing schema scripts
|
||||
this.clearStructuredData();
|
||||
|
||||
// Add new schema scripts
|
||||
schemas.forEach(schema => {
|
||||
const script = this.renderer.createElement('script');
|
||||
this.renderer.setAttribute(script, 'type', 'application/ld+json');
|
||||
this.renderer.setAttribute(script, 'data-schema', 'true');
|
||||
|
||||
const schemaText = this.renderer.createText(JSON.stringify(schema));
|
||||
this.renderer.appendChild(script, schemaText);
|
||||
this.renderer.appendChild(this.document.head, script);
|
||||
const script = document.createElement('script');
|
||||
script.type = 'application/ld+json';
|
||||
script.text = JSON.stringify(schema);
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
import {
|
||||
faArrowRight,
|
||||
faEnvelope,
|
||||
faLock,
|
||||
faTimes,
|
||||
faTrash,
|
||||
faUserGear,
|
||||
faUserPlus
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
export const APP_ICONS = {
|
||||
faArrowRight,
|
||||
faEnvelope,
|
||||
faLock,
|
||||
faTimes,
|
||||
faTrash,
|
||||
faUserGear,
|
||||
faUserPlus,
|
||||
};
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
import { environment } from '../../environments/environment';
|
||||
|
||||
// Lightweight logger implementation for both dev and production
|
||||
// Avoids dynamic require() which causes build issues
|
||||
const createLoggerImpl = (name: string) => ({
|
||||
info: (...args: any[]) => {
|
||||
if (!environment.production) {
|
||||
console.log(`[${name}]`, ...args);
|
||||
}
|
||||
},
|
||||
warn: (...args: any[]) => console.warn(`[${name}]`, ...args),
|
||||
error: (...args: any[]) => console.error(`[${name}]`, ...args),
|
||||
debug: (...args: any[]) => {
|
||||
if (!environment.production) {
|
||||
console.debug(`[${name}]`, ...args);
|
||||
}
|
||||
},
|
||||
trace: (...args: any[]) => {
|
||||
if (!environment.production) {
|
||||
console.trace(`[${name}]`, ...args);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const createLogger = createLoggerImpl;
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { Router } from '@angular/router';
|
||||
import { createLogger as _createLogger } from './logger';
|
||||
import { ConsoleFormattedStream, INFO, createLogger as _createLogger, stdSerializers } from 'browser-bunyan';
|
||||
import { jwtDecode } from 'jwt-decode';
|
||||
import onChange from 'on-change';
|
||||
import { SortByOptions, User } from '../../../../bizmatch-server/src/models/db.model';
|
||||
|
|
@ -141,8 +141,14 @@ export function createMailInfo(user?: User): MailInfo {
|
|||
listing: null,
|
||||
};
|
||||
}
|
||||
export function createLogger(name: string, level?: number, options?: any) {
|
||||
return _createLogger(name);
|
||||
export function createLogger(name: string, level: number = INFO, options: any = {}) {
|
||||
return _createLogger({
|
||||
name,
|
||||
streams: [{ level, stream: new ConsoleFormattedStream() }],
|
||||
serializers: stdSerializers,
|
||||
src: true,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
export function formatPhoneNumber(phone: string): string {
|
||||
const cleaned = ('' + phone).replace(/\D/g, '');
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 221 KiB |
|
After Width: | Height: | Size: 198 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 8.7 MiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 662 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 662 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 667 B |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
|
@ -1,6 +1,6 @@
|
|||
// Build information, automatically generated by `the_build_script` :zwinkern:
|
||||
const build = {
|
||||
timestamp: "GER: 06.02.2026 12:50 | TX: 02/06/2026 5:50 AM"
|
||||
timestamp: "GER: 03.02.2026 12:44 | TX: 02/03/2026 5:44 AM"
|
||||
};
|
||||
|
||||
export default build;
|
||||
|
|
@ -1,20 +1,8 @@
|
|||
// SSR-safe: check if window exists
|
||||
// Im Browser nehmen wir den aktuellen Host (z.B. localhost).
|
||||
// Auf dem Server (SSR in Docker) nehmen wir 'bizmatch-app' (der Name des Backend-Containers).
|
||||
const isBrowser = typeof window !== 'undefined' && window.navigator.userAgent !== 'node';
|
||||
const hostname = isBrowser ? window.location.hostname : 'bizmatch-app';
|
||||
// Im Server-Modus nutzen wir den internen Docker-Namen
|
||||
const internalUrl = 'http://bizmatch-app:3001';
|
||||
// Im Browser-Modus die öffentliche URL
|
||||
const publicUrl = 'https://api.bizmatch.net';
|
||||
const calculatedApiBaseUrl = isBrowser ? publicUrl : internalUrl;
|
||||
// WICHTIG: Port anpassen!
|
||||
// Lokal läuft das Backend auf 3001. Im Docker Container auch auf 3001.
|
||||
// Deine alte Config hatte hier :4200 stehen, das war falsch (das ist das Frontend).
|
||||
// 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',
|
||||
// GETTER FUNCTION für die API URL (besser als statischer String für diesen Fall)
|
||||
apiBaseUrl: calculatedApiBaseUrl,
|
||||
apiBaseUrl: `http://${hostname}:4200`,
|
||||
imageBaseUrl: 'https://dev.bizmatch.net',
|
||||
buildVersion: '<BUILD_VERSION>',
|
||||
mailinfoUrl: 'https://dev.bizmatch.net',
|
||||
|
|
|
|||
|
|
@ -1,15 +1,10 @@
|
|||
import { environment_base } from './environment.base';
|
||||
|
||||
export const environment = { ...environment_base }; // Kopie erstellen
|
||||
|
||||
export const environment = environment_base;
|
||||
environment.production = true;
|
||||
|
||||
// WICHTIG: Diese Zeile auskommentieren, solange du lokal testest!
|
||||
// Sonst greift er immer aufs Internet zu, statt auf deinen lokalen Docker-Container.
|
||||
environment.apiBaseUrl = 'https://api.bizmatch.net';
|
||||
|
||||
environment.mailinfoUrl = 'https://www.bizmatch.net';
|
||||
environment.imageBaseUrl = 'https://www.bizmatch.net';// Ggf. auch auskommentieren, wenn Bilder lokal liegen
|
||||
environment.imageBaseUrl = 'https://api.bizmatch.net';
|
||||
|
||||
environment.POSTHOG_KEY = 'phc_eUIcIq0UPVzEDtZLy78klKhGudyagBz3goDlKx8SQFe';
|
||||
environment.POSTHOG_HOST = 'https://eu.i.posthog.com';
|
||||
|
|
@ -35,9 +35,6 @@
|
|||
|
||||
<!-- Preload critical assets -->
|
||||
<link rel="preload" as="image" href="/assets/images/header-logo.png" type="image/png" />
|
||||
<!-- Hero background is LCP element - preload with high priority -->
|
||||
<link rel="preload" as="image" href="/assets/images/flags_bg.avif" type="image/avif" fetchpriority="high" />
|
||||
<link rel="preload" as="image" href="/assets/images/flags_bg.jpg" imagesrcset="/assets/images/flags_bg.jpg" type="image/jpeg" />
|
||||
|
||||
<!-- Prefetch common assets -->
|
||||
<link rel="prefetch" as="image" href="/assets/images/business_logo.png" />
|
||||
|
|
@ -61,44 +58,6 @@
|
|||
<link rel="icon" href="/assets/cropped-Favicon-32x32.png" sizes="32x32" />
|
||||
<link rel="icon" href="/assets/cropped-Favicon-192x192.png" sizes="192x192" />
|
||||
<link rel="apple-touch-icon" href="/assets/cropped-Favicon-180x180.png" />
|
||||
|
||||
<!-- Schema.org Structured Data -->
|
||||
<!-- Note: Organization and WebSite schemas are now injected dynamically by SeoService -->
|
||||
<!-- with more complete data (telephone, foundingDate, knowsAbout, dual search actions) -->
|
||||
|
||||
<!-- LocalBusiness Schema for local visibility -->
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "LocalBusiness",
|
||||
"@id": "https://www.bizmatch.net/#localbusiness",
|
||||
"name": "BizMatch",
|
||||
"description": "Business brokerage and commercial real estate marketplace connecting buyers and sellers across the United States.",
|
||||
"url": "https://www.bizmatch.net",
|
||||
"logo": "https://www.bizmatch.net/assets/images/bizmatch-logo.png",
|
||||
"image": "https://www.bizmatch.net/assets/images/bizmatch-logo.png",
|
||||
"priceRange": "$$",
|
||||
"address": {
|
||||
"@type": "PostalAddress",
|
||||
"streetAddress": "1001 Blucher Street",
|
||||
"addressLocality": "Corpus Christi",
|
||||
"addressRegion": "TX",
|
||||
"postalCode": "78401",
|
||||
"addressCountry": "US"
|
||||
},
|
||||
"geo": {
|
||||
"@type": "GeoCoordinates",
|
||||
"latitude": "27.7876",
|
||||
"longitude": "-97.3940"
|
||||
},
|
||||
"areaServed": {
|
||||
"@type": "Country",
|
||||
"name": "United States"
|
||||
},
|
||||
"serviceType": ["Business Brokerage", "Commercial Real Estate", "Business For Sale Listings"],
|
||||
"knowsAbout": ["Business Sales", "Commercial Properties", "Franchise Opportunities", "Business Valuation"]
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body class="flex flex-col min-h-screen">
|
||||
|
|
|
|||
|
|
@ -129,8 +129,13 @@ Disallow: /
|
|||
# ===========================================
|
||||
# Sitemap locations
|
||||
# ===========================================
|
||||
# Main sitemap index
|
||||
Sitemap: https://www.bizmatch.net/sitemap.xml
|
||||
# Main sitemap index (dynamically generated, contains all sub-sitemaps)
|
||||
Sitemap: https://www.bizmatch.net/bizmatch/sitemap.xml
|
||||
|
||||
# Individual sitemaps (auto-listed in sitemap index)
|
||||
# - https://www.bizmatch.net/bizmatch/sitemap/static.xml
|
||||
# - https://www.bizmatch.net/bizmatch/sitemap/business-1.xml
|
||||
# - https://www.bizmatch.net/bizmatch/sitemap/commercial-1.xml
|
||||
|
||||
# ===========================================
|
||||
# Host directive (for Yandex)
|
||||
|
|
|
|||
|
|
@ -5,8 +5,7 @@
|
|||
@tailwind utilities;
|
||||
|
||||
// External CSS imports - these URL imports don't trigger deprecation warnings
|
||||
// Using css2 API with specific weights for better performance
|
||||
@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;500;600;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css?family=Open+Sans&display=swap');
|
||||
@import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css');
|
||||
|
||||
// Local CSS files loaded as CSS (not SCSS) to avoid @import deprecation
|
||||
|
|
@ -122,7 +121,7 @@ p-menubarsub ul {
|
|||
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
color: #757575 !important; /* 4.54:1 contrast - WCAG AA compliant */
|
||||
color: #999 !important;
|
||||
}
|
||||
|
||||
/* Fix für Marker-Icons in Leaflet */
|
||||
|
|
|
|||
|
|
@ -1,62 +0,0 @@
|
|||
services:
|
||||
# --- FRONTEND ---
|
||||
bizmatch-ssr:
|
||||
build:
|
||||
context: . # Pfad zum Angular Ordner
|
||||
dockerfile: bizmatch/Dockerfile
|
||||
image: bizmatch-ssr
|
||||
container_name: bizmatch-ssr
|
||||
extra_hosts:
|
||||
- "localhost:host-gateway"
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- '4200:4000' # Extern 4200 -> Intern 4000 (SSR)
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
volumes:
|
||||
- ./bizmatch-server/pictures:/app/pictures
|
||||
|
||||
# --- BACKEND ---
|
||||
app:
|
||||
build:
|
||||
context: ./bizmatch-server # Pfad zum NestJS Ordner
|
||||
dockerfile: Dockerfile
|
||||
image: bizmatch-server:latest
|
||||
container_name: bizmatch-app
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- '3001:3001'
|
||||
env_file:
|
||||
- ./bizmatch-server/.env # Pfad zur .env Datei
|
||||
depends_on:
|
||||
- postgres
|
||||
networks:
|
||||
- bizmatch
|
||||
# WICHTIG: Kein Volume Mapping für node_modules im Prod-Modus!
|
||||
# Das Image bringt alles fertig mit.
|
||||
|
||||
# --- DATABASE ---
|
||||
postgres:
|
||||
container_name: bizmatchdb
|
||||
image: postgres:17-alpine
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- bizmatch-db-data:/var/lib/postgresql/data
|
||||
env_file:
|
||||
- ./bizmatch-server/.env
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
ports:
|
||||
- '5434:5432'
|
||||
networks:
|
||||
- bizmatch
|
||||
|
||||
volumes:
|
||||
bizmatch-db-data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
bizmatch:
|
||||
external: false # Oder true, falls du es manuell erstellt hast
|
||||
21
update.sh
|
|
@ -1,21 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
echo "🚀 Starte Update Prozess..."
|
||||
|
||||
# 1. Neuesten Code holen
|
||||
echo "📥 Git Pull..."
|
||||
git pull
|
||||
|
||||
# 2. Docker Images neu bauen und Container neu starten
|
||||
# --build: Zwingt Docker, die Images neu zu bauen (nutzt Multi-Stage)
|
||||
# -d: Detached mode (im Hintergrund)
|
||||
# --remove-orphans: Räumt alte Container auf, falls Services umbenannt wurden
|
||||
echo "🐳 Baue und starte Container..."
|
||||
docker compose up -d --build --remove-orphans
|
||||
|
||||
# 3. Aufräumen (Optional)
|
||||
# Löscht alte Images ("dangling images"), die beim Build übrig geblieben sind, um Platz zu sparen
|
||||
echo "🧹 Räume alte Images auf..."
|
||||
docker image prune -f
|
||||
|
||||
echo "✅ Fertig! Die App läuft in der neuesten Version."
|
||||