21 Commits

Author SHA1 Message Date
e62263622f move + fix 2026-04-11 15:15:59 -05:00
79098f59c6 asdas 2026-02-06 19:25:51 -06:00
345761da87 remove quill 2026-02-06 19:25:45 -06:00
7e00b4d71b xcvxcv 2026-02-06 19:22:14 -06:00
715220f6d5 quill activated 2026-02-06 19:18:26 -06:00
dc79ac3df7 comment quill 2026-02-06 19:13:16 -06:00
a545b84f6c variable 2026-02-06 18:57:41 -06:00
2d293d8b12 fix 2026-02-06 15:06:54 -06:00
d008b50892 changes 2026-02-06 14:46:12 -06:00
1a1eaa46ae add css 2026-02-06 13:31:04 -06:00
a9dcb66e5b hashing 2026-02-06 13:25:51 -06:00
33ea71dc12 update 2026-02-06 12:28:30 -06:00
91bcf3c2ed test 2026-02-06 12:11:38 -06:00
36ef7eb4bf access to whole repo 2026-02-06 11:32:35 -06:00
ae12eb87f0 neuer build 2026-02-06 11:21:39 -06:00
0b4e4207d1 change sentences 2026-02-06 10:10:18 -06:00
53537226cd SEO 2026-02-06 12:59:47 +01:00
00597a796a app.use('/pictures', express.static('pictures')); 2026-02-05 17:54:49 -06:00
8b3c79b5ff budgets 2026-02-05 17:27:57 -06:00
a7d3d2d958 revert 2026-02-05 17:24:12 -06:00
49528a5c37 revert 2026-02-05 17:18:28 -06:00
35 changed files with 353 additions and 264 deletions

6
.dockerignore Normal file
View File

@@ -0,0 +1,6 @@
node_modules
.git
.idea
.vscode
dist
coverage

View File

@@ -1,19 +1,25 @@
# Build Stage # --- STAGE 1: Build ---
FROM node:18-alpine AS build FROM node:22-alpine AS builder
WORKDIR /app WORKDIR /app
# HIER KEIN NODE_ENV=production setzen! Wir brauchen devDependencies zum Bauen.
COPY package*.json ./ COPY package*.json ./
RUN npm install RUN npm ci
COPY . . COPY . .
RUN npm run build RUN npm run build
# Runtime Stage # --- STAGE 2: Runtime ---
FROM node:18-alpine FROM node:22-alpine
WORKDIR /app 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"]

View File

@@ -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

View File

@@ -14,7 +14,7 @@ async function bootstrap() {
app.useLogger(logger); app.useLogger(logger);
//app.use('/bizmatch/payment/webhook', bodyParser.raw({ type: 'application/json' })); //app.use('/bizmatch/payment/webhook', bodyParser.raw({ type: 'application/json' }));
// Serve static files from pictures directory // Serve static files from pictures directory
// app.use('/pictures', express.static('pictures')); app.use('/pictures', express.static('pictures'));
app.setGlobalPrefix('bizmatch'); app.setGlobalPrefix('bizmatch');

View File

@@ -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 FROM node:22-alpine
WORKDIR /app WORKDIR /app
# GANZEN dist-Ordner kopieren, nicht nur bizmatch ENV NODE_ENV=production
COPY dist ./dist ENV PORT=4000
COPY package*.json ./
# 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 RUN npm ci --omit=dev
EXPOSE 4200 EXPOSE 4000
CMD ["node", "dist/bizmatch/server/server.mjs"] CMD ["node", "dist/bizmatch/server/server.mjs"]

View File

@@ -21,6 +21,11 @@
"outputPath": "dist/bizmatch", "outputPath": "dist/bizmatch",
"index": "src/index.html", "index": "src/index.html",
"browser": "src/main.ts", "browser": "src/main.ts",
"server": "src/main.server.ts",
"prerender": false,
"ssr": {
"entry": "server.ts"
},
"allowedCommonJsDependencies": [ "allowedCommonJsDependencies": [
"quill-delta", "quill-delta",
"leaflet", "leaflet",
@@ -48,7 +53,10 @@
], ],
"styles": [ "styles": [
"src/styles.scss", "src/styles.scss",
"src/styles/lazy-load.css" "src/styles/lazy-load.css",
"node_modules/quill/dist/quill.snow.css",
"node_modules/leaflet/dist/leaflet.css",
"node_modules/ngx-sharebuttons/themes/default.scss"
] ]
}, },
"configurations": { "configurations": {
@@ -61,7 +69,7 @@
}, },
{ {
"type": "anyComponentStyle", "type": "anyComponentStyle",
"maximumWarning": "10kb", "maximumWarning": "30kb",
"maximumError": "30kb" "maximumError": "30kb"
} }
], ],
@@ -70,7 +78,8 @@
"development": { "development": {
"optimization": false, "optimization": false,
"extractLicenses": false, "extractLicenses": false,
"sourceMap": true "sourceMap": true,
"ssr": false
}, },
"dev": { "dev": {
"fileReplacements": [ "fileReplacements": [
@@ -92,7 +101,8 @@
], ],
"optimization": true, "optimization": true,
"extractLicenses": false, "extractLicenses": false,
"sourceMap": true "sourceMap": true,
"outputHashing": "all"
} }
}, },
"defaultConfiguration": "production" "defaultConfiguration": "production"

