Compare commits
17 Commits
00597a796a
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| e62263622f | |||
| 79098f59c6 | |||
| 345761da87 | |||
| 7e00b4d71b | |||
| 715220f6d5 | |||
| dc79ac3df7 | |||
| a545b84f6c | |||
| 2d293d8b12 | |||
| d008b50892 | |||
| 1a1eaa46ae | |||
| a9dcb66e5b | |||
| 33ea71dc12 | |||
| 91bcf3c2ed | |||
| 36ef7eb4bf | |||
| ae12eb87f0 | |||
| 0b4e4207d1 | |||
| 53537226cd |
6
.dockerignore
Normal file
6
.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
.git
|
||||
.idea
|
||||
.vscode
|
||||
dist
|
||||
coverage
|
||||
@@ -1,19 +1,25 @@
|
||||
# Build Stage
|
||||
FROM node:18-alpine AS build
|
||||
|
||||
# --- STAGE 1: Build ---
|
||||
FROM node:22-alpine AS builder
|
||||
WORKDIR /app
|
||||
# HIER KEIN NODE_ENV=production setzen! Wir brauchen devDependencies zum Bauen.
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# Runtime Stage
|
||||
FROM node:18-alpine
|
||||
|
||||
# --- STAGE 2: Runtime ---
|
||||
FROM node:22-alpine
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/dist /app/dist
|
||||
COPY --from=build /app/package*.json /app/
|
||||
|
||||
RUN npm install --production
|
||||
# HIER ist es richtig!
|
||||
ENV NODE_ENV=production
|
||||
|
||||
CMD ["node", "dist/main.js"]
|
||||
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"]
|
||||
@@ -1,48 +0,0 @@
|
||||
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
|
||||
@@ -1,13 +1,41 @@
|
||||
# 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
|
||||
|
||||
# GANZEN dist-Ordner kopieren, nicht nur bizmatch
|
||||
COPY dist ./dist
|
||||
COPY package*.json ./
|
||||
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/
|
||||
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
EXPOSE 4200
|
||||
EXPOSE 4000
|
||||
|
||||
CMD ["node", "dist/bizmatch/server/server.mjs"]
|
||||
|
||||
@@ -101,7 +101,8 @@
|
||||
],
|
||||
"optimization": true,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true
|
||||
"sourceMap": true,
|
||||
"outputHashing": "all"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
services:
|
||||
bizmatch-ssr:
|
||||
build: .
|
||||
image: bizmatch-ssr
|
||||
container_name: bizmatch-ssr
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- '4200:4200'
|
||||
environment:
|
||||
NODE_ENV: DEVELOPMENT
|
||||
@@ -104,6 +104,8 @@ 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) {
|
||||
|
||||
@@ -7,6 +7,10 @@ import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@a
|
||||
import { initializeApp, provideFirebaseApp } from '@angular/fire/app';
|
||||
import { getAuth, provideAuth } from '@angular/fire/auth';
|
||||
import { provideAnimations } from '@angular/platform-browser/animations';
|
||||
import { GALLERY_CONFIG, GalleryConfig } from 'ng-gallery';
|
||||
import { provideQuillConfig } from 'ngx-quill';
|
||||
import { provideShareButtonsOptions, SharerMethods, withConfig } from 'ngx-sharebuttons';
|
||||
import { shareIcons } from 'ngx-sharebuttons/icons';
|
||||
import { environment } from '../environments/environment';
|
||||
import { routes } from './app.routes';
|
||||
import { AuthInterceptor } from './interceptors/auth.interceptor';
|
||||
@@ -44,6 +48,13 @@ export const appConfig: ApplicationConfig = {
|
||||
provide: 'TIMEOUT_DURATION',
|
||||
useValue: 5000, // Standard-Timeout von 5 Sekunden
|
||||
},
|
||||
{
|
||||
provide: GALLERY_CONFIG,
|
||||
useValue: {
|
||||
autoHeight: true,
|
||||
imageSize: 'cover',
|
||||
} as GalleryConfig,
|
||||
},
|
||||
{ provide: ErrorHandler, useClass: GlobalErrorHandler }, // Registriere den globalen ErrorHandler
|
||||
{
|
||||
provide: IMAGE_CONFIG,
|
||||
@@ -51,6 +62,13 @@ export const appConfig: ApplicationConfig = {
|
||||
disableImageSizeWarning: true,
|
||||
},
|
||||
},
|
||||
provideShareButtonsOptions(
|
||||
shareIcons(),
|
||||
withConfig({
|
||||
debug: true,
|
||||
sharerMethod: SharerMethods.Anchor,
|
||||
}),
|
||||
),
|
||||
provideRouter(
|
||||
routes,
|
||||
withEnabledBlockingInitialNavigation(),
|
||||
@@ -61,6 +79,18 @@ export const appConfig: ApplicationConfig = {
|
||||
),
|
||||
...(environment.production ? [POSTHOG_INIT_PROVIDER] : []),
|
||||
provideAnimations(),
|
||||
provideQuillConfig({
|
||||
modules: {
|
||||
syntax: true,
|
||||
toolbar: [
|
||||
['bold', 'italic', 'underline'], // Einige Standardoptionen
|
||||
[{ header: [1, 2, 3, false] }], // Benutzerdefinierte Header
|
||||
[{ list: 'ordered' }, { list: 'bullet' }],
|
||||
[{ color: [] }], // Dropdown mit Standardfarben
|
||||
['clean'], // Entfernt Formatierungen
|
||||
],
|
||||
},
|
||||
}),
|
||||
provideFirebaseApp(() => initializeApp(environment.firebaseConfig)),
|
||||
provideAuth(() => getAuth()),
|
||||
],
|
||||
|
||||
@@ -1,8 +1,24 @@
|
||||
import { RenderMode, ServerRoute } from '@angular/ssr';
|
||||
|
||||
export const serverRoutes: ServerRoute[] = [
|
||||
{
|
||||
path: '**',
|
||||
renderMode: RenderMode.Server
|
||||
}
|
||||
{ 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 }
|
||||
];
|
||||
|
||||
@@ -18,8 +18,7 @@
|
||||
|
||||
<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">BizMatch, Inc., 1001 Blucher Street, Corpus</p>
|
||||
<p class="text-sm text-neutral-600">Christi, Texas 78401</p>
|
||||
<p class="text-sm text-neutral-600 mb-1 lg:mb-2">Contact: BizMatch, Inc.</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-4 lg:mb-0 flex flex-col items-center lg:items-end">
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
@if(isFilterUrl()){
|
||||
|
||||
<div class="relative">
|
||||
<button type="button" id="sortDropdownButton"
|
||||
<button type="button" id="sortDropdownButton" aria-label="Sort listings" aria-haspopup="listbox"
|
||||
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' }">
|
||||
@@ -201,7 +201,7 @@
|
||||
</div>
|
||||
<!-- Mobile filter button -->
|
||||
<div class="md:hidden flex justify-center pb-4">
|
||||
<button (click)="toggleSortDropdown()" type="button" id="sortDropdownMobileButton"
|
||||
<button (click)="toggleSortDropdown()" type="button" id="sortDropdownMobileButton" aria-label="Sort listings" aria-haspopup="listbox"
|
||||
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) }}
|
||||
|
||||
@@ -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>
|
||||
<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>
|
||||
<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>
|
||||
<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">×</button>
|
||||
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>
|
||||
</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">×</button>
|
||||
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>
|
||||
</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">×</button>
|
||||
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>
|
||||
</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">×</button>
|
||||
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>
|
||||
</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">×</button>
|
||||
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>
|
||||
</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">×</button>
|
||||
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>
|
||||
</span>
|
||||
</div>
|
||||
@if(criteria.criteriaType==='brokerListings') {
|
||||
@@ -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>
|
||||
<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>
|
||||
<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>
|
||||
<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,22 +150,22 @@
|
||||
<!-- 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">×</button>
|
||||
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>
|
||||
</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">×</button>
|
||||
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>
|
||||
</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">×</button>
|
||||
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>
|
||||
</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">×</button>
|
||||
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>
|
||||
</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">×</button>
|
||||
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>
|
||||
</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">×</button>
|
||||
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>
|
||||
</span>
|
||||
</div>
|
||||
@if(criteria.criteriaType==='brokerListings') {
|
||||
|
||||
@@ -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>
|
||||
<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>
|
||||
<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>
|
||||
<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">×</button>
|
||||
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>
|
||||
</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">×</button>
|
||||
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>
|
||||
</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">×</button>
|
||||
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>
|
||||
</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">×</button>
|
||||
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>
|
||||
</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">×</button>
|
||||
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>
|
||||
</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">×</button>
|
||||
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>
|
||||
</span>
|
||||
</div>
|
||||
@if(criteria.criteriaType==='commercialPropertyListings') {
|
||||
@@ -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>
|
||||
<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>
|
||||
<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>
|
||||
<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,22 +146,22 @@
|
||||
<!-- 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">×</button>
|
||||
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>
|
||||
</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">×</button>
|
||||
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>
|
||||
</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">×</button>
|
||||
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>
|
||||
</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">×</button>
|
||||
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>
|
||||
</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">×</button>
|
||||
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>
|
||||
</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">×</button>
|
||||
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>
|
||||
</span>
|
||||
</div>
|
||||
@if(criteria.criteriaType==='commercialPropertyListings') {
|
||||
|
||||
@@ -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>
|
||||
<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>
|
||||
<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>
|
||||
<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,37 +25,37 @@
|
||||
<!-- 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">×</button>
|
||||
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>
|
||||
</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">×</button>
|
||||
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>
|
||||
</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">×</button>
|
||||
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>
|
||||
</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')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
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>
|
||||
</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')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
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>
|
||||
</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">×</button>
|
||||
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>
|
||||
</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">×</button>
|
||||
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>
|
||||
</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')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
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>
|
||||
</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')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
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>
|
||||
</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')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
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>
|
||||
</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">×</button>
|
||||
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>
|
||||
</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-6">
|
||||
@@ -148,6 +148,7 @@
|
||||
[multiple]="true"
|
||||
[closeOnSelect]="true"
|
||||
placeholder="Select categories"
|
||||
aria-label="Category"
|
||||
></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
@@ -160,6 +161,7 @@
|
||||
[ngModel]="selectedPropertyType"
|
||||
(ngModelChange)="onPropertyTypeChange($event)"
|
||||
placeholder="Select property type"
|
||||
aria-label="Type of Property"
|
||||
></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
@@ -220,7 +222,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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
@@ -229,37 +231,37 @@
|
||||
<!-- 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">×</button>
|
||||
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>
|
||||
</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">×</button>
|
||||
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>
|
||||
</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">×</button>
|
||||
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>
|
||||
</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')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
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>
|
||||
</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')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
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>
|
||||
</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">×</button>
|
||||
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>
|
||||
</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">×</button>
|
||||
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>
|
||||
</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')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
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>
|
||||
</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')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
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>
|
||||
</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')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
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>
|
||||
</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">×</button>
|
||||
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>
|
||||
</span>
|
||||
</div>
|
||||
@if(criteria.criteriaType==='businessListings') {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, forwardRef } from '@angular/core';
|
||||
import { Component, forwardRef, ViewEncapsulation } 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,9 +9,11 @@ import { ValidationMessagesService } from '../validation-messages.service';
|
||||
@Component({
|
||||
selector: 'app-validated-quill',
|
||||
templateUrl: './validated-quill.component.html',
|
||||
styles: `quill-editor {
|
||||
styleUrls: ['../../../../node_modules/quill/dist/quill.snow.css'],
|
||||
styles: [`quill-editor {
|
||||
width: 100%;
|
||||
}`,
|
||||
}`],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, QuillModule, TooltipComponent],
|
||||
providers: [
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Injectable, PLATFORM_ID, Inject } from '@angular/core';
|
||||
import { CanActivate, Router } from '@angular/router';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
import { createLogger } from '../utils/utils';
|
||||
const logger = createLogger('AuthGuard');
|
||||
import { isPlatformBrowser } from '@angular/common';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AuthGuard implements CanActivate {
|
||||
constructor(private authService: AuthService, private router: Router) {}
|
||||
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private router: Router,
|
||||
@Inject(PLATFORM_ID) private platformId: Object
|
||||
) {}
|
||||
|
||||
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()"
|
||||
class="absolute top-4 right-4 bg-red-500 text-white rounded-full w-8 h-8 flex items-center justify-center hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-opacity-50 print:hidden">
|
||||
<button (click)="historyService.goBack()" 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">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
@if(listing){
|
||||
|
||||
@@ -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()"
|
||||
class="print:hidden absolute top-4 right-4 bg-red-500 text-white rounded-full w-8 h-8 flex items-center justify-center hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-opacity-50">
|
||||
<button (click)="historyService.goBack()" 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">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
<div class="flex flex-col lg:flex-row">
|
||||
|
||||
@@ -42,8 +42,8 @@
|
||||
}
|
||||
<!-- <img src="https://placehold.co/45x60" class="w-11 h-14" /> -->
|
||||
</div>
|
||||
<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">
|
||||
<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">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -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)"
|
||||
[title]="isFavorite(user) ? 'Remove from favorites' : 'Save to favorites'"
|
||||
[attr.aria-label]="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"
|
||||
title="Share professional" (click)="shareProfessional($event, user)">
|
||||
aria-label="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 United States. Browse
|
||||
<p class="text-lg text-neutral-600">Discover profitable business opportunities across the State of Texas. Browse
|
||||
verified listings from business owners and brokers.</p>
|
||||
<div class="mt-4 text-base text-neutral-700 max-w-4xl">
|
||||
<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>
|
||||
<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>
|
||||
</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)"
|
||||
[title]="isFavorite(listing) ? 'Remove from favorites' : 'Save to favorites'"
|
||||
[attr.aria-label]="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"
|
||||
title="Share listing" (click)="shareListing($event, listing)">
|
||||
aria-label="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">Find office buildings, retail spaces, warehouses, and industrial properties across the United States. Investment opportunities from verified sellers and commercial real estate brokers.</p>
|
||||
<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>
|
||||
<div class="mt-4 text-base text-neutral-700 max-w-4xl">
|
||||
<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>
|
||||
<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>
|
||||
</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)"
|
||||
[title]="isFavorite(listing) ? 'Remove from favorites' : 'Save to favorites'"
|
||||
[attr.aria-label]="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"
|
||||
title="Share property" (click)="shareProperty($event, listing)">
|
||||
aria-label="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" />
|
||||
<div
|
||||
class="absolute top-[-0.5rem] right-[0rem] bg-white rounded-full p-1 drop-shadow-custom-bg hover:cursor-pointer"
|
||||
<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"
|
||||
(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>
|
||||
</div>
|
||||
</button>
|
||||
} @else {
|
||||
<img src="/assets/images/placeholder.png" class="max-w-full max-h-full" />
|
||||
<img src="/assets/images/placeholder.png" class="max-w-full max-h-full" alt="" width="80" height="80" />
|
||||
}
|
||||
</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" />
|
||||
<div
|
||||
class="absolute top-[-0.5rem] right-[0rem] bg-white rounded-full p-1 drop-shadow-custom-bg hover:cursor-pointer"
|
||||
<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"
|
||||
(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>
|
||||
</div>
|
||||
</button>
|
||||
} @else {
|
||||
<img src="/assets/images/placeholder.png" class="max-w-full max-h-full" />
|
||||
<img src="/assets/images/placeholder.png" class="max-w-full max-h-full" alt="" width="80" height="80" />
|
||||
}
|
||||
</div>
|
||||
<button type="button"
|
||||
@@ -129,23 +129,16 @@
|
||||
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>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ChangeDetectorRef, Component } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { APP_ICONS } from '../../../utils/fontawesome-icons';
|
||||
import { NgSelectModule } from '@ng-select/ng-select';
|
||||
import { QuillModule, provideQuillConfig } from 'ngx-quill';
|
||||
import { QuillModule } from 'ngx-quill';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
import { User } from '../../../../../../bizmatch-server/src/models/db.model';
|
||||
import { AutoCompleteCompleteEvent, Invoice, UploadParams, ValidationMessage, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
|
||||
@@ -47,19 +47,7 @@ import { TOOLBAR_OPTIONS } from '../../utils/defaults';
|
||||
],
|
||||
providers: [
|
||||
TitleCasePipe,
|
||||
DatePipe,
|
||||
provideQuillConfig({
|
||||
modules: {
|
||||
syntax: true,
|
||||
toolbar: [
|
||||
['bold', 'italic', 'underline'],
|
||||
[{ header: [1, 2, 3, false] }],
|
||||
[{ list: 'ordered' }, { list: 'bullet' }],
|
||||
[{ color: [] }],
|
||||
['clean'],
|
||||
],
|
||||
},
|
||||
}) as any,
|
||||
DatePipe
|
||||
],
|
||||
templateUrl: './account.component.html',
|
||||
styleUrls: [
|
||||
@@ -77,9 +65,6 @@ export class AccountComponent {
|
||||
editorModules = TOOLBAR_OPTIONS;
|
||||
env = environment;
|
||||
faTrash = APP_ICONS.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 }> = [];
|
||||
|
||||
@@ -11,7 +11,6 @@ import { QuillModule } from 'ngx-quill';
|
||||
|
||||
import { NgSelectModule } from '@ng-select/ng-select';
|
||||
import { NgxCurrencyDirective } from 'ngx-currency';
|
||||
import { provideQuillConfig } from 'ngx-quill';
|
||||
import { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
|
||||
import { AutoCompleteCompleteEvent, ImageProperty, createDefaultBusinessListing, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
|
||||
|
||||
@@ -48,20 +47,6 @@ import { TOOLBAR_OPTIONS } from '../../utils/defaults';
|
||||
ValidatedTextareaComponent,
|
||||
ValidatedLocationComponent,
|
||||
],
|
||||
providers: [
|
||||
provideQuillConfig({
|
||||
modules: {
|
||||
syntax: true,
|
||||
toolbar: [
|
||||
['bold', 'italic', 'underline'],
|
||||
[{ header: [1, 2, 3, false] }],
|
||||
[{ list: 'ordered' }, { list: 'bullet' }],
|
||||
[{ color: [] }],
|
||||
['clean'],
|
||||
],
|
||||
},
|
||||
}) as any,
|
||||
],
|
||||
templateUrl: './edit-business-listing.component.html',
|
||||
styleUrls: [
|
||||
'./edit-business-listing.component.scss',
|
||||
|
||||
@@ -11,7 +11,7 @@ import { APP_ICONS } from '../../../utils/fontawesome-icons';
|
||||
import { NgSelectModule } from '@ng-select/ng-select';
|
||||
import { NgxCurrencyDirective } from 'ngx-currency';
|
||||
import { ImageCropperComponent } from 'ngx-image-cropper';
|
||||
import { QuillModule, provideQuillConfig } from 'ngx-quill';
|
||||
import { QuillModule } from 'ngx-quill';
|
||||
import { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
|
||||
import { AutoCompleteCompleteEvent, ImageProperty, UploadParams, createDefaultCommercialPropertyListing, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
|
||||
|
||||
@@ -53,20 +53,6 @@ import { TOOLBAR_OPTIONS } from '../../utils/defaults';
|
||||
ValidatedLocationComponent,
|
||||
ImageCropAndUploadComponent,
|
||||
],
|
||||
providers: [
|
||||
provideQuillConfig({
|
||||
modules: {
|
||||
syntax: true,
|
||||
toolbar: [
|
||||
['bold', 'italic', 'underline'],
|
||||
[{ header: [1, 2, 3, false] }],
|
||||
[{ list: 'ordered' }, { list: 'bullet' }],
|
||||
[{ color: [] }],
|
||||
['clean'],
|
||||
],
|
||||
},
|
||||
}) as any,
|
||||
],
|
||||
templateUrl: './edit-commercial-property-listing.component.html',
|
||||
styleUrls: [
|
||||
'./edit-commercial-property-listing.component.scss',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Build information, automatically generated by `the_build_script` :zwinkern:
|
||||
const build = {
|
||||
timestamp: "GER: 05.02.2026 13:06 | TX: 02/05/2026 6:06 AM"
|
||||
timestamp: "GER: 06.02.2026 12:50 | TX: 02/06/2026 5:50 AM"
|
||||
};
|
||||
|
||||
export default build;
|
||||
@@ -1,8 +1,20 @@
|
||||
// SSR-safe: check if window exists (it doesn't on server-side)
|
||||
const hostname = typeof window !== 'undefined' ? window.location.hostname : 'localhost';
|
||||
// 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).
|
||||
export const environment_base = {
|
||||
// apiBaseUrl: 'http://localhost:3000',
|
||||
apiBaseUrl: `http://${hostname}:4200`,
|
||||
// GETTER FUNCTION für die API URL (besser als statischer String für diesen Fall)
|
||||
apiBaseUrl: calculatedApiBaseUrl,
|
||||
imageBaseUrl: 'https://dev.bizmatch.net',
|
||||
buildVersion: '<BUILD_VERSION>',
|
||||
mailinfoUrl: 'https://dev.bizmatch.net',
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { environment_base } from './environment.base';
|
||||
|
||||
export const environment = environment_base;
|
||||
export const environment = { ...environment_base }; // Kopie erstellen
|
||||
|
||||
environment.production = true;
|
||||
environment.apiBaseUrl = 'https://api.bizmatch.net';
|
||||
|
||||
// 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://api.bizmatch.net';
|
||||
environment.imageBaseUrl = 'https://www.bizmatch.net';// Ggf. auch auskommentieren, wenn Bilder lokal liegen
|
||||
|
||||
environment.POSTHOG_KEY = 'phc_eUIcIq0UPVzEDtZLy78klKhGudyagBz3goDlKx8SQFe';
|
||||
environment.POSTHOG_HOST = 'https://eu.i.posthog.com';
|
||||
environment.POSTHOG_HOST = 'https://eu.i.posthog.com';
|
||||
@@ -62,15 +62,22 @@
|
||||
<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 (Static) -->
|
||||
<!-- 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": "Organization",
|
||||
"@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",
|
||||
"description": "Buy and sell businesses, commercial properties, and franchises. Browse thousands of verified listings across the United States.",
|
||||
"image": "https://www.bizmatch.net/assets/images/bizmatch-logo.png",
|
||||
"priceRange": "$$",
|
||||
"address": {
|
||||
"@type": "PostalAddress",
|
||||
"streetAddress": "1001 Blucher Street",
|
||||
@@ -79,33 +86,17 @@
|
||||
"postalCode": "78401",
|
||||
"addressCountry": "US"
|
||||
},
|
||||
"contactPoint": {
|
||||
"@type": "ContactPoint",
|
||||
"contactType": "Customer Service",
|
||||
"email": "info@bizmatch.net"
|
||||
"geo": {
|
||||
"@type": "GeoCoordinates",
|
||||
"latitude": "27.7876",
|
||||
"longitude": "-97.3940"
|
||||
},
|
||||
"sameAs": [
|
||||
"https://www.facebook.com/bizmatch",
|
||||
"https://www.linkedin.com/company/bizmatch",
|
||||
"https://twitter.com/bizmatch"
|
||||
]
|
||||
}
|
||||
</script>
|
||||
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebSite",
|
||||
"name": "BizMatch",
|
||||
"url": "https://www.bizmatch.net",
|
||||
"potentialAction": {
|
||||
"@type": "SearchAction",
|
||||
"target": {
|
||||
"@type": "EntryPoint",
|
||||
"urlTemplate": "https://www.bizmatch.net/businessListings?search={search_term_string}"
|
||||
},
|
||||
"query-input": "required name=search_term_string"
|
||||
}
|
||||
"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>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
// 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://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
|
||||
// Note: These are loaded via angular.json styles array is the preferred approach,
|
||||
|
||||
53
docker-compose.yml
Normal file
53
docker-compose.yml
Normal file
@@ -0,0 +1,53 @@
|
||||
services:
|
||||
# --- FRONTEND (SSR) ---
|
||||
bizmatch-ssr:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: bizmatch/Dockerfile
|
||||
container_name: bizmatch-ssr
|
||||
restart: unless-stopped
|
||||
# In der Produktion brauchen wir keine Ports nach außen (Caddy regelt das intern)
|
||||
networks:
|
||||
- bizmatch
|
||||
volumes:
|
||||
- ./bizmatch-server/pictures:/app/pictures
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
# WICHTIG: Die URL, unter der das SSR-Frontend die API intern erreicht
|
||||
API_INTERNAL_URL: http://bizmatch-app:3001
|
||||
|
||||
# --- BACKEND ---
|
||||
app:
|
||||
build:
|
||||
context: ./bizmatch-server
|
||||
dockerfile: Dockerfile
|
||||
container_name: bizmatch-app
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- ./bizmatch-server/.env
|
||||
depends_on:
|
||||
- postgres
|
||||
networks:
|
||||
- bizmatch
|
||||
volumes:
|
||||
# Das Backend braucht ebenfalls Zugriff auf den Bilder-Ordner zum Speichern!
|
||||
- ./bizmatch-server/pictures:/app/dist/pictures
|
||||
|
||||
# --- 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
|
||||
networks:
|
||||
- bizmatch
|
||||
|
||||
volumes:
|
||||
bizmatch-db-data:
|
||||
|
||||
networks:
|
||||
bizmatch:
|
||||
external: true # Wir nutzen das gleiche Netzwerk wie für Caddy
|
||||
21
update.sh
Executable file
21
update.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/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."
|
||||
Reference in New Issue
Block a user