This commit is contained in:
Andreas Knuth 2025-11-09 16:19:01 -06:00
parent f973b87a2d
commit 351b560bcc
185 changed files with 0 additions and 9822 deletions

View File

@ -1,17 +0,0 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
ij_typescript_use_double_quotes = false
[*.md]
max_line_length = off
trim_trailing_whitespace = false

View File

@ -1,42 +0,0 @@
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
# System files
.DS_Store
Thumbs.db

View File

@ -1,5 +0,0 @@
{
"plugins": {
"@tailwindcss/postcss": {}
}
}

View File

@ -1,18 +0,0 @@
{
"arrowParens": "avoid",
"embeddedLanguageFormatting": "auto",
"htmlWhitespaceSensitivity": "css",
"insertPragma": false,
"jsxBracketSameLine": false,
"jsxSingleQuote": false,
"printWidth": 250,
"proseWrap": "always",
"quoteProps": "as-needed",
"requirePragma": false,
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all",
"useTabs": false,
"vueIndentScriptAndStyle": false
}

View File

@ -1,4 +0,0 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
"recommendations": ["angular.ng-template"]
}

View File

@ -1,20 +0,0 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "ng serve",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: start",
"url": "http://localhost:4200/"
},
{
"name": "ng test",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: test",
"url": "http://localhost:9876/debug.html"
}
]
}

View File

@ -1,28 +0,0 @@
{
"editor.suggestSelection": "first",
"vsintellicode.modify.editor.suggestSelection": "automaticallyOverrodeDefaultValue",
"explorer.confirmDelete": false,
"typescript.updateImportsOnFileMove.enabled": "always",
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "vscode.json-language-features"
},
"[scss]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
},
"prettier.printWidth": 240,
"git.autofetch": false,
"git.autorefresh": true
}

View File

@ -1,42 +0,0 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "start",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
},
{
"type": "npm",
"script": "test",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
}
]
}

View File

@ -1,59 +0,0 @@
# BizmatchClient
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 19.2.6.
## Development server
To start a local development server, run:
```bash
ng serve
```
Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files.
## Code scaffolding
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
```bash
ng generate component component-name
```
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
```bash
ng generate --help
```
## Building
To build the project run:
```bash
ng build
```
This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed.
## Running unit tests
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
```bash
ng test
```
## Running end-to-end tests
For end-to-end (e2e) testing, run:
```bash
ng e2e
```
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
## Additional Resources
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.

View File

@ -1,133 +0,0 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"bizmatch-client": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss",
"skipTests": true
},
"@schematics/angular:class": {
"skipTests": true
},
"@schematics/angular:directive": {
"skipTests": true
},
"@schematics/angular:guard": {
"skipTests": true
},
"@schematics/angular:interceptor": {
"skipTests": true
},
"@schematics/angular:pipe": {
"skipTests": true
},
"@schematics/angular:resolver": {
"skipTests": true
},
"@schematics/angular:service": {
"skipTests": true
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/bizmatch-client",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": [
"zone.js"
],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
{
"glob": "**/*",
"input": "public"
},
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss",
"node_modules/quill/dist/quill.snow.css"
],
"scripts": [],
"server": "src/main.server.ts",
"prerender": false,
"ssr": false
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kB",
"maximumError": "1MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "4kB",
"maximumError": "8kB"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "bizmatch-client:build:production"
},
"development": {
"buildTarget": "bizmatch-client:build:development"
}
},
"defaultConfiguration": "development",
"options": {
"proxyConfig": "proxy.conf.json"
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"src/styles.scss"
],
"scripts": []
}
}
}
}
}
}

View File

@ -1,67 +0,0 @@
{
"name": "bizmatch-client",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve --host 0.0.0.0 & http-server ../bizmatch-server",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test",
"serve:ssr:bizmatch-client": "node dist/bizmatch-client/server/server.mjs"
},
"private": true,
"dependencies": {
"@angular/animations": "^19.2.5",
"@angular/cdk": "^19.2.8",
"@angular/common": "^19.2.0",
"@angular/compiler": "^19.2.0",
"@angular/core": "^19.2.0",
"@angular/fire": "^19.0.0",
"@angular/forms": "^19.2.0",
"@angular/platform-browser": "^19.2.0",
"@angular/platform-browser-dynamic": "^19.2.0",
"@angular/platform-server": "^19.2.0",
"@angular/router": "^19.2.0",
"@angular/ssr": "^19.2.6",
"@fortawesome/angular-fontawesome": "^1.0.0",
"@fortawesome/fontawesome-free": "^6.7.2",
"@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-brands-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@ng-select/ng-select": "^14.2.6",
"@ngneat/until-destroy": "^10.0.0",
"@tailwindcss/postcss": "^4.1.3",
"browser-bunyan": "^1.8.0",
"dayjs": "^1.11.13",
"express": "^4.18.2",
"jwt-decode": "^4.0.0",
"ngx-currency": "^19.0.0",
"ngx-image-cropper": "^9.1.5",
"ngx-mask": "^19.0.6",
"ngx-quill": "^27.0.1",
"ngx-sharebuttons": "^17.0.0",
"on-change": "^5.0.1",
"postcss": "^8.5.3",
"rxjs": "~7.8.0",
"tailwindcss": "^4.1.3",
"tslib": "^2.3.0",
"urlcat": "^3.1.0",
"zone.js": "~0.15.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^19.2.6",
"@angular/cli": "^19.2.6",
"@angular/compiler-cli": "^19.2.0",
"@types/express": "^4.17.17",
"@types/jasmine": "~5.1.0",
"@types/node": "^18.18.0",
"http-server": "^14.1.1",
"jasmine-core": "~5.6.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.7.2"
}
}

View File

@ -1,28 +0,0 @@
{
"/bizmatch": {
"target": "http://localhost:3000",
"secure": false,
"changeOrigin": true,
"logLevel": "debug"
},
"/pictures": {
"target": "http://localhost:8080",
"secure": false
},
"/ipify": {
"target": "https://api.ipify.org",
"secure": true,
"changeOrigin": true,
"pathRewrite": {
"^/ipify": ""
}
},
"/ipinfo": {
"target": "https://ipinfo.io",
"secure": true,
"changeOrigin": true,
"pathRewrite": {
"^/ipinfo": ""
}
}
}

View File

@ -1,43 +0,0 @@
<!-- <div class="container"> -->
<div class="wrapper" [ngClass]="{ 'print:bg-white': actualRoute !== 'home' }">
@if (actualRoute !=='home' && actualRoute !=='login' &&
actualRoute!=='emailVerification' && actualRoute!=='email-authorized'){
<header></header>
}
<main class="flex-1">
<router-outlet></router-outlet>
</main>
<app-footer></app-footer>
</div>
@if (loadingService.isLoading$ | async) {
<div class="spinner-overlay">
<div class="spinner-container">
@let loadingText = (loadingService.loadingText$ | async); @if(loadingText){
<div class="spinner-text">{{ loadingText }}</div>
}
<div role="status">
<svg
aria-hidden="true"
class="inline w-10 h-10 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
</div>
</div>
</div>
}
<app-message-container></app-message-container>
<app-search-modal></app-search-modal>
<app-confirmation></app-confirmation>
<app-email></app-email>

View File

@ -1,51 +0,0 @@
import { CommonModule } from '@angular/common';
import { Component, HostListener } from '@angular/core';
import { ActivatedRoute, NavigationEnd, Router, RouterOutlet } from '@angular/router';
import { filter } from 'rxjs';
import build from '../build';
import { ConfirmationComponent } from './components/confirmation.component';
import { EMailComponent } from './components/email/email.component';
import { FooterComponent } from './components/footer/footer.component';
import { HeaderComponent } from './components/header/header.component';
import { MessageContainerComponent } from './components/message/message-container.component';
import { SearchModalComponent } from './components/search-modal/search-modal.component';
import { ConfirmationService } from './services/confirmation.service';
import { LoadingService } from './services/loading.service';
import { UserService } from './services/user.service';
@Component({
selector: 'app-root',
imports: [CommonModule, RouterOutlet, HeaderComponent, FooterComponent, MessageContainerComponent, SearchModalComponent, ConfirmationComponent, EMailComponent],
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
})
export class AppComponent {
build = build;
title = 'bizmatch';
actualRoute = '';
public constructor(public loadingService: LoadingService, private router: Router, private activatedRoute: ActivatedRoute, private userService: UserService, private confirmationService: ConfirmationService) {
this.router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe(() => {
let currentRoute = this.activatedRoute.root;
while (currentRoute.children[0] !== undefined) {
currentRoute = currentRoute.children[0];
}
this.actualRoute = currentRoute.snapshot.url[0].path;
});
}
ngOnInit() {}
@HostListener('window:keydown', ['$event'])
handleKeyboardEvent(event: KeyboardEvent) {
if (event.shiftKey && event.ctrlKey && event.key === 'V') {
this.showVersionDialog();
}
}
showVersionDialog() {
this.confirmationService.showConfirmation({
message: `App Version: ${this.build.timestamp}`,
buttons: 'none',
});
}
}

View File

@ -1,11 +0,0 @@
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
import { provideServerRendering } from '@angular/platform-server';
import { appConfig } from './app.config';
const serverConfig: ApplicationConfig = {
providers: [
provideServerRendering(),
]
};
export const config = mergeApplicationConfig(appConfig, serverConfig);

View File

@ -1,52 +0,0 @@
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { APP_INITIALIZER, ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { initializeApp, provideFirebaseApp } from '@angular/fire/app';
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
import { provideRouter, withEnabledBlockingInitialNavigation, withInMemoryScrolling } from '@angular/router';
import { provideShareButtonsOptions } 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';
import { LoadingInterceptor } from './interceptors/loading.interceptor';
import { TimeoutInterceptor } from './interceptors/timeout.interceptor';
import { SelectOptionsService } from './services/select-options.service';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(withInterceptorsFromDi()),
{
provide: APP_INITIALIZER,
useFactory: initServices,
multi: true,
deps: [SelectOptionsService],
},
{
provide: HTTP_INTERCEPTORS,
useClass: LoadingInterceptor,
multi: true,
},
{
provide: HTTP_INTERCEPTORS,
useClass: TimeoutInterceptor,
multi: true,
},
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
provideShareButtonsOptions(shareIcons()),
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(
routes,
withEnabledBlockingInitialNavigation(),
withInMemoryScrolling({
scrollPositionRestoration: 'enabled',
anchorScrolling: 'enabled',
}),
),
provideClientHydration(withEventReplay()),
provideFirebaseApp(() => initializeApp(environment.firebaseConfig)),
],
};
function initServices(selectOptions: SelectOptionsService) {
return async () => {
await selectOptions.init();
};
}

View File

@ -1,93 +0,0 @@
import { Routes } from '@angular/router';
import { AuthGuard } from './guards/auth.guard';
import { AccountComponent } from './pages/account/account.component';
import { BusinessListingsComponent } from './pages/business-listings/business-listings.component';
import { DetailsBusinessListingComponent } from './pages/details-business-listing/details-business-listing.component';
import { EditBusinessListingComponent } from './pages/edit-business-listing/edit-business-listing.component';
import { EmailAuthorizedComponent } from './pages/email-authorized/email-authorized.component';
import { EmailUsComponent } from './pages/email-us/email-us.component';
import { EmailVerificationComponent } from './pages/email-verification/email-verification.component';
import { FavoritesComponent } from './pages/favorites/favorites.component';
import { HomeComponent } from './pages/home/home.component';
import { LoginRegisterComponent } from './pages/login-register/login-register.component';
import { LogoutComponent } from './pages/logout/logout.component';
import { MyListingComponent } from './pages/my-listing/my-listing.component';
import { NotFoundComponent } from './pages/not-found/not-found.component';
import { UserListComponent } from './pages/user-list/user-list.component';
export const routes: Routes = [
{
path: 'businessListings',
component: BusinessListingsComponent,
},
{
path: 'home',
component: HomeComponent,
},
{
path: 'details-business-listing/:id',
component: DetailsBusinessListingComponent,
},
{
path: 'createBusinessListing',
component: EditBusinessListingComponent,
canActivate: [AuthGuard],
},
{
path: 'login/:page',
component: LoginRegisterComponent,
},
{
path: 'login',
component: LoginRegisterComponent,
},
{
path: 'notfound',
component: NotFoundComponent,
},
{
path: 'account',
component: AccountComponent,
canActivate: [AuthGuard],
},
{
path: 'editBusinessListing/:id',
component: EditBusinessListingComponent,
canActivate: [AuthGuard],
},
{
path: 'myListings',
component: MyListingComponent,
canActivate: [AuthGuard],
},
{
path: 'myFavorites',
component: FavoritesComponent,
canActivate: [AuthGuard],
},
{
path: 'emailUs',
component: EmailUsComponent,
// canActivate: [AuthGuard],
},
{
path: 'logout',
component: LogoutComponent,
canActivate: [AuthGuard],
},
{
path: 'emailVerification',
component: EmailVerificationComponent,
},
{
path: 'email-authorized',
component: EmailAuthorizedComponent,
},
{
path: 'admin/users',
component: UserListComponent,
canActivate: [AuthGuard],
},
{ path: '**', redirectTo: 'home' },
];

View File

