Compare commits

...

14 Commits

Author SHA1 Message Date
Andreas Knuth 79098f59c6 asdas 2026-02-06 19:25:51 -06:00
Andreas Knuth 345761da87 remove quill 2026-02-06 19:25:45 -06:00
Andreas Knuth 7e00b4d71b xcvxcv 2026-02-06 19:22:14 -06:00
Andreas Knuth 715220f6d5 quill activated 2026-02-06 19:18:26 -06:00
Andreas Knuth dc79ac3df7 comment quill 2026-02-06 19:13:16 -06:00
Andreas Knuth a545b84f6c variable 2026-02-06 18:57:41 -06:00
Andreas Knuth 2d293d8b12 fix 2026-02-06 15:06:54 -06:00
Andreas Knuth d008b50892 changes 2026-02-06 14:46:12 -06:00
Andreas Knuth 1a1eaa46ae add css 2026-02-06 13:31:04 -06:00
Andreas Knuth a9dcb66e5b hashing 2026-02-06 13:25:51 -06:00
Andreas Knuth 33ea71dc12 update 2026-02-06 12:28:30 -06:00
Andreas Knuth 91bcf3c2ed test 2026-02-06 12:11:38 -06:00
Andreas Knuth 36ef7eb4bf access to whole repo 2026-02-06 11:32:35 -06:00
Andreas Knuth ae12eb87f0 neuer build 2026-02-06 11:21:39 -06:00
19 changed files with 239 additions and 144 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
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"]

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

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

View File

@ -53,7 +53,10 @@
],
"styles": [
"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": {
@ -98,7 +101,8 @@
],
"optimization": true,
"extractLicenses": false,
"sourceMap": true
"sourceMap": true,
"outputHashing": "all"
}
},
"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

@ -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';
@ -20,7 +24,8 @@ import { createLogger } from './utils/utils';
const logger = createLogger('ApplicationConfig');
export const appConfig: ApplicationConfig = {
providers: [
provideClientHydration(),
// Temporarily disabled for SSR debugging
// provideClientHydration(),
provideHttpClient(withInterceptorsFromDi()),
{
provide: APP_INITIALIZER,
@ -43,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,
@ -50,6 +62,13 @@ export const appConfig: ApplicationConfig = {
disableImageSizeWarning: true,
},
},
provideShareButtonsOptions(
shareIcons(),
withConfig({
debug: true,
sharerMethod: SharerMethods.Anchor,
}),
),
provideRouter(
routes,
withEnabledBlockingInitialNavigation(),
@ -60,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()),
],

View File

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

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 { 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;

View File

@ -15,7 +15,7 @@
<!-- 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 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>

View File

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

View File

@ -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 }> = [];

View File

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

View File

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

View File

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

View File

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

View File

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

62
docker-compose.yml Normal file
View File

@ -0,0 +1,62 @@
services:
# --- FRONTEND ---
bizmatch-ssr:
build:
context: . # Pfad zum Angular Ordner
dockerfile: bizmatch/Dockerfile
image: bizmatch-ssr
container_name: bizmatch-ssr
extra_hosts:
- "localhost:host-gateway"
restart: unless-stopped
ports:
- '4200:4000' # Extern 4200 -> Intern 4000 (SSR)
environment:
NODE_ENV: production
volumes:
- ./bizmatch-server/pictures:/app/pictures
# --- BACKEND ---
app:
build:
context: ./bizmatch-server # Pfad zum NestJS Ordner
dockerfile: Dockerfile
image: bizmatch-server:latest
container_name: bizmatch-app
restart: unless-stopped
ports:
- '3001:3001'
env_file:
- ./bizmatch-server/.env # Pfad zur .env Datei
depends_on:
- postgres
networks:
- bizmatch
# WICHTIG: Kein Volume Mapping für node_modules im Prod-Modus!
# Das Image bringt alles fertig mit.
# --- DATABASE ---
postgres:
container_name: bizmatchdb
image: postgres:17-alpine
restart: unless-stopped
volumes:
- bizmatch-db-data:/var/lib/postgresql/data
env_file:
- ./bizmatch-server/.env
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
ports:
- '5434:5432'
networks:
- bizmatch
volumes:
bizmatch-db-data:
driver: local
networks:
bizmatch:
external: false # Oder true, falls du es manuell erstellt hast

21
update.sh 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."