View File

@@ -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

View File

@@ -104,6 +104,8 @@ export async function app(): Promise<express.Express> {
// All regular routes use the Angular engine // All regular routes use the Angular engine
server.get('*', async (req, res, next) => { server.get('*', async (req, res, next) => {
console.log(`[SSR] Handling request: ${req.method} ${req.url}`); 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 { try {
const response = await angularApp.handle(req); const response = await angularApp.handle(req);
if (response) { if (response) {

View File

@@ -7,6 +7,10 @@ import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@a
import { initializeApp, provideFirebaseApp } from '@angular/fire/app'; import { initializeApp, provideFirebaseApp } from '@angular/fire/app';
import { getAuth, provideAuth } from '@angular/fire/auth'; import { getAuth, provideAuth } from '@angular/fire/auth';
import { provideAnimations } from '@angular/platform-browser/animations'; 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 { environment } from '../environments/environment';
import { routes } from './app.routes'; import { routes } from './app.routes';
import { AuthInterceptor } from './interceptors/auth.interceptor'; import { AuthInterceptor } from './interceptors/auth.interceptor';
@@ -44,6 +48,13 @@ export const appConfig: ApplicationConfig = {
provide: 'TIMEOUT_DURATION', provide: 'TIMEOUT_DURATION',
useValue: 5000, // Standard-Timeout von 5 Sekunden 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: ErrorHandler, useClass: GlobalErrorHandler }, // Registriere den globalen ErrorHandler
{ {
provide: IMAGE_CONFIG, provide: IMAGE_CONFIG,
@@ -51,6 +62,13 @@ export const appConfig: ApplicationConfig = {
disableImageSizeWarning: true, disableImageSizeWarning: true,
}, },
}, },
provideShareButtonsOptions(
shareIcons(),
withConfig({
debug: true,
sharerMethod: SharerMethods.Anchor,
}),
),
provideRouter( provideRouter(
routes, routes,
withEnabledBlockingInitialNavigation(), withEnabledBlockingInitialNavigation(),
@@ -61,6 +79,18 @@ export const appConfig: ApplicationConfig = {
), ),
...(environment.production ? [POSTHOG_INIT_PROVIDER] : []), ...(environment.production ? [POSTHOG_INIT_PROVIDER] : []),
provideAnimations(), 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)), provideFirebaseApp(() => initializeApp(environment.firebaseConfig)),
provideAuth(() => getAuth()), provideAuth(() => getAuth()),
], ],

View File

@@ -1,8 +1,24 @@
import { RenderMode, ServerRoute } from '@angular/ssr'; import { RenderMode, ServerRoute } from '@angular/ssr';
export const serverRoutes: ServerRoute[] = [ export const serverRoutes: ServerRoute[] = [
{ { path: 'home', renderMode: RenderMode.Server }, // Das hatten wir vorhin gefixt
path: '**',
renderMode: RenderMode.Server // 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 }
]; ];

View File

@@ -18,8 +18,7 @@
<div class="flex flex-col lg:flex-row items-center order-2 lg:order-3"> <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"> <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 mb-1 lg:mb-2">Contact: BizMatch, Inc.</p>
<p class="text-sm text-neutral-600">Christi, Texas 78401</p>
</div> </div>
<div class="mb-4 lg:mb-0 flex flex-col items-center lg:items-end"> <div class="mb-4 lg:mb-0 flex flex-col items-center lg:items-end">

View File

@@ -9,7 +9,7 @@
@if(isFilterUrl()){ @if(isFilterUrl()){
<div class="relative"> <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" 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()" (click)="toggleSortDropdown()"
[ngClass]="{ 'text-primary-500': selectOptions.getSortByOption(sortBy) !== 'Sort', 'text-neutral-900': selectOptions.getSortByOption(sortBy) === 'Sort' }"> [ngClass]="{ 'text-primary-500': selectOptions.getSortByOption(sortBy) !== 'Sort', 'text-neutral-900': selectOptions.getSortByOption(sortBy) === 'Sort' }">
@@ -201,7 +201,7 @@
</div> </div>
<!-- Mobile filter button --> <!-- Mobile filter button -->
<div class="md:hidden flex justify-center pb-4"> <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" class="mx-4 w-1/2 px-4 py-2 text-sm font-medium bg-white border border-neutral-200 rounded-lg hover:bg-neutral-100 hover:text-primary-600 focus:ring-2 focus:ring-primary-600 focus:text-primary-600 dark:bg-neutral-800 dark:text-neutral-400 dark:border-neutral-600 dark:hover:text-white dark:hover:bg-neutral-700"
[ngClass]="{ 'text-primary-500': selectOptions.getSortByOption(sortBy) !== 'Sort', 'text-neutral-900': selectOptions.getSortByOption(sortBy) === 'Sort' }"> [ngClass]="{ 'text-primary-500': selectOptions.getSortByOption(sortBy) !== 'Sort', 'text-neutral-900': selectOptions.getSortByOption(sortBy) === 'Sort' }">
<i class="fas fa-sort mr-2"></i>{{ selectOptions.getSortByOption(sortBy) }} <i class="fas fa-sort mr-2"></i>{{ selectOptions.getSortByOption(sortBy) }}

View File

@@ -16,7 +16,7 @@
<div class="p-6 space-y-6"> <div class="p-6 space-y-6">
<div class="flex space-x-4 mb-4"> <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 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"> <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 Clear all Filter
<div class="tooltip-arrow" data-popper-arrow></div> <div class="tooltip-arrow" data-popper-arrow></div>
@@ -25,22 +25,22 @@
<!-- Display active filters as tags --> <!-- Display active filters as tags -->
<div class="flex flex-wrap gap-2 mb-4" *ngIf="hasActiveFilters()"> <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"> <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>
<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"> <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>
<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"> <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>
<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"> <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>
<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"> <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>
<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"> <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> </span>
</div> </div>
@if(criteria.criteriaType==='brokerListings') { @if(criteria.criteriaType==='brokerListings') {
@@ -141,7 +141,7 @@
<div *ngIf="!isModal" class="space-y-6 pb-10"> <div *ngIf="!isModal" class="space-y-6 pb-10">
<div class="flex space-x-4 mb-4"> <div class="flex space-x-4 mb-4">
<h3 class="text-xl font-semibold text-neutral-900">Filter ({{ numberOfResults$ | async }})</h3> <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"> <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 Clear all Filter
<div class="tooltip-arrow" data-popper-arrow></div> <div class="tooltip-arrow" data-popper-arrow></div>
@@ -150,22 +150,22 @@
<!-- Display active filters as tags --> <!-- Display active filters as tags -->
<div class="flex flex-wrap gap-2" *ngIf="hasActiveFilters()"> <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"> <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>
<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"> <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>
<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"> <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>
<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"> <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>
<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"> <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>
<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"> <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> </span>
</div> </div>
@if(criteria.criteriaType==='brokerListings') { @if(criteria.criteriaType==='brokerListings') {

View File

@@ -16,7 +16,7 @@
<div class="p-6 space-y-6"> <div class="p-6 space-y-6">
<div class="flex space-x-4 mb-4"> <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 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"> <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 Clear all Filter
<div class="tooltip-arrow" data-popper-arrow></div> <div class="tooltip-arrow" data-popper-arrow></div>
@@ -25,22 +25,22 @@
<!-- Display active filters as tags --> <!-- Display active filters as tags -->
<div class="flex flex-wrap gap-2 mb-4" *ngIf="hasActiveFilters()"> <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"> <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>
<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"> <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>
<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"> <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>
<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"> <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>
<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"> <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>
<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"> <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> </span>
</div> </div>
@if(criteria.criteriaType==='commercialPropertyListings') { @if(criteria.criteriaType==='commercialPropertyListings') {
@@ -137,7 +137,7 @@
<div *ngIf="!isModal" class="space-y-6 pb-10"> <div *ngIf="!isModal" class="space-y-6 pb-10">
<div class="flex space-x-4 mb-4"> <div class="flex space-x-4 mb-4">
<h3 class="text-xl font-semibold text-neutral-900">Filter ({{ numberOfResults$ | async }})</h3> <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"> <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 Clear all Filter
<div class="tooltip-arrow" data-popper-arrow></div> <div class="tooltip-arrow" data-popper-arrow></div>
@@ -146,22 +146,22 @@
<!-- Display active filters as tags --> <!-- Display active filters as tags -->
<div class="flex flex-wrap gap-2" *ngIf="hasActiveFilters()"> <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"> <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>
<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"> <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>
<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"> <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>
<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"> <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>
<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"> <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>
<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"> <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> </span>
</div> </div>
@if(criteria.criteriaType==='commercialPropertyListings') { @if(criteria.criteriaType==='commercialPropertyListings') {

View File

@@ -16,7 +16,7 @@
<div class="p-6 space-y-6"> <div class="p-6 space-y-6">
<div class="flex space-x-4 mb-4"> <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 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"> <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 Clear all Filter
<div class="tooltip-arrow" data-popper-arrow></div> <div class="tooltip-arrow" data-popper-arrow></div>
@@ -25,37 +25,37 @@
<!-- Display active filters as tags --> <!-- Display active filters as tags -->
<div class="flex flex-wrap gap-2 mb-4" *ngIf="hasActiveFilters()"> <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"> <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>
<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"> <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>
<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"> <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>
<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"> <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>
<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"> <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>
<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"> <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>
<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"> <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>
<span *ngIf="selectedPropertyType" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center"> <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>
<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"> <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>
<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"> <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>
<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"> <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> </span>
</div> </div>
<div class="grid grid-cols-1 gap-6"> <div class="grid grid-cols-1 gap-6">
@@ -148,6 +148,7 @@
[multiple]="true" [multiple]="true"
[closeOnSelect]="true" [closeOnSelect]="true"
placeholder="Select categories" placeholder="Select categories"
aria-label="Category"
></ng-select> ></ng-select>
</div> </div>
<div> <div>
@@ -160,6 +161,7 @@
[ngModel]="selectedPropertyType" [ngModel]="selectedPropertyType"
(ngModelChange)="onPropertyTypeChange($event)" (ngModelChange)="onPropertyTypeChange($event)"
placeholder="Select property type" placeholder="Select property type"
aria-label="Type of Property"
></ng-select> ></ng-select>
</div> </div>
<div> <div>
@@ -220,7 +222,7 @@
<div *ngIf="!isModal" class="space-y-6"> <div *ngIf="!isModal" class="space-y-6">
<div class="flex space-x-4 mb-4"> <div class="flex space-x-4 mb-4">
<h3 class="text-xl font-semibold text-neutral-900">Filter ({{ numberOfResults$ | async }})</h3> <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"> <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 Clear all Filter
<div class="tooltip-arrow" data-popper-arrow></div> <div class="tooltip-arrow" data-popper-arrow></div>
@@ -229,37 +231,37 @@
<!-- Display active filters as tags --> <!-- Display active filters as tags -->
<div class="flex flex-wrap gap-2" *ngIf="hasActiveFilters()"> <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"> <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>
<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"> <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>
<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"> <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>
<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"> <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>
<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"> <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>
<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"> <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>
<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"> <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>
<span *ngIf="selectedPropertyType" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center"> <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>
<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"> <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>
<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"> <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>
<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"> <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> </span>
</div> </div>
@if(criteria.criteriaType==='businessListings') { @if(criteria.criteriaType==='businessListings') {

View File

@@ -1,5 +1,5 @@
import { CommonModule } from '@angular/common'; 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 { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
import { QuillModule } from 'ngx-quill'; import { QuillModule } from 'ngx-quill';
import { BaseInputComponent } from '../base-input/base-input.component'; import { BaseInputComponent } from '../base-input/base-input.component';
@@ -9,9 +9,11 @@ import { ValidationMessagesService } from '../validation-messages.service';
@Component({ @Component({
selector: 'app-validated-quill', selector: 'app-validated-quill',
templateUrl: './validated-quill.component.html', templateUrl: './validated-quill.component.html',
styles: `quill-editor { styleUrls: ['../../../../node_modules/quill/dist/quill.snow.css'],
styles: [`quill-editor {
width: 100%; width: 100%;
}`, }`],
encapsulation: ViewEncapsulation.None,
standalone: true, standalone: true,
imports: [CommonModule, FormsModule, QuillModule, TooltipComponent], imports: [CommonModule, FormsModule, QuillModule, TooltipComponent],
providers: [ providers: [

View File

@@ -1,15 +1,27 @@
import { Injectable } from '@angular/core'; import { Injectable, PLATFORM_ID, Inject } from '@angular/core';
import { CanActivate, Router } from '@angular/router'; import { CanActivate, Router } from '@angular/router';
import { AuthService } from '../services/auth.service'; import { AuthService } from '../services/auth.service';
import { createLogger } from '../utils/utils'; import { isPlatformBrowser } from '@angular/common';
const logger = createLogger('AuthGuard');
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class AuthGuard implements CanActivate { 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> { 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(); const token = await this.authService.getToken();
if (token) { if (token) {
return true; return true;

View File

@@ -5,8 +5,8 @@
} }
<div class="bg-white rounded-lg drop-shadow-custom-bg-mobile md:drop-shadow-custom-bg overflow-hidden relative"> <div class="bg-white rounded-lg drop-shadow-custom-bg-mobile md:drop-shadow-custom-bg overflow-hidden relative">
<button (click)="historyService.goBack()" <button (click)="historyService.goBack()" aria-label="Go back"
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"> 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> <i class="fas fa-times"></i>
</button> </button>
@if(listing){ @if(listing){

View File

@@ -6,8 +6,8 @@
@if(listing){ @if(listing){
<div class="p-6 relative"> <div class="p-6 relative">
<h1 class="text-3xl font-bold mb-4 break-words">{{ listing?.title }}</h1> <h1 class="text-3xl font-bold mb-4 break-words">{{ listing?.title }}</h1>
<button (click)="historyService.goBack()" <button (click)="historyService.goBack()" aria-label="Go back"
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"> 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> <i class="fas fa-times"></i>
</button> </button>
<div class="flex flex-col lg:flex-row"> <div class="flex flex-col lg:flex-row">

View File

@@ -42,8 +42,8 @@
} }
<!-- <img src="https://placehold.co/45x60" class="w-11 h-14" /> --> <!-- <img src="https://placehold.co/45x60" class="w-11 h-14" /> -->
</div> </div>
<button (click)="historyService.goBack()" <button (click)="historyService.goBack()" aria-label="Go back"
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"> 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> <i class="fas fa-times"></i>
</button> </button>
</div> </div>

View File

@@ -13,14 +13,14 @@
</div> </div>
<!-- SEO-optimized heading --> <!-- 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> <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 <p class="text-lg text-neutral-600">Connect with licensed business brokers, CPAs, attorneys, and other
professionals across the United States.</p> professionals across the United States.</p>
<div class="mt-4 text-base text-neutral-700 max-w-4xl"> <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> <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> </div> -->
<!-- Mobile Filter Button --> <!-- Mobile Filter Button -->
<div class="md:hidden mb-4"> <div class="md:hidden mb-4">
@@ -44,14 +44,14 @@
@if(currentUser) { @if(currentUser) {
<button type="button" class="bg-white rounded-full p-2 shadow-lg transition-colors" <button type="button" class="bg-white rounded-full p-2 shadow-lg transition-colors"
[class.bg-red-50]="isFavorite(user)" [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)"> (click)="toggleFavorite($event, user)">
<i <i
[class]="isFavorite(user) ? 'fas fa-heart text-red-500' : 'far fa-heart text-red-500 hover:scale-110 transition-transform'"></i> [class]="isFavorite(user) ? 'fas fa-heart text-red-500' : 'far fa-heart text-red-500 hover:scale-110 transition-transform'"></i>
</button> </button>
} }
<button type="button" class="bg-white rounded-full p-2 shadow-lg hover:bg-blue-50 transition-colors" <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> <i class="fas fa-share-alt text-blue-500 hover:scale-110 transition-transform"></i>
</button> </button>
</div> </div>

View File

@@ -15,10 +15,10 @@
<!-- SEO-optimized heading --> <!-- 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">Businesses for Sale - Find Your Next Business Opportunity</h1> <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> verified listings from business owners and brokers.</p>
<div class="mt-4 text-base text-neutral-700 max-w-4xl"> <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>
</div> </div>
@@ -68,14 +68,14 @@
@if(user) { @if(user) {
<button class="bg-white rounded-full p-2 shadow-lg transition-colors" <button class="bg-white rounded-full p-2 shadow-lg transition-colors"
[class.bg-red-50]="isFavorite(listing)" [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)"> (click)="toggleFavorite($event, listing)">
<i <i
[class]="isFavorite(listing) ? 'fas fa-heart text-red-500' : 'far fa-heart text-red-500 hover:scale-110 transition-transform'"></i> [class]="isFavorite(listing) ? 'fas fa-heart text-red-500' : 'far fa-heart text-red-500 hover:scale-110 transition-transform'"></i>
</button> </button>
} }
<button type="button" class="bg-white rounded-full p-2 shadow-lg hover:bg-blue-50 transition-colors" <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> <i class="fas fa-share-alt text-blue-500 hover:scale-110 transition-transform"></i>
</button> </button>
</div> </div>

View File

@@ -15,9 +15,9 @@
<!-- SEO-optimized heading --> <!-- 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">Commercial Properties for Sale</h1> <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"> <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>
</div> </div>
@@ -33,13 +33,13 @@
class="bg-white rounded-full p-2 shadow-lg transition-colors" class="bg-white rounded-full p-2 shadow-lg transition-colors"
[class.bg-red-50]="isFavorite(listing)" [class.bg-red-50]="isFavorite(listing)"
[class.opacity-100]="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)"> (click)="toggleFavorite($event, listing)">
<i [class]="isFavorite(listing) ? 'fas fa-heart text-red-500' : 'far fa-heart text-red-500 hover:scale-110 transition-transform'"></i> <i [class]="isFavorite(listing) ? 'fas fa-heart text-red-500' : 'far fa-heart text-red-500 hover:scale-110 transition-transform'"></i>
</button> </button>
} }
<button type="button" class="bg-white rounded-full p-2 shadow-lg hover:bg-blue-50 transition-colors" <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> <i class="fas fa-share-alt text-blue-500 hover:scale-110 transition-transform"></i>
</button> </button>
</div> </div>

View File

@@ -18,16 +18,16 @@
<div class="w-20 h-20 w-full rounded-md flex items-center justify-center relative"> <div class="w-20 h-20 w-full rounded-md flex items-center justify-center relative">
@if(user?.hasCompanyLogo){ @if(user?.hasCompanyLogo){
<img src="{{ companyLogoUrl }}" alt="Company logo" class="max-w-full max-h-full" /> <img src="{{ companyLogoUrl }}" alt="Company logo" class="max-w-full max-h-full" />
<div <button type="button" aria-label="Delete company logo"
class="absolute top-[-0.5rem] right-[0rem] bg-white rounded-full p-1 drop-shadow-custom-bg hover:cursor-pointer" 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')"> (click)="deleteConfirm('logo')">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" <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"> class="w-4 h-4 text-gray-600">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg> </svg>
</div> </button>
} @else { } @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> </div>
<button type="button" <button type="button"
@@ -41,16 +41,16 @@
<div class="w-20 h-20 w-full rounded-md flex items-center justify-center relative"> <div class="w-20 h-20 w-full rounded-md flex items-center justify-center relative">
@if(user?.hasProfile){ @if(user?.hasProfile){
<img src="{{ profileUrl }}" alt="Profile picture" class="max-w-full max-h-full" /> <img src="{{ profileUrl }}" alt="Profile picture" class="max-w-full max-h-full" />
<div <button type="button" aria-label="Delete profile picture"
class="absolute top-[-0.5rem] right-[0rem] bg-white rounded-full p-1 drop-shadow-custom-bg hover:cursor-pointer" 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')"> (click)="deleteConfirm('profile')">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" <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"> class="w-4 h-4 text-gray-600">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg> </svg>
</div> </button>
} @else { } @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> </div>
<button type="button" <button type="button"
@@ -129,23 +129,16 @@
mask="(000) 000-0000"></app-validated-input> mask="(000) 000-0000"></app-validated-input>
<app-validated-input label="Company Website" name="companyWebsite" <app-validated-input label="Company Website" name="companyWebsite"
[(ngModel)]="user.companyWebsite"></app-validated-input> [(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" <app-validated-location label="Company Location" name="location"
[(ngModel)]="user.location"></app-validated-location> [(ngModel)]="user.location"></app-validated-location>
</div> </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> <div>
<app-validated-quill label="Company Overview" name="companyOverview" <app-validated-quill label="Company Overview" name="companyOverview"
[(ngModel)]="user.companyOverview"></app-validated-quill> [(ngModel)]="user.companyOverview"></app-validated-quill>
</div> </div>
<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" <app-validated-quill label="Services We Offer" name="offeredServices"
[(ngModel)]="user.offeredServices"></app-validated-quill> [(ngModel)]="user.offeredServices"></app-validated-quill>
</div> </div>

View File

@@ -3,7 +3,7 @@ import { ChangeDetectorRef, Component } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { APP_ICONS } from '../../../utils/fontawesome-icons'; import { APP_ICONS } from '../../../utils/fontawesome-icons';
import { NgSelectModule } from '@ng-select/ng-select'; import { NgSelectModule } from '@ng-select/ng-select';
import { QuillModule, provideQuillConfig } from 'ngx-quill'; import { QuillModule } from 'ngx-quill';
import { lastValueFrom } from 'rxjs'; import { lastValueFrom } from 'rxjs';
import { User } from '../../../../../../bizmatch-server/src/models/db.model'; import { User } from '../../../../../../bizmatch-server/src/models/db.model';
import { AutoCompleteCompleteEvent, Invoice, UploadParams, ValidationMessage, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.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: [ providers: [
TitleCasePipe, TitleCasePipe,
DatePipe, DatePipe
provideQuillConfig({
modules: {
syntax: true,
toolbar: [
['bold', 'italic', 'underline'],
[{ header: [1, 2, 3, false] }],
[{ list: 'ordered' }, { list: 'bullet' }],
[{ color: [] }],
['clean'],
],
},
}) as any,
], ],
templateUrl: './account.component.html', templateUrl: './account.component.html',
styleUrls: [ styleUrls: [
@@ -77,9 +65,6 @@ export class AccountComponent {
editorModules = TOOLBAR_OPTIONS; editorModules = TOOLBAR_OPTIONS;
env = environment; env = environment;
faTrash = APP_ICONS.faTrash; 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; uploadParams: UploadParams;
validationMessages: ValidationMessage[] = []; validationMessages: ValidationMessage[] = [];
customerTypeOptions: Array<{ value: string; label: string }> = []; customerTypeOptions: Array<{ value: string; label: string }> = [];

View File

@@ -11,7 +11,6 @@ import { QuillModule } from 'ngx-quill';
import { NgSelectModule } from '@ng-select/ng-select'; import { NgSelectModule } from '@ng-select/ng-select';
import { NgxCurrencyDirective } from 'ngx-currency'; import { NgxCurrencyDirective } from 'ngx-currency';
import { provideQuillConfig } from 'ngx-quill';
import { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model'; import { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
import { AutoCompleteCompleteEvent, ImageProperty, createDefaultBusinessListing, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model'; import { AutoCompleteCompleteEvent, ImageProperty, createDefaultBusinessListing, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
@@ -48,20 +47,6 @@ import { TOOLBAR_OPTIONS } from '../../utils/defaults';
ValidatedTextareaComponent, ValidatedTextareaComponent,
ValidatedLocationComponent, 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', templateUrl: './edit-business-listing.component.html',
styleUrls: [ styleUrls: [
'./edit-business-listing.component.scss', './edit-business-listing.component.scss',

View File

@@ -11,7 +11,7 @@ import { APP_ICONS } from '../../../utils/fontawesome-icons';
import { NgSelectModule } from '@ng-select/ng-select'; import { NgSelectModule } from '@ng-select/ng-select';
import { NgxCurrencyDirective } from 'ngx-currency'; import { NgxCurrencyDirective } from 'ngx-currency';
import { ImageCropperComponent } from 'ngx-image-cropper'; 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 { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
import { AutoCompleteCompleteEvent, ImageProperty, UploadParams, createDefaultCommercialPropertyListing, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.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, ValidatedLocationComponent,
ImageCropAndUploadComponent, 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', templateUrl: './edit-commercial-property-listing.component.html',
styleUrls: [ styleUrls: [
'./edit-commercial-property-listing.component.scss', './edit-commercial-property-listing.component.scss',

View File

@@ -1,6 +1,6 @@
// Build information, automatically generated by `the_build_script` :zwinkern: // Build information, automatically generated by `the_build_script` :zwinkern:
const build = { 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; export default build;

View File

@@ -1,8 +1,20 @@
// SSR-safe: check if window exists (it doesn't on server-side) // SSR-safe: check if window exists
const hostname = typeof window !== 'undefined' ? window.location.hostname : 'localhost'; // 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 = { export const environment_base = {
// apiBaseUrl: 'http://localhost:3000', // 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', imageBaseUrl: 'https://dev.bizmatch.net',
buildVersion: '<BUILD_VERSION>', buildVersion: '<BUILD_VERSION>',
mailinfoUrl: 'https://dev.bizmatch.net', mailinfoUrl: 'https://dev.bizmatch.net',

View File

@@ -1,10 +1,15 @@
import { environment_base } from './environment.base'; import { environment_base } from './environment.base';
export const environment = environment_base; export const environment = { ...environment_base }; // Kopie erstellen
environment.production = true; 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.apiBaseUrl = 'https://api.bizmatch.net';
environment.mailinfoUrl = 'https://www.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_KEY = 'phc_eUIcIq0UPVzEDtZLy78klKhGudyagBz3goDlKx8SQFe';
environment.POSTHOG_HOST = 'https://eu.i.posthog.com'; environment.POSTHOG_HOST = 'https://eu.i.posthog.com';

View File

@@ -62,15 +62,22 @@
<link rel="icon" href="/assets/cropped-Favicon-192x192.png" sizes="192x192" /> <link rel="icon" href="/assets/cropped-Favicon-192x192.png" sizes="192x192" />
<link rel="apple-touch-icon" href="/assets/cropped-Favicon-180x180.png" /> <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"> <script type="application/ld+json">
{ {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "Organization", "@type": "LocalBusiness",
"@id": "https://www.bizmatch.net/#localbusiness",
"name": "BizMatch", "name": "BizMatch",
"description": "Business brokerage and commercial real estate marketplace connecting buyers and sellers across the United States.",
"url": "https://www.bizmatch.net", "url": "https://www.bizmatch.net",
"logo": "https://www.bizmatch.net/assets/images/bizmatch-logo.png", "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": { "address": {
"@type": "PostalAddress", "@type": "PostalAddress",
"streetAddress": "1001 Blucher Street", "streetAddress": "1001 Blucher Street",
@@ -79,33 +86,17 @@
"postalCode": "78401", "postalCode": "78401",
"addressCountry": "US" "addressCountry": "US"
}, },
"contactPoint": { "geo": {
"@type": "ContactPoint", "@type": "GeoCoordinates",
"contactType": "Customer Service", "latitude": "27.7876",
"email": "info@bizmatch.net" "longitude": "-97.3940"
}, },
"sameAs": [ "areaServed": {
"https://www.facebook.com/bizmatch", "@type": "Country",
"https://www.linkedin.com/company/bizmatch", "name": "United States"
"https://twitter.com/bizmatch" },
] "serviceType": ["Business Brokerage", "Commercial Real Estate", "Business For Sale Listings"],
} "knowsAbout": ["Business Sales", "Commercial Properties", "Franchise Opportunities", "Business Valuation"]
</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"
}
} }
</script> </script>
</head> </head>

View File

@@ -7,6 +7,7 @@
// External CSS imports - these URL imports don't trigger deprecation warnings // External CSS imports - these URL imports don't trigger deprecation warnings
// Using css2 API with specific weights for better performance // 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/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 // 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, // Note: These are loaded via angular.json styles array is the preferred approach,

View File

@@ -8,7 +8,9 @@
] ]
}, },
"files": [ "files": [
"src/main.ts" "src/main.ts",
"src/main.server.ts",
"server.ts"
], ],
"include": [ "include": [
"src/**/*.d.ts" "src/**/*.d.ts"

53
docker-compose.yml Normal file
View 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
View 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."