@ -1,56 +0,0 @@
import { Component, Input } from '@angular/core';
import { ControlValueAccessor } from '@angular/forms';
import { Subscription } from 'rxjs';
import { ValidationMessagesService } from '../validation-messages.service';
@Component({
selector: 'app-base-input',
template: ``,
imports: [],
})
export abstract class BaseInputComponent implements ControlValueAccessor {
@Input() value: any = '';
validationMessage: string = '';
onChange: any = () => {};
onTouched: any = () => {};
subscription: Subscription | null = null;
@Input() label: string = '';
// @Input() id: string = '';
@Input() name: string = '';
isTooltipVisible = false;
constructor(protected validationMessagesService: ValidationMessagesService) {}
ngOnInit() {
this.subscription = this.validationMessagesService.messages$.subscribe(() => {
this.updateValidationMessage();
});
}
ngOnDestroy() {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
writeValue(value: any): void {
if (value !== undefined) {
this.value = value;
}
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
updateValidationMessage(): void {
this.validationMessage = this.validationMessagesService.getMessage(this.name);
}
setDisabledState?(isDisabled: boolean): void {}
toggleTooltip(event: Event) {
event.preventDefault();
event.stopPropagation();
this.isTooltipVisible = !this.isTooltipVisible;
}
}

View File

@ -1,54 +0,0 @@
import { AsyncPipe, NgIf } from '@angular/common';
import { Component } from '@angular/core';
import { ConfirmationService } from '../services/confirmation.service';
@Component({
selector: 'app-confirmation',
imports: [AsyncPipe, NgIf],
template: `
<div *ngIf="confirmationService.modalVisible$ | async" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full flex items-center justify-center">
<div class="relative p-4 w-full max-w-md max-h-full">
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
<button
(click)="confirmationService.reject()"
type="button"
class="absolute top-3 end-2.5 text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white"
>
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
</svg>
<span class="sr-only">Close modal</span>
</button>
<div class="p-4 md:p-5 text-center">
<svg class="mx-auto mb-4 text-gray-400 w-12 h-12 dark:text-gray-200" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 11V6m0 8h.01M19 10a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
@let confirmation = (confirmationService.confirmation$ | async);
<h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">
{{ confirmation?.message }}
</h3>
@if(confirmation?.buttons==='both'){
<button
(click)="confirmationService.accept()"
type="button"
class="text-white bg-red-600 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 dark:focus:ring-red-800 font-medium rounded-lg text-sm inline-flex items-center px-5 py-2.5 text-center mr-2"
>
Yes, I'm sure
</button>
<button
(click)="confirmationService.reject()"
type="button"
class="py-2.5 px-5 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
>
No, cancel
</button>
}
</div>
</div>
</div>
</div>
`,
})
export class ConfirmationComponent {
constructor(public confirmationService: ConfirmationService) {}
}

View File

@ -1,43 +0,0 @@
<!-- Main modal -->
<div *ngIf="eMailService.modalVisible$ | async" id="authentication-modal" tabindex="-1" class="z-40 fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full flex items-center justify-center">
<div class="relative p-4 w-full max-w-md max-h-full">
<!-- Modal content -->
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
<!-- Modal header -->
<div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600">
<h3 class="text-xl font-semibold text-gray-900 dark:text-white">Email listing to a friend</h3>
<button
(click)="eMailService.reject()"
type="button"
class="end-2.5 text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white"
>
<svg class="w-3 h-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
</svg>
<span class="sr-only">Close modal</span>
</button>
</div>
<!-- Modal body -->
<div class="p-4 md:p-5">
<form class="space-y-4" action="#">
<div>
<app-validated-input label="Your Email" name="yourEmail" [(ngModel)]="shareByEMail.yourEmail"></app-validated-input>
</div>
<div>
<app-validated-input label="Your Name" name="yourName" [(ngModel)]="shareByEMail.yourName"></app-validated-input>
</div>
<div>
<app-validated-input label="Your Friend's EMail" name="recipientEmail" [(ngModel)]="shareByEMail.recipientEmail"></app-validated-input>
</div>
<button
(click)="sendMail()"
class="w-full text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
>
Send EMail
</button>
</form>
</div>
</div>
</div>
</div>

View File

@ -1,40 +0,0 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { ShareByEMail } from '../../../../../bizmatch-server/src/models/db.model';
import { MailService } from '../../services/mail.service';
import { ValidatedInputComponent } from '../validated-input/validated-input.component';
import { ValidationMessagesService } from '../validation-messages.service';
import { EMailService } from './email.service';
@UntilDestroy()
@Component({
selector: 'app-email',
standalone: true,
imports: [CommonModule, FormsModule, ValidatedInputComponent],
templateUrl: './email.component.html',
template: ``,
})
export class EMailComponent {
shareByEMail: ShareByEMail = {};
constructor(public eMailService: EMailService, private mailService: MailService, private validationMessagesService: ValidationMessagesService) {}
ngOnInit() {
this.eMailService.shareByEMail$.pipe(untilDestroyed(this)).subscribe(val => {
this.shareByEMail = val;
});
}
async sendMail() {
try {
const result = await this.mailService.mailToFriend(this.shareByEMail);
this.eMailService.accept(this.shareByEMail);
} catch (error) {
if (error.error && Array.isArray(error.error?.message)) {
this.validationMessagesService.updateMessages(error.error.message);
}
}
}
ngOnDestroy() {
this.validationMessagesService.clearMessages(); // Löschen Sie alle bestehenden Validierungsnachrichten
}
}

View File

@ -1,33 +0,0 @@
import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import { ShareByEMail } from '../../../../../bizmatch-server/src/models/db.model';
@Injectable({
providedIn: 'root',
})
export class EMailService {
private modalVisibleSubject = new Subject<boolean>();
private shareByEMailSubject = new Subject<ShareByEMail>();
private resolvePromise!: (value: boolean | ShareByEMail) => void;
modalVisible$: Observable<boolean> = this.modalVisibleSubject.asObservable();
shareByEMail$: Observable<ShareByEMail> = this.shareByEMailSubject.asObservable();
showShareByEMail(shareByEMail: ShareByEMail): Promise<boolean | ShareByEMail> {
this.shareByEMailSubject.next(shareByEMail);
this.modalVisibleSubject.next(true);
return new Promise<boolean | ShareByEMail>(resolve => {
this.resolvePromise = resolve;
});
}
accept(value: ShareByEMail): void {
this.modalVisibleSubject.next(false);
this.resolvePromise(value);
}
reject(): void {
this.modalVisibleSubject.next(false);
this.resolvePromise(false);
}
}

View File

@ -1,547 +0,0 @@
<ng-template #otherRoute>
<footer class="bg-white px-4 py-2 md:px-6 mt-auto w-full print:hidden">
<div class="container mx-auto flex flex-col lg:flex-row justify-between items-center">
<div class="flex flex-col lg:flex-row items-center mb-4 lg:mb-0">
<a routerLink="/home" class="flex items-center space-x-3 rtl:space-x-reverse">
<img src="assets/images/header-logo.png" class="h-8" class="h-8 mb-2 lg:mb-0 lg:mr-4" />
</a>
<p class="text-sm text-gray-600 text-center lg:text-left">© {{ currentYear }} Bizmatch All rights reserved.</p>
</div>
<div class="flex flex-col lg:flex-row items-center order-3 lg:order-2">
<a class="text-sm text-blue-600 hover:underline hover:cursor-pointer mx-2" (click)="toggleTerms()">Terms of use</a>
<a class="text-sm text-blue-600 hover:underline hover:cursor-pointer mx-2" (click)="togglePrivacy()">Privacy statement</a>
</div>
<div class="flex flex-col lg:flex-row items-center order-2 lg:order-3">
<div class="mb-4 lg:mb-0 lg:mr-6 text-center lg:text-right">
<p class="text-sm text-gray-600 mb-1 lg:mb-2">BizMatch, Inc., 1001 Blucher Street, Corpus</p>
<p class="text-sm text-gray-600">Christi, Texas 78401</p>
</div>
<div class="mb-4 lg:mb-0 flex flex-col items-center lg:items-end">
<a class="text-sm text-gray-600 mb-1 lg:mb-2 hover:text-blue-600 w-full"> <i class="fas fa-phone-alt mr-2"></i>1-800-840-6025 </a>
<a class="text-sm text-gray-600 hover:text-blue-600"> <i class="fas fa-envelope mr-2"></i>info&#64;bizmatch.net </a>
</div>
</div>
</div>
</footer>
</ng-template>
<footer *ngIf="isHomeRoute; else otherRoute" class="bg-gray-800 text-white pt-12 pb-4">
<div class="container mx-auto px-6">
<div class="flex flex-wrap">
<div class="w-full md:w-1/3 mb-8 md:mb-0">
<h3 class="text-xl font-semibold mb-4">BizMatch</h3>
<p class="mb-2">Your trusted partner in business brokerage.</p>
<p class="mb-2">TREC License #0516 788</p>
</div>
<div class="w-full md:w-1/3 mb-8 md:mb-0">
<h3 class="text-xl font-semibold mb-4">Quick Links</h3>
<ul>
<li class="mb-2">
<a href="#" class="text-gray-300 hover:text-white">Home</a>
</li>
<li class="mb-2">
<a href="#services" class="text-gray-300 hover:text-white">Services</a>
</li>
<li class="mb-2">
<a href="#location" class="text-gray-300 hover:text-white">Location</a>
</li>
<li class="mb-2">
<a href="#contact" class="text-gray-300 hover:text-white">Contact</a>
</li>
<li class="mb-2">
<a (click)="toggleTerms()" class="text-gray-300 hover:text-white cursor-pointer">Terms of use</a>
</li>
<li class="mb-2">
<a (click)="togglePrivacy()" class="text-gray-300 hover:text-white cursor-pointer">Privacy statement</a>
</li>
</ul>
</div>
<div class="w-full md:w-1/3">
<h3 class="text-xl font-semibold mb-4">Contact Us</h3>
<p class="mb-2">1001 Blucher Street</p>
<p class="mb-2">Corpus Christi, TX 78401</p>
<p class="mb-4">United States</p>
<p class="mb-2">1-800-840-6025</p>
<p class="mb-2">info&#64;bizmatch.net</p>
</div>
</div>
<div class="pt-4 text-center">
<p class="text-sm text-gray-400 mt-4">&copy; 2025 BizMatch. All rights reserved.</p>
</div>
</div>
</footer>
<!-- Privacy Drawer -->
<div [class.translate-x-0]="privacyVisible" [class.translate-x-full]="!privacyVisible" class="fixed top-0 right-0 z-40 h-screen p-4 overflow-y-auto transition-transform duration-300 ease-in-out bg-white lg:w-1/3 w-96" tabindex="-1">
<div class="flex items-center justify-between mb-4">
<h5 class="inline-flex items-center text-base font-semibold text-gray-500">
<svg class="w-4 h-4 me-2.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z" />
</svg>
Privacy Statement
</h5>
<button type="button" (click)="togglePrivacy()" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 flex items-center justify-center">
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
</svg>
<span class="sr-only">Close menu</span>
</button>
</div>
<section class="overflow-y-auto max-h-[calc(100vh-80px)]">
<article>
<section>
<div class="container">
<p>
<strong>Privacy Policy</strong><br />
We are committed to protecting your privacy. We have established this statement as a testament to our commitment to your privacy.
</p>
<p>This Privacy Policy relates to the use of any personal information you provide to us through this websites.</p>
<p>By accepting the Privacy Policy during registration or the sending of an enquiry, you expressly consent to our collection, storage, use and disclosure of your personal information as described in this Privacy Policy.</p>
<p>
We may update our Privacy Policy from time to time. Our Privacy Policy was last updated in Febuary 2018 and is effective upon acceptance for new users. By continuing to use our websites or otherwise continuing to deal with us, you accept
this Privacy Policy.
</p>
<p>
<strong>Collection of personal information</strong><br />
Anyone can browse our websites without revealing any personally identifiable information.
</p>
<p>However, should you wish to contact a business for sale, a franchise opportunity or an intermediary, we will require you to provide some personal information.</p>
<p>Should you wish to advertise your services, your business (es) or your franchise opportunity, we will require you to provide some personal information.</p>
<p>By providing personal information, you are consenting to the transfer and storage of that information on our servers located in the United States.</p>
<p>We may collect and store the following personal information:</p>
<p>
Your name, email address, physical address, telephone numbers, and (depending on the service used), your business information, financial information, such as credit / payment card details;<br />
transactional information based on your activities on the site; information that you disclose in a forum on any of our websites, feedback, correspondence through our websites, and correspondence sent to us;<br />
other information from your interaction with our websites, services, content and advertising, including computer and connection information, statistics on page views, traffic to and from the sites, ad data, IP address and standard web log
information;<br />
supplemental information from third parties (for example, if you incur a debt, we will generally conduct a credit check by obtaining additional information about you from a credit bureau, as permitted by law; or if the information you
provide cannot be verified,<br />
we may ask you to send us additional information, or to answer additional questions online to help verify your information).
</p>
<p>
<strong>How we use your information</strong><br />
The primary reason we collect your personal information is to improve the services we deliver to you through our website. By registering or sending an enquiry through our website, you agree that we may use your personal information to:<br />
provide the services and customer support you request;<br />
connect you with relevant parties:<br />
If you are a buyer we will pass some or all of your details on to the seller / intermediary along with any message you have typed. This allows the seller to contact you in order to pursue a possible sale of a business;<br />
If you are a seller / intermediary, we will disclose your details where you have given us permission to do so;<br />
resolve disputes, collect fees, and troubleshoot problems;<br />
prevent potentially prohibited or illegal activities, and enforce our Terms and Conditions;<br />
customize, measure and improve our services, conduct internal market research, provide content and advertising;<br />
tell you about other Biz-Match products and services, target marketing, send you service updates, and promotional offers based on your communication preferences.
</p>
<p>
<strong>Our disclosure of your information</strong><br />
We may disclose personal information to respond to legal requirements, enforce our policies, respond to claims that a listing or other content infringes the rights of others, or protect anyones rights, property, or safety.
</p>
<p>
We may also share your personal information with<br />
When you select to register an account as a business buyer, you provide your personal details and we will pass this on to a seller of a business or franchise when you request more information.
</p>
<p>
When you select to register an account as a business broker or seller on the site, we provide a public platform on which to establish your business profile. This profile consists of pertinent facts about your business along with your
personal information; namely, the contact information you provide to facilitate contact between you and other users of the site. Direct email addresses and telephone numbers will not be publicly displayed unless you specifically include
it on your profile.
</p>
<p>
The information a user includes within the forums provided on the site is publicly available to other users of the site. Please be aware that any personal information you elect to provide in a public forum may be used to send you
unsolicited messages; we are not responsible for the personal information a user elects to disclose within their public profile, or in the private communications that users engage in on the site.
</p>
<p>
We post testimonials on the site obtained from users. These testimonials may include the name, city, state or region and business of the user. We obtain permission from our users prior to posting their testimonials on the site. We are
not responsible for any personal information a user selects to include within their testimonial.
</p>
<p>
When you elect to email a friend about the site, or a particular business, we request the third partys email address to send this one time email. We do not share this information with any third parties for their promotional purposes and
only store the information to gauge the effectiveness of our referral program.
</p>
<p>We may share your personal information with our service providers where necessary. We employ the services of a payment processor to fulfil payment for services purchased on the site.</p>
<p>
We works with a number of partners or affiliates, where we provide marketing services for these companies. These third party agents collect your personal information to facilitate your service request and the information submitted here is
governed by their privacy policy.
</p>
<p>
<strong>Masking Policy</strong><br />
In some cases, where the third party agent collects your information, the affiliate portal may appear within a BizMatch.net frame. It is presented as a BizMatch.net page for a streamlined user interface however the data collected on such
pages is governed by the third party agents privacy policy.
</p>
<p>
<strong>Legal Disclosure</strong><br />
In certain circumstances, we may be legally required to disclose information collected on the site to law enforcement, government agencies or other third parties. We reserve the right to disclose information to our service providers and
to law enforcement or government agencies where a formal request such as in response to a court order, subpoena or judicial proceeding is made. Where we believe in good faith that disclosure of information is necessary to prevent imminent
physical or financial harm, or loss, or in protecting against illegal activity on the site, we reserve to disclose information.
</p>
<p>
Should the company undergo the merger, acquisition or sale of some or all of its assets, your personal information may likely be a part of the transferred assets. In such an event, your personal information on the site, would be governed
by this privacy statement; any changes to the privacy practices governing your information as a result of transfer would be relayed to you by means of a prominent notice on the Site, or by email.
</p>
<p>
<strong>Using information from BizMatch.net website</strong><br />
In certain cases, (where you are receiving contact details of buyers interested in your business opportunity or a business opportunity you represent), you must comply with data protection laws, and give other users a chance to remove
themselves from your database and a chance to review what information you have collected about them.
</p>
<p>
<strong>You agree to use BizMatch.net user information only for:</strong>
</p>
<p>
BizMatch.net transaction-related purposes that are not unsolicited commercial messages;<br />
using services offered through BizMatch.net, or<br />
other purposes that a user expressly chooses.
</p>
<p>
<strong>Marketing</strong><br />
We do not sell or rent your personal information to third parties for their marketing purposes without your explicit consent. Where you explicitly express your consent at the point of collection to receive offers from third party partners
or affiliates, we will communicate to you on their behalf. We will not pass your information on.
</p>
<p>
You will receive email marketing communications from us throughout the duration of your relationship with our websites. If you do not wish to receive marketing communications from us you may unsubscribe and / or change your preferences at
any time by following instructions included within a communication or emailing Customer Services.
</p>
<p>If you have an account with one of our websites you can also log in and click the email preferences link to unsubscribe and / or change your preferences.</p>
<p>
Please note that we reserve the right to send all website users notifications and administrative emails where necessary which are considered a part of the service. Given that these messages arent promotional in nature, you will be unable
to opt-out of them.
</p>
<p>
<strong>Cookies</strong><br />
A cookie is a small text file written to your hard drive that contains information about you. Cookies do not contain any personal information about users. Once you close your browser or log out of the website, the cookie simply
terminates. We use cookies so that we can personalise your experience of our websites.
</p>
<p>
If you set up your browser to reject the cookie, you may still use the website however; doing so may interfere with your use of some aspects of our websites. Some of our business partners use cookies on our site (for example,
advertisers). We have no access to or control over these cookies.
</p>
<p>For more information about how BizMatch.net uses cookies please read our Cookie Policy.</p>
<p>
<strong>Spam, spyware or spoofing</strong><br />
We and our users do not tolerate spam. Make sure to set your email preferences so we can communicate with you, as you prefer. Please add us to your safe senders list. To report spam or spoof emails, please contact us using the contact
information provided in the Contact Us section of this privacy statement.
</p>
<p>
You may not use our communication tools to send spam or otherwise send content that would breach our Terms and Conditions. We automatically scan and may manually filter messages to check for spam, viruses, phishing attacks and other
malicious activity or illegal or prohibited content. We may also store these messages for back up purposes only.
</p>
<p>If you send an email to an email address that is not registered in our community, we do not permanently store that email or use that email address for any marketing purpose. We do not rent or sell these email addresses.</p>
<p>
<strong>Account protection</strong><br />
Your password is the key to your account. Make sure this is stored safely. Use unique numbers, letters and special characters, and do not disclose your password to anyone. If you do share your password or your personal information with
others, remember that you are responsible for all actions taken in the name of your account. If you lose control of your password, you may lose substantial control over your personal information and may be subject to legally binding
actions taken on your behalf. Therefore, if your password has been compromised for any reason, you should immediately notify us and change your password.
</p>
<p>
<strong>Accessing, reviewing and changing your personal information</strong><br />
You can view and amend your personal information at any time by logging in to your account online. You must promptly update your personal information if it changes or is inaccurate.
</p>
<p>If at any time you wish to close your account, please contact Customer Services and instruct us to do so. We will process your request as soon as we can.</p>
<p>You may also contact us at any time to find out what information we hold about you, what we do with it and ask us to update it for you.</p>
<p>
We do retain personal information from closed accounts to comply with law, prevent fraud, collect any fees owed, resolve disputes, troubleshoot problems, assist with any investigations, enforce our Terms and Conditions, and take other
actions otherwise permitted by law.
</p>
<p>
<strong>Security</strong><br />
Your information is stored on our servers located in the USA. We treat data as an asset that must be protected and use a variety of tools (encryption, passwords, physical security, etc.) to protect your personal information against
unauthorized access and disclosure. However, no method of security is 100% effective and while we take every measure to protect your personal information, we make no guarantees of its absolute security.
</p>
<p>We employ the use of SSL encryption during the transmission of sensitive data across our websites.</p>
<p>
<strong>Third parties</strong><br />
Except as otherwise expressly included in this Privacy Policy, this document addresses only the use and disclosure of information we collect from you. If you disclose your information to others, whether they are buyers or sellers on our
websites or other sites throughout the internet, different rules may apply to their use or disclosure of the information you disclose to them. Dynamis does not control the privacy policies of third parties, and you are subject to the
privacy policies of those third parties where applicable.
</p>
<p>We encourage you to ask questions before you disclose your personal information to others.</p>
<p>
<strong>General</strong><br />
We may change this Privacy Policy from time to time as we add new products and applications, as we improve our current offerings, and as technologies and laws change. You can determine when this Privacy Policy was last revised by
referring to the “Last Updated” legend at the top of this page.
</p>
<p>
Any changes will become effective upon our posting of the revised Privacy Policy on our affected websites. We will provide notice to you if these changes are material and, where required by applicable law, we will obtain your consent.
This notice may be provided by email, by posting notice of the changes on our affected websites or by other means, consistent with applicable laws.
</p>
<p>
<strong>Contact Us</strong><br />
If you have any questions or comments about our privacy policy, and you cant find the answer to your question on our help pages, please contact us using this form or email support&#64;bizmatch.net, or write to us at BizMatch, 715 S.
Tanahua, Corpus Christi, TX 78401.)
</p>
</div>
</section>
</article>
</section>
</div>
<!-- Terms of Use Drawer -->
<div [class.translate-x-0]="termsVisible" [class.translate-x-full]="!termsVisible" class="fixed top-0 right-0 z-40 h-screen p-4 overflow-y-auto transition-transform duration-300 ease-in-out bg-white lg:w-1/3 w-96" tabindex="-1">
<div class="flex items-center justify-between mb-4">
<h5 class="inline-flex items-center text-base font-semibold text-gray-500">
<svg class="w-4 h-4 me-2.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z" />
</svg>
Terms of use
</h5>
<button type="button" (click)="toggleTerms()" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 flex items-center justify-center">
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
</svg>
<span class="sr-only">Close menu</span>
</button>
</div>
<section class="overflow-y-auto max-h-[calc(100vh-80px)]">
<article>
<section>
<div class="container">
<b><span>AGREEMENT BETWEEN USER AND BizMatch</span></b
><span
><p></p>
<p><span>The BizMatch Web Site is comprised of various Web pages operated by BizMatch.</span><span></span></p>
<p>
<span
>The BizMatch Web Site is offered to you conditioned on your acceptance without modification of the terms, conditions, and notices contained herein. Your use of the BizMatch Web Site constitutes your agreement to all such terms,
conditions, and notices.</span
><span></span>
</p>
<p>
<b><span>MODIFICATION OF THESE TERMS OF USE</span></b
><span></span>
</p>
<p>
<span>BizMatch reserves the right to change the terms, conditions, and notices under which the BizMatch Web Site is offered, including but not limited to the charges associated with the use of the BizMatch Web Site.</span><span></span>
</p>
<p>
<b><span>LINKS TO THIRD PARTY SITES</span></b
><span></span>
</p>
<p>
<span
>The BizMatch Web Site may contain links to other Web Sites ("Linked Sites"). The Linked Sites are not under the control of BizMatch and BizMatch is not responsible for the contents of any Linked Site, including without limitation any
link contained in a Linked Site, or any changes or updates to a Linked Site. BizMatch is not responsible for webcasting or any other form of transmission received from any Linked Site. BizMatch is providing these links to you only as
a convenience, and the inclusion of any link does not imply endorsement by BizMatch of the site or any association with its operators.</span
><span></span>
</p>
<p>
<b><span>NO UNLAWFUL OR PROHIBITED USE</span></b
><span></span>
</p>
<p>
<span
>As a condition of your use of the BizMatch Web Site, you warrant to BizMatch that you will not use the BizMatch Web Site for any purpose that is unlawful or prohibited by these terms, conditions, and notices. You may not use the
BizMatch Web Site in any manner which could damage, disable, overburden, or impair the BizMatch Web Site or interfere with any other partys use and enjoyment of the BizMatch Web Site. You may not obtain or attempt to obtain any
materials or information through any means not intentionally made available or provided for through the BizMatch Web Sites.</span
><span></span>
</p>
<p>
<b><span>USE OF COMMUNICATION SERVICES</span></b
><span></span>
</p>
<p>
<span
>The BizMatch Web Site may contain bulletin board services, chat areas, news groups, forums, communities, personal web pages, calendars, and/or other message or communication facilities designed to enable you to communicate with the
public at large or with a group (collectively, "Communication Services"), you agree to use the Communication Services only to post, send and receive messages and material that are proper and related to the particular Communication
Service. By way of example, and not as a limitation, you agree that when using a Communication Service, you will not:</span
><span></span>
</p>
<p>&nbsp;</p>
<p class="MsoNormal"></p>
<p>
<span
><span>§<span>&nbsp; </span></span></span
><span>Defame, abuse, harass, stalk, threaten or otherwise violate the legal rights (such as rights of privacy and publicity) of others.</span><span></span>
</p>
<p>&nbsp;</p>
<p class="MsoNormal">
<span
><span>§<span>&nbsp; </span></span></span
><span>Publish, post, upload, distribute or disseminate any inappropriate, profane, defamatory, infringing, obscene, indecent or unlawful topic, name, material or information.</span>
</p>
<p class="MsoNormal">
<span
><span>§<span>&nbsp; </span></span></span
><span>Upload files that contain software or other material protected by intellectual property laws (or by rights of privacy of publicity) unless you own or control the rights thereto or have received all necessary consents.</span>
</p>
<p class="MsoNormal">
<span
><span>§<span>&nbsp; </span></span></span
><span>Upload files that contain viruses, corrupted files, or any other similar software or programs that may damage the operation of anothers computer.</span>
</p>
<p class="MsoNormal">
<span
><span>§<span>&nbsp; </span></span></span
><span>Advertise or offer to sell or buy any goods or services for any business purpose, unless such Communication Service specifically allows such messages.</span>
</p>
<p class="MsoNormal">
<span
><span>§<span>&nbsp; </span></span></span
><span>Conduct or forward surveys, contests, pyramid schemes or chain letters.</span>
</p>
<p class="MsoNormal">
<span
><span>§<span>&nbsp; </span></span></span
><span>Download any file posted by another user of a Communication Service that you know, or reasonably should know, cannot be legally distributed in such manner.</span>
</p>
<p class="MsoNormal">
<span
><span>§<span>&nbsp; </span></span></span
><span>Falsify or delete any author attributions, legal or other proper notices or proprietary designations or labels of the origin or source of software or other material contained in a file that is uploaded.</span>
</p>
<p class="MsoNormal">
<span
><span>§<span>&nbsp; </span></span></span
><span>Restrict or inhibit any other user from using and enjoying the Communication Services.</span>
</p>
<p class="MsoNormal">
<span
><span>§<span>&nbsp; </span></span></span
><span>Violate any code of conduct or other guidelines which may be applicable for any particular Communication Service.</span>
</p>
<p class="MsoNormal">
<span
><span>§<span>&nbsp; </span></span></span
><span>Harvest or otherwise collect information about others, including e-mail addresses, without their consent.</span>
</p>
<p class="MsoNormal">
<span
><span>§<span>&nbsp; </span></span></span
><span>Violate any applicable laws or regulations.</span>
</p>
<p class="MsoNormal">
<span
>BizMatch has no obligation to monitor the Communication Services. However, BizMatch reserves the right to review materials posted to a Communication Service and to remove any materials in its sole discretion. BizMatch reserves the
right to terminate your access to any or all of the Communication Services at any time without notice for any reason whatsoever.</span
><span></span>
</p>
<p>
<span
>BizMatch reserves the right at all times to disclose any information as necessary to satisfy any applicable law, regulation, legal process or governmental request, or to edit, refuse to post or to remove any information or materials,
in whole or in part, in BizMatchs sole discretion.</span
><span></span>
</p>
<p>
<span
>Always use caution when giving out any personally identifying information about yourself or your children in any Communication Service. BizMatch does not control or endorse the content, messages or information found in any
Communication Service and, therefore, BizMatch specifically disclaims any liability with regard to the Communication Services and any actions resulting from your participation in any Communication Service. Managers and hosts are not
authorized BizMatch spokespersons, and their views do not necessarily reflect those of BizMatch.</span
><span></span>
</p>
<p>
<span>Materials uploaded to a Communication Service may be subject to posted limitations on usage, reproduction and/or dissemination. You are responsible for adhering to such limitations if you download the materials.</span
><span></span>
</p>
<p>
<b><span>MATERIALS PROVIDED TO BizMatch OR POSTED AT ANY BizMatch WEB SITE</span></b
><span></span>
</p>
<p>
<span
>BizMatch does not claim ownership of the materials you provide to BizMatch (including feedback and suggestions) or post, upload, input or submit to any BizMatch Web Site or its associated services (collectively "Submissions").
However, by posting, uploading, inputting, providing or submitting your Submission you are granting BizMatch, its affiliated companies and necessary sublicensees permission to use your Submission in connection with the operation of
their Internet businesses including, without limitation, the rights to: copy, distribute, transmit, publicly display, publicly perform, reproduce, edit, translate and reformat your Submission; and to publish your name in connection
with your Submission.</span
><span></span>
</p>
<p>
<span
>No compensation will be paid with respect to the use of your Submission, as provided herein. BizMatch is under no obligation to post or use any Submission you may provide and may remove any Submission at any time in BizMatchs sole
discretion.</span
><span></span>
</p>
<p>
<span
>By posting, uploading, inputting, providing or submitting your Submission you warrant and represent that you own or otherwise control all of the rights to your Submission as described in this section including, without limitation,
all the rights necessary for you to provide, post, upload, input or submit the Submissions.</span
><span></span>
</p>
<p>
<b><span>LIABILITY DISCLAIMER</span></b
><span></span>
</p>
<p>
<span
>THE INFORMATION, SOFTWARE, PRODUCTS, AND SERVICES INCLUDED IN OR AVAILABLE THROUGH THE BizMatch WEB SITE MAY INCLUDE INACCURACIES OR TYPOGRAPHICAL ERRORS. CHANGES ARE PERIODICALLY ADDED TO THE INFORMATION HEREIN. BizMatch AND/OR ITS
SUPPLIERS MAY MAKE IMPROVEMENTS AND/OR CHANGES IN THE BizMatch WEB SITE AT ANY TIME. ADVICE RECEIVED VIA THE BizMatch WEB SITE SHOULD NOT BE RELIED UPON FOR PERSONAL, MEDICAL, LEGAL OR FINANCIAL DECISIONS AND YOU SHOULD CONSULT AN
APPROPRIATE PROFESSIONAL FOR SPECIFIC ADVICE TAILORED TO YOUR SITUATION.</span
><span></span>
</p>
<p>
<span
>BizMatch AND/OR ITS SUPPLIERS MAKE NO REPRESENTATIONS ABOUT THE SUITABILITY, RELIABILITY, AVAILABILITY, TIMELINESS, AND ACCURACY OF THE INFORMATION, SOFTWARE, PRODUCTS, SERVICES AND RELATED GRAPHICS CONTAINED ON THE BizMatch WEB SITE
FOR ANY PURPOSE. TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, ALL SUCH INFORMATION, SOFTWARE, PRODUCTS, SERVICES AND RELATED GRAPHICS ARE PROVIDED "AS IS" WITHOUT WARRANTY OR CONDITION OF ANY KIND. BizMatch AND/OR ITS SUPPLIERS
HEREBY DISCLAIM ALL WARRANTIES AND CONDITIONS WITH REGARD TO THIS INFORMATION, SOFTWARE, PRODUCTS, SERVICES AND RELATED GRAPHICS, INCLUDING ALL IMPLIED WARRANTIES OR CONDITIONS OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE,
TITLE AND NON-INFRINGEMENT.</span
><span></span>
</p>
<p>
<span
>TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT SHALL BizMatch AND/OR ITS SUPPLIERS BE LIABLE FOR ANY DIRECT, INDIRECT, PUNITIVE, INCIDENTAL, SPECIAL, CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER INCLUDING, WITHOUT
LIMITATION, DAMAGES FOR LOSS OF USE, DATA OR PROFITS, ARISING OUT OF OR IN ANY WAY CONNECTED WITH THE USE OR PERFORMANCE OF THE BizMatch WEB SITE, WITH THE DELAY OR INABILITY TO USE THE BizMatch WEB SITE OR RELATED SERVICES, THE
PROVISION OF OR FAILURE TO PROVIDE SERVICES, OR FOR ANY INFORMATION, SOFTWARE, PRODUCTS, SERVICES AND RELATED GRAPHICS OBTAINED THROUGH THE BizMatch WEB SITE, OR OTHERWISE ARISING OUT OF THE USE OF THE BizMatch WEB SITE, WHETHER BASED
ON CONTRACT, TORT, NEGLIGENCE, STRICT LIABILITY OR OTHERWISE, EVEN IF BizMatch OR ANY OF ITS SUPPLIERS HAS BEEN ADVISED OF THE POSSIBILITY OF DAMAGES. BECAUSE SOME STATES/JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF
LIABILITY FOR CONSEQUENTIAL OR INCIDENTAL DAMAGES, THE ABOVE LIMITATION MAY NOT APPLY TO YOU. IF YOU ARE DISSATISFIED WITH ANY PORTION OF THE BizMatch WEB SITE, OR WITH ANY OF THESE TERMS OF USE, YOUR SOLE AND EXCLUSIVE REMEDY IS TO
DISCONTINUE USING THE BizMatch WEB SITE.</span
><span></span>
</p>
<p><span>SERVICE CONTACT : info&#64;bizmatch.net</span><span></span></p>
<p>
<b><span>TERMINATION/ACCESS RESTRICTION</span></b
><span></span>
</p>
<p>
<span
>BizMatch reserves the right, in its sole discretion, to terminate your access to the BizMatch Web Site and the related services or any portion thereof at any time, without notice. GENERAL To the maximum extent permitted by law, this
agreement is governed by the laws of the State of Washington, U.S.A. and you hereby consent to the exclusive jurisdiction and venue of courts in King County, Washington, U.S.A. in all disputes arising out of or relating to the use of
the BizMatch Web Site. Use of the BizMatch Web Site is unauthorized in any jurisdiction that does not give effect to all provisions of these terms and conditions, including without limitation this paragraph. You agree that no joint
venture, partnership, employment, or agency relationship exists between you and BizMatch as a result of this agreement or use of the BizMatch Web Site. BizMatchs performance of this agreement is subject to existing laws and legal
process, and nothing contained in this agreement is in derogation of BizMatchs right to comply with governmental, court and law enforcement requests or requirements relating to your use of the BizMatch Web Site or information
provided to or gathered by BizMatch with respect to such use. If any part of this agreement is determined to be invalid or unenforceable pursuant to applicable law including, but not limited to, the warranty disclaimers and liability
limitations set forth above, then the invalid or unenforceable provision will be deemed superseded by a valid, enforceable provision that most closely matches the intent of the original provision and the remainder of the agreement
shall continue in effect. Unless otherwise specified herein, this agreement constitutes the entire agreement between the user and BizMatch with respect to the BizMatch Web Site and it supersedes all prior or contemporaneous
communications and proposals, whether electronic, oral or written, between the user and BizMatch with respect to the BizMatch Web Site. A printed version of this agreement and of any notice given in electronic form shall be admissible
in judicial or administrative proceedings based upon or relating to this agreement to the same extent an d subject to the same conditions as other business documents and records originally generated and maintained in printed form. It
is the express wish to the parties that this agreement and all related documents be drawn up in English.</span
><span></span>
</p>
<p>
<b><span>COPYRIGHT AND TRADEMARK NOTICES:</span></b
><span></span>
</p>
<p><span>All contents of the BizMatch Web Site are: Copyright 2011 by Bizmatch Business Solutions and/or its suppliers. All rights reserved.</span><span></span></p>
<p>
<b><span>TRADEMARKS</span></b
><span></span>
</p>
<p><span>The names of actual companies and products mentioned herein may be the trademarks of their respective owners.</span><span></span></p>
<p>
<span>The example companies, organizations, products, people and events depicted herein are fictitious. No association with any real company, organization, product, person, or event is intended or should be inferred.</span><span></span>
</p>
<p><span>Any rights not expressly granted herein are reserved.</span><span></span></p>
<p>
<b><span>NOTICES AND PROCEDURE FOR MAKING CLAIMS OF COPYRIGHT INFRINGEMENT</span></b
><span></span>
</p>
<p>
<span
>Pursuant to Title 17, United States Code, Section 512(c)(2), notifications of claimed copyright infringement under United States copyright law should be sent to Service Providers Designated Agent. ALL INQUIRIES NOT RELEVANT TO THE
FOLLOWING PROCEDURE WILL RECEIVE NO RESPONSE. See Notice and Procedure for Making Claims of Copyright Infringement.</span
><span><br /> </span>
</p>
<p class="MsoNormal">&nbsp;</p>
<p class="MsoNormal">
We reserve the right to update or revise these Terms of Use at any time without&nbsp;notice. Please check the Terms of Use periodically for changes. The revised&nbsp;terms will be effective immediately as soon as they are posted on the
WebSite&nbsp;and by continuing to use the Site you agree to be bound by the revised terms<span><br /> </span></p
></span>
</div>
</section>
</article>
</section>
</div>
<!-- Backdrop overlay when drawer is open -->
<div *ngIf="privacyVisible || termsVisible" class="fixed inset-0 bg-black bg-opacity-50 z-30 transition-opacity duration-300 ease-in-out" (click)="closeDrawers()"></div>

View File

@ -1,49 +0,0 @@
// Drawer animation styles
.translate-x-0 {
transform: translateX(0);
}
.translate-x-full {
transform: translateX(100%);
}
// Custom scrollbar for drawers
.overflow-y-auto {
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background-color: #f1f1f1;
border-radius: 10px;
}
&::-webkit-scrollbar-thumb {
background-color: #888;
border-radius: 10px;
}
&::-webkit-scrollbar-thumb:hover {
background-color: #555;
}
}
// Fix for iOS Safari elastic scroll behavior
.fixed {
-webkit-overflow-scrolling: touch;
}
// Focus styles for accessibility
button:focus,
a:focus {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
section p {
display: block;
margin-block-start: 1em;
margin-block-end: 1em;
margin-inline-start: 0px;
margin-inline-end: 0px;
unicode-bidi: isolate;
}

View File

@ -1,72 +0,0 @@
import { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { NavigationEnd, Router, RouterModule } from '@angular/router';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
@Component({
selector: 'app-footer',
standalone: true,
imports: [CommonModule, FormsModule, RouterModule, FontAwesomeModule],
templateUrl: './footer.component.html',
styleUrl: './footer.component.scss',
})
export class FooterComponent implements OnInit {
privacyVisible = false;
termsVisible = false;
currentYear: number = new Date().getFullYear();
isHomeRoute = false;
constructor(private router: Router) {}
ngOnInit() {
this.router.events.subscribe(event => {
if (event instanceof NavigationEnd) {
this.isHomeRoute = event.url === '/home';
}
});
// Listen for escape key to close drawers
document.addEventListener('keydown', event => {
if (event.key === 'Escape') {
this.closeDrawers();
}
});
}
// Toggle privacy drawer
togglePrivacy() {
this.termsVisible = false; // Close other drawer if open
this.privacyVisible = !this.privacyVisible;
this.toggleBodyScroll();
}
// Toggle terms drawer
toggleTerms() {
this.privacyVisible = false; // Close other drawer if open
this.termsVisible = !this.termsVisible;
this.toggleBodyScroll();
}
// Close all drawers
closeDrawers() {
this.privacyVisible = false;
this.termsVisible = false;
this.toggleBodyScroll();
}
// Prevent body scroll when drawer is open
private toggleBodyScroll() {
if (this.privacyVisible || this.termsVisible) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
}
// Clean up event listener on component destroy
ngOnDestroy() {
document.removeEventListener('keydown', () => {});
document.body.style.overflow = ''; // Ensure body scroll is restored
}
}

View File

@ -1,136 +0,0 @@
<nav class="bg-white border-gray-200 dark:bg-gray-900 print:hidden">
<div class="flex flex-wrap items-center justify-between mx-auto p-4">
<a routerLink="/home" class="flex items-center space-x-3 rtl:space-x-reverse">
<img src="assets/images/header-logo.png" class="h-10" alt="Logo" />
</a>
<div class="flex items-center md:order-2 space-x-3 rtl:space-x-reverse">
<!-- Filter button -->
@if(isFilterUrl()){
<button
type="button"
#triggerButton
(click)="openModal()"
id="filterDropdownButton"
class="max-sm:hidden px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg hover:bg-gray-100 hover:text-blue-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
>
<i class="fas fa-filter mr-2"></i>Filter ({{ getNumberOfFiltersSet() }})
</button>
<!-- Sort button -->
<div class="relative">
<button
type="button"
id="sortDropdownButton"
class="max-sm:hidden px-4 py-2 text-sm font-medium bg-white border border-gray-200 rounded-lg hover:bg-gray-100 hover:text-blue-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
(click)="toggleSortDropdown()"
[ngClass]="{ 'text-blue-500': selectOptions.getSortByOption(criteria?.sortBy) !== 'Sort', 'text-gray-900': selectOptions.getSortByOption(criteria?.sortBy) === 'Sort' }"
>
<i class="fas fa-sort mr-2"></i>{{ selectOptions.getSortByOption(criteria?.sortBy) }}
</button>
<!-- Sort options dropdown -->
<div *ngIf="sortDropdownVisible" class="absolute right-0 z-50 w-48 md:mt-2 max-md:mt-20 max-md:mr-[-2.5rem] bg-white border border-gray-200 rounded-lg drop-shadow-custom-bg dark:bg-gray-800 dark:border-gray-600">
<ul class="py-1 text-sm text-gray-700 dark:text-gray-200">
@for(item of sortByOptions; track item){
<li (click)="sortBy(item.value)" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer">{{ item.selectName ? item.selectName : item.name }}</li>
}
</ul>
</div>
</div>
} @if(!isEmailUsUrl()){
<div class="relative">
<button type="button" class="relative inline-flex justify-center items-center w-8 h-8 rounded-full bg-gray-400 focus:ring-4 focus:ring-gray-300 dark:focus:ring-gray-600" id="user-menu-button" aria-expanded="false" (click)="toggleUserMenu()">
<span class="sr-only">Open user menu</span>
@if(isProfessional || (authService.isAdmin() | async) && user?.hasProfile){
<img class="w-8 h-8 rounded-full object-cover" src="{{ profileUrl }}" alt="user photo" />
} @else {
<div class="flex justify-center items-center w-full h-full">
<div class="w-4 h-3 flex flex-col justify-between">
<span class="block h-0.5 w-full bg-stone-50"></span>
<span class="block h-0.5 w-full bg-stone-50"></span>
<span class="block h-0.5 w-full bg-stone-50"></span>
</div>
</div>
}
</button>
<!-- Dropdown menu für eingeloggte Benutzer -->
@if(user){
<div
class="drop-shadow-custom-bg z-10 size-max absolute right-0 top-full mt-1 text-base list-none bg-white divide-y divide-gray-100 rounded-lg shadow dark:bg-gray-700 dark:divide-gray-600"
id="user-menu-dropdown"
[ngClass]="{ hidden: !userMenuVisible }"
>
<div class="px-4 py-3">
<span class="block text-sm text-gray-900 dark:text-white">Welcome, {{ user.firstname }} </span>
<span class="block text-sm text-gray-500 truncate dark:text-gray-400">{{ user.email }}</span>
</div>
<ul class="py-2" aria-labelledby="user-menu-button">
<li>
<a routerLink="/account" (click)="userMenuVisible = false" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Account</a>
</li>
@if(isProfessional || (authService.isAdmin() | async) && user?.hasProfile){
<li>
<a routerLink="/createBusinessListing" (click)="userMenuVisible = false" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Create Listing</a>
</li>
}
<li>
<a routerLink="/myListings" (click)="userMenuVisible = false" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">My Listings</a>
</li>
<li>
<a routerLink="/myFavorites" (click)="userMenuVisible = false" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">My Favorites</a>
</li>
<li>
<a routerLink="/logout" (click)="userMenuVisible = false" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Logout</a>
</li>
</ul>
@if(authService.isAdmin() | async){
<ul class="py-2">
<li>
<a routerLink="admin/users" (click)="userMenuVisible = false" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Users (Admin)</a>
</li>
</ul>
}
</div>
} @else {
<!-- Dropdown menu für nicht eingeloggte Benutzer -->
<div
class="drop-shadow-custom-bg z-10 size-max absolute right-0 top-full mt-1 text-base list-none bg-white divide-y divide-gray-100 rounded-lg shadow dark:bg-gray-700 dark:divide-gray-600"
id="user-menu-dropdown"
[ngClass]="{ hidden: !userMenuVisible }"
>
<ul class="py-2" aria-labelledby="user-menu-button">
<li>
<a routerLink="/login" [queryParams]="{ mode: 'login' }" (click)="userMenuVisible = false" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Log In</a>
</li>
<li>
<a routerLink="/login" [queryParams]="{ mode: 'register' }" (click)="userMenuVisible = false" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Sign Up</a>
</li>
</ul>
</div>
}
</div>
}
</div>
</div>
<!-- Mobile filter button -->
<div class="md:hidden flex justify-center pb-4">
<button
(click)="openModal()"
type="button"
id="filterDropdownMobileButton"
class="w-full mx-4 px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg hover:bg-gray-100 hover:text-blue-700 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
>
<i class="fas fa-filter mr-2"></i>Filter ({{ getNumberOfFiltersSet() }})
</button>
<!-- Sorting -->
<button
(click)="toggleSortDropdown()"
type="button"
id="sortDropdownMobileButton"
class="mx-4 w-1/2 px-4 py-2 text-sm font-medium bg-white border border-gray-200 rounded-lg hover:bg-gray-100 hover:text-blue-700 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
[ngClass]="{ 'text-blue-500': selectOptions.getSortByOption(criteria?.sortBy) !== 'Sort', 'text-gray-900': selectOptions.getSortByOption(criteria?.sortBy) === 'Sort' }"
>
<i class="fas fa-sort mr-2"></i>{{ selectOptions.getSortByOption(criteria?.sortBy) }}
</button>
</div>
</nav>

View File

@ -1,13 +0,0 @@
::ng-deep p-menubarsub{
margin-left: auto;
}
::ng-deep .p-tabmenu .p-tabmenu-nav .p-tabmenuitem .p-menuitem-link{
border:1px solid #ffffff;
}
::ng-deep .p-tabmenu .p-tabmenu-nav .p-tabmenuitem.p-highlight .p-menuitem-link {
border-bottom: 2px solid #3B82F6 !important;
}
::ng-deep .p-menubar{
border:unset;
background: unset;
}

View File

@ -1,186 +0,0 @@
import { BreakpointObserver } from '@angular/cdk/layout';
import { CommonModule } from '@angular/common';
import { Component, HostListener } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { NavigationEnd, Router, RouterModule } from '@angular/router';
import { faUserGear } from '@fortawesome/free-solid-svg-icons';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { debounceTime, filter, Observable, Subject, Subscription } from 'rxjs';
import { SortByOptions, User } from '../../../../../bizmatch-server/src/models/db.model';
import { BusinessListingCriteria, emailToDirName, KeycloakUser, KeyValueAsSortBy } from '../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../environments/environment';
import { SharedService } from '../../services/shared.service';
import { AuthService } from '../../services/auth.service';
import { CriteriaChangeService } from '../../services/criteria-change.service';
import { ListingsService } from '../../services/listings.service';
import { ModalService } from '../../services/modal.service';
import { SearchService } from '../../services/search.service';
import { SelectOptionsService } from '../../services/select-options.service';
import { UserService } from '../../services/user.service';
import { assignProperties, compareObjects, createEmptyBusinessListingCriteria, getCriteriaProxy, map2User } from '../../utils/utils';
@UntilDestroy()
@Component({
selector: 'header',
imports: [CommonModule, RouterModule, FormsModule],
templateUrl: './header.component.html',
styleUrl: './header.component.scss',
})
export class HeaderComponent {
public buildVersion = environment.buildVersion;
user$: Observable<KeycloakUser>;
keycloakUser: KeycloakUser;
user: User;
activeItem;
faUserGear = faUserGear;
profileUrl: string;
env = environment;
isMobile: boolean = false;
private destroy$ = new Subject<void>();
prompt: string;
private subscription: Subscription;
criteria: BusinessListingCriteria;
private routerSubscription: Subscription | undefined;
baseRoute: string;
sortDropdownVisible: boolean;
sortByOptions: KeyValueAsSortBy[] = [];
numberOfBroker$: Observable<number>;
numberOfCommercial$: Observable<number>;
userMenuVisible: boolean = false;
constructor(
private router: Router,
private userService: UserService,
private sharedService: SharedService,
private breakpointObserver: BreakpointObserver,
private modalService: ModalService,
private searchService: SearchService,
private criteriaChangeService: CriteriaChangeService,
public selectOptions: SelectOptionsService,
public authService: AuthService,
private listingService: ListingsService,
) {}
@HostListener('document:click', ['$event'])
handleGlobalClick(event: Event) {
const target = event.target as HTMLElement;
if (target.id !== 'sortDropdownButton' && target.id !== 'sortDropdownMobileButton') {
this.sortDropdownVisible = false;
}
}
@HostListener('document:click', ['$event'])
handleOutsideClick(event: Event) {
const target = event.target as HTMLElement;
// Schließe das User-Menü, wenn außerhalb geklickt wird
if (this.userMenuVisible && !target.closest('#user-menu-button') && !target.closest('#user-menu-dropdown')) {
this.userMenuVisible = false;
}
}
async ngOnInit() {
this.router.events.subscribe(event => {
if (event instanceof NavigationEnd) {
}
});
const token = await this.authService.getToken();
this.keycloakUser = map2User(token);
if (this.keycloakUser) {
this.user = await this.userService.getByMail(this.keycloakUser?.email);
this.profileUrl = this.user.hasProfile ? `${this.env.imageBaseUrl}/pictures/profile/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}` : `/assets/images/placeholder.png`;
}
this.sharedService.currentProfilePhoto.subscribe(photoUrl => {
this.profileUrl = photoUrl;
});
this.checkCurrentRoute(this.router.url);
this.setupSortByOptions();
this.routerSubscription = this.router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe((event: any) => {
this.checkCurrentRoute(event.urlAfterRedirects);
this.setupSortByOptions();
});
this.subscription = this.criteriaChangeService.criteriaChange$.pipe(debounceTime(400)).subscribe(() => {
this.criteria = getCriteriaProxy(this.baseRoute, this);
});
this.userService.currentUser.pipe(untilDestroyed(this)).subscribe(u => {
this.user = u;
});
}
private checkCurrentRoute(url: string): void {
this.baseRoute = url.split('/')[1]; // Nimmt den ersten Teil der Route nach dem ersten '/'
const specialRoutes = [, '', ''];
this.criteria = getCriteriaProxy(this.baseRoute, this);
// this.searchService.search(this.criteria);
}
setupSortByOptions() {
this.sortByOptions = [];
if (this.isProfessionalListing()) {
this.sortByOptions = [...this.sortByOptions, ...this.selectOptions.sortByOptions.filter(s => s.type === 'professional')];
}
if (this.isBusinessListing()) {
this.sortByOptions = [...this.sortByOptions, ...this.selectOptions.sortByOptions.filter(s => s.type === 'business' || s.type === 'listing')];
}
if (this.isCommercialPropertyListing()) {
this.sortByOptions = [...this.sortByOptions, ...this.selectOptions.sortByOptions.filter(s => s.type === 'commercial' || s.type === 'listing')];
}
this.sortByOptions = [...this.sortByOptions, ...this.selectOptions.sortByOptions.filter(s => !s.type)];
}
ngAfterViewInit() {}
async openModal() {
const modalResult = await this.modalService.showModal(this.criteria);
if (modalResult.accepted) {
this.searchService.search(this.criteria);
} else {
this.criteria = assignProperties(this.criteria, modalResult.criteria);
}
}
navigateWithState(dest: string, state: any) {
this.router.navigate([dest], { state: state });
}
isActive(route: string): boolean {
return this.router.url === route;
}
isEmailUsUrl(): boolean {
return ['/emailUs'].includes(this.router.url);
}
isFilterUrl(): boolean {
return ['/businessListings', '/commercialPropertyListings', '/brokerListings'].includes(this.router.url);
}
isBusinessListing(): boolean {
return ['/businessListings'].includes(this.router.url);
}
isCommercialPropertyListing(): boolean {
return ['/commercialPropertyListings'].includes(this.router.url);
}
isProfessionalListing(): boolean {
return ['/brokerListings'].includes(this.router.url);
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
getNumberOfFiltersSet() {
if (this.criteria?.criteriaType === 'businessListings') {
return compareObjects(createEmptyBusinessListingCriteria(), this.criteria, ['start', 'length', 'page', 'searchType', 'radius', 'sortBy']);
} else {
return 0;
}
}
sortBy(sortBy: SortByOptions) {
this.criteria.sortBy = sortBy;
this.sortDropdownVisible = false;
this.searchService.search(this.criteria);
}
toggleSortDropdown() {
this.sortDropdownVisible = !this.sortDropdownVisible;
}
toggleUserMenu() {
this.userMenuVisible = !this.userMenuVisible;
}
get isProfessional() {
return this.user?.customerType === 'professional';
}
}

View File

@ -1,12 +0,0 @@
<!-- Modal -->
<div *ngIf="showModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full flex items-center justify-center">
<div class="bg-white p-5 rounded-lg shadow-xl" style="width: 90%; max-width: 600px">
<h3 class="text-lg font-semibold mb-4">Crop Image</h3>
<image-cropper (loadImageFailed)="loadImageFailed()" [imageChangedEvent]="imageChangedEvent" [maintainAspectRatio]="false" format="png" (imageCropped)="imageCropped($event)"></image-cropper>
<div class="mt-4 flex justify-end">
<button (click)="closeModal()" class="mr-2 px-4 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300">Cancel</button>
<button (click)="uploadImage()" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">Upload</button>
</div>
</div>
</div>
<input type="file" #fileInput style="display: none" (change)="fileChangeEvent($event)" accept="image/*" />

View File

@ -1,6 +0,0 @@
::ng-deep image-cropper {
justify-content: center;
& > div {
width: unset !important;
}
}

View File

@ -1,69 +0,0 @@
import { CommonModule } from '@angular/common';
import { Component, ElementRef, Input, output, ViewChild } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { ImageCroppedEvent, ImageCropperComponent } from 'ngx-image-cropper';
import { UploadParams } from '../../../../../bizmatch-server/src/models/main.model';
import { ImageService } from '../../services/image.service';
import { ListingsService } from '../../services/listings.service';
export interface UploadReponse {
success: boolean;
type: 'uploadPropertyPicture' | 'uploadCompanyLogo' | 'uploadProfile';
}
@Component({
selector: 'app-image-crop-and-upload',
standalone: true,
imports: [CommonModule, FormsModule, RouterModule, FontAwesomeModule, ImageCropperComponent],
templateUrl: './image-crop-and-upload.component.html',
styleUrl: './image-crop-and-upload.component.scss',
})
export class ImageCropAndUploadComponent {
showModal = false;
imageChangedEvent: any = '';
croppedImage: Blob | null = null;
@Input() uploadParams: UploadParams;
uploadFinished = output<UploadReponse>();
@ViewChild('fileInput', { static: true }) fileInput!: ElementRef<HTMLInputElement>;
constructor(private imageService: ImageService, private listingsService: ListingsService) {}
ngOnInit() {}
ngOnChanges() {
this.openFileDialog();
}
openFileDialog() {
if (this.uploadParams) {
this.fileInput.nativeElement.click();
}
}
fileChangeEvent(event: any): void {
this.imageChangedEvent = event;
this.showModal = true;
}
imageCropped(event: ImageCroppedEvent) {
this.croppedImage = event.blob;
}
closeModal() {
this.imageChangedEvent = null;
this.croppedImage = null;
this.showModal = false;
this.fileInput.nativeElement.value = '';
this.uploadFinished.emit({ success: false, type: this.uploadParams.type });
}
async uploadImage() {
if (this.croppedImage) {
await this.imageService.uploadImage(this.croppedImage, this.uploadParams.type, this.uploadParams.imagePath, this.uploadParams.serialId);
this.closeModal();
this.uploadFinished.emit({ success: true, type: this.uploadParams.type });
}
}
loadImageFailed() {
console.error('Load image failed');
}
}

View File

@ -1,35 +0,0 @@
import { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { Message, MessageService } from '../../services/message.service';
import { MessageComponent } from './message.component';
@Component({
selector: 'app-message-container',
standalone: true,
imports: [CommonModule, MessageComponent],
template: `
<div class="fixed top-5 right-5 z-50 flex flex-col items-end">
<app-message
*ngFor="let message of messages"
[message]="message"
(close)="removeMessage(message)"
>
</app-message>
</div>
`,
})
export class MessageContainerComponent implements OnInit {
messages: Message[] = [];
constructor(private messageService: MessageService) {}
ngOnInit(): void {
this.messageService.messages$.subscribe((messages) => {
this.messages = messages;
});
}
removeMessage(message: Message): void {
this.messageService.removeMessage(message);
}
}

View File

@ -1,94 +0,0 @@
import { animate, style, transition, trigger } from '@angular/animations';
import { CommonModule } from '@angular/common';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Message } from '../../services/message.service';
@Component({
selector: 'app-message',
standalone: true,
imports: [CommonModule],
template: `
<div [@toastAnimation]="'in'" [ngClass]="getClasses()" role="alert">
<div class="ms-3 text-sm font-medium">{{ message.text }}</div>
<button
type="button"
(click)="onClose()"
class="ms-auto -mx-1.5 -my-1.5 rounded-lg p-1.5 inline-flex items-center justify-center h-8 w-8"
[ngClass]="getCloseButtonClasses()"
aria-label="Close"
>
<span class="sr-only">Close</span>
<svg
class="w-3 h-3"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 14 14"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"
/>
</svg>
</button>
</div>
`,
animations: [
trigger('toastAnimation', [
transition(':enter', [
style({ transform: 'translateY(100%)', opacity: 0 }),
animate(
'300ms ease-out',
style({ transform: 'translateY(0)', opacity: 1 })
),
]),
transition(':leave', [
animate(
'300ms ease-in',
style({ transform: 'translateY(100%)', opacity: 0 })
),
]),
]),
],
})
export class MessageComponent {
@Input() message!: Message;
@Output() close = new EventEmitter<void>();
onClose(): void {
this.close.emit();
}
getClasses(): string {
return `flex items-center w-full max-w-xs p-4 mb-4 text-gray-500 rounded-lg shadow ${this.getSeverityClasses()}`;
}
getCloseButtonClasses(): string {
switch (this.message.severity) {
case 'success':
return 'text-green-600 hover:bg-green-200 focus:ring-green-400';
case 'danger':
return 'text-red-600 hover:bg-red-200 focus:ring-red-400';
case 'warning':
return 'text-yellow-600 hover:bg-yellow-200 focus:ring-yellow-400';
default:
return 'text-blue-600 hover:bg-blue-200 focus:ring-blue-400';
}
}
private getSeverityClasses(): string {
switch (this.message.severity) {
case 'success':
return 'bg-green-100 text-green-700';
case 'danger':
return 'bg-red-100 text-red-700';
case 'warning':
return 'bg-yellow-100 text-yellow-700';
default:
return 'bg-blue-100 text-blue-700';
}
}
}

View File

@ -1 +0,0 @@
<p>paginator works!</p>

View File

@ -1,98 +0,0 @@
import { CommonModule } from '@angular/common';
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
@Component({
selector: 'app-paginator',
standalone: true,
imports: [CommonModule],
template: `
<nav class="my-2" aria-label="Page navigation">
<ul class="flex justify-center items-center -space-x-px h-8 text-sm">
<li>
<a
(click)="onPageChange(currentPage - 1)"
[class.pointer-events-none]="currentPage === 1"
class="flex items-center justify-center px-3 h-8 ms-0 leading-tight text-gray-500 bg-white border border-e-0 border-gray-300 rounded-s-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
>
<span class="sr-only">Previous</span>
<svg class="w-2.5 h-2.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 1 1 5l4 4" />
</svg>
</a>
</li>
<ng-container *ngFor="let page of visiblePages">
<li *ngIf="page !== '...'">
<a
(click)="onPageChange(page)"
[ngClass]="
page === currentPage
? 'z-10 flex items-center justify-center px-3 h-8 leading-tight text-blue-600 border border-blue-300 bg-blue-50 hover:bg-blue-100 hover:text-blue-700 dark:border-gray-700 dark:bg-gray-700 dark:text-white'
: 'flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white'
"
>
{{ page }}
</a>
</li>
<li *ngIf="page === '...'">
<span class="flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400">...</span>
</li>
</ng-container>
<li>
<a
(click)="onPageChange(currentPage + 1)"
[class.pointer-events-none]="currentPage === pageCount"
class="flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 rounded-e-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
>
<span class="sr-only">Next</span>
<svg class="w-2.5 h-2.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 9 4-4-4-4" />
</svg>
</a>
</li>
</ul>
</nav>
`,
})
export class PaginatorComponent implements OnChanges {
@Input() page = 1;
@Input() pageCount = 1;
@Output() pageChange = new EventEmitter<number>();
currentPage = 1;
visiblePages: (number | string)[] = [];
ngOnChanges(changes: SimpleChanges): void {
if (changes['page'] || changes['pageCount']) {
this.currentPage = this.page;
this.updateVisiblePages();
}
}
updateVisiblePages(): void {
const totalPages = this.pageCount;
const current = this.currentPage;
if (totalPages <= 6) {
this.visiblePages = Array.from({ length: totalPages }, (_, i) => i + 1);
} else {
if (current <= 3) {
this.visiblePages = [1, 2, 3, 4, '...', totalPages];
} else if (current >= totalPages - 2) {
this.visiblePages = [1, '...', totalPages - 3, totalPages - 2, totalPages - 1, totalPages];
} else {
this.visiblePages = [1, '...', current - 1, current, current + 1, '...', totalPages];
}
}
}
onPageChange(page: number | string): void {
if (typeof page === 'string') {
return;
}
if (page >= 1 && page <= this.pageCount && page !== this.currentPage) {
this.currentPage = page;
this.pageChange.emit(page);
this.updateVisiblePages();
}
}
}

View File

@ -1,394 +0,0 @@
<div
*ngIf="modalService.modalVisible$ | async"
class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full flex items-center justify-center"
>
<div class="relative w-full max-w-4xl max-h-full">
<div class="relative bg-white rounded-lg shadow">
<div class="flex items-start justify-between p-4 border-b rounded-t">
@if(criteria.criteriaType==='businessListings'){
<h3 class="text-xl font-semibold text-gray-900">
Business Listing Search
</h3>
} @else if (criteria.criteriaType==='commercialPropertyListings'){
<h3 class="text-xl font-semibold text-gray-900">
Property Listing Search
</h3>
} @else {
<h3 class="text-xl font-semibold text-gray-900">
Professional Listing Search
</h3>
}
<button
(click)="close()"
type="button"
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ml-auto inline-flex justify-center items-center"
>
<svg
class="w-3 h-3"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 14 14"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"
/>
</svg>
<span class="sr-only">Close Modal</span>
</button>
</div>
<div class="p-6 space-y-6">
<div class="flex space-x-4 mb-4">
<button
class="text-blue-600 font-medium border-b-2 border-blue-600 pb-2"
>
Classic Search
</button>
<i
data-tooltip-target="tooltip-light"
class="fa-solid fa-trash-can flex self-center ml-2 hover:cursor-pointer text-blue-500"
(click)="clearFilter()"
></i>
<div
id="tooltip-light"
role="tooltip"
class="absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg shadow-sm opacity-0 tooltip"
>
Clear all Filter
<div class="tooltip-arrow" data-popper-arrow></div>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-4">
<div>
<label
for="state"
class="block mb-2 text-sm font-medium text-gray-900"
>Location - State</label
>
<ng-select
class="custom"
[items]="selectOptions?.states"
bindLabel="name"
bindValue="value"
[ngModel]="criteria.state"
(ngModelChange)="setState($event)"
name="state"
>
</ng-select>
</div>
<!-- <div>
<app-validated-city
label="Location - City"
name="city"
[ngModel]="criteria.city"
(ngModelChange)="setCity($event)"
labelClasses="text-gray-900 font-medium"
[state]="criteria.state"
></app-validated-city>
</div> -->
<!-- New section for city search type -->
<div *ngIf="criteria.city">
<label class="block mb-2 text-sm font-medium text-gray-900"
>Search Type</label
>
<div class="flex items-center space-x-4">
<label class="inline-flex items-center">
<input
type="radio"
class="form-radio"
name="searchType"
[(ngModel)]="criteria.searchType"
value="exact"
/>
<span class="ml-2">Exact City</span>
</label>
<label class="inline-flex items-center">
<input
type="radio"
class="form-radio"
name="searchType"
[(ngModel)]="criteria.searchType"
value="radius"
/>
<span class="ml-2">Radius Search</span>
</label>
</div>
</div>
<!-- New section for radius selection -->
<div
*ngIf="criteria.city && criteria.searchType === 'radius'"
class="space-y-2"
>
<label class="block mb-2 text-sm font-medium text-gray-900"
>Select Radius (in miles)</label
>
<div class="flex flex-wrap">
@for (radius of [5, 20, 50, 100, 200, 300, 400, 500]; track
radius) {
<button
type="button"
class="px-3 py-2 text-xs font-medium text-center border border-gray-200 hover:bg-gray-500 hover:text-white"
[ngClass]="
criteria.radius === radius
? 'text-white bg-gray-500'
: 'text-gray-900 bg-white'
"
(click)="criteria.radius = radius"
>
{{ radius }}
</button>
}
</div>
</div>
<div>
<label
for="price"
class="block mb-2 text-sm font-medium text-gray-900"
>Price</label
>
<div class="flex items-center space-x-2">
<app-validated-price
name="price-from"
[(ngModel)]="criteria.minPrice"
placeholder="From"
inputClasses="bg-gray-50 text-sm !mt-0 p-2.5"
></app-validated-price>
<span>-</span>
<app-validated-price
name="price-to"
[(ngModel)]="criteria.maxPrice"
placeholder="To"
inputClasses="bg-gray-50 text-sm !mt-0 p-2.5"
></app-validated-price>
</div>
</div>
<div>
<label
for="salesRevenue"
class="block mb-2 text-sm font-medium text-gray-900"
>Sales Revenue</label
>
<div class="flex items-center space-x-2">
<app-validated-price
name="salesRevenue-from"
[(ngModel)]="criteria.minRevenue"
placeholder="From"
inputClasses="bg-gray-50 text-sm !mt-0 p-2.5"
></app-validated-price>
<span>-</span>
<app-validated-price
name="salesRevenue-to"
[(ngModel)]="criteria.maxRevenue"
placeholder="To"
inputClasses="bg-gray-50 text-sm !mt-0 p-2.5"
></app-validated-price>
</div>
</div>
<div>
<label
for="cashflow"
class="block mb-2 text-sm font-medium text-gray-900"
>Cashflow</label
>
<div class="flex items-center space-x-2">
<app-validated-price
name="cashflow-from"
[(ngModel)]="criteria.minCashFlow"
placeholder="From"
inputClasses="bg-gray-50 text-sm !mt-0 p-2.5"
></app-validated-price>
<span>-</span>
<app-validated-price
name="cashflow-to"
[(ngModel)]="criteria.maxCashFlow"
placeholder="To"
inputClasses="bg-gray-50 text-sm !mt-0 p-2.5"
></app-validated-price>
</div>
</div>
<div>
<label
for="title"
class="block mb-2 text-sm font-medium text-gray-900"
>Title / Description (Free Search)</label
>
<input
type="text"
id="title"
[(ngModel)]="criteria.title"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
placeholder="e.g. Restaurant"
/>
</div>
</div>
<div class="space-y-4">
<div>
<label class="block mb-2 text-sm font-medium text-gray-900"
>Category</label
>
<div class="grid grid-cols-2 gap-2">
@for(tob of selectOptions.typesOfBusiness; track tob){
<div class="flex items-center">
<input
type="checkbox"
id="automotive"
[ngModel]="isTypeOfBusinessClicked(tob)"
(ngModelChange)="categoryClicked($event, tob.value)"
value="{{ tob.value }}"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
/>
<label
for="automotive"
class="ml-2 text-sm font-medium text-gray-900"
>{{ tob.name }}</label
>
</div>
}
</div>
</div>
<div>
<label class="block mb-2 text-sm font-medium text-gray-900"
>Type of Property</label
>
<div class="space-y-2">
<div class="flex items-center">
<input
[(ngModel)]="criteria.realEstateChecked"
(ngModelChange)="
onCheckboxChange('realEstateChecked', $event)
"
type="checkbox"
name="realEstateChecked"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
/>
<label
for="realEstateChecked"
class="ml-2 text-sm font-medium text-gray-900"
>Real Estate</label
>
</div>
<div class="flex items-center">
<input
[(ngModel)]="criteria.leasedLocation"
(ngModelChange)="onCheckboxChange('leasedLocation', $event)"
type="checkbox"
name="leasedLocation"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
/>
<label
for="leasedLocation"
class="ml-2 text-sm font-medium text-gray-900"
>Leased Location</label
>
</div>
<div class="flex items-center">
<input
[(ngModel)]="criteria.franchiseResale"
(ngModelChange)="
onCheckboxChange('franchiseResale', $event)
"
type="checkbox"
name="franchiseResale"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
/>
<label
for="franchiseResale"
class="ml-2 text-sm font-medium text-gray-900"
>Franchise</label
>
</div>
</div>
</div>
<div>
<label
for="numberEmployees"
class="block mb-2 text-sm font-medium text-gray-900"
>Number of Employees</label
>
<div class="flex items-center space-x-2">
<input
type="number"
id="numberEmployees-from"
[(ngModel)]="criteria.minNumberEmployees"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-1/2 p-2.5"
placeholder="From"
/>
<span>-</span>
<input
type="number"
id="numberEmployees-to"
[(ngModel)]="criteria.maxNumberEmployees"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-1/2 p-2.5"
placeholder="To"
/>
</div>
</div>
<div>
<label
for="establishedSince"
class="block mb-2 text-sm font-medium text-gray-900"
>Established Since</label
>
<div class="flex items-center space-x-2">
<input
type="number"
id="establishedSince-From"
[(ngModel)]="criteria.establishedSince"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-1/2 p-2.5"
placeholder="YYYY"
/>
<span>-</span>
<input
type="number"
id="establishedSince-To"
[(ngModel)]="criteria.establishedUntil"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-1/2 p-2.5"
placeholder="YYYY"
/>
</div>
</div>
<div>
<label
for="brokername"
class="block mb-2 text-sm font-medium text-gray-900"
>Broker Name / Company Name</label
>
<input
type="text"
id="brokername"
[(ngModel)]="criteria.brokerName"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
placeholder="e.g. Brokers Invest"
/>
</div>
</div>
</div>
</div>
<div
class="flex items-center p-6 space-x-2 border-t border-gray-200 rounded-b"
>
<button
type="button"
(click)="modalService.accept()"
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center"
>
Search ({{ numberOfResults$ | async }})
</button>
<button
type="button"
(click)="close()"
class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10"
>
Cancel
</button>
</div>
</div>
</div>
</div>

View File

@ -1,9 +0,0 @@
:host ::ng-deep .ng-select.custom .ng-select-container {
--tw-bg-opacity: 1;
background-color: rgb(249 250 251 / var(--tw-bg-opacity));
height: 46px;
border-radius: 0.5rem;
.ng-value-container .ng-input {
top: 10px;
}
}

View File

@ -1,153 +0,0 @@
import { AsyncPipe, CommonModule, NgIf } from '@angular/common';
import { Component } from '@angular/core';
import { NgSelectModule } from '@ng-select/ng-select';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { debounceTime, Observable, of, Subject, Subscription } from 'rxjs';
import { BusinessListingCriteria, CountyResult, GeoResult, KeyValue, KeyValueStyle } from '../../../../../bizmatch-server/src/models/main.model';
import { ModalService } from '../../services/modal.service';
import { FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { CriteriaChangeService } from '../../services/criteria-change.service';
import { ListingsService } from '../../services/listings.service';
import { SelectOptionsService } from '../../services/select-options.service';
import { UserService } from '../../services/user.service';
import { resetBusinessListingCriteria } from '../../utils/utils';
import { ValidatedPriceComponent } from '../validated-price/validated-price.component';
@UntilDestroy()
@Component({
selector: 'app-search-modal',
standalone: true,
imports: [CommonModule, FormsModule, RouterModule, FontAwesomeModule, AsyncPipe, NgIf, NgSelectModule, ValidatedPriceComponent],
templateUrl: './search-modal.component.html',
styleUrl: './search-modal.component.scss',
})
export class SearchModalComponent {
// cities$: Observable<GeoResult[]>;
counties$: Observable<CountyResult[]>;
// cityLoading = false;
countyLoading = false;
// cityInput$ = new Subject<string>();
countyInput$ = new Subject<string>();
private criteriaChangeSubscription: Subscription;
public criteria: BusinessListingCriteria;
backupCriteria: BusinessListingCriteria;
numberOfResults$: Observable<number>;
cancelDisable = false;
constructor(public selectOptions: SelectOptionsService, public modalService: ModalService, private criteriaChangeService: CriteriaChangeService, private listingService: ListingsService, private userService: UserService) {}
ngOnInit() {
this.setupCriteriaChangeListener();
this.modalService.message$.pipe(untilDestroyed(this)).subscribe(msg => {
this.criteria = msg as BusinessListingCriteria;
this.backupCriteria = JSON.parse(JSON.stringify(msg));
this.setTotalNumberOfResults();
});
this.modalService.modalVisible$.pipe(untilDestroyed(this)).subscribe(val => {
if (val) {
this.criteria.page = 1;
this.criteria.start = 0;
}
});
// this.loadCities();
// this.loadCounties();
}
ngOnChanges() {}
categoryClicked(checked: boolean, value: string) {
if (checked) {
this.criteria.types.push(value);
} else {
const index = this.criteria.types.findIndex(t => t === value);
if (index > -1) {
this.criteria.types.splice(index, 1);
}
}
}
// private loadCounties() {
// this.counties$ = concat(
// of([]), // default items
// this.countyInput$.pipe(
// distinctUntilChanged(),
// tap(() => (this.countyLoading = true)),
// switchMap(term =>
// this.geoService.findCountiesStartingWith(term).pipe(
// catchError(() => of([])), // empty list on error
// map(counties => counties.map(county => county.name)), // transform the list of objects to a list of city names
// tap(() => (this.countyLoading = false)),
// ),
// ),
// ),
// );
// }
setCity(city) {
if (city) {
this.criteria.city = city;
this.criteria.state = city.state;
} else {
this.criteria.city = null;
this.criteria.radius = null;
this.criteria.searchType = 'exact';
}
}
setState(state: string) {
if (state) {
this.criteria.state = state;
} else {
this.criteria.state = null;
this.setCity(null);
}
}
private setupCriteriaChangeListener() {
this.criteriaChangeSubscription = this.criteriaChangeService.criteriaChange$.pipe(debounceTime(400)).subscribe(() => {
this.setTotalNumberOfResults();
this.cancelDisable = true;
});
}
trackByFn(item: GeoResult) {
return item.id;
}
search() {
console.log('Search criteria:', this.criteria);
}
// getCounties() {
// this.geoService.findCountiesStartingWith('');
// }
closeModal() {
console.log('Closing modal');
}
isTypeOfBusinessClicked(v: KeyValueStyle) {
return this.criteria.types.find(t => t === v.value);
}
isTypeOfProfessionalClicked(v: KeyValue) {
return this.criteria.types.find(t => t === v.value);
}
setTotalNumberOfResults() {
if (this.criteria) {
console.log(`Getting total number of results for ${this.criteria.criteriaType}`);
if (this.criteria.criteriaType === 'businessListings' || this.criteria.criteriaType === 'commercialPropertyListings') {
this.numberOfResults$ = this.listingService.getNumberOfListings(this.criteria);
} else if (this.criteria.criteriaType === 'brokerListings') {
this.numberOfResults$ = this.userService.getNumberOfBroker();
} else {
this.numberOfResults$ = of();
}
}
}
clearFilter() {
resetBusinessListingCriteria(this.criteria);
}
close() {
this.modalService.reject(this.backupCriteria);
}
onCheckboxChange(checkbox: string, value: boolean) {
// Deaktivieren Sie alle Checkboxes
(<BusinessListingCriteria>this.criteria).realEstateChecked = false;
(<BusinessListingCriteria>this.criteria).leasedLocation = false;
(<BusinessListingCriteria>this.criteria).franchiseResale = false;
// Aktivieren Sie nur die aktuell ausgewählte Checkbox
this.criteria[checkbox] = value;
}
}

View File

@ -1,3 +0,0 @@
<div [id]="id" role="tooltip" class="max-w-72 w-max absolute z-50 invisible inline-block px-3 py-2 text-sm font-medium text-white transition-opacity duration-300 bg-gray-900 rounded-lg shadow-sm opacity-0 dark:bg-gray-700">
{{ text }}
</div>

View File

@ -1,29 +0,0 @@
/* Diese Styles kannst du in deine globale styles.css oder in eine eigene tooltip.component.css-Datei packen */
.tooltip-arrow {
position: absolute;
width: 8px;
height: 8px;
background: inherit;
transform: rotate(45deg);
}
.arrow-top {
top: -4px;
left: calc(50% - 4px);
}
.arrow-right {
right: -4px;
top: calc(50% - 4px);
}
.arrow-bottom {
bottom: -4px;
left: calc(50% - 4px);
}
.arrow-left {
left: -4px;
top: calc(50% - 4px);
}

View File

@ -1,167 +0,0 @@
import { CommonModule } from '@angular/common';
import { AfterViewInit, Component, ElementRef, Input, OnChanges, OnDestroy, Renderer2, SimpleChanges } from '@angular/core';
@Component({
selector: 'app-tooltip',
standalone: true,
imports: [CommonModule],
templateUrl: './tooltip.component.html',
})
export class TooltipComponent implements AfterViewInit, OnDestroy, OnChanges {
@Input() id: string;
@Input() text: string;
@Input() isVisible: boolean = false;
@Input() position: 'top' | 'right' | 'bottom' | 'left' = 'top';
private tooltipElement: HTMLElement | null = null;
private arrowElement: HTMLElement | null = null;
private resizeObserver: ResizeObserver | null = null;
private parentElement: HTMLElement | null = null;
constructor(private elementRef: ElementRef, private renderer: Renderer2) {}
ngAfterViewInit() {
this.tooltipElement = document.getElementById(this.id);
this.parentElement = this.elementRef.nativeElement.parentElement;
if (this.tooltipElement && this.parentElement) {
// Create arrow element
this.arrowElement = this.renderer.createElement('div');
this.renderer.addClass(this.arrowElement, 'tooltip-arrow');
this.renderer.appendChild(this.tooltipElement, this.arrowElement);
// Setup resize observer
this.setupResizeObserver();
// Initial positioning
this.updatePosition();
// Initial visibility
this.updateTooltipVisibility();
}
}
ngOnChanges(changes: SimpleChanges) {
if (changes['isVisible'] && this.tooltipElement) {
this.updateTooltipVisibility();
}
if ((changes['position'] || changes['isVisible']) && this.isVisible) {
setTimeout(() => this.updatePosition(), 0);
}
}
ngOnDestroy() {
if (this.resizeObserver) {
this.resizeObserver.disconnect();
}
}
private setupResizeObserver() {
if (window.ResizeObserver && this.parentElement) {
this.resizeObserver = new ResizeObserver(() => {
if (this.isVisible) {
this.updatePosition();
}
});
this.resizeObserver.observe(this.parentElement);
if (this.tooltipElement) {
this.resizeObserver.observe(this.tooltipElement);
}
}
}
private updateTooltipVisibility() {
if (!this.tooltipElement) return;
if (this.isVisible) {
this.renderer.removeClass(this.tooltipElement, 'invisible');
this.renderer.removeClass(this.tooltipElement, 'opacity-0');
this.renderer.addClass(this.tooltipElement, 'visible');
this.renderer.addClass(this.tooltipElement, 'opacity-100');
this.updatePosition();
} else {
this.renderer.removeClass(this.tooltipElement, 'visible');
this.renderer.removeClass(this.tooltipElement, 'opacity-100');
this.renderer.addClass(this.tooltipElement, 'invisible');
this.renderer.addClass(this.tooltipElement, 'opacity-0');
}
}
private updatePosition() {
if (!this.tooltipElement || !this.parentElement || !this.arrowElement) return;
const parentRect = this.parentElement.getBoundingClientRect();
const tooltipRect = this.tooltipElement.getBoundingClientRect();
// Reset any previous positioning
this.renderer.removeStyle(this.tooltipElement, 'top');
this.renderer.removeStyle(this.tooltipElement, 'right');
this.renderer.removeStyle(this.tooltipElement, 'bottom');
this.renderer.removeStyle(this.tooltipElement, 'left');
// Reset arrow classes
this.renderer.removeClass(this.arrowElement, 'arrow-top');
this.renderer.removeClass(this.arrowElement, 'arrow-right');
this.renderer.removeClass(this.arrowElement, 'arrow-bottom');
this.renderer.removeClass(this.arrowElement, 'arrow-left');
let top: number = 0;
let left: number = 0;
switch (this.position) {
case 'top':
top = -tooltipRect.height - 8;
left = (parentRect.width - tooltipRect.width) / 2;
this.renderer.addClass(this.arrowElement, 'arrow-bottom');
break;
case 'right':
top = (parentRect.height - tooltipRect.height) / 2;
left = parentRect.width + 8;
this.renderer.addClass(this.arrowElement, 'arrow-left');
break;
case 'bottom':
top = parentRect.height + 8;
left = (parentRect.width - tooltipRect.width) / 2;
this.renderer.addClass(this.arrowElement, 'arrow-top');
break;
case 'left':
top = (parentRect.height - tooltipRect.height) / 2;
left = -tooltipRect.width - 8;
this.renderer.addClass(this.arrowElement, 'arrow-right');
break;
}
// Apply positioning
this.renderer.setStyle(this.tooltipElement, 'top', `${top}px`);
this.renderer.setStyle(this.tooltipElement, 'left', `${left}px`);
// Make sure the tooltip stays within viewport
this.adjustToViewport();
}
private adjustToViewport() {
if (!this.tooltipElement) return;
const tooltipRect = this.tooltipElement.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
// Adjust horizontal position
if (tooltipRect.left < 0) {
this.renderer.setStyle(this.tooltipElement, 'left', '0px');
} else if (tooltipRect.right > viewportWidth) {
const newLeft = Math.max(0, viewportWidth - tooltipRect.width);
this.renderer.setStyle(this.tooltipElement, 'left', `${newLeft}px`);
}
// Adjust vertical position
if (tooltipRect.top < 0) {
this.renderer.setStyle(this.tooltipElement, 'top', '0px');
} else if (tooltipRect.bottom > viewportHeight) {
const newTop = Math.max(0, viewportHeight - tooltipRect.height);
this.renderer.setStyle(this.tooltipElement, 'top', `${newTop}px`);
}
}
}

View File

@ -1,31 +0,0 @@
<div>
<label for="type" class="block text-sm font-bold text-gray-700 mb-1 relative w-fit {{ labelClasses }}"
>{{ label }} @if(validationMessage){
<div
attr.data-tooltip-target="tooltip-{{ name }}"
class="absolute inline-flex items-center justify-center w-6 h-6 text-xs font-bold text-white bg-red-500 border-2 border-white rounded-full -top-2 dark:border-gray-900 hover:cursor-pointer"
(click)="toggleTooltip($event)"
(touchstart)="toggleTooltip($event)"
>
!
</div>
<app-tooltip id="tooltip-{{ name }}" [text]="validationMessage" [isVisible]="isTooltipVisible"></app-tooltip>
}
</label>
<ng-select
class="custom"
[multiple]="false"
[hideSelected]="true"
[trackByFn]="trackByFn"
[minTermLength]="2"
[loading]="cityLoading"
typeToSearchText="Please enter 2 or more characters"
[typeahead]="cityInput$"
ngModel="{{ value?.name }} {{ value ? '-' : '' }} {{ value?.state }}"
(ngModelChange)="onInputChange($event)"
>
@for (city of cities$ | async; track city.id) {
<ng-option [value]="city">{{ city.name }} - {{ city.state }}</ng-option>
}
</ng-select>
</div>

View File

@ -1,9 +0,0 @@
:host ::ng-deep .ng-select.custom .ng-select-container {
// --tw-bg-opacity: 1;
// background-color: rgb(249 250 251 / var(--tw-bg-opacity));
// height: 42px;
border-radius: 0.5rem;
.ng-value-container .ng-input {
top: 10px;
}
}

View File

@ -1,70 +0,0 @@
import { CommonModule } from '@angular/common';
import { Component, forwardRef, Input } from '@angular/core';
import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
import { NgSelectModule } from '@ng-select/ng-select';
import { catchError, concat, distinctUntilChanged, Observable, of, Subject, switchMap, tap } from 'rxjs';
import { GeoResult } from '../../../../../bizmatch-server/src/models/main.model';
import { City } from '../../../../../bizmatch-server/src/models/server.model';
import { GeoService } from '../../services/geo.service';
import { SelectOptionsService } from '../../services/select-options.service';
import { BaseInputComponent } from '../base-input/base-input.component';
import { TooltipComponent } from '../tooltip/tooltip.component';
import { ValidationMessagesService } from '../validation-messages.service';
@Component({
selector: 'app-validated-city',
standalone: true,
imports: [CommonModule, FormsModule, NgSelectModule, TooltipComponent],
templateUrl: './validated-city.component.html',
styleUrl: './validated-city.component.scss',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ValidatedCityComponent),
multi: true,
},
],
})
export class ValidatedCityComponent extends BaseInputComponent {
@Input() items;
@Input() labelClasses: string;
@Input() state: string;
cities$: Observable<GeoResult[]>;
cityInput$ = new Subject<string>();
countyInput$ = new Subject<string>();
cityLoading = false;
constructor(validationMessagesService: ValidationMessagesService, private geoService: GeoService, public selectOptions: SelectOptionsService) {
super(validationMessagesService);
}
override ngOnInit() {
super.ngOnInit();
this.loadCities();
}
onInputChange(event: City): void {
this.value = event; //{ ...event, longitude: parseFloat(event.longitude), latitude: parseFloat(event.latitude) };
this.onChange(this.value);
}
private loadCities() {
this.cities$ = concat(
of([]), // default items
this.cityInput$.pipe(
distinctUntilChanged(),
tap(() => (this.cityLoading = true)),
switchMap(term =>
this.geoService.findCitiesStartingWith(term, this.state).pipe(
catchError(() => of([])), // empty list on error
// map(cities => cities.map(city => city.city)), // transform the list of objects to a list of city names
tap(() => (this.cityLoading = false)),
),
),
),
);
}
trackByFn(item: GeoResult) {
return item.id;
}
compareFn = (item, selected) => {
return item.id === selected.id;
};
}

View File

@ -1,34 +0,0 @@
<div>
@if(label){
<label for="type" class="block text-sm font-bold text-gray-700 mb-1 relative w-fit {{ labelClasses }}"
>{{ label }} @if(validationMessage){
<div
attr.data-tooltip-target="tooltip-{{ name }}"
class="absolute inline-flex items-center justify-center w-6 h-6 text-xs font-bold text-white bg-red-500 border-2 border-white rounded-full -top-2 dark:border-gray-900 hover:cursor-pointer"
(click)="toggleTooltip($event)"
(touchstart)="toggleTooltip($event)"
>
!
</div>
<app-tooltip id="tooltip-{{ name }}" [text]="validationMessage" [isVisible]="isTooltipVisible"></app-tooltip>
}
</label>
}
<ng-select
class="custom"
[multiple]="false"
[hideSelected]="true"
[trackByFn]="trackByFn"
[minTermLength]="2"
[loading]="countyLoading"
typeToSearchText="Please enter 2 or more characters"
[typeahead]="countyInput$"
ngModel="{{ value }}"
(ngModelChange)="onInputChange($event)"
[readonly]="readonly"
>
@for (county of counties$ | async; track county.id) {
<ng-option [value]="county">{{ county }}</ng-option>
}
</ng-select>
</div>

View File

@ -1,9 +0,0 @@
:host ::ng-deep .ng-select.custom .ng-select-container {
// --tw-bg-opacity: 1;
// background-color: rgb(249 250 251 / var(--tw-bg-opacity));
// height: 42px;
border-radius: 0.5rem;
.ng-value-container .ng-input {
top: 10px;
}
}

View File

@ -1,70 +0,0 @@
import { CommonModule } from '@angular/common';
import { Component, forwardRef, Input } from '@angular/core';
import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
import { NgSelectModule } from '@ng-select/ng-select';
import { catchError, concat, distinctUntilChanged, map, Observable, of, Subject, switchMap, tap } from 'rxjs';
import { CountyResult, GeoResult } from '../../../../../bizmatch-server/src/models/main.model';
import { City } from '../../../../../bizmatch-server/src/models/server.model';
import { GeoService } from '../../services/geo.service';
import { SelectOptionsService } from '../../services/select-options.service';
import { BaseInputComponent } from '../base-input/base-input.component';
import { TooltipComponent } from '../tooltip/tooltip.component';
import { ValidationMessagesService } from '../validation-messages.service';
@Component({
selector: 'app-validated-county',
standalone: true,
imports: [CommonModule, FormsModule, NgSelectModule, TooltipComponent],
templateUrl: './validated-county.component.html',
styleUrl: './validated-county.component.scss',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ValidatedCountyComponent),
multi: true,
},
],
})
export class ValidatedCountyComponent extends BaseInputComponent {
@Input() items;
@Input() labelClasses: string;
@Input() state: string;
@Input() readonly = false;
counties$: Observable<CountyResult[]>;
countyLoading = false;
countyInput$ = new Subject<string>();
constructor(validationMessagesService: ValidationMessagesService, private geoService: GeoService, public selectOptions: SelectOptionsService) {
super(validationMessagesService);
}
override ngOnInit() {
super.ngOnInit();
this.loadCounties();
}
onInputChange(event: City): void {
this.value = event; //{ ...event, longitude: parseFloat(event.longitude), latitude: parseFloat(event.latitude) };
this.onChange(this.value);
}
private loadCounties() {
this.counties$ = concat(
of([]), // default items
this.countyInput$.pipe(
distinctUntilChanged(),
tap(() => (this.countyLoading = true)),
switchMap(term =>
this.geoService.findCountiesStartingWith(term, this.state ? [this.state] : null).pipe(
catchError(() => of([])), // empty list on error
map(counties => counties.map(county => county.name)), // transform the list of objects to a list of city names
tap(() => (this.countyLoading = false)),
),
),
),
);
}
trackByFn(item: GeoResult) {
return item.id;
}
compareFn = (item, selected) => {
return item.id === selected.id;
};
}

View File

@ -1,27 +0,0 @@
<div>
<label [for]="name" class="block text-sm font-bold text-gray-700 mb-1 relative w-fit">
{{ label }}
@if(validationMessage){
<div
attr.data-tooltip-target="tooltip-{{ name }}"
class="absolute inline-flex items-center justify-center w-6 h-6 text-xs font-bold text-white bg-red-500 border-2 border-white rounded-full -top-2 dark:border-gray-900 hover:cursor-pointer"
(click)="toggleTooltip($event)"
(touchstart)="toggleTooltip($event)"
>
!
</div>
<app-tooltip id="tooltip-{{ name }}" [text]="validationMessage" [isVisible]="isTooltipVisible"></app-tooltip>
}
</label>
<input
type="text"
[id]="name"
[ngModel]="value"
(ngModelChange)="onInputChange($event)"
(blur)="onTouched()"
[attr.name]="name"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
[mask]="mask"
[dropSpecialCharacters]="false"
/>
</div>

View File

@ -1,44 +0,0 @@
import { CommonModule } from '@angular/common';
import { Component, forwardRef, Input } from '@angular/core';
import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
import { NgxMaskDirective, provideNgxMask } from 'ngx-mask';
import { BaseInputComponent } from '../base-input/base-input.component';
import { TooltipComponent } from '../tooltip/tooltip.component';
import { ValidationMessagesService } from '../validation-messages.service';
@Component({
selector: 'app-validated-input',
templateUrl: './validated-input.component.html',
standalone: true,
imports: [CommonModule, FormsModule, TooltipComponent, NgxMaskDirective],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ValidatedInputComponent),
multi: true,
},
provideNgxMask(),
],
})
export class ValidatedInputComponent extends BaseInputComponent {
@Input() kind: 'text' | 'number' | 'email' = 'text';
@Input() mask: string;
constructor(validationMessagesService: ValidationMessagesService) {
super(validationMessagesService);
}
onInputChange(event: string | number): void {
if (this.kind === 'number') {
if (typeof event === 'number') {
this.value = event;
} else {
this.value = parseFloat(event);
}
} else {
const text = event as string;
this.value = text?.length > 0 ? event : null;
}
// this.value = event?.length > 0 ? (this.kind === 'number' ? parseFloat(event) : event) : null;
this.onChange(this.value);
}
}

View File

@ -1,31 +0,0 @@
<div>
<label for="type" class="block text-sm font-bold text-gray-700 mb-1 relative w-fit {{ labelClasses }}"
>{{ label }} @if(validationMessage){
<div
attr.data-tooltip-target="tooltip-{{ name }}"
class="absolute inline-flex items-center justify-center w-6 h-6 text-xs font-bold text-white bg-red-500 border-2 border-white rounded-full -top-2 dark:border-gray-900 hover:cursor-pointer"
(click)="toggleTooltip($event)"
(touchstart)="toggleTooltip($event)"
>
!
</div>
<app-tooltip id="tooltip-{{ name }}" [text]="validationMessage" [isVisible]="isTooltipVisible"></app-tooltip>
}
</label>
<ng-select
class="custom"
[multiple]="false"
[hideSelected]="true"
[trackByFn]="trackByFn"
[minTermLength]="2"
[loading]="placeLoading"
typeToSearchText="Please enter 2 or more characters"
[typeahead]="placeInput$"
ngModel="{{ formatGeoAddress(value) }}"
(ngModelChange)="onInputChange($event)"
>
@for (place of places$ | async; track place.place_id) {
<ng-option [value]="place">{{ formatPlaceAddress(place) }}</ng-option>
}
</ng-select>
</div>

View File

@ -1,9 +0,0 @@
:host ::ng-deep .ng-select.custom .ng-select-container {
// --tw-bg-opacity: 1;
// background-color: rgb(249 250 251 / var(--tw-bg-opacity));
// height: 42px;
border-radius: 0.5rem;
.ng-value-container .ng-input {
top: 10px;
}
}

View File

@ -1,159 +0,0 @@
import { CommonModule } from '@angular/common';
import { Component, forwardRef, Input } from '@angular/core';
import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
import { NgSelectModule } from '@ng-select/ng-select';
import { catchError, concat, debounceTime, distinctUntilChanged, Observable, of, Subject, switchMap, tap } from 'rxjs';
import { GeoResult } from '../../../../../bizmatch-server/src/models/main.model';
import { Place } from '../../../../../bizmatch-server/src/models/server.model';
import { GeoService } from '../../services/geo.service';
import { SelectOptionsService } from '../../services/select-options.service';
import { BaseInputComponent } from '../base-input/base-input.component';
import { TooltipComponent } from '../tooltip/tooltip.component';
import { ValidationMessagesService } from '../validation-messages.service';
@Component({
selector: 'app-validated-location',
standalone: true,
imports: [CommonModule, FormsModule, NgSelectModule, TooltipComponent],
templateUrl: './validated-location.component.html',
styleUrl: './validated-location.component.scss',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ValidatedLocationComponent),
multi: true,
},
],
})
export class ValidatedLocationComponent extends BaseInputComponent {
@Input() items;
@Input() labelClasses: string;
places$: Observable<Place[]>;
placeInput$ = new Subject<string>();
placeLoading = false;
constructor(validationMessagesService: ValidationMessagesService, private geoService: GeoService, public selectOptions: SelectOptionsService) {
super(validationMessagesService);
}
override ngOnInit() {
super.ngOnInit();
this.loadCities();
}
onInputChange(event: Place): void {
this.value = event; //{ ...event, longitude: parseFloat(event.longitude), latitude: parseFloat(event.latitude) };
if (event) {
this.value = {
id: event?.place_id,
name: event?.address.city,
county: event?.address.county,
street: event?.address.road,
housenumber: event?.address.house_number,
state: event?.address['ISO3166-2-lvl4'].substr(3),
latitude: event ? parseFloat(event?.lat) : undefined,
longitude: event ? parseFloat(event?.lon) : undefined,
};
}
this.onChange(this.value);
}
private loadCities() {
this.places$ = concat(
of([]), // default items
this.placeInput$.pipe(
debounceTime(300),
distinctUntilChanged(),
tap(() => (this.placeLoading = true)),
switchMap(term =>
this.geoService.findLocationStartingWith(term).pipe(
catchError(() => of([])), // empty list on error
// map(cities => cities.map(city => city.city)), // transform the list of objects to a list of city names
tap(() => (this.placeLoading = false)),
),
),
),
);
}
trackByFn(item: GeoResult) {
return item.id;
}
compareFn = (item, selected) => {
return item.id === selected.id;
};
formatGeoAddress(geoResult: GeoResult | null | undefined): string {
// Überprüfen, ob geoResult null oder undefined ist
if (!geoResult) {
return '';
}
let addressParts: string[] = [];
// Füge Hausnummer hinzu, wenn vorhanden
if (geoResult.housenumber) {
addressParts.push(geoResult.housenumber);
}
// Füge Straße hinzu, wenn vorhanden
if (geoResult.street) {
addressParts.push(geoResult.street);
}
// Kombiniere Hausnummer und Straße
let address = addressParts.join(' ');
// Füge Namen hinzu, wenn vorhanden
if (geoResult.name) {
address = address ? `${address}, ${geoResult.name}` : geoResult.name;
}
// Füge County hinzu, wenn vorhanden
if (geoResult.county) {
address = address ? `${address}, ${geoResult.county}` : geoResult.county;
}
// Füge Bundesland hinzu, wenn vorhanden
if (geoResult.state) {
address = address ? `${address} - ${geoResult.state}` : geoResult.state;
}
return address;
}
formatPlaceAddress(place: Place | null | undefined): string {
// Überprüfen, ob place null oder undefined ist
if (!place) {
return '';
}
const { house_number, road, city, county, state } = place.address;
let addressParts: string[] = [];
// Füge Hausnummer hinzu, wenn vorhanden
if (house_number) {
addressParts.push(house_number);
}
// Füge Straße hinzu, wenn vorhanden
if (road) {
addressParts.push(road);
}
// Kombiniere Hausnummer und Straße
let address = addressParts.join(' ');
// Füge Stadt hinzu, wenn vorhanden
if (city) {
address = address ? `${address}, ${city}` : city;
}
// Füge County hinzu, wenn vorhanden
if (county) {
address = address ? `${address}, ${county}` : county;
}
// Füge Bundesland hinzu, wenn vorhanden
if (state) {
address = address ? `${address} - ${state}` : state;
}
return address;
}
}

View File

@ -1 +0,0 @@
<ng-select [items]="items" bindLabel="name" bindValue="value" [(ngModel)]="value" (ngModelChange)="onInputChange($event)" name="type"> </ng-select>

View File

@ -1,31 +0,0 @@
import { CommonModule } from '@angular/common';
import { Component, forwardRef, Input } from '@angular/core';
import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
import { NgSelectModule } from '@ng-select/ng-select';
import { BaseInputComponent } from '../base-input/base-input.component';
import { TooltipComponent } from '../tooltip/tooltip.component';
import { ValidationMessagesService } from '../validation-messages.service';
@Component({
selector: 'app-validated-ng-select',
standalone: true,
imports: [CommonModule, FormsModule, NgSelectModule, TooltipComponent],
templateUrl: './validated-ng-select.component.html',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ValidatedNgSelectComponent),
multi: true,
},
],
})
export class ValidatedNgSelectComponent extends BaseInputComponent {
@Input() items;
constructor(validationMessagesService: ValidationMessagesService) {
super(validationMessagesService);
}
onInputChange(event: Event): void {
this.value = event;
this.onChange(this.value);
}
}

View File

@ -1,31 +0,0 @@
<div>
@if(label){
<label [for]="name" class="block text-sm font-bold text-gray-700 mb-1 relative w-fit">
{{ label }}
@if(validationMessage){
<div
attr.data-tooltip-target="tooltip-{{ name }}"
class="absolute inline-flex items-center justify-center w-6 h-6 text-xs font-bold text-white bg-red-500 border-2 border-white rounded-full -top-2 dark:border-gray-900 hover:cursor-pointer"
(click)="toggleTooltip($event)"
(touchstart)="toggleTooltip($event)"
>
!
</div>
<app-tooltip id="tooltip-{{ name }}" [text]="validationMessage" [isVisible]="isTooltipVisible"></app-tooltip>
}
</label>
}
<input
type="text"
[id]="name"
[ngModel]="value"
(ngModelChange)="onInputChange($event)"
(blur)="onTouched()"
[attr.name]="name"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 {{ inputClasses }}"
[options]="{ prefix: '$', thousands: ',', decimal: '.', precision: 0, align: 'left' }"
currencyMask
autocomplete="off"
[placeholder]="placeholder"
/>
</div>

View File

@ -1,34 +0,0 @@
import { CommonModule } from '@angular/common';
import { Component, forwardRef, Input } from '@angular/core';
import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
import { NgxCurrencyDirective } from 'ngx-currency';
import { BaseInputComponent } from '../base-input/base-input.component';
import { TooltipComponent } from '../tooltip/tooltip.component';
import { ValidationMessagesService } from '../validation-messages.service';
@Component({
selector: 'app-validated-price',
standalone: true,
imports: [CommonModule, FormsModule, TooltipComponent, NgxCurrencyDirective],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ValidatedPriceComponent),
multi: true,
},
],
templateUrl: './validated-price.component.html',
styles: `:host{width:100%}`,
})
export class ValidatedPriceComponent extends BaseInputComponent {
@Input() inputClasses: string;
@Input() placeholder: string = '';
constructor(validationMessagesService: ValidationMessagesService) {
super(validationMessagesService);
}
onInputChange(event: Event): void {
this.value = !event ? null : event;
this.onChange(this.value);
}
}

View File

@ -1,15 +0,0 @@
<label [for]="name" class="block text-sm font-bold text-gray-700 mb-1 relative w-fit">
{{ label }}
@if(validationMessage){
<div
attr.data-tooltip-target="tooltip-{{ name }}"
class="absolute inline-flex items-center justify-center w-6 h-6 text-xs font-bold text-white bg-red-500 border-2 border-white rounded-full -top-2 dark:border-gray-900 hover:cursor-pointer"
(click)="toggleTooltip($event)"
(touchstart)="toggleTooltip($event)"
>
!
</div>
<app-tooltip id="tooltip-{{ name }}" [text]="validationMessage" [isVisible]="isTooltipVisible"></app-tooltip>
}
</label>
<quill-editor [(ngModel)]="value" (ngModelChange)="onInputChange($event)" (onBlur)="onTouched()" [id]="name" [attr.name]="name" [modules]="quillModules"></quill-editor>

View File

@ -1,37 +0,0 @@
import { CommonModule } from '@angular/common';
import { Component, forwardRef } from '@angular/core';
import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
import { QuillModule } from 'ngx-quill';
import { BaseInputComponent } from '../base-input/base-input.component';
import { TooltipComponent } from '../tooltip/tooltip.component';
import { ValidationMessagesService } from '../validation-messages.service';
@Component({
selector: 'app-validated-quill',
templateUrl: './validated-quill.component.html',
styles: `quill-editor {
width: 100%;
}`,
standalone: true,
imports: [CommonModule, FormsModule, QuillModule, TooltipComponent],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ValidatedQuillComponent),
multi: true,
},
],
})
export class ValidatedQuillComponent extends BaseInputComponent {
quillModules = {
toolbar: [['bold', 'italic', 'underline', 'strike'], [{ list: 'ordered' }, { list: 'bullet' }], [{ header: [1, 2, 3, 4, 5, 6, false] }], [{ color: [] }, { background: [] }], ['clean']],
};
constructor(validationMessagesService: ValidationMessagesService) {
super(validationMessagesService);
}
onInputChange(event: Event): void {
this.value = event;
this.onChange(this.value);
}
}

View File

@ -1,30 +0,0 @@
<div>
<label [for]="name" class="block text-sm font-bold text-gray-700 mb-1 relative w-fit">
{{ label }}
@if(validationMessage){
<div
attr.data-tooltip-target="tooltip-{{ name }}"
class="absolute inline-flex items-center justify-center w-6 h-6 text-xs font-bold text-white bg-red-500 border-2 border-white rounded-full -top-2 dark:border-gray-900 hover:cursor-pointer"
(click)="toggleTooltip($event)"
(touchstart)="toggleTooltip($event)"
>
!
</div>
<app-tooltip id="tooltip-{{ name }}" [text]="validationMessage" [isVisible]="isTooltipVisible"></app-tooltip>
}
</label>
<select
[disabled]="disabled"
[id]="name"
[name]="name"
[ngModel]="value"
(change)="onSelectChange($event)"
(blur)="onTouched()"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
>
<option value="" disabled selected>Select an option</option>
<option *ngFor="let option of options" [value]="option.value">
{{ option.label }}
</option>
</select>
</div>

View File

@ -1,36 +0,0 @@
import { CommonModule } from '@angular/common';
import { Component, EventEmitter, forwardRef, Input, Output } from '@angular/core';
import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
import { BaseInputComponent } from '../base-input/base-input.component';
import { TooltipComponent } from '../tooltip/tooltip.component';
import { ValidationMessagesService } from '../validation-messages.service';
@Component({
selector: 'app-validated-select',
templateUrl: './validated-select.component.html',
standalone: true,
imports: [CommonModule, FormsModule, TooltipComponent],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ValidatedSelectComponent),
multi: true,
},
],
})
export class ValidatedSelectComponent extends BaseInputComponent {
@Input() options: Array<{ value: any; label: string }> = [];
@Input() disabled = false;
@Output() valueChange = new EventEmitter<any>();
constructor(validationMessagesService: ValidationMessagesService) {
super(validationMessagesService);
}
onSelectChange(event: Event): void {
const value = (event.target as HTMLSelectElement).value;
this.value = value;
this.onChange(value);
this.valueChange.emit(value);
}
}

View File

@ -1,17 +0,0 @@
<div>
<label [for]="name" class="block text-sm font-bold text-gray-700 mb-1 relative w-fit">
{{ label }}
@if(validationMessage){
<div
attr.data-tooltip-target="tooltip-{{ name }}"
class="absolute inline-flex items-center justify-center w-6 h-6 text-xs font-bold text-white bg-red-500 border-2 border-white rounded-full -top-2 dark:border-gray-900 hover:cursor-pointer"
(click)="toggleTooltip($event)"
(touchstart)="toggleTooltip($event)"
>
!
</div>
<app-tooltip id="tooltip-{{ name }}" [text]="validationMessage" [isVisible]="isTooltipVisible"></app-tooltip>
}
</label>
<textarea [id]="name" [ngModel]="value" (ngModelChange)="onInputChange($event)" [attr.name]="name" class="w-full p-2 border border-gray-300 rounded-md" rows="3"></textarea>
</div>

View File

@ -1,30 +0,0 @@
import { CommonModule } from '@angular/common';
import { Component, forwardRef } from '@angular/core';
import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
import { BaseInputComponent } from '../base-input/base-input.component';
import { TooltipComponent } from '../tooltip/tooltip.component';
import { ValidationMessagesService } from '../validation-messages.service';
@Component({
selector: 'app-validated-textarea',
templateUrl: './validated-textarea.component.html',
standalone: true,
imports: [CommonModule, FormsModule, TooltipComponent],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ValidatedTextareaComponent),
multi: true,
},
],
})
export class ValidatedTextareaComponent extends BaseInputComponent {
constructor(validationMessagesService: ValidationMessagesService) {
super(validationMessagesService);
}
onInputChange(event: string): void {
this.value = event?.length > 0 ? event : null;
this.onChange(this.value);
}
}

View File

@ -1,31 +0,0 @@
import { Injectable, InjectionToken } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
export interface ValidationMessage {
field: string;
message: string;
}
export const VALIDATION_MESSAGES = new InjectionToken<ValidationMessagesService>('VALIDATION_MESSAGES');
@Injectable({
providedIn: 'root',
})
export class ValidationMessagesService {
private messagesSubject = new BehaviorSubject<ValidationMessage[]>([]);
messages$: Observable<ValidationMessage[]> = this.messagesSubject.asObservable();
updateMessages(messages: ValidationMessage[]): void {
this.messagesSubject.next(messages);
}
clearMessages(): void {
this.messagesSubject.next([]);
}
getMessage(field: string): string | null {
const messages = this.messagesSubject.value;
const message = messages.find(m => m.field === field);
return message ? message.message : null;
}
}

View File

@ -1,21 +0,0 @@
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { AuthService } from '../services/auth.service';
import { createLogger } from '../utils/utils';
const logger = createLogger('AuthGuard');
@Injectable({
providedIn: 'root',
})
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router) {}
async canActivate(): Promise<boolean> {
const token = await this.authService.getToken();
if (token) {
return true;
} else {
this.router.navigate(['/login-register']);
return false;
}
}
}

View File

@ -1,35 +0,0 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
import { Observable, of } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import { environment } from '../../environments/environment';
@Injectable({
providedIn: 'root',
})
export class ListingCategoryGuard implements CanActivate {
private apiBaseUrl = environment.apiBaseUrl;
constructor(private http: HttpClient, private router: Router) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
const id = route.paramMap.get('id');
const url = `${this.apiBaseUrl}/bizmatch/listings/undefined/${id}`;
return this.http.get<any>(url).pipe(
tap(response => {
const category = response.listingsCategory;
if (category === 'business') {
this.router.navigate(['details-business-listing', id]);
} else if (category === 'commercialProperty') {
this.router.navigate(['details-commercial-property-listing', id]);
} else {
this.router.navigate(['not-found']);
}
}),
catchError(() => {
return of(this.router.createUrlTree(['/not-found']));
}),
);
}
}

View File

@ -1,34 +0,0 @@
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, from } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { environment } from '../../environments/environment';
import { AuthService } from '../services/auth.service';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(private authService: AuthService) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// Prüfe, ob die Anfrage an die apiBaseUrl gerichtet ist
const isApiRequest = req.url.startsWith(environment.apiBaseUrl);
if (!isApiRequest) {
// Wenn es keine API-Anfrage ist, leite die Anfrage unverändert weiter
return next.handle(req);
}
// Wenn es eine API-Anfrage ist, füge den Token hinzu (falls vorhanden)
return from(this.authService.getToken()).pipe(
switchMap(token => {
if (token) {
const clonedReq = req.clone({
setHeaders: { Authorization: `Bearer ${token}` },
});
return next.handle(clonedReq);
}
return next.handle(req);
}),
);
}
}

View File

@ -1,29 +0,0 @@
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, tap } from 'rxjs';
import { v4 } from 'uuid';
import { LoadingService } from '../services/loading.service';
@Injectable()
export class LoadingInterceptor implements HttpInterceptor {
constructor(private loadingService: LoadingService) {}
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
const hideLoading = request.headers.get('X-Hide-Loading') === 'true';
const requestId = `HTTP-${v4()}`;
if (!hideLoading) {
this.loadingService.startLoading(requestId, request.url);
}
return next.handle(request).pipe(
tap({
finalize: () => {
if (!hideLoading) {
this.loadingService.stopLoading(requestId);
}
},
}),
);
}
}

View File

@ -1,34 +0,0 @@
import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Inject, Injectable, Optional } from '@angular/core';
import { Observable, throwError, TimeoutError } from 'rxjs';
import { catchError, timeout } from 'rxjs/operators';
@Injectable()
export class TimeoutInterceptor implements HttpInterceptor {
constructor(@Optional() @Inject('TIMEOUT_DURATION') private timeoutDuration: number = 5000) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// Überprüfen, ob die URL mit '/ai' endet
if (req.url.endsWith('/ai')) {
return next.handle(req).pipe(
timeout(this.timeoutDuration),
catchError((error: any) => {
if (error instanceof TimeoutError) {
// Timeout error handling
return throwError(
() =>
new HttpErrorResponse({
error: 'Request timed out',
status: 408, // HTTP status code for Request Timeout
}),
);
}
return throwError(() => error);
}),
);
}
// Für alle anderen URLs ohne Timeout fortfahren
return next.handle(req);
}
}

View File

@ -1,552 +0,0 @@
<div class="container mx-auto p-4">
@if (user){
<div class="bg-white rounded-lg drop-shadow-custom-bg-mobile md:drop-shadow-custom-bg p-6">
<form #accountForm="ngForm" class="space-y-6">
<h2 class="text-2xl font-bold mb-6">Account Details</h2>
<!-- E-Mail und Bilder -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="md:col-span-2">
<label for="email" class="block text-gray-700 mb-2 font-medium">E-mail (required)</label>
<div class="relative">
<input type="email" id="email" name="email" [(ngModel)]="user.email" disabled class="w-full px-3 py-3 pl-10 border border-gray-300 rounded-lg bg-gray-100 focus:outline-none focus:border-black focus:ring-0" />
<svg xmlns="http://www.w3.org/2000/svg" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</div>
<p class="text-xs text-gray-500 mt-1">You can only modify your email by contacting us at support&#64;bizmatch.net</p>
</div>
@if (isProfessional || (authService.isAdmin() | async)){
<div class="flex flex-row items-center justify-around md:space-x-4">
<!-- Company Logo -->
<div class="flex h-full justify-between flex-col">
<p class="text-sm font-medium text-gray-700 mb-1">Company Logo</p>
<div class="w-20 h-20 w-full rounded-md flex items-center justify-center relative border border-gray-200 overflow-hidden">
@if(user?.hasCompanyLogo){
<img src="{{ companyLogoUrl }}" alt="Company logo" class="max-w-full max-h-full object-cover" />
<div class="absolute top-[-0.5rem] right-[0rem] bg-white rounded-full p-1 drop-shadow-custom-bg hover:cursor-pointer" (click)="deleteConfirm('logo')">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" class="w-4 h-4 text-gray-600">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
} @else {
<img src="assets/images/placeholder.png" class="max-w-full max-h-full object-cover" />
}
</div>
<button
type="button"
class="mt-2 w-full px-3 py-2 border border-gray-300 rounded-lg shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:border-black focus:ring-0 transition-colors duration-200"
(click)="uploadCompanyLogo()"
>
Upload
</button>
</div>
<!-- Profile Picture -->
<div class="flex h-full justify-between flex-col">
<p class="text-sm font-medium text-gray-700 mb-1">Your Profile Picture</p>
<div class="w-20 h-20 w-full rounded-md flex items-center justify-center relative border border-gray-200 overflow-hidden">
@if(user?.hasProfile){
<img src="{{ profileUrl }}" alt="Profile picture" class="max-w-full max-h-full object-cover" />
<div class="absolute top-[-0.5rem] right-[0rem] bg-white rounded-full p-1 drop-shadow-custom-bg hover:cursor-pointer" (click)="deleteConfirm('profile')">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" class="w-4 h-4 text-gray-600">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
} @else {
<img src="assets/images/placeholder.png" class="max-w-full max-h-full object-cover" />
}
</div>
<button
type="button"
class="mt-2 w-full px-3 py-2 border border-gray-300 rounded-lg shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:border-black focus:ring-0 transition-colors duration-200"
(click)="uploadProfile()"
>
Upload
</button>
</div>
</div>
}
</div>
<!-- Name Fields -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- First Name -->
<div>
<label for="firstname" class="block text-gray-700 mb-2 font-medium flex items-center">
First Name
<div *ngIf="validationService.hasMessage('firstname')" class="ml-2 relative group">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 hidden group-hover:block bg-gray-800 text-white text-xs px-2 py-1 rounded whitespace-nowrap">
{{ validationService.getMessage('firstname')?.message }}
</div>
</div>
</label>
<div class="relative">
<input
type="text"
id="firstname"
name="firstname"
[(ngModel)]="user.firstname"
placeholder="Enter your first name"
class="w-full px-3 py-3 pl-10 border border-gray-300 rounded-lg focus:outline-none focus:border-black focus:ring-0"
[class.border-red-500]="validationService.hasMessage('firstname')"
/>
<svg xmlns="http://www.w3.org/2000/svg" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
</div>
<!-- Last Name -->
<div>
<label for="lastname" class="block text-gray-700 mb-2 font-medium flex items-center">
Last Name
<div *ngIf="validationService.hasMessage('lastname')" class="ml-2 relative group">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 hidden group-hover:block bg-gray-800 text-white text-xs px-2 py-1 rounded whitespace-nowrap">
{{ validationService.getMessage('lastname')?.message }}
</div>
</div>
</label>
<div class="relative">
<input
type="text"
id="lastname"
name="lastname"
[(ngModel)]="user.lastname"
placeholder="Enter your last name"
class="w-full px-3 py-3 pl-10 border border-gray-300 rounded-lg focus:outline-none focus:border-black focus:ring-0"
[class.border-red-500]="validationService.hasMessage('lastname')"
/>
<svg xmlns="http://www.w3.org/2000/svg" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
</div>
</div>
<!-- Customer Type Fields -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
@if ((authService.isAdmin() | async) && !id){
<div>
<label for="customerType" class="block text-gray-700 mb-2 font-medium">User Type</label>
<span class="bg-blue-100 text-blue-800 text-sm font-medium me-2 px-2.5 py-0.5 rounded dark:bg-blue-900 dark:text-blue-300">ADMIN</span>
</div>
}
<!-- Customer Type -->
<div>
<label for="customerType" class="block text-gray-700 mb-2 font-medium flex items-center">
Customer Type
<div *ngIf="validationService.hasMessage('customerType')" class="ml-2 relative group">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 hidden group-hover:block bg-gray-800 text-white text-xs px-2 py-1 rounded whitespace-nowrap">
{{ validationService.getMessage('customerType')?.message }}
</div>
</div>
</label>
<div class="relative">
<select
id="customerType"
name="customerType"
[(ngModel)]="user.customerType"
disabled
class="w-full px-3 py-3 pl-10 border border-gray-300 rounded-lg bg-gray-100 focus:outline-none focus:border-black focus:ring-0 appearance-none"
[class.border-red-500]="validationService.hasMessage('customerType')"
>
<option *ngFor="let option of customerTypeOptions" [value]="option.value">{{ option.label }}</option>
</select>
<svg xmlns="http://www.w3.org/2000/svg" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" class="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5 pointer-events-none" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</div>
</div>
@if (isProfessional){
<!-- Professional Type -->
<div>
<label for="customerSubType" class="block text-gray-700 mb-2 font-medium flex items-center">
Professional Type
<div *ngIf="validationService.hasMessage('customerSubType')" class="ml-2 relative group">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 hidden group-hover:block bg-gray-800 text-white text-xs px-2 py-1 rounded whitespace-nowrap">
{{ validationService.getMessage('customerSubType')?.message }}
</div>
</div>
</label>
<div class="relative">
<select
id="customerSubType"
name="customerSubType"
[(ngModel)]="user.customerSubType"
class="w-full px-3 py-3 pl-10 border border-gray-300 rounded-lg focus:outline-none focus:border-black focus:ring-0 appearance-none"
[class.border-red-500]="validationService.hasMessage('customerSubType')"
>
<option *ngFor="let option of customerSubTypeOptions" [value]="option.value">{{ option.label }}</option>
</select>
<svg xmlns="http://www.w3.org/2000/svg" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" class="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5 pointer-events-none" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</div>
</div>
}
</div>
@if (isProfessional){
<!-- Company Info -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Company Name -->
<div>
<label for="companyName" class="block text-gray-700 mb-2 font-medium flex items-center">
Company Name
<div *ngIf="validationService.hasMessage('companyName')" class="ml-2 relative group">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 hidden group-hover:block bg-gray-800 text-white text-xs px-2 py-1 rounded whitespace-nowrap">
{{ validationService.getMessage('companyName')?.message }}
</div>
</div>
</label>
<div class="relative">
<input
type="text"
id="companyName"
name="companyName"
[(ngModel)]="user.companyName"
placeholder="Enter company name"
class="w-full px-3 py-3 pl-10 border border-gray-300 rounded-lg focus:outline-none focus:border-black focus:ring-0"
[class.border-red-500]="validationService.hasMessage('companyName')"
/>
<svg xmlns="http://www.w3.org/2000/svg" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
</div>
</div>
<!-- Description -->
<div>
<label for="description" class="block text-gray-700 mb-2 font-medium flex items-center">
Describe Yourself
<div *ngIf="validationService.hasMessage('description')" class="ml-2 relative group">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 hidden group-hover:block bg-gray-800 text-white text-xs px-2 py-1 rounded whitespace-nowrap">
{{ validationService.getMessage('description')?.message }}
</div>
</div>
</label>
<div class="relative">
<input
type="text"
id="description"
name="description"
[(ngModel)]="user.description"
placeholder="Short description about yourself"
class="w-full px-3 py-3 pl-10 border border-gray-300 rounded-lg focus:outline-none focus:border-black focus:ring-0"
[class.border-red-500]="validationService.hasMessage('description')"
/>
<svg xmlns="http://www.w3.org/2000/svg" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
</div>
</div>
<!-- Contact Info -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<!-- Phone Number -->
<div>
<label for="phoneNumber" class="block text-gray-700 mb-2 font-medium flex items-center">
Your Phone Number
<div *ngIf="validationService.hasMessage('phoneNumber')" class="ml-2 relative group">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 hidden group-hover:block bg-gray-800 text-white text-xs px-2 py-1 rounded whitespace-nowrap">
{{ validationService.getMessage('phoneNumber')?.message }}
</div>
</div>
</label>
<div class="relative">
<input
type="tel"
id="phoneNumber"
name="phoneNumber"
[(ngModel)]="user.phoneNumber"
mask="(000) 000-0000"
placeholder="(123) 456-7890"
class="w-full px-3 py-3 pl-10 border border-gray-300 rounded-lg focus:outline-none focus:border-black focus:ring-0"
[class.border-red-500]="validationService.hasMessage('phoneNumber')"
/>
<svg xmlns="http://www.w3.org/2000/svg" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"
/>
</svg>
</div>
</div>
<!-- Company Website -->
<div>
<label for="companyWebsite" class="block text-gray-700 mb-2 font-medium flex items-center">
Company Website
<div *ngIf="validationService.hasMessage('companyWebsite')" class="ml-2 relative group">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 hidden group-hover:block bg-gray-800 text-white text-xs px-2 py-1 rounded whitespace-nowrap">
{{ validationService.getMessage('companyWebsite')?.message }}
</div>
</div>
</label>
<div class="relative">
<input
type="url"
id="companyWebsite"
name="companyWebsite"
[(ngModel)]="user.companyWebsite"
placeholder="https://example.com"
class="w-full px-3 py-3 pl-10 border border-gray-300 rounded-lg focus:outline-none focus:border-black focus:ring-0"
[class.border-red-500]="validationService.hasMessage('companyWebsite')"
/>
<svg xmlns="http://www.w3.org/2000/svg" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
</div>
<!-- Company Location -->
<div>
<label for="location" class="block text-gray-700 mb-2 font-medium flex items-center">
Company Location
<div *ngIf="validationService.hasMessage('location')" class="ml-2 relative group">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 hidden group-hover:block bg-gray-800 text-white text-xs px-2 py-1 rounded whitespace-nowrap">
{{ validationService.getMessage('location')?.message }}
</div>
</div>
</label>
<div class="relative">
<app-validated-location id="location" name="location" [(ngModel)]="user.location" [class.border-red-500]="validationService.hasMessage('location')"></app-validated-location>
</div>
</div>
</div>
<!-- Rich Text Editors -->
<div>
<label for="companyOverview" class="block text-gray-700 mb-2 font-medium flex items-center">
Company Overview
<div *ngIf="validationService.hasMessage('companyOverview')" class="ml-2 relative group">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 hidden group-hover:block bg-gray-800 text-white text-xs px-2 py-1 rounded whitespace-nowrap">
{{ validationService.getMessage('companyOverview')?.message }}
</div>
</div>
</label>
<app-validated-quill id="companyOverview" name="companyOverview" [(ngModel)]="user.companyOverview" [class.border-red-500]="validationService.hasMessage('companyOverview')"></app-validated-quill>
</div>
<div>
<label for="offeredServices" class="block text-gray-700 mb-2 font-medium flex items-center">
Services We Offer
<div *ngIf="validationService.hasMessage('offeredServices')" class="ml-2 relative group">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 hidden group-hover:block bg-gray-800 text-white text-xs px-2 py-1 rounded whitespace-nowrap">
{{ validationService.getMessage('offeredServices')?.message }}
</div>
</div>
</label>
<app-validated-quill id="offeredServices" name="offeredServices" [(ngModel)]="user.offeredServices" [class.border-red-500]="validationService.hasMessage('offeredServices')"></app-validated-quill>
</div>
<!-- Areas We Serve Section -->
<div>
<h3 class="text-lg font-medium text-gray-700 mb-4 relative">
Areas We Serve
<div *ngIf="validationService.hasMessage('areasServed')" class="inline-flex items-center justify-center w-6 h-6 ml-2 text-xs font-bold text-white bg-red-500 border-2 border-white rounded-full hover:cursor-pointer relative group">
!
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 hidden group-hover:block bg-gray-800 text-white text-xs px-2 py-1 rounded whitespace-nowrap">
{{ validationService.getMessage('areasServed')?.message }}
</div>
</div>
</h3>
<div class="grid grid-cols-12 gap-4 mb-2">
<div class="col-span-6">
<span class="block text-sm font-medium text-gray-700">State</span>
</div>
<div class="col-span-5">
<span class="block text-sm font-medium text-gray-700">County</span>
</div>
<div class="col-span-1"></div>
</div>
@for (areasServed of user.areasServed; track areasServed; let i=$index){
<div class="grid grid-cols-12 gap-4 mb-4">
<div class="col-span-6">
<div class="relative">
<ng-select
[items]="selectOptions?.states"
bindLabel="name"
bindValue="value"
[(ngModel)]="areasServed.state"
(ngModelChange)="setState(i, $event)"
name="areasServed_state{{ i }}"
[class.border-red-500]="validationService.hasMessage('areasServed')"
class="area-state-select"
placeholder="Select state"
></ng-select>
</div>
</div>
<div class="col-span-5">
<app-validated-county name="county{{ i }}" [(ngModel)]="areasServed.county" [state]="areasServed.state" [readonly]="!areasServed.state" [class.border-red-500]="validationService.hasMessage('areasServed')"></app-validated-county>
</div>
<div class="col-span-1 flex items-center">
<button type="button" class="h-10 w-10 bg-red-500 text-white rounded-lg flex items-center justify-center hover:bg-red-600 focus:outline-none focus:border-black focus:ring-0" (click)="removeArea(i)">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 12H4" />
</svg>
</button>
</div>
</div>
}
<div class="mt-2 flex items-center">
<button type="button" class="h-10 w-10 bg-green-500 text-white rounded-lg flex items-center justify-center hover:bg-green-600 focus:outline-none focus:border-black focus:ring-0" (click)="addArea()">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
</button>
<span class="text-sm text-gray-500 ml-3">[Add more Areas or remove existing ones.]</span>
</div>
</div>
<!-- Licensed In Section -->
<div>
<h3 class="text-lg font-medium text-gray-700 mb-4 relative">
Licensed In
<div *ngIf="validationService.hasMessage('licensedIn')" class="inline-flex items-center justify-center w-6 h-6 ml-2 text-xs font-bold text-white bg-red-500 border-2 border-white rounded-full hover:cursor-pointer relative group">
!
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 hidden group-hover:block bg-gray-800 text-white text-xs px-2 py-1 rounded whitespace-nowrap">
{{ validationService.getMessage('licensedIn')?.message }}
</div>
</div>
</h3>
<div class="grid grid-cols-12 gap-4 mb-2">
<div class="col-span-6">
<span class="block text-sm font-medium text-gray-700">State</span>
</div>
<div class="col-span-5">
<span class="block text-sm font-medium text-gray-700">License Number</span>
</div>
<div class="col-span-1"></div>
</div>
@for (licensedIn of user.licensedIn; track licensedIn; let i=$index){
<div class="grid grid-cols-12 gap-4 mb-4">
<div class="col-span-6">
<div class="relative">
<ng-select
[items]="selectOptions?.states"
bindLabel="name"
bindValue="value"
[(ngModel)]="licensedIn.state"
name="licensedIn_state{{ i }}"
[class.border-red-500]="validationService.hasMessage('licensedIn')"
class="license-state-select"
placeholder="Select state"
></ng-select>
</div>
</div>
<div class="col-span-5">
<div class="relative">
<input
type="text"
id="licenseNumber{{ i }}"
name="licenseNumber{{ i }}"
[(ngModel)]="licensedIn.registerNo"
placeholder="Enter license number"
class="w-full px-3 py-3 border border-gray-300 rounded-lg focus:outline-none focus:border-black focus:ring-0"
[class.border-red-500]="validationService.hasMessage('licensedIn')"
/>
</div>
</div>
<div class="col-span-1 flex items-center">
<button type="button" class="h-10 w-10 bg-red-500 text-white rounded-lg flex items-center justify-center hover:bg-red-600 focus:outline-none focus:border-black focus:ring-0" (click)="removeLicence(i)">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 12H4" />
</svg>
</button>
</div>
</div>
}
<div class="mt-2 flex items-center">
<button type="button" class="h-10 w-10 bg-green-500 text-white rounded-lg flex items-center justify-center hover:bg-green-600 focus:outline-none focus:border-black focus:ring-0" (click)="addLicence()">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
</button>
<span class="text-sm text-gray-500 ml-3">[Add more licenses or remove existing ones.]</span>
</div>
</div>
}
<!-- Submit Button -->
<div class="flex justify-start">
<button type="submit" class="px-6 py-3 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 focus:outline-none focus:border-black focus:ring-0 transition-colors duration-200 flex items-center" (click)="updateProfile(user)">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Update Profile
</button>
</div>
</form>
</div>
}
</div>
<app-image-crop-and-upload [uploadParams]="uploadParams" (uploadFinished)="uploadFinished($event)"></app-image-crop-and-upload>
<app-confirmation></app-confirmation>

View File

@ -1,42 +0,0 @@
.rounded-logo {
border-radius: 6px;
width: 120px;
height: 30px;
border: 1px solid #6b7280;
padding: 1px 1px;
object-fit: contain;
}
.rounded-profile {
// @extend .rounded-logo;
max-width: 100px;
max-height: 120px;
border-radius: 6px;
border: 1px solid #6b7280;
padding: 1px 1px;
object-fit: contain;
}
.wfull {
width: 100%;
}
.image-wrap {
position: relative; /* Ermöglicht die absolute Positionierung des Icons bezogen auf diesen Container */
display: inline-block; /* Erlaubt die Inline-Anordnung, falls mehrere Bilder vorhanden sind */
}
/* Stil für das FontAwesome Icon */
.image-wrap fa-icon {
position: absolute;
top: -5px; /* Positioniert das Icon am oberen Rand des Bildes */
right: -18px; /* Positioniert das Icon am rechten Rand des Bildes */
color: #fff; /* Weiße Farbe für das Icon */
background-color: rgba(0, 0, 0, 0.5); /* Halbtransparenter Hintergrund für bessere Sichtbarkeit */
padding: 5px; /* Ein wenig Platz um das Icon */
cursor: pointer; /* Verwandelt den Cursor in eine Hand, um Interaktivität anzudeuten */
border-radius: 8px; /* Optional: Abrunden der linken unteren Ecke für ästhetische Zwecke */
font-size: 0.7rem;
}
quill-editor {
width: 100%;
}
:host ::ng-deep .ng-select.ng-select-single .ng-select-container {
height: 50px;
}

View File

@ -1,229 +0,0 @@
import { CommonModule, DatePipe, TitleCasePipe } from '@angular/common';
import { ChangeDetectorRef, Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { faTrash } from '@fortawesome/free-solid-svg-icons';
import { NgSelectModule } from '@ng-select/ng-select';
import { QuillModule } from 'ngx-quill';
import { lastValueFrom } from 'rxjs';
import { User } from '../../../../../bizmatch-server/src/models/db.model';
import { AutoCompleteCompleteEvent, Invoice, UploadParams, emailToDirName } from '../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../environments/environment';
import { ConfirmationComponent } from '../../components/confirmation.component';
import { ImageCropAndUploadComponent, UploadReponse } from '../../components/image-crop-and-upload/image-crop-and-upload.component';
import { ValidatedCountyComponent } from '../../components/validated-county/validated-county.component';
import { ValidatedLocationComponent } from '../../components/validated-location/validated-location.component';
import { ValidatedQuillComponent } from '../../components/validated-quill/validated-quill.component';
import { ValidationMessage } from '../../components/validation-messages.service';
import { AuthService } from '../../services/auth.service';
import { ConfirmationService } from '../../services/confirmation.service';
import { GeoService } from '../../services/geo.service';
import { ImageService } from '../../services/image.service';
import { LoadingService } from '../../services/loading.service';
import { MessageService } from '../../services/message.service';
import { SelectOptionsService } from '../../services/select-options.service';
import { SharedService } from '../../services/shared.service';
import { UserService } from '../../services/user.service';
import { ValidationService } from '../../services/validation.service';
import { TOOLBAR_OPTIONS, map2User } from '../../utils/utils';
@Component({
selector: 'app-account',
standalone: true,
imports: [CommonModule, FormsModule, RouterModule, FontAwesomeModule, QuillModule, NgSelectModule, ConfirmationComponent, ImageCropAndUploadComponent, ValidatedQuillComponent, ValidatedCountyComponent, ValidatedLocationComponent],
providers: [TitleCasePipe, DatePipe],
templateUrl: './account.component.html',
styleUrl: './account.component.scss',
})
export class AccountComponent {
id: string | undefined;
user: User;
companyLogoUrl: string;
profileUrl: string;
type: 'company' | 'profile';
environment = environment;
editorModules = TOOLBAR_OPTIONS;
env = environment;
faTrash = faTrash;
quillModules = {
toolbar: [['bold', 'italic', 'underline', 'strike'], [{ list: 'ordered' }, { list: 'bullet' }], [{ header: [1, 2, 3, 4, 5, 6, false] }], [{ color: [] }, { background: [] }], ['clean']],
};
uploadParams: UploadParams;
validationMessages: ValidationMessage[] = [];
customerTypeOptions: Array<{ value: string; label: string }> = [];
customerSubTypeOptions: Array<{ value: string; label: string }> = [];
tooltipTargetAreasServed = 'tooltip-areasServed';
tooltipTargetLicensed = 'tooltip-licensedIn';
// subscriptions: StripeSubscription[] | any[];
constructor(
public userService: UserService,
private geoService: GeoService,
public selectOptions: SelectOptionsService,
private cdref: ChangeDetectorRef,
private activatedRoute: ActivatedRoute,
private loadingService: LoadingService,
private imageUploadService: ImageService,
private imageService: ImageService,
private confirmationService: ConfirmationService,
private messageService: MessageService,
private sharedService: SharedService,
private titleCasePipe: TitleCasePipe,
public validationService: ValidationService,
// private subscriptionService: SubscriptionsService,
private datePipe: DatePipe,
private router: Router,
public authService: AuthService,
) {}
async ngOnInit() {
this.id = this.activatedRoute.snapshot.params['id'] as string | undefined;
if (this.id) {
this.user = await this.userService.getById(this.id);
} else {
const token = await this.authService.getToken();
const keycloakUser = map2User(token);
const email = keycloakUser.email;
this.user = await this.userService.getByMail(email);
}
// this.subscriptions = await lastValueFrom(this.subscriptionService.getAllSubscriptions(this.user.email));
// await this.synchronizeSubscriptions(this.subscriptions);
this.profileUrl = this.user.hasProfile ? `${this.env.imageBaseUrl}/pictures/profile/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}` : `/assets/images/placeholder.png`;
this.companyLogoUrl = this.user.hasCompanyLogo ? `${this.env.imageBaseUrl}/pictures/logo/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}` : `/assets/images/placeholder.png`;
this.customerTypeOptions = this.selectOptions.customerTypes
// .filter(ct => ct.value === 'buyer' || ct.value === 'seller' || this.user.customerType === 'professional')
.map(type => ({
value: type.value,
label: this.titleCasePipe.transform(type.name),
}));
this.customerSubTypeOptions = this.selectOptions.customerSubTypes
// .filter(ct => ct.value !== 'broker' || this.user.customerSubType === 'broker')
.map(type => ({
value: type.value,
label: this.titleCasePipe.transform(type.name),
}));
}
ngOnDestroy() {
this.validationService.clearMessages(); // Löschen Sie alle bestehenden Validierungsnachrichten
}
printInvoice(invoice: Invoice) {}
async updateProfile(user: User) {
try {
await this.userService.save(this.user);
this.userService.changeUser(this.user);
this.messageService.addMessage({ severity: 'success', text: 'Account changes have been persisted', duration: 3000 });
this.validationService.clearMessages(); // Löschen Sie alle bestehenden Validierungsnachrichten
this.validationMessages = [];
} catch (error) {
this.messageService.addMessage({
severity: 'danger',
text: 'An error occurred while saving the profile - Please check your inputs',
duration: 5000,
});
this.validationService.handleApiError(error.error);
}
}
onUploadCompanyLogo(event: any) {
const uniqueSuffix = '?_ts=' + new Date().getTime();
this.companyLogoUrl = `${this.env.imageBaseUrl}/pictures/logo/${emailToDirName(this.user.email)}${uniqueSuffix}`;
}
onUploadProfilePicture(event: any) {
const uniqueSuffix = '?_ts=' + new Date().getTime();
this.profileUrl = `${this.env.imageBaseUrl}/pictures/profile/${emailToDirName(this.user.email)}${uniqueSuffix}`;
}
setImageToFallback(event: Event) {
(event.target as HTMLImageElement).src = `/assets/images/placeholder.png`; // Pfad zum Platzhalterbild
}
suggestions: string[] | undefined;
async search(event: AutoCompleteCompleteEvent) {
const result = await lastValueFrom(this.geoService.findCitiesStartingWith(event.query));
this.suggestions = result.map(r => `${r.name} - ${r.state}`).slice(0, 5);
}
addLicence() {
this.user.licensedIn.push({ registerNo: '', state: '' });
}
removeLicence(index: number) {
this.user.licensedIn.splice(index, 1);
}
addArea() {
this.user.areasServed.push({ county: '', state: '' });
}
removeArea(index: number) {
this.user.areasServed.splice(index, 1);
}
get isProfessional() {
return this.user.customerType === 'professional';
}
uploadCompanyLogo() {
this.uploadParams = { type: 'uploadCompanyLogo', imagePath: emailToDirName(this.user.email) };
}
uploadProfile() {
this.uploadParams = { type: 'uploadProfile', imagePath: emailToDirName(this.user.email) };
}
async uploadFinished(response: UploadReponse) {
if (response.success) {
if (response.type === 'uploadCompanyLogo') {
this.user.hasCompanyLogo = true; //
this.companyLogoUrl = `${this.env.imageBaseUrl}/pictures/logo/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}`;
} else {
this.user.hasProfile = true;
this.profileUrl = `${this.env.imageBaseUrl}/pictures/profile/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}`;
this.sharedService.changeProfilePhoto(this.profileUrl);
}
this.userService.changeUser(this.user);
await this.userService.saveGuaranteed(this.user);
}
}
async deleteConfirm(type: 'profile' | 'logo') {
const confirmed = await this.confirmationService.showConfirmation({ message: `Do you want to delete your ${type === 'logo' ? 'Logo' : 'Profile'} image` });
if (confirmed) {
if (type === 'profile') {
this.user.hasProfile = false;
await Promise.all([this.imageService.deleteProfileImagesByMail(this.user.email), this.userService.saveGuaranteed(this.user)]);
} else {
this.user.hasCompanyLogo = false;
await Promise.all([this.imageService.deleteLogoImagesByMail(this.user.email), this.userService.saveGuaranteed(this.user)]);
}
this.user = await this.userService.getById(this.user.id);
// this.messageService.showMessage('Image deleted');
this.messageService.addMessage({
severity: 'success',
text: 'Image deleted.',
duration: 3000, // 3 seconds
});
}
}
getValidationMessage(fieldName: string): string {
const message = this.validationMessages.find(msg => msg.field === fieldName);
return message ? message.message : '';
}
setState(index: number, state: string) {
if (state === null) {
this.user.areasServed[index].county = null;
}
}
// getLevel(i: number) {
// return this.subscriptions[i].metadata.plan;
// }
// getStartDate(i: number) {
// return this.datePipe.transform(new Date(this.subscriptions[i].start_date * 1000));
// }
// getEndDate(i: number) {
// return this.subscriptions[i].status === 'trialing' ? this.datePipe.transform(new Date(this.subscriptions[i].current_period_end * 1000)) : '---';
// }
// getNextSettlement(i: number) {
// return this.subscriptions[i].status === 'active' ? this.datePipe.transform(new Date(this.subscriptions[i].current_period_end * 1000)) : '---';
// }
// getStatus(i: number) {
// return this.subscriptions[i].status ? this.subscriptions[i].status : '';
// }
}

View File

@ -1,111 +0,0 @@
<div class="container mx-auto p-4">
@if(listings?.length>0){
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Anzahl der Spalten auf 3 reduziert und den Abstand erhöht -->
@for (listing of listings; track listing.id) {
<div class="bg-white rounded-lg drop-shadow-custom-bg-mobile md:drop-shadow-custom-bg overflow-hidden hover:shadow-xl">
<!-- Hover-Effekt hinzugefügt -->
<div class="p-6 flex flex-col h-full relative z-[0]">
<div class="flex items-center justify-between mb-4">
<img src="assets/images/{{ listing.type }}.png" alt="Image" class="w-full h-18 object-contain object-left" />
<!-- <i [class]="selectOptions.getIconAndTextColorType(listing.type)" class="mr-2 text-xl"></i> -->
<!-- Icon vergrößert -->
<span [class]="selectOptions.getTextColorType(listing.type)" class="w-full text-right font-bold text-2xl">{{ selectOptions.getBusiness(listing.type) }}</span>
<!-- Schriftgröße erhöht -->
</div>
<h2 class="text-xl font-semibold mb-4">
<!-- Überschrift vergrößert -->
{{ listing.title }}
@if(listing.draft){
<span class="bg-red-100 text-red-800 text-sm font-medium me-2 ml-2 px-2.5 py-0.5 rounded dark:bg-red-900 dark:text-red-300">Draft</span>
}
</h2>
<div class="flex justify-between">
<!-- State Badge -->
<span class="w-fit inline-flex items-center justify-center px-2 py-1 mb-4 text-xs font-bold leading-none bg-gray-200 text-gray-700 rounded-full">
{{ selectOptions.getState(listing.location.state) }}
</span>
<p class="text-sm text-gray-600 mb-4">
<strong>{{ getDaysListed(listing) }} days listed</strong>
</p>
</div>
<!-- Asking Price hervorgehoben -->
<p class="text-base font-bold text-gray-800 mb-2">
<strong>Asking price:</strong> <span class="text-green-600"> {{ listing.price | currency : 'USD' : 'symbol' : '1.0-0' }}</span>
</p>
<p class="text-sm text-gray-600 mb-2"><strong>Sales revenue:</strong> {{ listing.salesRevenue | currency : 'USD' : 'symbol' : '1.0-0' }}</p>
<p class="text-sm text-gray-600 mb-2"><strong>Net profit:</strong> {{ listing.cashFlow | currency : 'USD' : 'symbol' : '1.0-0' }}</p>
<p class="text-sm text-gray-600 mb-2"><strong>Location:</strong> {{ listing.location.name ? listing.location.name : listing.location.county }}</p>
<p class="text-sm text-gray-600 mb-4"><strong>Established:</strong> {{ listing.established }}</p>
<img src="{{ env.imageBaseUrl }}/pictures/logo/{{ listing.imageName }}.avif?_ts={{ ts }}" alt="Company logo" class="absolute bottom-[80px] right-[20px] h-[45px] w-auto" />
<!-- Position und Größe des Bildes angepasst -->
<div class="flex-grow"></div>
<button class="bg-emerald-500 hover:bg-emerald-600 text-white px-5 py-3 rounded-full w-full flex items-center justify-center mt-4 transition-colors duration-200" [routerLink]="['/details-business-listing', listing.id]">
<!-- Button-Größe und Hover-Effekt verbessert -->
View Full Listing
<i class="fas fa-arrow-right ml-2"></i>
</button>
</div>
</div>
}
</div>
} @else if (listings?.length===0){
<div class="w-full flex items-center flex-wrap justify-center gap-10">
<div class="grid gap-4 w-60">
<svg class="mx-auto" xmlns="http://www.w3.org/2000/svg" width="154" height="161" viewBox="0 0 154 161" fill="none">
<path
d="M0.0616455 84.4268C0.0616455 42.0213 34.435 7.83765 76.6507 7.83765C118.803 7.83765 153.224 42.0055 153.224 84.4268C153.224 102.42 147.026 118.974 136.622 132.034C122.282 150.138 100.367 161 76.6507 161C52.7759 161 30.9882 150.059 16.6633 132.034C6.25961 118.974 0.0616455 102.42 0.0616455 84.4268Z"
fill="#EEF2FF"
/>
<path
d="M96.8189 0.632498L96.8189 0.632384L96.8083 0.630954C96.2034 0.549581 95.5931 0.5 94.9787 0.5H29.338C22.7112 0.5 17.3394 5.84455 17.3394 12.4473V142.715C17.3394 149.318 22.7112 154.662 29.338 154.662H123.948C130.591 154.662 135.946 149.317 135.946 142.715V38.9309C135.946 38.0244 135.847 37.1334 135.648 36.2586L135.648 36.2584C135.117 33.9309 133.874 31.7686 132.066 30.1333C132.066 30.1331 132.065 30.1329 132.065 30.1327L103.068 3.65203C103.068 3.6519 103.067 3.65177 103.067 3.65164C101.311 2.03526 99.1396 0.995552 96.8189 0.632498Z"
fill="white"
stroke="#E5E7EB"
/>
<ellipse cx="80.0618" cy="81" rx="28.0342" ry="28.0342" fill="#EEF2FF" />
<path
d="M99.2393 61.3061L99.2391 61.3058C88.498 50.5808 71.1092 50.5804 60.3835 61.3061C49.6423 72.0316 49.6422 89.4361 60.3832 100.162C71.109 110.903 88.4982 110.903 99.2393 100.162C109.965 89.4363 109.965 72.0317 99.2393 61.3061ZM105.863 54.6832C120.249 69.0695 120.249 92.3985 105.863 106.785C91.4605 121.171 68.1468 121.171 53.7446 106.785C39.3582 92.3987 39.3582 69.0693 53.7446 54.683C68.1468 40.2965 91.4605 40.2966 105.863 54.6832Z"
stroke="#E5E7EB"
/>
<path d="M110.782 119.267L102.016 110.492C104.888 108.267 107.476 105.651 109.564 102.955L118.329 111.729L110.782 119.267Z" stroke="#E5E7EB" />
<path
d="M139.122 125.781L139.122 125.78L123.313 109.988C123.313 109.987 123.313 109.987 123.312 109.986C121.996 108.653 119.849 108.657 118.521 109.985L118.871 110.335L118.521 109.985L109.047 119.459C107.731 120.775 107.735 122.918 109.044 124.247L109.047 124.249L124.858 140.06C128.789 143.992 135.191 143.992 139.122 140.06C143.069 136.113 143.069 129.728 139.122 125.781Z"
fill="#A5B4FC"
stroke="#818CF8"
/>
<path
d="M83.185 87.2285C82.5387 87.2285 82.0027 86.6926 82.0027 86.0305C82.0027 83.3821 77.9987 83.3821 77.9987 86.0305C77.9987 86.6926 77.4627 87.2285 76.8006 87.2285C76.1543 87.2285 75.6183 86.6926 75.6183 86.0305C75.6183 80.2294 84.3831 80.2451 84.3831 86.0305C84.3831 86.6926 83.8471 87.2285 83.185 87.2285Z"
fill="#4F46E5"
/>
<path
d="M93.3528 77.0926H88.403C87.7409 77.0926 87.2049 76.5567 87.2049 75.8946C87.2049 75.2483 87.7409 74.7123 88.403 74.7123H93.3528C94.0149 74.7123 94.5509 75.2483 94.5509 75.8946C94.5509 76.5567 94.0149 77.0926 93.3528 77.0926Z"
fill="#4F46E5"
/>
<path
d="M71.5987 77.0925H66.6488C65.9867 77.0925 65.4507 76.5565 65.4507 75.8945C65.4507 75.2481 65.9867 74.7122 66.6488 74.7122H71.5987C72.245 74.7122 72.781 75.2481 72.781 75.8945C72.781 76.5565 72.245 77.0925 71.5987 77.0925Z"
fill="#4F46E5"
/>
<rect x="38.3522" y="21.5128" width="41.0256" height="2.73504" rx="1.36752" fill="#4F46E5" />
<rect x="38.3522" y="133.65" width="54.7009" height="5.47009" rx="2.73504" fill="#A5B4FC" />
<rect x="38.3522" y="29.7179" width="13.6752" height="2.73504" rx="1.36752" fill="#4F46E5" />
<circle cx="56.13" cy="31.0854" r="1.36752" fill="#4F46E5" />
<circle cx="61.6001" cy="31.0854" r="1.36752" fill="#4F46E5" />
<circle cx="67.0702" cy="31.0854" r="1.36752" fill="#4F46E5" />
</svg>
<div>
<h2 class="text-center text-black text-xl font-semibold leading-loose pb-2">Theres no listing here</h2>
<p class="text-center text-black text-base font-normal leading-relaxed pb-4">Try changing your filters to <br />see listings</p>
<div class="flex gap-3">
<button (click)="clearAllFilters()" class="w-full px-3 py-2 rounded-full border border-gray-300 text-gray-900 text-xs font-semibold leading-4">Clear Filter</button>
<button (click)="openFilterModal()" class="w-full px-3 py-2 bg-indigo-600 hover:bg-indigo-700 transition-all duration-500 rounded-full text-white text-xs font-semibold leading-4">Change Filter</button>
</div>
</div>
</div>
</div>
}
</div>
@if(pageCount>1){
<app-paginator [page]="page" [pageCount]="pageCount" (pageChange)="onPageChange($event)"></app-paginator>
}

View File

@ -1,32 +0,0 @@
// #sky-line {
// background-image: url(../../../../assets/images/bw-sky.jpg);
// height: 204px;
// background-position: bottom;
// background-size: cover;
// margin-bottom: -1px;
// }
// .search {
// background-color: #343f69;
// }
// ::ng-deep p-paginator div {
// background-color: var(--surface-200) !important;
// }
// .icon-pos {
// position: absolute;
// bottom: 1.5rem;
// right: 1.5rem;
// }
// .rounded-image {
// border-radius: 6px;
// max-width: 100px;
// height: 35px;
// border: 1px solid rgba(0, 0, 0, 0.2);
// padding: 1px 1px;
// object-fit: contain;
// }
// ::ng-deep span.p-button-label {
// font-weight: 500;
// }
:host {
width: 100%;
}

View File

@ -1,118 +0,0 @@
import { CommonModule } from '@angular/common';
import { ChangeDetectorRef, Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import dayjs from 'dayjs';
import { BusinessListing } from '../../../../../bizmatch-server/src/models/db.model';
import { BusinessListingCriteria, emailToDirName, LISTINGS_PER_PAGE, ListingType } from '../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../environments/environment';
import { PaginatorComponent } from '../../components/paginator/paginator.component';
import { CriteriaChangeService } from '../../services/criteria-change.service';
import { ListingsService } from '../../services/listings.service';
import { ModalService } from '../../services/modal.service';
import { SearchService } from '../../services/search.service';
import { SelectOptionsService } from '../../services/select-options.service';
import { assignProperties, getCriteriaProxy, resetBusinessListingCriteria } from '../../utils/utils';
@UntilDestroy()
@Component({
selector: 'app-business-listings',
standalone: true,
imports: [CommonModule, FormsModule, RouterModule, PaginatorComponent],
templateUrl: './business-listings.component.html',
styleUrls: ['./business-listings.component.scss'],
})
export class BusinessListingsComponent {
environment = environment;
listings: Array<BusinessListing>;
filteredListings: Array<BusinessListing>;
criteria: BusinessListingCriteria;
realEstateChecked: boolean;
maxPrice: string;
minPrice: string;
type: string;
state: string;
totalRecords: number = 0;
ts = new Date().getTime();
first: number = 0;
rows: number = 12;
env = environment;
public category: 'business' | 'commercialProperty' | 'professionals_brokers' | undefined;
page = 1;
pageCount = 1;
emailToDirName = emailToDirName;
constructor(
public selectOptions: SelectOptionsService,
private listingsService: ListingsService,
private activatedRoute: ActivatedRoute,
private router: Router,
private cdRef: ChangeDetectorRef,
private route: ActivatedRoute,
private searchService: SearchService,
private modalService: ModalService,
private criteriaChangeService: CriteriaChangeService,
) {
this.criteria = getCriteriaProxy('businessListings', this) as BusinessListingCriteria;
this.init();
this.searchService.currentCriteria.pipe(untilDestroyed(this)).subscribe(criteria => {
if (criteria && criteria.criteriaType === 'businessListings') {
this.criteria = criteria as BusinessListingCriteria;
this.search();
}
});
}
async ngOnInit() {
this.search();
}
async init() {
this.reset();
}
async search() {
const listingReponse = await this.listingsService.getListings(this.criteria);
this.listings = listingReponse.results;
this.totalRecords = listingReponse.totalCount;
this.pageCount = this.totalRecords % LISTINGS_PER_PAGE === 0 ? this.totalRecords / LISTINGS_PER_PAGE : Math.floor(this.totalRecords / LISTINGS_PER_PAGE) + 1;
this.page = this.criteria.page ? this.criteria.page : 1;
this.cdRef.markForCheck();
this.cdRef.detectChanges();
}
onPageChange(page: any) {
this.criteria.start = (page - 1) * LISTINGS_PER_PAGE;
this.criteria.length = LISTINGS_PER_PAGE;
this.criteria.page = page;
this.search();
}
imageErrorHandler(listing: ListingType) {}
reset() {
this.criteria.title = null;
}
getDaysListed(listing: BusinessListing) {
return dayjs().diff(listing.created, 'day');
}
// New methods for filter actions
clearAllFilters() {
// Reset criteria to default values
resetBusinessListingCriteria(this.criteria);
// Reset pagination
this.criteria.page = 1;
this.criteria.start = 0;
this.criteriaChangeService.notifyCriteriaChange();
// Search with cleared filters
this.searchService.search(this.criteria);
}
async openFilterModal() {
// Open the search modal with current criteria
const modalResult = await this.modalService.showModal(this.criteria);
if (modalResult.accepted) {
this.searchService.search(this.criteria);
} else {
this.criteria = assignProperties(this.criteria, modalResult.criteria);
}
}
}

View File

@ -1,277 +0,0 @@
<div class="container mx-auto p-4">
<div class="bg-white rounded-lg drop-shadow-custom-bg-mobile md:drop-shadow-custom-bg overflow-hidden relative">
<button
(click)="historyService.goBack()"
class="absolute top-4 right-4 bg-red-500 text-white rounded-full w-8 h-8 flex items-center justify-center hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-opacity-50 print:hidden"
>
<i class="fas fa-times"></i>
</button>
@if(listing){
<div class="p-6 flex flex-col lg:flex-row">
<!-- Left column -->
<div class="w-full lg:w-1/2 pr-0 lg:pr-6">
<h1 class="text-2xl font-bold mb-4">{{ listing.title }}</h1>
<p class="mb-4" [innerHTML]="description"></p>
<div class="space-y-2">
<div *ngFor="let detail of listingDetails; let i = index" class="flex flex-col sm:flex-row" [ngClass]="{ 'bg-gray-100': i % 2 === 0 }">
<div class="w-full sm:w-1/3 font-semibold p-2">{{ detail.label }}</div>
<div class="w-full sm:w-2/3 p-2" *ngIf="!detail.isHtml && !detail.isListingBy">{{ detail.value }}</div>
<div class="w-full sm:w-2/3 p-2 flex space-x-2" [innerHTML]="detail.value" *ngIf="detail.isHtml && !detail.isListingBy"></div>
<div class="w-full sm:w-2/3 p-2 flex space-x-2" *ngIf="detail.isListingBy">
<a routerLink="/details-user/{{ listingUser.id }}" class="text-blue-600 dark:text-blue-500 hover:underline">{{ listingUser.firstname }} {{ listingUser.lastname }}</a>
<img src="{{ env.imageBaseUrl }}/pictures/logo/{{ listing.imageName }}.avif?_ts={{ ts }}" class="mr-5 lg:mb-0" style="max-height: 30px; max-width: 100px" />
</div>
</div>
</div>
<div class="py-4 print:hidden">
@if(listing && listingUser && (listingUser?.email===user?.email || (authService.isAdmin() | async))){
<!-- <div class="inline">
<button class="share share-edit text-white font-bold text-xs py-1.5 px-2 inline-flex items-center" [routerLink]="['/editBusinessListing', listing.id]">
<i class="fa-regular fa-pen-to-square"></i>
<span class="ml-2">Edit</span>
</button>
</div> -->
<div class="inline">
<button [routerLink]="['/editBusinessListing', listing.id]" class="sb-wrapper sb-show-icon sb-show-text" style="--button-color: #0088cc" aria-label="Share by email">
<div class="sb-content">
<div class="sb-icon">
<i class="fa-solid fa-envelope fa-fw"></i>
</div>
<div class="sb-text">Edit</div>
</div>
</button>
</div>
} @if(user){
<!-- <div class="inline">
<button class="share share-save text-white font-bold text-xs py-1.5 px-2 inline-flex items-center" (click)="save()" [disabled]="listing.favoritesForUser.includes(user.email)">
<i class="fa-regular fa-heart"></i>
@if(listing.favoritesForUser.includes(user.email)){
<span class="ml-2">Saved ...</span>
}@else {
<span class="ml-2">Save</span>
}
</button>
</div> -->
<div class="inline">
<button class="sb-wrapper sb-show-icon sb-show-text" style="--button-color: #e60023" (click)="save()" aria-label="Share by email">
<div class="sb-content">
<div class="sb-icon">
<i class="fa-solid fa-envelope fa-fw"></i>
</div>
@if(listing.favoritesForUser.includes(user.email)){
<div class="sb-text">Saved ...</div>
}@else {
<div class="sb-text">Save</div>
}
</div>
</button>
</div>
}
<share-button button="print" showText="true" (click)="createEvent('print')"></share-button>
<div class="inline">
<button class="sb-wrapper sb-show-icon sb-show-text" style="--button-color: #ff961c" (click)="showShareByEMail()" aria-label="Share by email">
<div class="sb-content">
<div class="sb-icon">
<i class="fa-solid fa-envelope fa-fw"></i>
</div>
<div class="sb-text">Email</div>
</div>
</button>
</div>
<share-button button="facebook" showText="true" (click)="createEvent('facebook')"></share-button>
<share-button button="x" showText="true" (click)="createEvent('x')"></share-button>
<share-button button="linkedin" showText="true" (click)="createEvent('linkedin')"></share-button>
</div>
<!-- Karte hinzufügen, wenn Straße vorhanden ist -->
<!-- <div *ngIf="listing.location.street" class="mt-6">
<h2 class="text-lg font-semibold mb-2">Location Map</h2>
<div style="height: 400px" leaflet [leafletOptions]="mapOptions" [leafletLayers]="mapLayers" [leafletCenter]="mapCenter" [leafletZoom]="mapZoom" (leafletMapReady)="onMapReady($event)"></div>
</div> -->
</div>
<!-- Right column -->
<!-- Kontaktformular mit konsistenten Höhen und Breiten -->
<!-- Kontaktformular mit konsistenten Höhen und Breiten -->
<div class="w-full lg:w-1/2 mt-6 lg:mt-0 print:hidden">
<div class="md:mt-8 mb-4 text-2xl font-bold">Contact the Author of this Listing</div>
<p class="text-sm mb-6">Please include your contact info below</p>
<form class="space-y-6">
<!-- Erste Zeile: Name und E-Mail -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Name Eingabe -->
<div>
<label for="name" class="block text-gray-700 mb-2 font-medium flex items-center">
Your Name
<div *ngIf="validationService.hasMessage('name')" class="ml-2 relative group">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 hidden group-hover:block bg-gray-800 text-white text-xs px-2 py-1 rounded whitespace-nowrap">
{{ validationService.getMessage('name')?.message }}
</div>
</div>
</label>
<div class="relative">
<input
id="name"
name="name"
type="text"
[(ngModel)]="mailinfo.sender.name"
placeholder="Enter your name"
class="w-full px-3 py-3 pl-10 border border-gray-300 rounded-lg focus:outline-none focus:border-black focus:ring-0"
[class.border-red-500]="validationService.hasMessage('name')"
/>
<svg xmlns="http://www.w3.org/2000/svg" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
</div>
<!-- E-Mail Eingabe -->
<div>
<label for="email" class="block text-gray-700 mb-2 font-medium flex items-center">
Your Email
<div *ngIf="validationService.hasMessage('email')" class="ml-2 relative group">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 hidden group-hover:block bg-gray-800 text-white text-xs px-2 py-1 rounded whitespace-nowrap">
{{ validationService.getMessage('email')?.message }}
</div>
</div>
</label>
<div class="relative">
<input
id="email"
name="email"
type="email"
[(ngModel)]="mailinfo.sender.email"
placeholder="Enter your email"
class="w-full px-3 py-3 pl-10 border border-gray-300 rounded-lg focus:outline-none focus:border-black focus:ring-0"
[class.border-red-500]="validationService.hasMessage('email')"
/>
<svg xmlns="http://www.w3.org/2000/svg" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</div>
</div>
</div>
<!-- Zweite Zeile: Telefon und Bundesland -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Telefon Eingabe -->
<div>
<label for="phone" class="block text-gray-700 mb-2 font-medium flex items-center">
Phone Number
<div *ngIf="validationService.hasMessage('phoneNumber')" class="ml-2 relative group">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 hidden group-hover:block bg-gray-800 text-white text-xs px-2 py-1 rounded whitespace-nowrap">
{{ validationService.getMessage('phoneNumber')?.message }}
</div>
</div>
</label>
<div class="relative">
<input
id="phone"
name="phoneNumber"
type="tel"
[(ngModel)]="mailinfo.sender.phoneNumber"
mask="(000) 000-0000"
placeholder="(123) 456-7890"
class="w-full px-3 py-3 pl-10 border border-gray-300 rounded-lg focus:outline-none focus:border-black focus:ring-0"
[class.border-red-500]="validationService.hasMessage('phoneNumber')"
/>
<svg xmlns="http://www.w3.org/2000/svg" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"
/>
</svg>
</div>
</div>
<!-- Bundesland Select -->
<div>
<label for="state" class="block text-gray-700 mb-2 font-medium flex items-center">
State
<div *ngIf="validationService.hasMessage('state')" class="ml-2 relative group">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 hidden group-hover:block bg-gray-800 text-white text-xs px-2 py-1 rounded whitespace-nowrap">
{{ validationService.getMessage('state')?.message }}
</div>
</div>
</label>
<div class="relative">
<select
id="state"
name="state"
[(ngModel)]="mailinfo.sender.state"
class="w-full px-3 py-3 pl-10 border border-gray-300 rounded-lg bg-white focus:outline-none focus:border-black focus:ring-0 appearance-none"
[class.border-red-500]="validationService.hasMessage('state')"
>
<option value="" disabled selected>Select your state</option>
<option *ngFor="let state of selectOptions?.states" [value]="state.value">{{ state.name }}</option>
</select>
<svg xmlns="http://www.w3.org/2000/svg" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 21v-4m0 0V5a2 2 0 012-2h6.5l1 1H21l-3 6 3 6h-8.5l-1-1H5a2 2 0 00-2 2zm9-13.5V9" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" class="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5 pointer-events-none" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</div>
</div>
</div>
<!-- Kommentarfeld -->
<div>
<label for="comments" class="block text-gray-700 mb-2 font-medium flex items-center">
Questions/Comments
<div *ngIf="validationService.hasMessage('comments')" class="ml-2 relative group">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 hidden group-hover:block bg-gray-800 text-white text-xs px-2 py-1 rounded whitespace-nowrap">
{{ validationService.getMessage('comments')?.message }}
</div>
</div>
</label>
<textarea
id="comments"
name="comments"
[(ngModel)]="mailinfo.sender.comments"
placeholder="Type your questions or comments here..."
rows="4"
class="w-full px-3 py-3 border border-gray-300 rounded-lg focus:outline-none focus:border-black focus:ring-0"
[class.border-red-500]="validationService.hasMessage('comments')"
></textarea>
</div>
<!-- Submit Button -->
<div>
<button (click)="mail()" class="w-full sm:w-auto px-6 py-3 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 transition-colors duration-200">
<span class="flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
</svg>
Submit
</span>
</button>
</div>
</form>
</div>
</div>
}
</div>
</div>

View File

@ -1,248 +0,0 @@
import { ChangeDetectorRef, Component } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { ActivatedRoute, NavigationEnd, Router, RouterModule } from '@angular/router';
import { ShareButton } from 'ngx-sharebuttons/button';
import { lastValueFrom } from 'rxjs';
// Import für Leaflet
// Benannte Importe für Leaflet
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import dayjs from 'dayjs';
import { BusinessListing, EventTypeEnum, ShareByEMail, User } from '../../../../../bizmatch-server/src/models/db.model';
import { KeycloakUser, MailInfo } from '../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../environments/environment';
import { EMailService } from '../../components/email/email.service';
import { AuditService } from '../../services/audit.service';
import { AuthService } from '../../services/auth.service';
import { GeoService } from '../../services/geo.service';
import { HistoryService } from '../../services/history.service';
import { ListingsService } from '../../services/listings.service';
import { MailService } from '../../services/mail.service';
import { MessageService } from '../../services/message.service';
import { SelectOptionsService } from '../../services/select-options.service';
import { UserService } from '../../services/user.service';
import { ValidationService } from '../../services/validation.service';
import { createMailInfo, map2User } from '../../utils/utils';
@Component({
selector: 'app-details-business-listing',
standalone: true,
imports: [CommonModule, FormsModule, RouterModule, FontAwesomeModule, ShareButton],
providers: [],
templateUrl: './details-business-listing.component.html',
styles: `
.inline .sb-wrapper {
margin: var(--sb-margin, .3125em);
padding: var(--sb-padding, 0);
min-width: var(--sb-min-width, 4.125em);
height: var(--sb-height, 2.5em);
color: var(--sb-color, #fff);
background: var(--sb-background);
font-size: var(--sb-font-size, 13px);
line-height: var(--sb-line-height, 2.571em);
border: var(--sb-border);
border-radius: var(--sb-border-radius, 4px);
transition: var(--sb-transition);
box-shadow: var(--sb-box-shadow);
text-shadow: var(--sb-text-shadow);
overflow: var(--sb-overflow, hidden);
--sb-color: #fff;
--sb-background: var(--button-color);
cursor: pointer;
position: relative;
outline: 0;
}
.inline .sb-content {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
padding: 0 1em;
}
.inline .sb-icon {
display: flex;
align-items: center;
}
.inline .sb-text {
margin-left: 0.5em;
font-weight: bold;
}
`,
})
export class DetailsBusinessListingComponent {
// listings: Array<BusinessListing>;
responsiveOptions = [
{
breakpoint: '1199px',
numVisible: 1,
numScroll: 1,
},
{
breakpoint: '991px',
numVisible: 2,
numScroll: 1,
},
{
breakpoint: '767px',
numVisible: 1,
numScroll: 1,
},
];
private id: string | undefined;
listing: BusinessListing;
mailinfo: MailInfo;
environment = environment;
keycloakUser: KeycloakUser;
user: User;
listingUser: User;
description: SafeHtml;
private history: string[] = [];
ts = new Date().getTime();
env = environment;
constructor(
private activatedRoute: ActivatedRoute,
private listingsService: ListingsService,
private router: Router,
private userService: UserService,
public selectOptions: SelectOptionsService,
private mailService: MailService,
private sanitizer: DomSanitizer,
public historyService: HistoryService,
private messageService: MessageService,
private auditService: AuditService,
public emailService: EMailService,
private geoService: GeoService,
public authService: AuthService,
private cdref: ChangeDetectorRef,
public validationService: ValidationService,
) {
this.router.events.subscribe(event => {
if (event instanceof NavigationEnd) {
this.history.push(event.urlAfterRedirects);
}
});
this.mailinfo = { sender: {}, email: '', url: environment.mailinfoUrl };
this.id = this.activatedRoute.snapshot.params['id'] as string | undefined;
}
async ngOnInit() {
const token = await this.authService.getToken();
this.keycloakUser = map2User(token);
if (this.keycloakUser) {
this.user = await this.userService.getByMail(this.keycloakUser.email);
this.mailinfo = createMailInfo(this.user);
}
try {
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id));
this.auditService.createEvent(this.listing.id, 'view', this.user?.email);
this.listingUser = await this.userService.getByMail(this.listing.email);
this.description = this.sanitizer.bypassSecurityTrustHtml(this.listing.description);
} catch (error) {
this.auditService.log({ severity: 'error', text: error.error.message });
this.router.navigate(['notfound']);
}
}
ngOnDestroy() {
this.validationService.clearMessages(); // Löschen Sie alle bestehenden Validierungsnachrichten
}
async mail() {
try {
this.mailinfo.email = this.listingUser.email;
this.mailinfo.listing = this.listing;
await this.mailService.mail(this.mailinfo);
this.validationService.clearMessages();
this.auditService.createEvent(this.listing.id, 'contact', this.user?.email, this.mailinfo.sender);
this.messageService.addMessage({ severity: 'success', text: 'Your message has been sent to the creator of the listing', duration: 3000 });
this.mailinfo = createMailInfo(this.user);
} catch (error) {
this.messageService.addMessage({
severity: 'danger',
text: 'An error occurred while sending the request - Please check your inputs',
duration: 5000,
});
this.validationService.handleApiError(error.error);
}
}
get listingDetails() {
let typeOfRealEstate = '';
if (this.listing.realEstateIncluded) {
typeOfRealEstate = 'Real Estate Included';
} else if (this.listing.leasedLocation) {
typeOfRealEstate = 'Leased Location';
} else if (this.listing.franchiseResale) {
typeOfRealEstate = 'Franchise Re-Sale';
}
const result = [
{ label: 'Category', value: this.selectOptions.getBusiness(this.listing.type) },
{ label: 'Located in', value: `${this.listing.location.name ? this.listing.location.name : this.listing.location.county}, ${this.selectOptions.getState(this.listing.location.state)}` },
{ label: 'Asking Price', value: `${this.listing.price ? `$${this.listing.price.toLocaleString()}` : ''}` },
{ label: 'Sales revenue', value: `${this.listing.salesRevenue ? `$${this.listing.salesRevenue.toLocaleString()}` : ''}` },
{ label: 'Cash flow', value: `${this.listing.cashFlow ? `$${this.listing.cashFlow.toLocaleString()}` : ''}` },
{ label: 'Type of Real Estate', value: typeOfRealEstate },
{ label: 'Employees', value: this.listing.employees },
{ label: 'Established since', value: this.listing.established },
{ label: 'Support & Training', value: this.listing.supportAndTraining },
{ label: 'Reason for Sale', value: this.listing.reasonForSale },
{ label: 'Broker licensing', value: this.listing.brokerLicencing },
{ label: 'Listed since', value: `${this.dateInserted()} - ${this.getDaysListed()} days` },
{
label: 'Listing by',
value: null, // Wird nicht verwendet
isHtml: true,
isListingBy: true, // Flag für den speziellen Fall
user: this.listingUser, // Übergebe das User-Objekt
imagePath: this.listing.imageName,
imageBaseUrl: this.env.imageBaseUrl,
ts: this.ts,
},
];
if (this.listing.draft) {
result.push({ label: 'Draft', value: this.listing.draft ? 'Yes' : 'No' });
}
return result;
}
save() {
this.listing.favoritesForUser.push(this.user.email);
this.listingsService.save(this.listing);
this.auditService.createEvent(this.listing.id, 'favorite', this.user?.email);
}
isAlreadyFavorite() {
return this.listing.favoritesForUser.includes(this.user.email);
}
async showShareByEMail() {
const result = await this.emailService.showShareByEMail({
yourEmail: this.user ? this.user.email : null,
yourName: this.user ? `${this.user.firstname} ${this.user.lastname}` : null,
url: environment.mailinfoUrl,
listingTitle: this.listing.title,
id: this.listing.id,
type: 'business',
});
if (result) {
this.auditService.createEvent(this.listing.id, 'email', this.user?.email, <ShareByEMail>result);
this.messageService.addMessage({
severity: 'success',
text: 'Your Email has beend sent',
duration: 5000,
});
}
}
createEvent(eventType: EventTypeEnum) {
this.auditService.createEvent(this.listing.id, eventType, this.user?.email);
}
getDaysListed() {
return dayjs().diff(this.listing.created, 'day');
}
dateInserted() {
return dayjs(this.listing.created).format('DD/MM/YYYY');
}
}

View File

@ -1,520 +0,0 @@
<div class="container mx-auto p-4">
<div class="bg-white rounded-lg drop-shadow-custom-bg-mobile md:drop-shadow-custom-bg p-6">
<h1 class="text-2xl font-bold mb-6">Edit Listing</h1>
@if(listing){
<form #listingForm="ngForm" class="space-y-6">
<!-- Listing Category -->
<div>
<label for="listingsCategory" class="block text-gray-700 mb-2 font-medium flex items-center">
Listing Category
<div *ngIf="validationService.hasMessage('listingsCategory')" class="ml-2 relative group">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 hidden group-hover:block bg-gray-800 text-white text-xs px-2 py-1 rounded whitespace-nowrap">
{{ validationService.getMessage('listingsCategory')?.message }}
</div>
</div>
</label>
<div class="relative">
<ng-select
[readonly]="true"
[items]="selectOptions?.listingCategories"
bindLabel="name"
bindValue="value"
[(ngModel)]="listing.listingsCategory"
(ngModelChange)="changeListingCategory($event)"
name="listingsCategory"
[class.border-red-500]="validationService.hasMessage('listingsCategory')"
class="listing-category-select"
placeholder="Select listing category"
></ng-select>
</div>
</div>
<!-- Title -->
<div>
<label for="title" class="block text-gray-700 mb-2 font-medium flex items-center">
Title of Listing
<div *ngIf="validationService.hasMessage('title')" class="ml-2 relative group">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 hidden group-hover:block bg-gray-800 text-white text-xs px-2 py-1 rounded whitespace-nowrap">
{{ validationService.getMessage('title')?.message }}
</div>
</div>
</label>
<div class="relative">
<input
type="text"
id="title"
name="title"
[(ngModel)]="listing.title"
placeholder="Enter listing title"
class="w-full px-3 py-3 pl-10 border border-gray-300 rounded-lg focus:outline-none focus:border-black focus:ring-0"
[class.border-red-500]="validationService.hasMessage('title')"
/>
<svg xmlns="http://www.w3.org/2000/svg" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
</div>
<!-- Description -->
<div>
<label for="description" class="block text-gray-700 mb-2 font-medium flex items-center">
Description
<div *ngIf="validationService.hasMessage('description')" class="ml-2 relative group">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 hidden group-hover:block bg-gray-800 text-white text-xs px-2 py-1 rounded whitespace-nowrap">
{{ validationService.getMessage('description')?.message }}
</div>
</div>
</label>
<app-validated-quill id="description" name="description" [(ngModel)]="listing.description" [class.border-red-500]="validationService.hasMessage('description')"></app-validated-quill>
</div>
<!-- Type of Business -->
<div>
<label for="type" class="block text-gray-700 mb-2 font-medium flex items-center">
Type of Business
<div *ngIf="validationService.hasMessage('type')" class="ml-2 relative group">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 hidden group-hover:block bg-gray-800 text-white text-xs px-2 py-1 rounded whitespace-nowrap">
{{ validationService.getMessage('type')?.message }}
</div>
</div>
</label>
<div class="relative">
<app-validated-ng-select id="type" name="type" [(ngModel)]="listing.type" [items]="typesOfBusiness" [class.border-red-500]="validationService.hasMessage('type')" placeholder="Select business type"></app-validated-ng-select>
</div>
</div>
<!-- Location and Price -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Location -->
<div>
<label for="location" class="block text-gray-700 mb-2 font-medium flex items-center">
Location
<div *ngIf="validationService.hasMessage('location')" class="ml-2 relative group">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 hidden group-hover:block bg-gray-800 text-white text-xs px-2 py-1 rounded whitespace-nowrap">
{{ validationService.getMessage('location')?.message }}
</div>
</div>
</label>
<app-validated-location id="location" name="location" [(ngModel)]="listing.location" [class.border-red-500]="validationService.hasMessage('location')"></app-validated-location>
</div>
<!-- Price -->
<div>
<label for="price" class="block text-gray-700 mb-2 font-medium flex items-center">
Price
<div *ngIf="validationService.hasMessage('price')" class="ml-2 relative group">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 hidden group-hover:block bg-gray-800 text-white text-xs px-2 py-1 rounded whitespace-nowrap">
{{ validationService.getMessage('price')?.message }}
</div>
</div>
</label>
<div class="relative">
<input
type="text"
id="price"
name="price"
[(ngModel)]="listing.price"
currencyMask
[options]="{ prefix: '$', thousands: ',', decimal: '.', precision: 0, align: 'left' }"
placeholder="Enter listing price"
class="w-full px-3 py-3 pl-10 border border-gray-300 rounded-lg focus:outline-none focus:border-black focus:ring-0"
[class.border-red-500]="validationService.hasMessage('price')"
autocomplete="off"
/>
<svg xmlns="http://www.w3.org/2000/svg" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
</div>
</div>
<!-- Sales Revenue and Cash Flow -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Sales Revenue -->
<div>
<label for="salesRevenue" class="block text-gray-700 mb-2 font-medium flex items-center">
Sales Revenue
<div *ngIf="validationService.hasMessage('salesRevenue')" class="ml-2 relative group">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 hidden group-hover:block bg-gray-800 text-white text-xs px-2 py-1 rounded whitespace-nowrap">
{{ validationService.getMessage('salesRevenue')?.message }}
</div>
</div>
</label>
<div class="relative">
<input
type="text"
id="salesRevenue"
name="salesRevenue"
[(ngModel)]="listing.salesRevenue"
currencyMask
[options]="{ prefix: '$', thousands: ',', decimal: '.', precision: 0, align: 'left' }"
placeholder="Enter annual sales revenue"
class="w-full px-3 py-3 pl-10 border border-gray-300 rounded-lg focus:outline-none focus:border-black focus:ring-0"
[class.border-red-500]="validationService.hasMessage('salesRevenue')"
/>
<svg xmlns="http://www.w3.org/2000/svg" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 8v8m-4-5v5m-4-2v2m-2 4h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</div>
</div>
<!-- Cash Flow -->
<div>
<label for="cashFlow" class="block text-gray-700 mb-2 font-medium flex items-center">
Cash Flow
<div *ngIf="validationService.hasMessage('cashFlow')" class="ml-2 relative group">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 hidden group-hover:block bg-gray-800 text-white text-xs px-2 py-1 rounded whitespace-nowrap">
{{ validationService.getMessage('cashFlow')?.message }}
</div>
</div>
</label>
<div class="relative">
<input
type="text"
id="cashFlow"
name="cashFlow"
[(ngModel)]="listing.cashFlow"
currencyMask
[options]="{ prefix: '$', thousands: ',', decimal: '.', precision: 0, align: 'left' }"
placeholder="Enter annual cash flow"
class="w-full px-3 py-3 pl-10 border border-gray-300 rounded-lg focus:outline-none focus:border-black focus:ring-0"
[class.border-red-500]="validationService.hasMessage('cashFlow')"
/>
<svg xmlns="http://www.w3.org/2000/svg" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 9V7a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2m2 4h10a2 2 0 002-2v-6a2 2 0 00-2-2H9a2 2 0 00-2 2v6a2 2 0 002 2z" />
</svg>
</div>
</div>
</div>
<!-- Established and Employees -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Established In -->
<div>
<label for="established" class="block text-gray-700 mb-2 font-medium flex items-center">
Established In
<div *ngIf="validationService.hasMessage('established')" class="ml-2 relative group">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 hidden group-hover:block bg-gray-800 text-white text-xs px-2 py-1 rounded whitespace-nowrap">
{{ validationService.getMessage('established')?.message }}
</div>
</div>
</label>
<div class="relative">
<input
type="text"
id="established"
name="established"
[(ngModel)]="listing.established"
mask="0000"
kind="number"
placeholder="Enter year (e.g. 2010)"
class="w-full px-3 py-3 pl-10 border border-gray-300 rounded-lg focus:outline-none focus:border-black focus:ring-0"
[class.border-red-500]="validationService.hasMessage('established')"
/>
<svg xmlns="http://www.w3.org/2000/svg" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</div>
</div>
<!-- Employees -->
<div>
<label for="employees" class="block text-gray-700 mb-2 font-medium flex items-center">
Employees
<div *ngIf="validationService.hasMessage('employees')" class="ml-2 relative group">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 hidden group-hover:block bg-gray-800 text-white text-xs px-2 py-1 rounded whitespace-nowrap">
{{ validationService.getMessage('employees')?.message }}
</div>
</div>
</label>
<div class="relative">
<input
type="text"
id="employees"
name="employees"
[(ngModel)]="listing.employees"
mask="0000"
kind="number"
placeholder="Number of employees"
class="w-full px-3 py-3 pl-10 border border-gray-300 rounded-lg focus:outline-none focus:border-black focus:ring-0"
[class.border-red-500]="validationService.hasMessage('employees')"
/>
<svg xmlns="http://www.w3.org/2000/svg" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
/>
</svg>
</div>
</div>
</div>
<!-- Property Checkboxes -->
<div>
<label class="block text-gray-700 mb-3 font-medium">Property Details</label>
<div class="flex flex-wrap gap-6">
<!-- Real Estate Included -->
<div class="flex items-center">
<input
type="checkbox"
id="realEstateIncluded"
[(ngModel)]="listing.realEstateIncluded"
(ngModelChange)="onCheckboxChange('realEstateIncluded', $event)"
name="realEstateIncluded"
class="w-5 h-5 text-blue-600 border-gray-300 rounded focus:ring-0 focus:border-black mr-2"
/>
<label for="realEstateIncluded" class="text-gray-700">Real Estate Included</label>
</div>
<!-- Leased Location -->
<div class="flex items-center">
<input
type="checkbox"
id="leasedLocation"
[(ngModel)]="listing.leasedLocation"
(ngModelChange)="onCheckboxChange('leasedLocation', $event)"
name="leasedLocation"
class="w-5 h-5 text-blue-600 border-gray-300 rounded focus:ring-0 focus:border-black mr-2"
/>
<label for="leasedLocation" class="text-gray-700">Leased Location</label>
</div>
<!-- Franchise Re-Sale -->
<div class="flex items-center">
<input
type="checkbox"
id="franchiseResale"
[(ngModel)]="listing.franchiseResale"
(ngModelChange)="onCheckboxChange('franchiseResale', $event)"
name="franchiseResale"
class="w-5 h-5 text-blue-600 border-gray-300 rounded focus:ring-0 focus:border-black mr-2"
/>
<label for="franchiseResale" class="text-gray-700">Franchise Re-Sale</label>
</div>
</div>
</div>
<!-- Support and Reason for Sale -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Support & Training -->
<div>
<label for="supportAndTraining" class="block text-gray-700 mb-2 font-medium flex items-center">
Support & Training
<div *ngIf="validationService.hasMessage('supportAndTraining')" class="ml-2 relative group">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 hidden group-hover:block bg-gray-800 text-white text-xs px-2 py-1 rounded whitespace-nowrap">
{{ validationService.getMessage('supportAndTraining')?.message }}
</div>
</div>
</label>
<div class="relative">
<input
type="text"
id="supportAndTraining"
name="supportAndTraining"
[(ngModel)]="listing.supportAndTraining"
placeholder="Describe support & training offered"
class="w-full px-3 py-3 pl-10 border border-gray-300 rounded-lg focus:outline-none focus:border-black focus:ring-0"
[class.border-red-500]="validationService.hasMessage('supportAndTraining')"
/>
<svg xmlns="http://www.w3.org/2000/svg" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
/>
</svg>
</div>
</div>
<!-- Reason for Sale -->
<div>
<label for="reasonForSale" class="block text-gray-700 mb-2 font-medium flex items-center">
Reason for Sale
<div *ngIf="validationService.hasMessage('reasonForSale')" class="ml-2 relative group">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 hidden group-hover:block bg-gray-800 text-white text-xs px-2 py-1 rounded whitespace-nowrap">
{{ validationService.getMessage('reasonForSale')?.message }}
</div>
</div>
</label>
<div class="relative">
<input
type="text"
id="reasonForSale"
name="reasonForSale"
[(ngModel)]="listing.reasonForSale"
placeholder="Why are you selling this business?"
class="w-full px-3 py-3 pl-10 border border-gray-300 rounded-lg focus:outline-none focus:border-black focus:ring-0"
[class.border-red-500]="validationService.hasMessage('reasonForSale')"
/>
<svg xmlns="http://www.w3.org/2000/svg" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
</div>
</div>
<!-- Broker Licensing and Internal Number -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Broker Licensing -->
<div>
<label for="brokerLicencing" class="block text-gray-700 mb-2 font-medium flex items-center">
Broker Licensing
<span class="text-sm font-normal text-gray-500 ml-1">(please maintain your license in your account)</span>
<div *ngIf="validationService.hasMessage('brokerLicencing')" class="ml-2 relative group">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 hidden group-hover:block bg-gray-800 text-white text-xs px-2 py-1 rounded whitespace-nowrap">
{{ validationService.getMessage('brokerLicencing')?.message }}
</div>
</div>
</label>
<div class="relative">
<ng-select [(ngModel)]="listing.brokerLicencing" name="brokerLicencing" placeholder="Select your broker license" [class.border-red-500]="validationService.hasMessage('brokerLicencing')" class="broker-license-select">
@for (licensedIn of listingUser?.licensedIn; track listingUser?.licensedIn) {
<ng-option [value]="licensedIn.registerNo">{{ licensedIn.state }} {{ licensedIn.registerNo }}</ng-option>
}
</ng-select>
</div>
</div>
<!-- Internal Listing Number -->
<div>
<label for="internalListingNumber" class="block text-gray-700 mb-2 font-medium flex items-center">
Internal Listing Number
<div *ngIf="validationService.hasMessage('internalListingNumber')" class="ml-2 relative group">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 hidden group-hover:block bg-gray-800 text-white text-xs px-2 py-1 rounded whitespace-nowrap">
{{ validationService.getMessage('internalListingNumber')?.message }}
</div>
</div>
</label>
<div class="relative">
<input
type="text"
id="internalListingNumber"
name="internalListingNumber"
[(ngModel)]="listing.internalListingNumber"
kind="number"
mask="00000000000000000000"
placeholder="Enter reference number"
class="w-full px-3 py-3 pl-10 border border-gray-300 rounded-lg focus:outline-none focus:border-black focus:ring-0"
[class.border-red-500]="validationService.hasMessage('internalListingNumber')"
/>
<svg xmlns="http://www.w3.org/2000/svg" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 20l4-16m2 16l4-16M6 9h14M4 15h14" />
</svg>
</div>
</div>
</div>
<!-- Internal Notes -->
<div>
<label for="internals" class="block text-gray-700 mb-2 font-medium flex items-center">
Internal Notes
<span class="text-sm font-normal text-gray-500 ml-1">(Will not be shown on the listing, for your records only.)</span>
<div *ngIf="validationService.hasMessage('internals')" class="ml-2 relative group">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 hidden group-hover:block bg-gray-800 text-white text-xs px-2 py-1 rounded whitespace-nowrap">
{{ validationService.getMessage('internals')?.message }}
</div>
</div>
</label>
<div class="relative">
<textarea
id="internals"
name="internals"
[(ngModel)]="listing.internals"
rows="4"
placeholder="Enter your private notes about this listing..."
class="w-full px-3 py-3 border border-gray-300 rounded-lg focus:outline-none focus:border-black focus:ring-0"
[class.border-red-500]="validationService.hasMessage('internals')"
></textarea>
</div>
</div>
<!-- Draft Mode Toggle -->
<div class="flex items-center">
<label class="flex items-center cursor-pointer">
<div class="relative">
<input type="checkbox" [(ngModel)]="listing.draft" name="draft" class="sr-only" />
<div class="w-14 h-7 bg-gray-300 rounded-full"></div>
<div class="dot absolute left-1 top-1 bg-white w-5 h-5 rounded-full transition-transform duration-300 ease-in-out" [ngClass]="{ 'translate-x-7': listing.draft }"></div>
</div>
<div class="ml-3 text-gray-700">
Draft Mode
<span class="text-sm font-normal text-gray-500">(Will not be shown as public listing)</span>
</div>
</label>
</div>
<!-- Submit Button -->
<div class="flex justify-start">
@if (mode==='create'){
<button (click)="save()" class="px-6 py-3 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 focus:outline-none focus:border-black focus:ring-0 transition-colors duration-200 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Post Listing
</button>
} @else {
<button (click)="save()" class="px-6 py-3 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 focus:outline-none focus:border-black focus:ring-0 transition-colors duration-200 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Update Listing
</button>
}
</div>
</form>
}
</div>
</div>

View File

@ -1,34 +0,0 @@
:host {
display: block;
font-family: 'Inter', sans-serif;
}
// .container {
// max-width: 100%;
// margin: 0 auto;
// }
:host ::ng-deep quill-editor {
.ql-container {
min-height: 200px;
}
}
// input[type='checkbox'] {
// &:checked + .dot {
// transform: translateX(100%);
// }
// &:checked + .block {
// background-color: #4299e1;
// }
// }
.dot {
transition: all 0.3s ease-in-out;
}
::ng-deep .ng-select.ng-select-single .ng-select-container {
height: 50px;
}
quill-editor {
width: 100%;
}

View File

@ -1,173 +0,0 @@
import { Component } from '@angular/core';
import { ActivatedRoute, NavigationEnd, Router, RouterModule } from '@angular/router';
import { lastValueFrom } from 'rxjs';
import { DragDropModule } from '@angular/cdk/drag-drop';
import { faTrash } from '@fortawesome/free-solid-svg-icons';
import { QuillModule } from 'ngx-quill';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { NgSelectModule } from '@ng-select/ng-select';
import { NgxCurrencyDirective } from 'ngx-currency';
import { BusinessListing, CommercialPropertyListing, User } from '../../../../../bizmatch-server/src/models/db.model';
import { AutoCompleteCompleteEvent, createDefaultBusinessListing, emailToDirName, ImageProperty } from '../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../environments/environment';
import { ValidatedInputComponent } from '../../components/validated-input/validated-input.component';
import { ValidatedLocationComponent } from '../../components/validated-location/validated-location.component';
import { ValidatedNgSelectComponent } from '../../components/validated-ng-select/validated-ng-select.component';
import { ValidatedPriceComponent } from '../../components/validated-price/validated-price.component';
import { ValidatedQuillComponent } from '../../components/validated-quill/validated-quill.component';
import { ValidatedTextareaComponent } from '../../components/validated-textarea/validated-textarea.component';
import { AuthService } from '../../services/auth.service';
import { GeoService } from '../../services/geo.service';
import { ImageService } from '../../services/image.service';
import { ListingsService } from '../../services/listings.service';
import { LoadingService } from '../../services/loading.service';
import { MessageService } from '../../services/message.service';
import { SelectOptionsService } from '../../services/select-options.service';
import { UserService } from '../../services/user.service';
import { ValidationService } from '../../services/validation.service';
import { map2User, routeListingWithState, TOOLBAR_OPTIONS } from '../../utils/utils';
@Component({
selector: 'business-listing',
standalone: true,
imports: [
CommonModule,
FormsModule,
RouterModule,
FontAwesomeModule,
DragDropModule,
QuillModule,
NgSelectModule,
ValidatedInputComponent,
ValidatedQuillComponent,
ValidatedNgSelectComponent,
ValidatedPriceComponent,
ValidatedTextareaComponent,
ValidatedLocationComponent,
NgxCurrencyDirective,
],
providers: [],
templateUrl: './edit-business-listing.component.html',
styleUrl: './edit-business-listing.component.scss',
})
export class EditBusinessListingComponent {
listingsCategory = 'business';
category: string;
location: string;
mode: 'edit' | 'create';
separator: '\n\n';
listing: BusinessListing;
private id: string | undefined;
user: User;
environment = environment;
config = { aspectRatio: 16 / 9 };
editorModules = TOOLBAR_OPTIONS;
draggedImage: ImageProperty;
faTrash = faTrash;
data: CommercialPropertyListing;
typesOfBusiness = [];
quillModules = {
toolbar: [['bold', 'italic', 'underline', 'strike'], [{ list: 'ordered' }, { list: 'bullet' }], [{ header: [1, 2, 3, 4, 5, 6, false] }], [{ color: [] }, { background: [] }], ['clean']],
};
listingUser: User;
constructor(
public selectOptions: SelectOptionsService,
private router: Router,
private activatedRoute: ActivatedRoute,
private listingsService: ListingsService,
public userService: UserService,
private geoService: GeoService,
private imageService: ImageService,
private loadingService: LoadingService,
private messageService: MessageService,
private route: ActivatedRoute,
public validationService: ValidationService,
private authService: AuthService,
) {
this.router.events.subscribe(event => {
if (event instanceof NavigationEnd) {
this.mode = event.url.startsWith('/createBusinessListing') ? 'create' : 'edit';
}
});
this.route.data.subscribe(async () => {
if (this.router.getCurrentNavigation().extras.state) {
this.data = this.router.getCurrentNavigation().extras.state['data'];
}
});
this.typesOfBusiness = selectOptions.typesOfBusiness.map(e => {
return { name: e.name, value: e.value };
});
this.id = this.activatedRoute.snapshot.params['id'] as string | undefined;
}
async ngOnInit() {
const token = await this.authService.getToken();
const keycloakUser = map2User(token);
this.listingUser = await this.userService.getByMail(keycloakUser.email);
if (this.mode === 'edit') {
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id));
} else {
this.listing = createDefaultBusinessListing();
this.listing.email = this.listingUser.email;
this.listing.imageName = emailToDirName(keycloakUser.email);
if (this.data) {
this.listing.title = this.data?.title;
this.listing.description = this.data?.description;
}
}
}
ngOnDestroy() {
this.validationService.clearMessages(); // Löschen Sie alle bestehenden Validierungsnachrichten
}
async save() {
try {
this.listing = await this.listingsService.save(this.listing);
this.router.navigate(['editBusinessListing', this.listing.id]);
this.messageService.addMessage({ severity: 'success', text: 'Listing changes have been persisted', duration: 3000 });
this.validationService.clearMessages(); // Löschen Sie alle bestehenden Validierungsnachrichten
} catch (error) {
this.messageService.addMessage({
severity: 'danger',
text: 'An error occurred while saving the profile - Please check your inputs',
duration: 5000,
});
this.validationService.handleApiError(error.error);
}
}
suggestions: string[] | undefined;
async search(event: AutoCompleteCompleteEvent) {
const result = await lastValueFrom(this.geoService.findCitiesStartingWith(event.query));
this.suggestions = result.map(r => r.name).slice(0, 5);
}
changeListingCategory(value: 'business' | 'commercialProperty') {
routeListingWithState(this.router, value, this.listing);
}
onNumericInputChange(value: string, modelProperty: string): void {
const newValue = value === '' ? null : +value;
this.setPropertyByPath(this, modelProperty, newValue);
}
private setPropertyByPath(obj: any, path: string, value: any): void {
const keys = path.split('.');
let target = obj;
for (let i = 0; i < keys.length - 1; i++) {
target = target[keys[i]];
}
target[keys[keys.length - 1]] = value;
}
onCheckboxChange(checkbox: string, value: boolean) {
// Deaktivieren Sie alle Checkboxes
this.listing.realEstateIncluded = false;
this.listing.leasedLocation = false;
this.listing.franchiseResale = false;
// Aktivieren Sie nur die aktuell ausgewählte Checkbox
this.listing[checkbox] = value;
}
}

View File

@ -1,35 +0,0 @@
<div class="container mx-auto py-8 px-4 max-w-md">
<div class="bg-white p-6 rounded-lg drop-shadow-custom-bg-mobile md:drop-shadow-custom-bg text-center">
<!-- Loading state -->
<ng-container *ngIf="verificationStatus === 'pending'">
<div class="flex justify-center mb-4">
<div class="w-10 h-10 border-4 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
</div>
<p class="text-gray-700">Verifying your email address...</p>
</ng-container>
<!-- Success state -->
<ng-container *ngIf="verificationStatus === 'success'">
<div class="flex justify-center mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
</div>
<h2 class="text-2xl font-bold text-green-600 mb-5">Your email has been verified</h2>
<p class="text-gray-700 mb-4">You will be redirected to your account page in 5 seconds</p>
<a routerLink="/account" class="inline-block px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"> Go to Account Page Now </a>
</ng-container>
<!-- Error state -->
<ng-container *ngIf="verificationStatus === 'error'">
<div class="flex justify-center mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
<h2 class="text-2xl font-bold text-red-600 mb-3">Verification Failed</h2>
<p class="text-gray-700 mb-4">{{ errorMessage }}</p>
<a routerLink="/login" class="inline-block px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"> Return to Login </a>
</ng-container>
</div>
</div>

View File

@ -1,68 +0,0 @@
import { CommonModule } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { environment } from '../../../environments/environment';
import { AuthService } from '../../services/auth.service';
import { UserService } from '../../services/user.service';
@Component({
selector: 'app-email-authorized',
standalone: true,
imports: [CommonModule, RouterModule],
templateUrl: './email-authorized.component.html',
})
export class EmailAuthorizedComponent implements OnInit {
verificationStatus: 'pending' | 'success' | 'error' = 'pending';
errorMessage: string | null = null;
constructor(private route: ActivatedRoute, private router: Router, private http: HttpClient, private authService: AuthService, private userService: UserService) {}
ngOnInit(): void {
const oobCode = this.route.snapshot.queryParamMap.get('oobCode');
const email = this.route.snapshot.queryParamMap.get('email');
const mode = this.route.snapshot.queryParamMap.get('mode');
if (mode === 'verifyEmail' && oobCode && email) {
this.verifyEmail(oobCode, email);
} else {
this.verificationStatus = 'error';
this.errorMessage = 'Invalid verification link';
}
}
private verifyEmail(oobCode: string, email: string): void {
this.http.post<{ message: string; token: string }>(`${environment.apiBaseUrl}/bizmatch/auth/verify-email`, { oobCode, email }).subscribe({
next: async response => {
this.verificationStatus = 'success';
try {
// Use the custom token from the server to sign in with Firebase
await this.authService.signInWithCustomToken(response.token);
// Try to get user info
try {
const user = await this.userService.getByMail(email);
console.log('User retrieved:', user);
} catch (userError) {
console.error('Error getting user:', userError);
// Don't change verification status - it's still a success
}
// Redirect to dashboard after a short delay
setTimeout(() => {
this.router.navigate(['/account']);
}, 5000);
} catch (authError) {
console.error('Error signing in with custom token:', authError);
// Keep success status for verification, but add warning about login
this.errorMessage = 'Email verified, but there was an issue signing you in. Please try logging in manually.';
}
},
error: err => {
this.verificationStatus = 'error';
this.errorMessage = err.error?.message || 'Verification failed';
},
});
}
}

View File

@ -1,174 +0,0 @@
<div class="container mx-auto p-4">
<div class="bg-white rounded-lg drop-shadow-custom-bg-mobile md:drop-shadow-custom-bg p-6">
<h2 class="text-2xl font-semibold mb-6">Contact Us</h2>
<form class="space-y-6">
<!-- Erste Zeile: Name und E-Mail -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Name Eingabe -->
<div>
<label for="name" class="block text-gray-700 mb-2 font-medium flex items-center">
Your Name
<div *ngIf="validationService.hasMessage('name')" class="ml-2 relative group">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 hidden group-hover:block bg-gray-800 text-white text-xs px-2 py-1 rounded whitespace-nowrap">
{{ validationService.getMessage('name')?.message }}
</div>
</div>
</label>
<div class="relative">
<input
id="name"
name="name"
type="text"
[(ngModel)]="mailinfo.sender.name"
placeholder="Enter your name"
class="w-full px-3 py-3 pl-10 border border-gray-300 rounded-lg focus:outline-none focus:border-black focus:ring-0"
[class.border-red-500]="validationService.hasMessage('name')"
/>
<svg xmlns="http://www.w3.org/2000/svg" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
</div>
<!-- E-Mail Eingabe -->
<div>
<label for="email" class="block text-gray-700 mb-2 font-medium flex items-center">
Your Email
<div *ngIf="validationService.hasMessage('email')" class="ml-2 relative group">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 hidden group-hover:block bg-gray-800 text-white text-xs px-2 py-1 rounded whitespace-nowrap">
{{ validationService.getMessage('email')?.message }}
</div>
</div>
</label>
<div class="relative">
<input
id="email"
name="email"
type="email"
[(ngModel)]="mailinfo.sender.email"
placeholder="Enter your email"
class="w-full px-3 py-3 pl-10 border border-gray-300 rounded-lg focus:outline-none focus:border-black focus:ring-0"
[class.border-red-500]="validationService.hasMessage('email')"
/>
<svg xmlns="http://www.w3.org/2000/svg" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</div>
</div>
</div>
<!-- Zweite Zeile: Telefon und Bundesland -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Telefon Eingabe -->
<div>
<label for="phone" class="block text-gray-700 mb-2 font-medium flex items-center">
Phone Number
<div *ngIf="validationService.hasMessage('phoneNumber')" class="ml-2 relative group">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 hidden group-hover:block bg-gray-800 text-white text-xs px-2 py-1 rounded whitespace-nowrap">
{{ validationService.getMessage('phoneNumber')?.message }}
</div>
</div>
</label>
<div class="relative">
<input
id="phone"
name="phoneNumber"
type="tel"
[(ngModel)]="mailinfo.sender.phoneNumber"
mask="(000) 000-0000"
placeholder="(123) 456-7890"
class="w-full px-3 py-3 pl-10 border border-gray-300 rounded-lg focus:outline-none focus:border-black focus:ring-0"
[class.border-red-500]="validationService.hasMessage('phoneNumber')"
/>
<svg xmlns="http://www.w3.org/2000/svg" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"
/>
</svg>
</div>
</div>
<!-- Bundesland Select -->
<div>
<label for="state" class="block text-gray-700 mb-2 font-medium flex items-center">
State
<div *ngIf="validationService.hasMessage('state')" class="ml-2 relative group">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 hidden group-hover:block bg-gray-800 text-white text-xs px-2 py-1 rounded whitespace-nowrap">
{{ validationService.getMessage('state')?.message }}
</div>
</div>
</label>
<div class="relative">
<select
id="state"
name="state"
[(ngModel)]="mailinfo.sender.state"
class="w-full px-3 py-3 pl-10 border border-gray-300 rounded-lg bg-white focus:outline-none focus:border-black focus:ring-0 appearance-none"
[class.border-red-500]="validationService.hasMessage('state')"
>
<option value="" disabled selected>Select your state</option>
<option *ngFor="let state of selectOptions?.states" [value]="state.value">{{ state.name }}</option>
</select>
<svg xmlns="http://www.w3.org/2000/svg" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 21v-4m0 0V5a2 2 0 012-2h6.5l1 1H21l-3 6 3 6h-8.5l-1-1H5a2 2 0 00-2 2zm9-13.5V9" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" class="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5 pointer-events-none" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</div>
</div>
</div>
<!-- Kommentarfeld -->
<div>
<label for="comments" class="block text-gray-700 mb-2 font-medium flex items-center">
Questions/Comments
<div *ngIf="validationService.hasMessage('comments')" class="ml-2 relative group">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 hidden group-hover:block bg-gray-800 text-white text-xs px-2 py-1 rounded whitespace-nowrap">
{{ validationService.getMessage('comments')?.message }}
</div>
</div>
</label>
<textarea
id="comments"
name="comments"
[(ngModel)]="mailinfo.sender.comments"
placeholder="Type your questions or comments here..."
rows="4"
class="w-full px-3 py-3 border border-gray-300 rounded-lg focus:outline-none focus:border-black focus:ring-0"
[class.border-red-500]="validationService.hasMessage('comments')"
></textarea>
</div>
<!-- Submit Button -->
<div>
<button (click)="mail()" class="w-full sm:w-auto px-6 py-3 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 transition-colors duration-200">
<span class="flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
</svg>
Submit
</span>
</button>
</div>
</form>
</div>
</div>

View File

@ -1,7 +0,0 @@
:host ::ng-deep .ng-select-container {
height: 42px !important;
border-radius: 0.5rem;
.ng-value-container .ng-input {
top: 10px;
}
}

View File

@ -1,72 +0,0 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { User } from '../../../../../bizmatch-server/src/models/db.model';
import { ErrorResponse, KeycloakUser, MailInfo } from '../../../../../bizmatch-server/src/models/main.model';
import { AuditService } from '../../services/audit.service';
import { AuthService } from '../../services/auth.service';
import { MailService } from '../../services/mail.service';
import { MessageService } from '../../services/message.service';
import { SelectOptionsService } from '../../services/select-options.service';
import { UserService } from '../../services/user.service';
import { ValidationService } from '../../services/validation.service';
import { createMailInfo, map2User } from '../../utils/utils';
@Component({
selector: 'app-email-us',
standalone: true,
imports: [CommonModule, FormsModule, RouterModule, FontAwesomeModule],
providers: [],
templateUrl: './email-us.component.html',
styleUrl: './email-us.component.scss',
})
export class EmailUsComponent {
mailinfo: MailInfo;
keycloakUser: KeycloakUser;
user: User;
errorResponse: ErrorResponse;
constructor(
private mailService: MailService,
private userService: UserService,
public validationService: ValidationService,
private messageService: MessageService,
public selectOptions: SelectOptionsService,
private auditService: AuditService,
private authService: AuthService,
) {
this.mailinfo = createMailInfo();
}
async ngOnInit() {
const token = await this.authService.getToken();
this.keycloakUser = map2User(token);
if (this.keycloakUser) {
this.user = await this.userService.getByMail(this.keycloakUser.email);
this.mailinfo = createMailInfo(this.user);
}
}
ngOnDestroy() {
this.validationService.clearMessages(); // Löschen Sie alle bestehenden Validierungsnachrichten
}
async mail() {
try {
this.validationService.clearMessages();
this.mailinfo.email = 'support@bizmatch.net';
await this.mailService.mail(this.mailinfo);
this.messageService.addMessage({ severity: 'success', text: 'Your request has been forwarded to the support team of bizmatch.', duration: 3000 });
this.auditService.createEvent(null, 'emailus', this.mailinfo.email, this.mailinfo);
this.mailinfo = createMailInfo(this.user);
} catch (error) {
this.messageService.addMessage({
severity: 'danger',
text: 'Please check your inputs',
duration: 5000,
});
this.validationService.handleApiError(error.error);
}
}
containsError(fieldname: string) {
return this.errorResponse?.fields.map(f => f.fieldname).includes(fieldname);
}
}

View File

@ -1,7 +0,0 @@
<div class="flex flex-col items-center justify-center min-h-screen bg-gray-100">
<div class="bg-white p-8 rounded drop-shadow-custom-bg-mobile md:drop-shadow-custom-bg w-full max-w-md text-center">
<h2 class="text-2xl font-bold mb-4">Email Verification</h2>
<p class="mb-4">A verification email has been sent to your email address. Please check your inbox and click the link to verify your account.</p>
<p>Once verified, please return to the application.</p>
</div>
</div>

View File

@ -1,9 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-email-verification',
standalone: true,
imports: [],
templateUrl: './email-verification.component.html',
})
export class EmailVerificationComponent {}

View File

@ -1,85 +0,0 @@
<div class="container mx-auto p-4">
<div class="bg-white rounded-lg drop-shadow-custom-bg-mobile md:drop-shadow-custom-bg p-6">
<h1 class="text-2xl font-bold md:mb-4">My Favorites</h1>
<!-- Desktop view -->
<div class="hidden md:block">
<table class="w-full bg-white drop-shadow-inner-faint rounded-lg overflow-hidden">
<thead class="bg-gray-100">
<tr>
<th class="py-2 px-4 text-left">Title</th>
<th class="py-2 px-4 text-left">Category</th>
<th class="py-2 px-4 text-left">Located in</th>
<th class="py-2 px-4 text-left">Price</th>
<th class="py-2 px-4 text-left">Action</th>
</tr>
</thead>
<tbody>
@for(listing of favorites; track listing){
<tr class="border-b">
<td class="py-2 px-4">{{ listing.title }}</td>
<td class="py-2 px-4">{{ listing.listingsCategory === 'commercialProperty' ? 'Commercial Property' : 'Business' }}</td>
<td class="py-2 px-4">{{ listing.location.name ? listing.location.name : listing.location.county }}, {{ listing.location.state }}</td>
<td class="py-2 px-4">${{ listing.price.toLocaleString() }}</td>
<td class="py-2 px-4 flex">
@if(listing.listingsCategory==='business'){
<button class="bg-green-500 text-white w-10 h-10 flex items-center justify-center rounded-full mr-2" [routerLink]="['/details-business-listing', listing.id]">
<i class="fa-regular fa-eye"></i>
</button>
} @if(listing.listingsCategory==='commercialProperty'){
<button class="bg-green-500 text-white w-10 h-10 flex items-center justify-center rounded-full mr-2" [routerLink]="['/details-commercial-property-listing', listing.id]">
<i class="fa-regular fa-eye"></i>
</button>
}
<button class="bg-red-500 text-white w-10 h-10 flex items-center justify-center rounded-full mr-2" (click)="confirmDelete(listing)">
<i class="fa-solid fa-trash"></i>
</button>
</td>
</tr>
}
</tbody>
</table>
</div>
<!-- Mobile view -->
<div class="md:hidden">
<div *ngFor="let listing of favorites" class="bg-white drop-shadow-inner-faint rounded-lg p-4 mb-4">
<h2 class="text-xl font-semibold mb-2">{{ listing.title }}</h2>
<p class="text-gray-600 mb-2">Category: {{ listing.listingsCategory === 'commercialProperty' ? 'Commercial Property' : 'Business' }}</p>
<p class="text-gray-600 mb-2">Located in: {{ listing.location.name ? listing.location.name : listing.location.county }}, {{ listing.location.state }}</p>
<p class="text-gray-600 mb-2">Price: ${{ listing.price.toLocaleString() }}</p>
<div class="flex justify-start">
@if(listing.listingsCategory==='business'){
<button class="bg-green-500 text-white w-10 h-10 flex items-center justify-center rounded-full mr-2" [routerLink]="['/details-business-listing', listing.id]">
<i class="fa-regular fa-eye"></i>
</button>
} @if(listing.listingsCategory==='commercialProperty'){
<button class="bg-green-500 text-white w-10 h-10 flex items-center justify-center rounded-full mr-2" [routerLink]="['/details-commercial-property-listing', listing.id]">
<i class="fa-regular fa-eye"></i>
</button>
}
<button class="bg-red-500 text-white w-10 h-10 flex items-center justify-center rounded-full mr-2" (click)="confirmDelete(listing)">
<i class="fa-solid fa-trash"></i>
</button>
</div>
</div>
</div>
<!-- <div class="flex items-center justify-between mt-4">
<p class="text-sm text-gray-600">Showing 1 to 2 of 2 entries</p>
<div class="flex items-center">
<button class="px-2 py-1 border rounded-l-md bg-gray-100">&lt;&lt;</button>
<button class="px-2 py-1 border-t border-b bg-gray-100">&lt;</button>
<button class="px-2 py-1 border bg-blue-500 text-white">1</button>
<button class="px-2 py-1 border-t border-b bg-gray-100">&gt;</button>
<button class="px-2 py-1 border rounded-r-md bg-gray-100">&gt;&gt;</button>
<select class="ml-2 border rounded-md px-2 py-1">
<option>10</option>
</select>
</div>
</div> -->
</div>
</div>
<app-confirmation></app-confirmation>

View File

@ -1,3 +0,0 @@
.wide-column{
width: 40%;
}

View File

@ -1,43 +0,0 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { BusinessListing } from '../../../../../bizmatch-server/src/models/db.model';
import { KeycloakUser } from '../../../../../bizmatch-server/src/models/main.model';
import { ConfirmationComponent } from '../../components/confirmation.component';
import { AuthService } from '../../services/auth.service';
import { ConfirmationService } from '../../services/confirmation.service';
import { ListingsService } from '../../services/listings.service';
import { SelectOptionsService } from '../../services/select-options.service';
import { map2User } from '../../utils/utils';
@Component({
selector: 'app-favorites',
standalone: true,
imports: [CommonModule, FormsModule, RouterModule, FontAwesomeModule, ConfirmationComponent],
templateUrl: './favorites.component.html',
styleUrl: './favorites.component.scss',
})
export class FavoritesComponent {
user: KeycloakUser;
// listings: Array<ListingType> = []; //= dataListings as unknown as Array<BusinessListing>;
favorites: Array<BusinessListing>;
constructor(private listingsService: ListingsService, public selectOptions: SelectOptionsService, private confirmationService: ConfirmationService, private authService: AuthService) {}
async ngOnInit() {
const token = await this.authService.getToken();
this.user = map2User(token);
this.favorites = await this.listingsService.getFavoriteListings();
}
async confirmDelete(listing: BusinessListing) {
const confirmed = await this.confirmationService.showConfirmation({ message: `Are you sure you want to remove this listing from your Favorites?` });
if (confirmed) {
// this.messageService.showMessage('Listing has been deleted');
this.deleteListing(listing);
}
}
async deleteListing(listing: BusinessListing) {
await this.listingsService.removeFavorite(listing.id);
this.favorites = await this.listingsService.getFavoriteListings();
}
}

View File

@ -1,251 +0,0 @@
<!-- Navigation -->
<nav class="bg-white">
<div class="container mx-auto px-6 py-3 flex justify-between items-center">
<div class="flex items-center">
<a href="#" class="text-2xl font-bold text-blue-800">
<img src="assets/images/header-logo.png" alt="BizMatch.net" class="h-10" />
</a>
</div>
<div class="hidden md:flex items-center space-x-8">
<a href="#" class="text-gray-800 hover:text-blue-600">Home</a>
<a routerLink="/businessListings" class="text-blue-700 hover:font-bold">Businesses</a>
<a href="#services" class="text-gray-800 hover:text-blue-600">Services</a>
<a href="#location" class="text-gray-800 hover:text-blue-600">Location</a>
<a href="#contact" class="text-gray-800 hover:text-blue-600">Contact</a>
@if(user){
<a routerLink="/logout" class="text-gray-800 hover:text-blue-600">Logout</a>
}@else{
<a routerLink="/login" [queryParams]="{ mode: 'login' }" class="text-gray-800 hover:text-blue-600">Log In</a>
}
</div>
<div class="md:hidden">
<button class="text-gray-800 focus:outline-none" (click)="toggleMobileMenu()">
<svg class="h-6 w-6 fill-current" viewBox="0 0 24 24">
<path d="M4 5h16a1 1 0 0 1 0 2H4a1 1 0 1 1 0-2zm0 6h16a1 1 0 0 1 0 2H4a1 1 0 0 1 0-2zm0 6h16a1 1 0 0 1 0 2H4a1 1 0 0 1 0-2z"></path>
</svg>
</button>
</div>
</div>
<!-- Mobile menu (only shows when toggleMobileMenu is true) -->
<div *ngIf="showMobileMenu" class="md:hidden bg-white py-2 px-4">
<a href="#" class="block py-2 text-gray-800 hover:text-blue-600">Home</a>
<a href="#services" class="block py-2 text-gray-800 hover:text-blue-600">Services</a>
<a href="#location" class="block py-2 text-gray-800 hover:text-blue-600">Location</a>
<a href="#contact" class="block py-2 text-gray-800 hover:text-blue-600">Contact</a>
@if(user){
<a routerLink="/logout" class="block py-2 text-gray-800 hover:text-blue-600">Logout</a>
}@else{
<a routerLink="/login" [queryParams]="{ mode: 'login' }" class="block py-2 text-gray-800 hover:text-blue-600">Log In</a>
}
</div>
</nav>
<!-- Hero Section (made narrower) -->
<section class="hero-section flex items-center px-[2rem] py-[5rem]">
<div class="container mx-auto px-6 flex flex-col">
<!-- max-w-5xl makes it narrower -->
<div class="flex flex-col md:flex-row items-center">
<div class="md:w-1/2 text-white">
<h1 class="text-4xl md:text-5xl lg:text-6xl font-bold leading-tight mb-4">Connect with Your Ideal Business Opportunity</h1>
<p class="text-xl mb-8">BizMatch is your trusted partner in buying, selling, and valuing businesses in Texas.</p>
</div>
<div class="md:w-1/2 flex justify-center">
<img src="assets/images/corpusChristiSkyline.jpg" alt="Business handshake" class="rounded-lg shadow-2xl" />
</div>
</div>
<div class="flex justify-center mt-10">
<a routerLink="/businessListings" class="bg-emerald-500 hover:bg-emerald-700 md:text-2xl text-lg text-white font-semibold px-8 py-4 rounded-full shadow-lg transition duration-300"> View Available Businesses </a>
</div>
</div>
</section>
<!-- Services Section -->
<section id="services" class="py-20 bg-gray-50">
<div class="container mx-auto px-6">
<div class="text-center mb-16">
<h2 class="text-3xl font-bold text-blue-800 mb-4">Our Services</h2>
<p class="text-gray-600 max-w-2xl mx-auto">We offer comprehensive business brokerage services to help you navigate the complex process of buying or selling a business.</p>
</div>
<div class="flex flex-wrap -mx-4">
<!-- Service 1 -->
<div class="w-full md:w-1/3 px-4 mb-8">
<div class="service-card bg-white rounded-lg filter md:drop-shadow-custom-bg drop-shadow-custom-bg-mobile p-8 h-full">
<div class="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mb-6 mx-auto">
<svg class="w-8 h-8 text-blue-600" fill="currentColor" viewBox="0 0 20 20">
<path
d="M13 6a3 3 0 11-6 0 3 3 0 016 0zM18 8a2 2 0 11-4 0 2 2 0 014 0zM14 15a4 4 0 00-8 0v3h8v-3zM6 8a2 2 0 11-4 0 2 2 0 014 0zM16 18v-3a5.972 5.972 0 00-.75-2.906A3.005 3.005 0 0119 15v3h-3zM4.75 12.094A5.973 5.973 0 004 15v3H1v-3a3 3 0 013.75-2.906z"
></path>
</svg>
</div>
<h3 class="text-xl font-semibold text-blue-800 mb-4 text-center">Business Sales</h3>
<p class="text-gray-600 text-center">We help business owners prepare and market their businesses to qualified buyers, ensuring confidentiality throughout the process.</p>
</div>
</div>
<!-- Service 2 -->
<div class="w-full md:w-1/3 px-4 mb-8">
<div class="service-card bg-white rounded-lg filter md:drop-shadow-custom-bg drop-shadow-custom-bg-mobile p-8 h-full">
<div class="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mb-6 mx-auto">
<svg class="w-8 h-8 text-blue-600" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4 4a2 2 0 00-2 2v4a2 2 0 002 2V6h10a2 2 0 00-2-2H4zm2 6a2 2 0 012-2h8a2 2 0 012 2v4a2 2 0 01-2 2H8a2 2 0 01-2-2v-4zm6 4a2 2 0 100-4 2 2 0 000 4z" clip-rule="evenodd"></path>
</svg>
</div>
<h3 class="text-xl font-semibold text-blue-800 mb-4 text-center">Business Acquisitions</h3>
<p class="text-gray-600 text-center">We assist buyers in finding the right business opportunity, perform due diligence, and negotiate favorable terms for acquisition.</p>
</div>
</div>
<!-- Service 3 -->
<div class="w-full md:w-1/3 px-4 mb-8">
<div class="service-card bg-white rounded-lg filter md:drop-shadow-custom-bg drop-shadow-custom-bg-mobile p-8 h-full">
<div class="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mb-6 mx-auto">
<svg class="w-8 h-8 text-blue-600" fill="currentColor" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M5 2a1 1 0 011 1v1h1a1 1 0 010 2H6v1a1 1 0 01-2 0V6H3a1 1 0 010-2h1V3a1 1 0 011-1zm0 10a1 1 0 011 1v1h1a1 1 0 110 2H6v1a1 1 0 11-2 0v-1H3a1 1 0 110-2h1v-1a1 1 0 011-1zM12 2a1 1 0 01.967.744L14.146 7.2 17.5 9.134a1 1 0 010 1.732l-3.354 1.935-1.18 4.455a1 1 0 01-1.933 0L9.854 12.8 6.5 10.866a1 1 0 010-1.732l3.354-1.935 1.18-4.455A1 1 0 0112 2z"
clip-rule="evenodd"
></path>
</svg>
</div>
<h3 class="text-xl font-semibold text-blue-800 mb-4 text-center">Business Valuation</h3>
<p class="text-gray-600 text-center">Our expert team provides accurate business valuations based on industry standards, financial performance, and market conditions.</p>
</div>
</div>
</div>
<!-- Video Section -->
<div class="mt-16 text-center">
<h3 class="text-2xl font-semibold text-blue-800 mb-8">See How We Work</h3>
<div class="max-w-4xl mx-auto">
<div class="relative pb-[66.67%]">
<!-- 2/3 = 66.67% für 3:2 Seitenverhältnis -->
<video controls poster="assets/images/video-poster1.png" class="absolute inset-0 w-full h-full rounded-lg shadow-xl object-cover">
<source src="assets/videos/Bizmatch30Spot.mp4" type="video/mp4" />
Your browser does not support the video tag.
</video>
</div>
</div>
</div>
</div>
</section>
<!-- Why Choose Us Section -->
<section class="py-20 bg-white">
<div class="container mx-auto px-6">
<div class="text-center mb-16">
<h2 class="text-3xl font-bold text-blue-800 mb-4">Why Choose BizMatch</h2>
<p class="text-gray-600 max-w-2xl mx-auto">With decades of experience in the business brokerage industry, we provide unparalleled service to our clients.</p>
</div>
<div class="flex flex-wrap -mx-4">
<!-- Feature 1 -->
<div class="w-full md:w-1/4 px-4 mb-8">
<div class="text-center">
<div class="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mb-6 mx-auto">
<svg class="w-8 h-8 text-blue-600" fill="currentColor" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M2.166 4.999A11.954 11.954 0 0010 1.944 11.954 11.954 0 0017.834 5c.11.65.166 1.32.166 2.001 0 5.225-3.34 9.67-8 11.317C5.34 16.67 2 12.225 2 7c0-.682.057-1.35.166-2.001zm11.541 3.708a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd"
></path>
</svg>
</div>
<h3 class="text-xl font-semibold text-blue-800 mb-2">Experience</h3>
<p class="text-gray-600">Over 25 years of combined experience in business brokerage.</p>
</div>
</div>
<!-- Feature 2 -->
<div class="w-full md:w-1/4 px-4 mb-8">
<div class="text-center">
<div class="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mb-6 mx-auto">
<svg class="w-8 h-8 text-green-600" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path>
</svg>
</div>
<h3 class="text-xl font-semibold text-blue-800 mb-2">Confidentiality</h3>
<p class="text-gray-600">We maintain strict confidentiality throughout the entire transaction process.</p>
</div>
</div>
<!-- Feature 3 -->
<div class="w-full md:w-1/4 px-4 mb-8">
<div class="text-center">
<div class="w-16 h-16 bg-purple-100 rounded-full flex items-center justify-center mb-6 mx-auto">
<svg class="w-8 h-8 text-purple-600" fill="currentColor" viewBox="0 0 20 20">
<path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z"></path>
</svg>
</div>
<h3 class="text-xl font-semibold text-blue-800 mb-2">Network</h3>
<p class="text-gray-600">Extensive network of qualified buyers and business owners throughout Texas.</p>
</div>
</div>
<!-- Feature 4 -->
<div class="w-full md:w-1/4 px-4 mb-8">
<div class="text-center">
<div class="w-16 h-16 bg-yellow-100 rounded-full flex items-center justify-center mb-6 mx-auto">
<svg class="w-8 h-8 text-yellow-600" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z" clip-rule="evenodd"></path>
</svg>
</div>
<h3 class="text-xl font-semibold text-blue-800 mb-2">Personalized Approach</h3>
<p class="text-gray-600">Customized strategy for each client based on their unique business goals.</p>
</div>
</div>
</div>
</div>
</section>
<!-- Location Section -->
<section id="location" class="py-20 bg-gray-50">
<div class="container mx-auto px-6">
<div class="flex flex-wrap items-stretch">
<!-- Changed from items-center to items-stretch -->
<div class="w-full lg:w-2/5 mb-12 lg:mb-0">
<div class="h-full flex flex-col">
<!-- Added flex container with h-full -->
<h2 class="text-3xl font-bold text-blue-800 mb-6">Visit Our Office</h2>
<p class="text-gray-600 mb-8 text-lg">Our team of business brokers is ready to assist you at our Corpus Christi location.</p>
<div class="bg-white p-6 rounded-lg shadow-lg flex-grow">
<!-- Added flex-grow to make it fill available space -->
<h3 class="text-xl font-semibold text-blue-800 mb-4">BizMatch Headquarters</h3>
<p class="text-gray-600 mb-2">1001 Blucher Street</p>
<p class="text-gray-600 mb-2">Corpus Christi, TX 78401</p>
<p class="text-gray-600 mb-6">United States</p>
<p class="text-gray-600 mb-2"><strong>Phone:</strong> (555) 123-4567</p>
<p class="text-gray-600"><strong>Email:</strong> info&#64;bizmatch.net</p>
</div>
</div>
</div>
<div class="w-full lg:w-3/5">
<div class="rounded-lg overflow-hidden shadow-xl h-full min-h-[384px]">
<!-- Changed h-96 to h-full with min-height -->
<iframe
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3533.7894679685755!2d-97.38527228476843!3d27.773756032788047!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x866c1e3b8a9d0c0b%3A0x8f2c1d4c1a5c5b2c!2s1001%20Blucher%20St%2C%20Corpus%20Christi%2C%20TX%2078401%2C%20USA!5e0!3m2!1sen!2sde!4v1672531192743!5m2!1sen!2sde"
width="100%"
height="100%"
class="rounded-lg border-0"
style="min-height: 384px; display: block"
allowfullscreen=""
loading="lazy"
referrerpolicy="no-referrer-when-downgrade"
>
</iframe>
</div>
</div>
</div>
</div>
</section>
<!-- Contact Section -->
<section id="contact" class="py-20 bg-blue-700">
<div class="container mx-auto px-6 text-center">
<h2 class="text-3xl font-bold text-white mb-8">Ready to Get Started?</h2>
<p class="text-white text-xl mb-12 max-w-3xl mx-auto">Contact our team of experienced business brokers today for a confidential consultation about buying or selling a business.</p>
<a routerLink="/emailUs" class="bg-white text-blue-700 font-bold px-8 py-4 rounded-lg shadow-lg hover:bg-gray-100 transition duration-300 text-lg">Contact Us Now</a>
</div>
</section>
<!-- Footer -->

View File

@ -1,85 +0,0 @@
// Hero section styles
.hero-section {
background: linear-gradient(135deg, #0046b5 0%, #00a0e9 100%);
// height: 70vh; // Made shorter as requested
// min-height: 500px; // Reduced from 600px
}
// Button hover effects
.btn-primary {
background-color: #0046b5;
transition: all 0.3s ease;
&:hover {
background-color: #003492;
}
}
// Service card animation
.service-card {
transition: all 0.3s ease;
&:hover {
transform: translateY(-10px);
}
}
// Responsive adjustments
@media (max-width: 768px) {
.hero-section {
height: auto;
padding: 4rem 0;
}
}
// Make sure the Google Map is responsive
google-map {
display: block;
width: 100%;
}
// Override Tailwind default styling for video
video {
max-width: 100%;
object-fit: cover;
}
// Zusätzliche Styles für den Location-Bereich
// Verbesserte Map-Container Styles
#location {
.rounded-lg.overflow-hidden {
position: relative;
height: 100%;
min-height: 384px;
}
iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: 0;
}
// Stellen Sie sicher, dass der Kartencontainer im mobilen Layout
// eine angemessene Höhe hat
@media (max-width: 1023px) {
.rounded-lg.overflow-hidden {
height: 400px;
}
}
}
// Adressbox-Styling verbessern
.bg-white.p-6.rounded-lg.shadow-lg.flex-grow {
display: flex;
flex-direction: column;
justify-content: space-between;
// Sicherstellen, dass der untere Bereich sichtbar bleibt
.contact-info {
margin-top: auto;
}
}

View File

@ -1,51 +0,0 @@
import { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { RouterLink } from '@angular/router';
import { User } from '../../../../../bizmatch-server/src/models/db.model';
import { KeycloakUser } from '../../../../../bizmatch-server/src/models/main.model';
import { AuthService } from '../../services/auth.service';
import { UserService } from '../../services/user.service';
import { map2User } from '../../utils/utils';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss'],
standalone: true,
imports: [CommonModule, RouterLink],
})
export class HomeComponent implements OnInit {
showMobileMenu = false;
keycloakUser: KeycloakUser;
user: User;
constructor(private authService: AuthService, private userService: UserService) {}
async ngOnInit() {
// Add smooth scrolling for anchor links
this.setupSmoothScrolling();
const token = await this.authService.getToken();
this.keycloakUser = map2User(token);
if (this.keycloakUser) {
this.user = await this.userService.getByMail(this.keycloakUser.email);
this.userService.changeUser(this.user);
}
}
toggleMobileMenu(): void {
this.showMobileMenu = !this.showMobileMenu;
}
private setupSmoothScrolling(): void {
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const target = document.querySelector((this as HTMLAnchorElement).getAttribute('href') || '');
if (target) {
target.scrollIntoView({
behavior: 'smooth',
});
}
});
});
}
}

View File

@ -1,79 +0,0 @@
<div class="flex flex-col items-center justify-center min-h-screen">
<div class="bg-white p-8 rounded-lg drop-shadow-custom-bg w-full max-w-md">
<h2 class="text-2xl font-bold mb-6 text-center text-gray-800">
{{ isLoginMode ? 'Login' : 'Sign Up' }}
</h2>
<div class="flex items-center justify-center mb-6">
<span class="mr-3 text-gray-700 font-medium">Login</span>
<label for="toggle-switch" class="inline-flex relative items-center cursor-pointer">
<input type="checkbox" id="toggle-switch" class="sr-only peer" [checked]="!isLoginMode" (change)="toggleMode()" />
<div
class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:bg-gray-700 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"
></div>
</label>
<span class="ml-3 text-gray-700 font-medium">Sign Up</span>
</div>
<!-- E-Mail Eingabe -->
<div class="mb-4">
<label for="email" class="block text-gray-700 mb-2 font-medium">E-Mail</label>
<div class="relative">
<input id="email" type="email" [(ngModel)]="email" placeholder="Please enter E-Mail Address" class="w-full px-3 py-2 pl-10 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
<fa-icon [icon]="envelope" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></fa-icon>
</div>
</div>
<!-- Passwort Eingabe -->
<div class="mb-4">
<label for="password" class="block text-gray-700 mb-2 font-medium">Password</label>
<div class="relative">
<input id="password" type="password" [(ngModel)]="password" placeholder="Please enter Password" class="w-full px-3 py-2 pl-10 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
<fa-icon [icon]="lock" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></fa-icon>
</div>
</div>
<!-- Passwort-Bestätigung nur im Registrierungsmodus -->
<div *ngIf="!isLoginMode" class="mb-6">
<label for="confirmPassword" class="block text-gray-700 mb-2 font-medium">Confirm Password</label>
<div class="relative">
<input id="confirmPassword" type="password" [(ngModel)]="confirmPassword" placeholder="Repeat Password" class="w-full px-3 py-2 pl-10 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
<fa-icon [icon]="lock" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></fa-icon>
</div>
</div>
<!-- Fehlermeldung -->
<div *ngIf="errorMessage" class="text-red-500 text-center mb-4 text-sm">
{{ errorMessage }}
</div>
<!-- Submit Button -->
<button (click)="onSubmit()" class="w-full flex items-center justify-center bg-blue-600 hover:bg-blue-700 text-white py-2.5 rounded-lg mb-4 transition-colors duration-200">
<!-- <fa-icon [icon]="isLoginMode ? 'fas fas-user-plus' : 'userplus'" class="mr-2"></fa-icon> -->
<i *ngIf="isLoginMode" class="fa-solid fa-user-plus mr-2"></i>
<i *ngIf="!isLoginMode" class="fa-solid fa-arrow-right mr-2"></i>
{{ isLoginMode ? 'Sign in with Email' : 'Register' }}
</button>
<!-- Trennlinie -->
<div class="flex items-center justify-center my-4">
<span class="border-b w-1/5 md:w-1/4 border-gray-300"></span>
<span class="text-xs text-gray-500 uppercase mx-2">or</span>
<span class="border-b w-1/5 md:w-1/4 border-gray-300"></span>
</div>
<!-- Google Button -->
<button (click)="loginWithGoogle()" class="w-full flex items-center justify-center bg-white border border-gray-300 hover:bg-gray-50 text-gray-700 py-2.5 rounded-lg transition-colors duration-200">
<svg class="w-6 h-6 mr-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<path
fill="#FFC107"
d="M43.611 20.083H42V20H24v8h11.303c-1.649 4.657-6.08 8-11.303 8-6.627 0-12-5.373-12-12s5.373-12 12-12c3.059 0 5.842 1.154 7.961 3.039l5.657-5.657C34.046 6.053 29.268 4 24 4 12.955 4 4 12.955 4 24s8.955 20 20 20 20-8.955 20-20c0-1.341-.138-2.65-.389-3.917z"
/>
<path fill="#FF3D00" d="M6.306 14.691l6.571 4.819C14.655 15.108 18.961 12 24 12c3.059 0 5.842 1.154 7.961 3.039l5.657-5.657C34.046 6.053 29.268 4 24 4 16.318 4 9.656 8.337 6.306 14.691z" />
<path fill="#4CAF50" d="M24 44c5.166 0 9.86-1.977 13.409-5.192l-6.19-5.238A11.91 11.91 0 0124 36c-5.202 0-9.619-3.317-11.283-7.946l-6.522 5.025C9.505 39.556 16.227 44 24 44z" />
<path fill="#1976D2" d="M43.611 20.083H42V20H24v8h11.303a12.04 12.04 0 01-4.087 5.571l.003-.002 6.19 5.238C36.971 39.205 44 34 44 24c0-1.341-.138-2.65-.389-3.917z" />
</svg>
Continue with Google
</button>
</div>
</div>

View File

@ -1,98 +0,0 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { faArrowRight, faEnvelope, faLock, faUserPlus } from '@fortawesome/free-solid-svg-icons';
import { AuthService } from '../../services/auth.service';
import { LoadingService } from '../../services/loading.service';
@Component({
selector: 'app-login-register',
standalone: true,
imports: [CommonModule, FormsModule, FontAwesomeModule],
templateUrl: './login-register.component.html',
})
export class LoginRegisterComponent {
email: string = '';
password: string = '';
confirmPassword: string = '';
isLoginMode: boolean = true; // true: Login, false: Registration
errorMessage: string = '';
envelope = faEnvelope;
lock = faLock;
arrowRight = faArrowRight;
userplus = faUserPlus;
constructor(private authService: AuthService, private route: ActivatedRoute, private router: Router, private loadingService: LoadingService) {}
ngOnInit(): void {
// Set mode based on query parameter "mode"
this.route.queryParamMap.subscribe(params => {
const mode = params.get('mode');
this.isLoginMode = mode !== 'register';
});
}
toggleMode(): void {
this.isLoginMode = !this.isLoginMode;
this.errorMessage = '';
}
// Login with Email
onSubmit(): void {
this.errorMessage = '';
if (this.isLoginMode) {
this.authService.clearRoleCache();
this.authService
.loginWithEmail(this.email, this.password)
.then(userCredential => {
console.log('Successfully logged in:', userCredential);
this.router.navigate([`home`]);
})
.catch(error => {
console.error('Error during email login:', error);
this.errorMessage = error.message;
});
} else {
// Registration mode: also check if passwords match
if (this.password !== this.confirmPassword) {
console.error('Passwords do not match');
this.errorMessage = 'Passwords do not match.';
return;
}
this.loadingService.startLoading('googleAuth');
this.authService
.registerWithEmail(this.email, this.password)
.then(userCredential => {
console.log('Successfully registered:', userCredential);
this.loadingService.stopLoading('googleAuth');
this.router.navigate(['emailVerification']);
})
.catch(error => {
this.loadingService.stopLoading('googleAuth');
console.error('Error during registration:', error);
if (error.code === 'auth/email-already-in-use') {
this.errorMessage = 'This email address is already in use. Please try logging in.';
} else {
this.errorMessage = error.message;
}
});
}
}
// Login with Google
loginWithGoogle(): void {
this.errorMessage = '';
this.authService.clearRoleCache();
this.authService
.loginWithGoogle()
.then(userCredential => {
console.log('Successfully logged in with Google:', userCredential);
this.router.navigate([`home`]);
})
.catch(error => {
console.error('Error during Google login:', error);
this.errorMessage = error.message;
});
}
}

View File

@ -1 +0,0 @@
<p>logout works!</p>

View File

@ -1,17 +0,0 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { Router, RouterModule } from '@angular/router';
import { AuthService } from '../../services/auth.service';
@Component({
selector: 'logout',
standalone: true,
imports: [CommonModule, RouterModule],
template: ``,
})
export class LogoutComponent {
constructor(private authService: AuthService, private router: Router) {
this.authService.logout();
this.router.navigate(['/home']);
}
}

View File

@ -1,113 +0,0 @@
<div class="container mx-auto p-4">
<div class="bg-white rounded-lg drop-shadow-custom-bg-mobile md:drop-shadow-custom-bg p-6">
<h1 class="text-2xl font-bold md:mb-4">My Listings</h1>
<!-- Desktop view -->
<div class="hidden md:block">
<table class="w-full bg-white drop-shadow-inner-faint rounded-lg overflow-hidden">
<thead class="bg-gray-100">
<tr>
<th class="py-2 px-4 text-left">Title</th>
<th class="py-2 px-4 text-left">Category</th>
<th class="py-2 px-4 text-left">Located in</th>
<th class="py-2 px-4 text-left">Price</th>
<th class="py-2 px-4 text-left">Publication Status</th>
<th class="py-2 px-4 text-left">Actions</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let listing of myListings" class="border-b">
<td class="py-2 px-4">{{ listing.title }}</td>
<td class="py-2 px-4">{{ listing.listingsCategory === 'commercialProperty' ? 'Commercial Property' : 'Business' }}</td>
<td class="py-2 px-4">{{ listing.location.name ? listing.location.name : listing.location.county }}, {{ listing.location.state }}</td>
<td class="py-2 px-4">${{ listing.price.toLocaleString() }}</td>
<td class="py-2 px-4">
<span class="{{ listing.draft ? 'bg-yellow-100 text-yellow-800' : 'bg-green-100 text-green-800' }} py-1 px-2 rounded-full text-xs font-medium">
{{ listing.draft ? 'Draft' : 'Published' }}
</span>
</td>
<td class="py-2 px-4">
@if(listing.listingsCategory==='business'){
<button class="bg-green-500 text-white p-2 rounded-full mr-2" [routerLink]="['/editBusinessListing', listing.id]">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
</svg>
</button>
} @if(listing.listingsCategory==='commercialProperty'){
<button class="bg-green-500 text-white p-2 rounded-full mr-2" [routerLink]="['/editCommercialPropertyListing', listing.id]">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
</svg>
</button>
}
<button class="bg-orange-500 text-white p-2 rounded-full" (click)="confirm(listing)">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fill-rule="evenodd"
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
clip-rule="evenodd"
/>
</svg>
</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Mobile view -->
<div class="md:hidden">
<div *ngFor="let listing of myListings" class="bg-white drop-shadow-inner-faint rounded-lg p-4 mb-4">
<h2 class="text-xl font-semibold mb-2">{{ listing.title }}</h2>
<p class="text-gray-600 mb-2">Category: {{ listing.listingsCategory === 'commercialProperty' ? 'Commercial Property' : 'Business' }}</p>
<p class="text-gray-600 mb-2">Located in: {{ listing.location.name ? listing.location.name : listing.location.county }} - {{ listing.location.state }}</p>
<p class="text-gray-600 mb-2">Price: ${{ listing.price.toLocaleString() }}</p>
<div class="flex items-center gap-2 mb-2">
<span class="text-gray-600">Publication Status:</span>
<span class="{{ listing.draft ? 'bg-yellow-100 text-yellow-800' : 'bg-green-100 text-green-800' }} py-1 px-2 rounded-full text-xs font-medium">
{{ listing.draft ? 'Draft' : 'Published' }}
</span>
</div>
<div class="flex justify-start">
@if(listing.listingsCategory==='business'){
<button class="bg-green-500 text-white p-2 rounded-full mr-2" [routerLink]="['/editBusinessListing', listing.id]">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
</svg>
</button>
} @if(listing.listingsCategory==='commercialProperty'){
<button class="bg-green-500 text-white p-2 rounded-full mr-2" [routerLink]="['/editCommercialPropertyListing', listing.id]">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
</svg>
</button>
}
<button class="bg-orange-500 text-white p-2 rounded-full" (click)="confirm(listing)">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fill-rule="evenodd"
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
clip-rule="evenodd"
/>
</svg>
</button>
</div>
</div>
</div>
<!-- <div class="flex items-center justify-between mt-4">
<p class="text-sm text-gray-600">Showing 1 to 2 of 2 entries</p>
<div class="flex items-center">
<button class="px-2 py-1 border rounded-l-md bg-gray-100">&lt;&lt;</button>
<button class="px-2 py-1 border-t border-b bg-gray-100">&lt;</button>
<button class="px-2 py-1 border bg-blue-500 text-white">1</button>
<button class="px-2 py-1 border-t border-b bg-gray-100">&gt;</button>
<button class="px-2 py-1 border rounded-r-md bg-gray-100">&gt;&gt;</button>
<select class="ml-2 border rounded-md px-2 py-1">
<option>10</option>
</select>
</div>
</div> -->
</div>
</div>
<app-confirmation></app-confirmation>

View File

@ -1,3 +0,0 @@
.wide-column{
width: 40%;
}

Some files were not shown because too many files have changed in this diff Show More