initial release

This commit is contained in:
2024-02-29 10:23:41 -06:00
commit 5146c8e919
210 changed files with 11040 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
<div class="container">
<div class="content">
@if (actualRoute !=='home' && actualRoute !=='pricing'){
<header></header>
}
<router-outlet></router-outlet>
@if (loadingService.isLoading$ | async) {
<div class="progress-spinner flex h-full align-items-center justify-content-center">
<p-progressSpinner></p-progressSpinner>
</div>
}
</div>
<footer></footer>
</div>

View File

@@ -0,0 +1,10 @@
.container {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.content {
flex: 1;
/* Optional: Padding für den Inhalt, um sicherzustellen, dass er nicht direkt am Footer klebt */
// padding-bottom: 20px;
}

View File

@@ -0,0 +1,59 @@
import { CommonModule } from '@angular/common';
import { Component, ViewChild } from '@angular/core';
import { ActivatedRoute, NavigationEnd, Router, RouterOutlet } from '@angular/router';
import { HeaderComponent } from './components/header/header.component';
import { ProgressSpinnerModule } from 'primeng/progressspinner';
import { ToastModule } from 'primeng/toast';
import { LoadingService } from './services/loading.service';
import { HomeComponent } from './pages/home/home.component';
import { filter } from 'rxjs/operators';
import { FooterComponent } from './components/footer/footer.component';
import { KeycloakService } from './services/keycloak.service';
import { KeycloakEventType } from './models/keycloak-event';
import { ListingCriteria, User } from './models/main.model';
import { createGenericObject } from './utils/utils';
import onChange from 'on-change';
import { UserService } from './services/user.service';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, RouterOutlet, HeaderComponent, ProgressSpinnerModule, FooterComponent],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
title = 'bizmatch';
actualRoute ='';
user:User;
listingCriteria:ListingCriteria = onChange(createGenericObject<ListingCriteria>(),(path, value, previousValue, applyData)=>{
sessionStorage.setItem('criteria',JSON.stringify(value));
});
public constructor(public loadingService: LoadingService, private router: Router,private activatedRoute: ActivatedRoute, private keycloakService:KeycloakService,private userService:UserService) {
this.router.events.pipe(
filter(event => event instanceof NavigationEnd)
).subscribe(() => {
let currentRoute = this.activatedRoute.root;
while (currentRoute.children[0] !== undefined) {
currentRoute = currentRoute.children[0];
}
// Hier haben Sie Zugriff auf den aktuellen Route-Pfad
this.actualRoute=currentRoute.snapshot.url[0].path
});
// keycloakService.keycloakEvents$.subscribe({
// next(event) {
// if (event.type == KeycloakEventType.OnTokenExpired) {
// keycloakService.updateToken(20);
// }
// if (event.type == KeycloakEventType.OnActionUpdate) {
// }
// }
// });
}
ngOnInit(){
this.user = this.userService.getUser();
}
}

View File

@@ -0,0 +1,11 @@
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

@@ -0,0 +1,56 @@
import { APP_INITIALIZER, ApplicationConfig, importProvidersFrom } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { provideClientHydration } from '@angular/platform-browser';
import { provideAnimations } from '@angular/platform-browser/animations';
import { HTTP_INTERCEPTORS, provideHttpClient, withFetch, withInterceptorsFromDi } from '@angular/common/http';
import { environment } from '../environments/environment';
import { SelectOptionsService } from './services/select-options.service';
import { KeycloakService } from './services/keycloak.service';
import { UserService } from './services/user.service';
// provideClientHydration()
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(withInterceptorsFromDi()),
{provide:KeycloakService},
{
provide: APP_INITIALIZER,
useFactory: initializeKeycloak,
multi: true,
deps: [KeycloakService],
},
{
provide: APP_INITIALIZER,
useFactory: initServices,
multi: true,
deps: [SelectOptionsService],
},
provideRouter(routes),provideAnimations()
]
};
function initUserService(userService:UserService) {
return () => {
//selectOptions.init();
}
}
function initServices(selectOptions:SelectOptionsService) {
return () => {
selectOptions.init();
}
}
function initializeKeycloak(keycloak: KeycloakService) {
return () =>
keycloak.init({
config: {
url: environment.keycloak.url,
realm: environment.keycloak.realm,
clientId: environment.keycloak.clientId,
},
initOptions: {
onLoad: 'check-sso',
silentCheckSsoRedirectUri: (<any>window).location.origin + '/assets/silent-check-sso.html'
},
});
}

View File

@@ -0,0 +1,75 @@
import { Routes } from '@angular/router';
import { ListingsComponent } from './pages/listings/listings.component';
import { HomeComponent } from './pages/home/home.component';
import { DetailsComponent } from './pages/details/details.component';
import { AccountComponent } from './pages/subscription/account/account.component';
import { EditListingComponent } from './pages/subscription/edit-listing/edit-listing.component';
import { MyListingComponent } from './pages/subscription/my-listing/my-listing.component';
import { FavoritesComponent } from './pages/subscription/favorites/favorites.component';
import { EmailUsComponent } from './pages/subscription/email-us/email-us.component';
import { authGuard } from './guards/auth.guard';
import { PricingComponent } from './pages/pricing/pricing.component';
import { LogoutComponent } from './components/logout/logout.component';
export const routes: Routes = [
{
path: 'listings/:type',
component: ListingsComponent,
},
// Umleitung von /listing zu /listing/business
{
path: 'listings',
pathMatch: 'full',
redirectTo: 'listings/business',
runGuardsAndResolvers:'always'
},
{
path: 'home',
component: HomeComponent,
},
{
path: 'details/:id',
component: DetailsComponent,
},
{
path: 'account',
component: AccountComponent,
canActivate: [authGuard],
},
{
path: 'editListing/:id',
component: EditListingComponent,
canActivate: [authGuard],
},
{
path: 'createListing',
component: EditListingComponent,
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: 'pricing',
component: PricingComponent
},
{ path: '**', redirectTo: 'home' },
];

View File

@@ -0,0 +1,27 @@
<div class="surface-0 px-4 py-4 md:px-6 lg:px-8">
<div class="surface-0">
<div class="grid">
<div class="col-12 md:col-3 md:mb-0 mb-3">
<img src="assets/images/header-logo.png" alt="footer sections" height="30" class="mr-3">
<div class="text-500">© 2024 Bizmatch All rights reserved.</div>
<!-- <div class="text-gray-300 font-bold text-5xl">Bastion</div> -->
</div>
<div class="col-12 md:col-3">
<div class="text-black mb-4 flex flex-wrap" style="max-width: 290px">BizMatch, Inc., 1001 Blucher Street, Corpus Christi, Texas 78401</div>
<div class="text-black mb-3"><i class="text-white pi pi-phone surface-800 border-round p-1 mr-2"></i>1-800-840-6025</div>
<div class="text-black mb-3"><i class="text-white pi pi-inbox surface-800 border-round p-1 mr-2"></i>bizmatch&#64;biz-match.com</div>
</div>
<div class="col-12 md:col-3 text-500">
<div class="text-black font-bold line-height-3 mb-3">Legal</div>
<a class="line-height-3 block cursor-pointer mb-2">Terms of use</a>
<a class="line-height-3 block cursor-pointer mb-2">Privacy statement</a>
</div>
<div class="col-12 md:col-3 text-500">
<div class="text-black font-bold line-height-3 mb-3">Actions</div>
<a *ngIf="!userService.isLoggedIn()" (click)="login()" class="text-500 line-height-3 block cursor-pointer mb-2 no-underline">Login</a>
<a *ngIf="userService.isLoggedIn()" [routerLink]="['/account']" class="text-500 line-height-3 block cursor-pointer mb-2 no-underline">Account</a>
<a *ngIf="userService.isLoggedIn()" class="text-500 line-height-3 block cursor-pointer mb-2 no-underline" (click)="userService.logout()">Log Out</a>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,6 @@
:host{
height: 192px;
}
div {
font-size: small;
}

View File

@@ -0,0 +1,26 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { ButtonModule } from 'primeng/button';
import { CheckboxModule } from 'primeng/checkbox';
import { InputTextModule } from 'primeng/inputtext';
import {StyleClassModule} from 'primeng/styleclass';
import { KeyValue } from '../../models/main.model';
import { DropdownModule } from 'primeng/dropdown';
import { FormsModule } from '@angular/forms';
import { UserService } from '../../services/user.service';
import { SharedModule } from '../../shared/shared/shared.module';
@Component({
selector: 'footer',
standalone: true,
imports: [SharedModule],
templateUrl: './footer.component.html',
styleUrl: './footer.component.scss'
})
export class FooterComponent {
constructor(public userService:UserService){}
login(){
this.userService.login(window.location.href);
}
}

View File

@@ -0,0 +1,11 @@
<div class="wrapper">
<div class="pl-3 flex align-items-center gap-2">
<a routerLink="/home"><img src="assets/images/header-logo.png" height="40" alt="bizmatch" /></a>
<p-tabMenu [model]="tabItems" ariaLabelledBy="label" styleClass="flex" [activeItem]="activeItem">
</p-tabMenu>
<p-menubar [model]="menuItems"></p-menubar>
<div *ngIf="user$ | async as user else empty">Welcome, {{user.firstname}}</div>
<ng-template #empty>
</ng-template>
</div>
</div>

View File

@@ -0,0 +1,13 @@
::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

@@ -0,0 +1,115 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { MenuItem } from 'primeng/api';
import { ButtonModule } from 'primeng/button';
import { MenubarModule } from 'primeng/menubar';
import { OverlayPanelModule } from 'primeng/overlaypanel';
import { environment } from '../../../environments/environment';
import { UserService } from '../../services/user.service';
import { User } from '../../models/main.model';
import { TabMenuModule } from 'primeng/tabmenu';
import { Observable } from 'rxjs';
import { faUserGear } from '@fortawesome/free-solid-svg-icons';
import { Router } from '@angular/router';
@Component({
selector: 'header',
standalone: true,
imports: [CommonModule, MenubarModule, ButtonModule, OverlayPanelModule, TabMenuModule ],
templateUrl: './header.component.html',
styleUrl: './header.component.scss'
})
export class HeaderComponent {
public buildVersion = environment.buildVersion;
user:User;
user$:Observable<User>
public tabItems: MenuItem[];
public menuItems: MenuItem[];
activeItem
faUserGear=faUserGear
constructor(public userService: UserService,private router: Router) {
}
ngOnInit(){
this.user$=this.userService.getUserObservable();
this.tabItems = [
{
label: 'Businesses for Sale',
routerLink: '/listings/business',
fragment:''
},
{
label: 'Professionals/Brokers Directory',
routerLink: '/listings/professionals_brokers',
fragment:''
},
{
label: 'Investment Property',
routerLink: '/listings/investment',
fragment:''
}
];
this.menuItems = [
{
label: 'User Actions',
icon: 'fas fa-cog',
items: [
{
label: 'Account',
icon: 'pi pi-user',
routerLink: '/account',
visible: this.isUserLoogedIn()
},
{
label: 'Create Listing',
icon: 'pi pi-plus-circle',
routerLink: "/createListing",
visible: this.isUserLoogedIn()
},
{
label: 'My Listings',
icon: 'pi pi-list',
routerLink:"/myListings",
visible: this.isUserLoogedIn()
},
{
label: 'My Favorites',
icon: 'pi pi-star',
routerLink:"/myFavorites",
visible: this.isUserLoogedIn()
},
{
label: 'EMail Us',
icon: 'fa-regular fa-envelope',
routerLink:"/emailUs",
visible: this.isUserLoogedIn()
},
{
label: 'Logout',
icon: 'fa-solid fa-right-from-bracket',
routerLink:"/logout",
visible: this.isUserLoogedIn()
},
{
label: 'Login',
icon: 'fa-solid fa-right-from-bracket',
//routerLink:"/account",
command: () => this.login(),
visible: !this.isUserLoogedIn()
},
]
}
]
this.activeItem=this.tabItems[0];
}
navigateWithState(dest: string, state: any) {
this.router.navigate([dest], { state: state });
}
isUserLoogedIn(){
return this.userService?.isLoggedIn();
}
login(){
this.userService.login(window.location.href);
}
}

View File

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

View File

@@ -0,0 +1,16 @@
import { Component } from '@angular/core';
import { UserService } from '../../services/user.service';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
@Component({
selector: 'logout',
standalone: true,
imports: [CommonModule,RouterModule],
template:``
})
export class LogoutComponent {
constructor(private userService:UserService){
userService.logout();
}
}

View File

@@ -0,0 +1,38 @@
import { CanMatchFn, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
import { inject } from '@angular/core';
// Services
import { UserService } from '../services/user.service';
export const authGuard: CanMatchFn = async (route, segments): Promise<boolean | UrlTree> => {
const router = inject(Router);
const userService = inject(UserService);
const authenticated: boolean = userService.isLoggedIn();
if (!authenticated) {
console.log(window.location.origin)
console.log(window.location.href)
await userService.login(`${window.location.origin}${segments['url']}`);
}
// Get the user Keycloak roles and the required from the route
const roles: string[] = userService.getUserRoles();//keycloakService.getUserRoles(true);
const requiredRoles = route.data?.['roles'];
// Allow the user to proceed if no additional roles are required to access the route
if (!Array.isArray(requiredRoles) || requiredRoles.length === 0) {
return true;
}
// Allow the user to proceed if ALL of the required roles are present
const authorized = requiredRoles.every((role) => roles.includes(role));
// Allow the user to proceed if ONE of the required roles is present
//const authorized = requiredRoles.some((role) => roles.includes(role));
if (authorized) {
return true;
}
// Display my custom HTTP 403 access denied page
return router.createUrlTree(['/access']);
};

View File

@@ -0,0 +1,77 @@
import { Injectable, inject } from '@angular/core';
import {
HttpInterceptor,
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptorFn,
HttpHandlerFn,
} from '@angular/common/http';
import { Observable, combineLatest, from, of } from 'rxjs';
import { mergeMap } from 'rxjs/operators';
import { KeycloakService } from '../services/keycloak.service';
import { ExcludedUrlRegex } from '../models/keycloak-options';
export const keycloakBearerInterceptor: HttpInterceptorFn = (req, next) => {
//return next(req);
const keycloak = inject(KeycloakService);
const { enableBearerInterceptor, excludedUrls } = keycloak;
if (!enableBearerInterceptor) {
return next(req);
}
const shallPass: boolean =
!keycloak.shouldAddToken(req) ||
excludedUrls.findIndex((item) => isUrlExcluded(req, item)) > -1;
if (shallPass) {
return next(req);
}
return combineLatest([
from(conditionallyUpdateToken(req)),
of(keycloak.isLoggedIn()),
]).pipe(
mergeMap(([_, isLoggedIn]) =>
isLoggedIn ? handleRequestWithTokenHeader(req, next) : next(req)
)
);
};
function isUrlExcluded(
{ method, url }: HttpRequest<unknown>,
{ urlPattern, httpMethods }: ExcludedUrlRegex
): boolean {
const httpTest =
httpMethods.length === 0 ||
httpMethods.join().indexOf(method.toUpperCase()) > -1;
const urlTest = urlPattern.test(url);
return httpTest && urlTest;
}
function handleRequestWithTokenHeader(
req: HttpRequest<unknown>,
next: HttpHandlerFn
): Observable<HttpEvent<unknown>> {
return this.keycloak.addTokenToHeader(req.headers).pipe(
mergeMap((headersWithBearer:string) => {
const kcReq = req.clone({
headers: req.headers.set('Authorization', headersWithBearer)
});//req.clone({ headers: headersWithBearer });
return next(kcReq);
})
);
}
async function conditionallyUpdateToken(
req: HttpRequest<unknown>
): Promise<boolean> {
if (this.keycloak.shouldUpdateToken(req)) {
return await this.keycloak.updateToken();
}
return true;
}

View File

@@ -0,0 +1,21 @@
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { tap } from 'rxjs';
import { v4 } from 'uuid';
import { LoadingService } from '../services/loading.service';
export const loadingInterceptor: HttpInterceptorFn = (req, next) => {
const loadingService = inject(LoadingService);
const requestId = `HTTP-${v4()}`;
loadingService.startLoading(requestId);
return next(req).pipe(
tap({
finalize: () => loadingService.stopLoading(requestId),
error: () => loadingService.stopLoading(requestId),
complete: () => loadingService.stopLoading(requestId),
})
);
};

View File

@@ -0,0 +1,52 @@
export enum KeycloakEventType {
/**
* Called if there was an error during authentication.
*/
OnAuthError,
/**
* Called if the user is logged out
* (will only be called if the session status iframe is enabled, or in Cordova mode).
*/
OnAuthLogout,
/**
* Called if there was an error while trying to refresh the token.
*/
OnAuthRefreshError,
/**
* Called when the token is refreshed.
*/
OnAuthRefreshSuccess,
/**
* Called when a user is successfully authenticated.
*/
OnAuthSuccess,
/**
* Called when the adapter is initialized.
*/
OnReady,
/**
* Called when the access token is expired. If a refresh token is available the token
* can be refreshed with updateToken, or in cases where it is not (that is, with implicit flow)
* you can redirect to login screen to obtain a new access token.
*/
OnTokenExpired,
/**
* Called when a AIA has been requested by the application.
*/
OnActionUpdate
}
/**
* Structure of an event triggered by Keycloak, contains it's type
* and arguments (if any).
*/
export interface KeycloakEvent {
/**
* Event type as described at {@link KeycloakEventType}.
*/
type: KeycloakEventType;
/**
* Arguments from the keycloak-js event function.
*/
args?: unknown;
}

View File

@@ -0,0 +1,142 @@
/**
* @license
* Copyright Mauricio Gemelli Vigolo and contributors.
*
* Use of this source code is governed by a MIT-style license that can be
* found in the LICENSE file at https://github.com/mauriciovigolo/keycloak-angular/blob/main/LICENSE.md
*/
import { HttpRequest } from '@angular/common/http';
/**
* HTTP Methods
*/
export type HttpMethods =
| 'GET'
| 'POST'
| 'PUT'
| 'DELETE'
| 'OPTIONS'
| 'HEAD'
| 'PATCH';
/**
* ExcludedUrl type may be used to specify the url and the HTTP method that
* should not be intercepted by the KeycloakBearerInterceptor.
*
* Example:
* const excludedUrl: ExcludedUrl[] = [
* {
* url: 'reports/public'
* httpMethods: ['GET']
* }
* ]
*
* In the example above for URL reports/public and HTTP Method GET the
* bearer will not be automatically added.
*
* If the url is informed but httpMethod is undefined, then the bearer
* will not be added for all HTTP Methods.
*/
export interface ExcludedUrl {
url: string;
httpMethods?: HttpMethods[];
}
/**
* Similar to ExcludedUrl, contains the HTTP methods and a regex to
* include the url patterns.
* This interface is used internally by the KeycloakService.
*/
export interface ExcludedUrlRegex {
urlPattern: RegExp;
httpMethods?: HttpMethods[];
}
/**
* keycloak-angular initialization options.
*/
export interface KeycloakOptions {
/**
* Configs to init the keycloak-js library. If undefined, will look for a keycloak.json file
* at root of the project.
* If not undefined, can be a string meaning the url to the keycloak.json file or an object
* of {@link Keycloak.KeycloakConfig}. Use this configuration if you want to specify the keycloak server,
* realm, clientId. This is usefull if you have different configurations for production, stage
* and development environments. Hint: Make use of Angular environment configuration.
*/
config?: string | Keycloak.KeycloakConfig;
/**
* Options to initialize the Keycloak adapter, matches the options as provided by Keycloak itself.
*/
initOptions?: Keycloak.KeycloakInitOptions;
/**
* By default all requests made by Angular HttpClient will be intercepted in order to
* add the bearer in the Authorization Http Header. However, if this is a not desired
* feature, the enableBearerInterceptor must be false.
*
* Briefly, if enableBearerInterceptor === false, the bearer will not be added
* to the authorization header.
*
* The default value is true.
*/
enableBearerInterceptor?: boolean;
/**
* Forces the execution of loadUserProfile after the keycloak initialization considering that the
* user logged in.
* This option is recommended if is desirable to have the user details at the beginning,
* so after the login, the loadUserProfile function will be called and its value cached.
*
* The default value is true.
*/
loadUserProfileAtStartUp?: boolean;
/**
* @deprecated
* String Array to exclude the urls that should not have the Authorization Header automatically
* added. This library makes use of Angular Http Interceptor, to automatically add the Bearer
* token to the request.
*/
bearerExcludedUrls?: (string | ExcludedUrl)[];
/**
* This value will be used as the Authorization Http Header name. The default value is
* **Authorization**. If the backend expects requests to have a token in a different header, you
* should change this value, i.e: **JWT-Authorization**. This will result in a Http Header
* Authorization as "JWT-Authorization: bearer <token>".
*/
authorizationHeaderName?: string;
/**
* This value will be included in the Authorization Http Header param. The default value is
* **Bearer**, which will result in a Http Header Authorization as "Authorization: Bearer <token>".
*
* If any other value is needed by the backend in the authorization header, you should change this
* value.
*
* Warning: this value must be in compliance with the keycloak server instance and the adapter.
*/
bearerPrefix?: string;
/**
* This value will be used to determine whether or not the token needs to be updated. If the token
* will expire is fewer seconds than the updateMinValidity value, then it will be updated.
*
* The default value is 20.
*/
updateMinValidity?: number;
/**
* A function that will tell the KeycloakBearerInterceptor whether to add the token to the request
* or to leave the request as it is. If the returned value is `true`, the request will have the token
* present on it. If it is `false`, the token will be left off the request.
*
* The default is a function that always returns `true`.
*/
shouldAddToken?: (request: HttpRequest<unknown>) => boolean;
/**
* A function that will tell the KeycloakBearerInterceptor if the token should be considered for
* updating as a part of the request being made. If the returned value is `true`, the request will
* check the token's expiry time and if it is less than the number of seconds configured by
* updateMinValidity then it will be updated before the request is made. If the returned value is
* false, the token will not be updated.
*
* The default is a function that always returns `true`.
*/
shouldUpdateToken?: (request: HttpRequest<unknown>) => boolean;
}

View File

@@ -0,0 +1 @@
../../../../common-models/src/main.model.ts

View File

@@ -0,0 +1,146 @@
<div class="surface-ground h-full">
<div class="px-6 py-5">
<div class="surface-card p-4 shadow-2 border-round">
<div class="flex justify-content-between align-items-center align-content-center">
<div class="font-medium text-3xl text-900 mb-3">{{listing?.title}}</div>
<!-- <button pButton pRipple type="button" label="Go back to listings" icon="pi pi-user-plus" class="mr-3 p-button-rounded"></button> -->
<p-button icon="pi pi-times" [rounded]="true" severity="danger" (click)="back()"></p-button>
</div>
<!-- <div class="text-500 mb-5">Egestas sed tempus urna et pharetra pharetra massa massa ultricies.</div> -->
<div class="grid">
<div class="col-12 md:col-6">
<ul class="list-none p-0 m-0 border-top-1 border-300">
@if (listing && (listing.listingsCategory==='business' || listing.listingsCategory==='professionals_brokers')){
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
<div class="text-500 w-full md:w-2 font-medium">Summary</div>
<div class="w-full md:w-10">
@for (summary of listing.summary; track summary; let idx = $index; let last = $last) {
<div class="text-900">{{summary}}</div>
@if (!last) {
<br/>
}
}
</div>
</li>
}
<li class="flex align-items-center py-3 px-2 flex-wrap">
<div class="text-500 w-full md:w-2 font-medium">Description</div>
<div class="text-900 w-full md:w-10 line-height-3">{{listing?.description}}</div>
</li>
@if (listing && (listing.listingsCategory==='business')){
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
<div class="text-500 w-full md:w-2 font-medium">Category</div>
<div class="text-900 w-full md:w-10">
<p-chip [label]="selectOptions.getBusiness(listing.type)"></p-chip>
</div>
</li>
<li class="flex align-items-center py-3 px-2 flex-wrap ">
<div class="text-500 w-full md:w-2 font-medium">Located in</div>
<div class="text-900 w-full md:w-10">{{selectOptions.getLocation(listing.location)}}</div>
</li>
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
<div class="text-500 w-full md:w-2 font-medium">Asking Price</div>
<div class="text-900 w-full md:w-10">{{listing.price | currency}}</div>
</li>
<li class="flex align-items-center py-3 px-2 flex-wrap ">
<div class="text-500 w-full md:w-2 font-medium">Real Estate Included</div>
<div class="text-900 w-full md:w-10">{{listing.realEstateIncluded?'Yes':'No'}}</div>
</li>
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
<div class="text-500 w-full md:w-2 font-medium">Sales revenue</div>
<div class="text-900 w-full md:w-10">{{listing.salesRevenue | currency}}</div>
</li>
<li class="flex align-items-center py-3 px-2 flex-wrap ">
<div class="text-500 w-full md:w-2 font-medium">Cash flow</div>
<div class="text-900 w-full md:w-10">{{listing.cashFlow | currency}}</div>
</li>
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
<div class="text-500 w-full md:w-2 font-medium">Employees</div>
<div class="text-900 w-full md:w-10">{{listing.employees}}</div>
</li>
<li class="flex align-items-center py-3 px-2 flex-wrap ">
<div class="text-500 w-full md:w-2 font-medium">Broker licensing</div>
<div class="text-900 w-full md:w-10">{{listing.brokerLicencing}}</div>
</li>
}
@if (listing && (listing.listingsCategory==='professionals_brokers')){
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
<div class="text-500 w-full md:w-2 font-medium">Located in</div>
<div class="text-900 w-full md:w-10">{{selectOptions.getLocation(listing.location)}}</div>
</li>
<li class="flex align-items-center py-3 px-2 flex-wrap ">
<div class="text-500 w-full md:w-2 font-medium">Address</div>
<div class="text-900 w-full md:w-10">{{listing.address}}</div>
</li>
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
<div class="text-500 w-full md:w-2 font-medium">EMail</div>
<div class="text-900 w-full md:w-10">{{listing.email}}</div>
</li>
<li class="flex align-items-center py-3 px-2 flex-wrap ">
<div class="text-500 w-full md:w-2 font-medium">Website</div>
<div class="text-900 w-full md:w-10">{{listing.website}}</div>
</li>
<li class="flex align-items-center py-3 px-2 flex-wrap ">
<div class="text-500 w-full md:w-2 font-medium">Category</div>
<div class="text-900 w-full md:w-10">{{listing.category}}</div>
</li>
}
@if (listing && (listing.listingsCategory==='investment')){
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
<div class="text-500 w-full md:w-2 font-medium">Located in</div>
<div class="text-900 w-full md:w-10">{{selectOptions.getLocation(listing.location)}}</div>
</li>
<li class="flex align-items-center py-3 px-2 flex-wrap">
<div class="text-500 w-full md:w-2 font-medium">EMail</div>
<div class="text-900 w-full md:w-10">{{listing.email}}</div>
</li>
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
<div class="text-500 w-full md:w-2 font-medium">Website</div>
<div class="text-900 w-full md:w-10">{{listing.website}}</div>
</li>
<li class="flex align-items-center py-3 px-2 flex-wrap">
<div class="text-500 w-full md:w-2 font-medium">Phone Number</div>
<div class="text-900 w-full md:w-10">{{listing.phoneNumber}}</div>
</li>
}
</ul>
@if(listing && user && (user.id===listing?.userId || isAdmin())){
<button pButton pRipple label="Edit" icon="pi pi-file-edit" class="w-auto" [routerLink]="['/editListing',listing.id]"></button>
}
</div>
<div class="col-12 md:col-6">
<div class="surface-card p-4 border-round p-fluid">
<div class="font-medium text-xl text-primary text-900 mb-3">Contact The Author of This Listing
</div>
<div class="font-italic text-sm text-900 mb-5">Please Include your contact info below:</div>
<div class="grid formgrid p-fluid">
<div class="field mb-4 col-12 md:col-6">
<label for="company_name" class="font-medium text-900">Your Name</label>
<input id="company_name" type="text" pInputText>
</div>
<div class="field mb-4 col-12 md:col-6">
<label for="invoice_id" class="font-medium text-900">Your Email</label>
<input id="invoice_id" type="text" pInputText>
</div>
<div class="field mb-4 col-12 md:col-6">
<label for="customer_name" class="font-medium text-900">Phone Number</label>
<input id="customer_name" type="text" pInputText>
</div>
<div class="field mb-4 col-12 md:col-6">
<label for="customer_email" class="font-medium text-900">Country/State</label>
<input id="customer_email" type="text" pInputText>
</div>
<div class="surface-border border-top-1 opacity-50 mb-4 col-12"></div>
<div class="field mb-4 col-12">
<label for="notes" class="font-medium text-900">Questions/Comments</label>
<textarea id="notes" pInputTextarea [autoResize]="true" [rows]="5"></textarea>
</div>
<div class="surface-border border-top-1 opacity-50 mb-4 col-12"></div>
</div>
<button pButton pRipple label="Submit" icon="pi pi-file" class="w-auto"></button>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,50 @@
import { Component } from '@angular/core';
import { ButtonModule } from 'primeng/button';
import { CheckboxModule } from 'primeng/checkbox';
import { InputTextModule } from 'primeng/inputtext';
import { StyleClassModule } from 'primeng/styleclass';
import { BusinessListing, InvestmentsListing, KeyValue, ListingCriteria, ProfessionalsBrokersListing, User } from '../../models/main.model';
import { SelectOptionsService } from '../../services/select-options.service';
import { DropdownModule } from 'primeng/dropdown';
import { FormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { ToggleButtonModule } from 'primeng/togglebutton';
import { TagModule } from 'primeng/tag';
import data from '../../../assets/data/listings.json';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { InputTextareaModule } from 'primeng/inputtextarea';
import { ChipModule } from 'primeng/chip';
import { lastValueFrom } from 'rxjs';
import { ListingsService } from '../../services/listings.service';
import { UserService } from '../../services/user.service';
import onChange from 'on-change';
import { getCriteriaStateObject, getSessionStorageHandler } from '../../utils/utils';
@Component({
selector: 'app-details',
standalone: true,
imports: [CommonModule, StyleClassModule, ButtonModule, CheckboxModule, InputTextModule, DropdownModule, FormsModule, ChipModule,InputTextareaModule,RouterModule],
templateUrl: './details.component.html',
styleUrl: './details.component.scss'
})
export class DetailsComponent {
// listings: Array<BusinessListing>;
private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;
listing: BusinessListing|ProfessionalsBrokersListing|InvestmentsListing;
user:User;
criteria:ListingCriteria
constructor(private activatedRoute: ActivatedRoute,private listingsService:ListingsService,private router:Router,private userService:UserService,public selectOptions: SelectOptionsService){
this.criteria = onChange(getCriteriaStateObject(),getSessionStorageHandler);
}
async ngOnInit(){
this.user = this.userService.getUser();
this.listing=await lastValueFrom(this.listingsService.getListingById(this.id));
}
back(){
this.router.navigate(['listings',this.criteria.listingsCategory])
}
isAdmin(){
return this.userService.hasAdminRole();
}
}

View File

@@ -0,0 +1,93 @@
<div class="container">
<div class="wrapper">
<div class="py-3 px-6 flex align-items-center justify-content-between relative">
<a routerLink="/home"><img src="../../../assets/images/header-logo.png" alt="Image" height="50" ></a>
<div
class="align-items-center flex-grow-1 justify-content-between hidden lg:flex absolute lg:static w-full left-0 top-100 px-6 lg:px-0 shadow-2 lg:shadow-none z-2">
<section></section>
<ul
class="list-none p-0 m-0 flex lg:align-items-center text-blue-900 select-none flex-column lg:flex-row cursor-pointer">
<li>
<a pRipple
class="flex px-0 lg:px-5 py-3 hover:text-blue-600 font-medium transition-colors transition-duration-150">
<span>Corporate</span>
</a>
</li>
<li>
<a pRipple
class="flex px-0 lg:px-5 py-3 hover:text-blue-600 font-medium transition-colors transition-duration-150">
<span>Resources</span>
</a>
</li>
<li>
<a pRipple
class="flex px-0 lg:px-5 py-3 hover:text-blue-600 font-medium transition-colors transition-duration-150">
<span>Pricing</span>
</a>
</li>
</ul>
<div
class="flex justify-content-between lg:block border-top-1 lg:border-top-none border-gray-800 py-3 lg:py-0 mt-3 lg:mt-0">
<!-- <p-button label="Account" class="ml-3 font-bold" [outlined]="true" severity="secondary" [routerLink]="['/account']"></p-button> -->
<p-button label="Account" class="ml-3 font-bold" [outlined]="true" severity="secondary" (click)="account()"></p-button>
</div>
</div>
</div>
<div class="px-4 py-8 md:px-6 lg:px-8">
<div class="flex flex-wrap">
<div class="w-12 lg:w-6 p-4">
<h1 class="text-6xl font-bold text-blue-900 mt-0 mb-3">Find businesses for sale</h1>
<p class="text-3xl text-blue-600 mt-0 mb-5">Arcu cursus euismod quis viverra nibh cras. Amet justo
donec
enim diam vulputate ut.</p>
<ul class="list-none p-0 m-0">
<li class="mb-3 flex align-items-center"><i
class="pi pi-compass text-yellow-500 text-xl mr-2"></i><span
class="text-blue-600 line-height-3">Senectus et netus et malesuada fames.</span></li>
<li class="mb-3 flex align-items-center"><i
class="pi pi-map text-yellow-500 text-xl mr-2"></i><span
class="text-blue-600 line-height-3">Orci a scelerisque purus semper eget.</span></li>
<li class="mb-3 flex align-items-center"><i
class="pi pi-calendar text-yellow-500 text-xl mr-2"></i><span
class="text-blue-600 line-height-3">Aenean sed adipiscing diam donec adipiscing
tristique.</span></li>
</ul>
</div>
<div class="w-12 lg:w-6 text-center lg:text-right flex">
<div class="mt-5">
<ul class="flex flex-column align-items-left gap-3 px-2 py-3 list-none surface-border">
<li><button pButton pRipple icon="pi pi-user" (click)="activeTabAction = 'business'"
label="Businesses"
[ngClass]="{'p-button-text text-700': activeTabAction !== 'business'}"></button></li>
<li><button pButton pRipple icon="pi pi-globe" (click)="activeTabAction = 'professionals_brokers'"
label="Professionals/Brokers Directory"
[ngClass]="{'p-button-text text-700': activeTabAction != 'professionals_brokers'}"></button></li>
<li><button pButton pRipple icon="pi pi-shield" (click)="activeTabAction = 'investment'"
label="Investment Property"
[ngClass]="{'p-button-text text-700': activeTabAction != 'investment'}"></button>
</li>
</ul>
</div>
<div class="mt-5">
<div class="flex flex-column align-items-right gap-3 px-2 py-3 my-3 surface-border">
<p-dropdown [options]="selectOptions.typesOfBusiness" [(ngModel)]="criteria.type" optionLabel="name"
optionValue="value" [showClear]="true" placeholder="Category"
[style]="{ width: '200px'}"></p-dropdown>
<p-dropdown [options]="selectOptions.prices" [(ngModel)]="criteria.minPrice" optionLabel="name" optionValue="value"
[showClear]="true" placeholder="Min Price" [style]="{ width: '200px'}"></p-dropdown>
<p-dropdown [options]="selectOptions.prices" [(ngModel)]="criteria.maxPrice" optionLabel="name" optionValue="value"
[showClear]="true" placeholder="Max Price" [style]="{ width: '200px'}"></p-dropdown>
<button pButton pRipple label="Find" class="ml-3 font-bold"
[style]="{ width: '170px'}" (click)="search()"></button>
</div>
</div>
</div>
<div class="w-12 flex justify-content-center">
<button type="button" pButton pRipple label="Create Your Listing"
class="block mt-7 mb-7 lg:mb-0 p-button-rounded p-button-success p-button-lg font-medium" [routerLink]="userService.isLoggedIn()?'/createListing':'/pricing'"></button>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,17 @@
:host {
height: 100%
}
.container {
background-image: url(../../../assets/images/index-bg.webp);
//background-image: url(../../../assets/images/corpusChristiSkyline.jpg);
background-size: cover;
background-position: center;
height: 100vh;
}
.combo_lp{
width: 200px;
}
.p-button-white{
color:aliceblue
}

View File

@@ -0,0 +1,45 @@
import { Component } from '@angular/core';
import { DropdownModule } from 'primeng/dropdown';
import { KeyValue, ListingCriteria } from '../../models/main.model';
import { FormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { BrowserAnimationsModule, NoopAnimationsModule } from '@angular/platform-browser/animations';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { StyleClassModule } from 'primeng/styleclass';
import { ButtonModule } from 'primeng/button';
import { CheckboxModule } from 'primeng/checkbox';
import { InputTextModule } from 'primeng/inputtext';
import { SelectOptionsService } from '../../services/select-options.service';
import { UserService } from '../../services/user.service';
import onChange from 'on-change';
import { getCriteriaStateObject, getSessionStorageHandler } from '../../utils/utils';
@Component({
selector: 'app-home',
standalone: true,
imports: [CommonModule, StyleClassModule,ButtonModule, CheckboxModule,InputTextModule,DropdownModule,FormsModule, RouterModule],
templateUrl: './home.component.html',
styleUrl: './home.component.scss'
})
export class HomeComponent {
activeTabAction = 'business';
type:string;
maxPrice:string;
minPrice:string;
criteria:ListingCriteria
public constructor(private router: Router,private activatedRoute: ActivatedRoute, public selectOptions:SelectOptionsService, public userService:UserService) {
this.criteria = onChange(getCriteriaStateObject(),getSessionStorageHandler);
}
ngOnInit(){
}
search(){
this.router.navigate([`listings/${this.activeTabAction}`])
}
account(){
setTimeout(()=>{
this.router.navigate([`account`])
},10);
}
}

View File

@@ -0,0 +1,130 @@
<div id="sky-line" class="hidden-lg-down">
</div>
<div class="search">
<div class="wrapper">
<div class="grid p-4 align-items-center">
@if (listingCategory==='business'){
<div class="col-2">
<p-dropdown [options]="selectOptions.typesOfBusiness" [(ngModel)]="criteria.type" optionLabel="name"
optionValue="value" [showClear]="true" placeholder="Categorie of Business"
[style]="{ width: '100%'}"></p-dropdown>
</div>
<div class="col-2">
<p-dropdown [options]="locations" [(ngModel)]="location" optionLabel="criteria.location" optionLabel="name" optionValue="value"
[showClear]="true" placeholder="Location" [style]="{ width: '100%'}"></p-dropdown>
</div>
<div class="col-2">
<p-dropdown [options]="selectOptions.prices" [(ngModel)]="criteria.minPrice" optionLabel="name"
optionValue="value" [showClear]="true" placeholder="Min Price"
[style]="{ width: '100%'}"></p-dropdown>
</div>
<div class="col-2">
<p-dropdown [options]="selectOptions.prices" [(ngModel)]="criteria.maxPrice" optionLabel="name"
optionValue="value" [showClear]="true" placeholder="Max Price"
[style]="{ width: '100%'}"></p-dropdown>
</div>
<div class="col-3">
<!-- <p-toggleButton [(ngModel)]="checked1" onLabel="Sustainable" offLabel="Unsustainable" onIcon="pi pi-check" offIcon="pi pi-times" styleClass="mb-3 lg:mt-0 mr-4 flex-shrink-0 w-12rem"></p-toggleButton> -->
<p-toggleButton [(ngModel)]="criteria.realEstateChecked" onLabel="Real Estate not included"
offLabel="Real Estate included"></p-toggleButton>
</div>
}
@if (listingCategory==='investment'){
<div class="col-2">
<p-dropdown [options]="locations" [(ngModel)]="criteria.location" optionLabel="name" optionValue="value"
[showClear]="true" placeholder="Location" [style]="{ width: '100%'}"></p-dropdown>
</div>
}
@if (listingCategory==='professionals_brokers'){
<div class="col-2">
<p-dropdown [options]="selectOptions.categories" [(ngModel)]="criteria.category" optionLabel="name"
optionValue="value" [showClear]="true" placeholder="Category"
[style]="{ width: '100%'}"></p-dropdown>
</div>
<div class="col-2">
<p-dropdown [options]="locations" [(ngModel)]="criteria.location" optionLabel="name" optionValue="value"
[showClear]="true" placeholder="Location" [style]="{ width: '100%'}"></p-dropdown>
</div>
}
<div [ngClass]="{'col-offset-9':type==='investment','col-offset-7':type==='professionals_brokers'}" class="col-1">
<p-button label="Refine" (click)="search()"></p-button>
</div>
</div>
</div>
</div>
<div class="surface-200 h-full">
<div class="wrapper">
<div class="grid">
@for (listing of filteredListings; track listing.id) {
<div class="col-12 lg:col-3 p-3">
<div class="shadow-2 border-round surface-card mb-3 h-full flex-column justify-content-between flex">
<div class="p-4 h-full flex flex-column">
@if (listing.listingsCategory==='business'){
<div class="flex align-items-center">
<span [class]="selectOptions.getBgColorType(listing.type)"
class="inline-flex border-circle align-items-center justify-content-center mr-3"
style="width:38px;height:38px">
<i [class]="selectOptions.getIconAndTextColorType(listing.type)" class="pi text-xl"></i>
</span>
<span class="text-900 font-medium text-2xl">{{selectOptions.getBusiness(listing.type)}}</span>
</div>
}
@if (listing.listingsCategory==='professionals_brokers'){
<div class="flex align-items-center">
<span [class]="selectOptions.getBgColor(listing.category)" class="inline-flex border-circle align-items-center justify-content-center mr-3"
style="width:38px;height:38px">
<i [class]="selectOptions.getIconAndTextColor(listing.category)" class="text-xl"></i>
</span>
<span class="text-900 font-medium text-2xl">{{selectOptions.getCategory(listing.category)}}</span>
</div>
}
@if (listing.listingsCategory==='investment'){
<div class="flex align-items-center">
<span
class="inline-flex border-circle align-items-center justify-content-center bg-green-100 mr-3"
style="width:38px;height:38px">
<i class="pi pi-globe text-xl text-green-600"></i>
</span>
<span class="text-900 font-medium text-2xl">Investment</span>
</div>
}
<div class="text-900 my-3 text-xl font-medium">{{listing.title}}</div>
@if (listing.listingsCategory==='business'){
<p class="mt-0 mb-1 text-700 line-height-3">Asking price: {{listing.price | currency}}</p>
<p class="mt-0 mb-1 text-700 line-height-3">Sales revenue: {{listing.salesRevenue | currency}}</p>
<p class="mt-0 mb-1 text-700 line-height-3">Net profit: {{listing.cashFlow | currency}}</p>
<p class="mt-0 mb-1 text-700 line-height-3">Location: {{selectOptions.getLocation(listing.location)}}</p>
<p class="mt-0 mb-1 text-700 line-height-3">Established: {{listing.established}}</p>
}
@if (listing.listingsCategory==='professionals_brokers'){
<!-- <p class="mt-0 mb-1 text-700 line-height-3">Category: {{listing.category}}</p> -->
<p class="mt-0 mb-1 text-700 line-height-3">Location: {{selectOptions.getLocation(listing.location)}}</p>
<p class="mt-0 mb-1 text-700 line-height-3">EMail: {{listing.email}}</p>
<p class="mt-0 mb-1 text-700 line-height-3">Website: {{listing.website}}</p>
}
@if (listing.listingsCategory==='investment'){
<p class="mt-0 mb-1 text-700 line-height-3">Location: {{selectOptions.getLocation(listing.location)}}</p>
<p class="mt-0 mb-1 text-700 line-height-3">EMail: {{listing.email}}</p>
<p class="mt-0 mb-1 text-700 line-height-3">Website: {{listing.website}}</p>
<p class="mt-0 mb-1 text-700 line-height-3">Phone Number: {{listing.phoneNumber}}</p>
}
<div class="mt-auto ml-auto">
<img *ngIf="!listing.hideImage" src="{{environment.apiBaseUrl}}/profile_{{listing.userId}}" (error)="imageErrorHandler(listing)" class="rounded-image"/>
</div>
</div>
<div class="px-4 py-3 surface-100 text-right">
<button pButton pRipple icon="pi pi-arrow-right" iconPos="right" label="View Full Listing"
class="p-button-rounded p-button-success" [routerLink]="['/details',listing.id]"></button>
</div>
</div>
</div>
}
</div>
<div class="mb-2 surface-200 flex align-items-center justify-content-center">
<div class="mx-1 text-color">Total number of Listings: {{totalRecords}}</div>
<p-paginator (onPageChange)="onPageChange($event)" [first]="first" [rows]="rows" [totalRecords]="totalRecords" [rowsPerPageOptions]="[12, 24, 48]" ></p-paginator>
</div>
</div>
</div>

View File

@@ -0,0 +1,22 @@
#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;
// background-color: var(--surface-400) !important;
}
.rounded-image {
border-radius: 6px;
width: 100px;
height: 25px;
border: 1px solid rgba(0,0,0,0.2);
padding: 1px 1px;
object-fit: contain;
}

View File

@@ -0,0 +1,96 @@
import { ChangeDetectorRef, Component } from '@angular/core';
import { ButtonModule } from 'primeng/button';
import { CheckboxModule } from 'primeng/checkbox';
import { InputTextModule } from 'primeng/inputtext';
import { StyleClassModule } from 'primeng/styleclass';
import { BusinessListing, InvestmentsListing, KeyValue, ListingCriteria, ListingType, PageEvent, ProfessionalsBrokersListing, } from '../../models/main.model';
import { SelectOptionsService } from '../../services/select-options.service';
import { DropdownModule } from 'primeng/dropdown';
import { FormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { ToggleButtonModule } from 'primeng/togglebutton';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { ListingsService } from '../../services/listings.service';
import { Observable, lastValueFrom } from 'rxjs';
import { PaginatorModule } from 'primeng/paginator';
import onChange from 'on-change';
import { createGenericObject, getCriteriaStateObject, getSessionStorageHandler } from '../../utils/utils';
import { InitEditableRow } from 'primeng/table';
import { environment } from '../../../environments/environment';
@Component({
selector: 'app-listings',
standalone: true,
imports: [CommonModule, StyleClassModule, ButtonModule, CheckboxModule, InputTextModule, DropdownModule, FormsModule, StyleClassModule, ToggleButtonModule, RouterModule, PaginatorModule],
templateUrl: './listings.component.html',
styleUrls: ['./listings.component.scss', '../pages.scss']
})
export class ListingsComponent {
environment=environment;
listings: Array<ListingType>;
filteredListings: Array<ListingType>;
criteria:ListingCriteria;
realEstateChecked: boolean;
category: string;
maxPrice: string;
minPrice: string;
type:string;
locations = [];
locationsSet = new Set();
location:string;
first: number = 0;
rows: number = 12;
totalRecords:number = 0;
public listingCategory: 'business' | 'professionals_brokers' | 'investment' | undefined;
constructor(public selectOptions: SelectOptionsService, private listingsService:ListingsService,private activatedRoute: ActivatedRoute, private router:Router, private cdRef:ChangeDetectorRef) {
this.criteria = onChange(getCriteriaStateObject(),getSessionStorageHandler);
this.router.getCurrentNavigation()
this.activatedRoute.snapshot
this.activatedRoute.params.subscribe(params => {
if (this.activatedRoute.snapshot.fragment===''){
this.criteria = onChange(createGenericObject<ListingCriteria>(),getSessionStorageHandler)
this.first=0;
}
this.listingCategory = (<any>params).type;
this.criteria.listingsCategory=this.listingCategory;
this.init()
})
}
async ngOnInit(){
}
async init(){
this.listings=await this.listingsService.getListings(this.criteria);
this.setLocations();
this.filteredListings=[...this.listings];
this.totalRecords=this.listings.length
this.filteredListings=[...this.listings].splice(this.first,this.rows);
this.cdRef.markForCheck();
this.cdRef.detectChanges();
}
setLocations(){
this.locationsSet=new Set();
this.listings.forEach(l=>{
if (l.location){
this.locationsSet.add(l.location)
}
})
this.locations = [...this.locationsSet].map((ls) =>({name:this.selectOptions.getLocation(ls as string),value:ls}))
}
async search() {
this.listings= await this.listingsService.getListings(this.criteria);
this.setLocations();
this.totalRecords=this.listings.length
this.filteredListings =[...this.listings].splice(this.first,this.rows);
this.cdRef.markForCheck();
this.cdRef.detectChanges();
}
onPageChange(event: any) {
this.first = event.first;
this.rows = event.rows;
this.filteredListings=[...this.listings].splice(this.first,this.rows);
}
imageErrorHandler(listing: ListingType) {
listing.hideImage = true; // Bild ausblenden, wenn es nicht geladen werden kann
}
}

View File

@@ -0,0 +1,42 @@
<div class="surface-ground ">
<div class="p-fluid flex flex-column lg:flex-row">
<ul class="list-none m-0 p-0 flex flex-row lg:flex-column justify-content-evenly md:justify-content-between lg:justify-content-start mb-5 lg:pr-8 lg:mb-0">
<li>
<a routerLink="/account" routerLinkActive="text-blue-500" pRipple class="flex align-items-center cursor-pointer p-3 border-round text-800 hover:surface-200 transition-duration-150 transition-colors no-underline" >
<i class="pi pi-user md:mr-2"></i>
<span class="font-medium hidden md:block">Account</span>
</a>
</li>
<li>
<a routerLink="/createListing" routerLinkActive="text-blue-500" pRipple class="flex align-items-center cursor-pointer p-3 border-round text-800 hover:surface-200 transition-duration-150 transition-colors no-underline">
<i class="pi pi-plus-circle md:mr-2"></i>
<span class="font-medium hidden md:block">Create Listing</span>
</a>
</li>
<li>
<a routerLink="/myListings" routerLinkActive="text-blue-500" pRipple class="flex align-items-center cursor-pointer p-3 border-round text-800 hover:surface-200 transition-duration-150 transition-colors no-underline">
<i class="pi pi-list md:mr-2"></i>
<span class="font-medium hidden md:block">My Listings</span>
</a>
</li>
<li>
<a routerLink="/myFavorites" routerLinkActive="text-blue-500" pRipple class="flex align-items-center cursor-pointer p-3 border-round text-800 hover:surface-200 transition-duration-150 transition-colors no-underline">
<i class="pi pi-star md:mr-2"></i>
<span class="font-medium hidden md:block">My Favorites</span>
</a>
</li>
<li>
<a routerLink="/emailUs" routerLinkActive="text-blue-500" pRipple class="flex align-items-center cursor-pointer p-3 border-round text-800 hover:surface-200 transition-duration-150 transition-colors no-underline">
<fa-icon [icon]="faEnvelope" class="mr-2 flex"></fa-icon>
<span class="font-medium hidden md:block">Email Us</span>
</a>
</li>
<li>
<a (click)="userService.logout()" routerLinkActive="text-blue-500" pRipple class="flex align-items-center cursor-pointer p-3 border-round text-800 hover:surface-200 transition-duration-150 transition-colors no-underline">
<fa-icon [icon]="faRightFromBracket" class="mr-2 flex"></fa-icon>
<span class="font-medium hidden md:block">Logout</span>
</a>
</li>
</ul>
</div>
</div>

View File

@@ -0,0 +1,3 @@
ul {
text-wrap: nowrap;
}

View File

@@ -0,0 +1,43 @@
import { Component } from '@angular/core';
import { ButtonModule } from 'primeng/button';
import { CheckboxModule } from 'primeng/checkbox';
import { InputTextModule } from 'primeng/inputtext';
import { StyleClassModule } from 'primeng/styleclass';
import { BusinessListing, KeyValue } from '../../models/main.model';
import { SelectOptionsService } from '../../services/select-options.service';
import { DropdownModule } from 'primeng/dropdown';
import { FormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { ToggleButtonModule } from 'primeng/togglebutton';
import { TagModule } from 'primeng/tag';
import data from '../../../assets/data/listings.json';
import { ActivatedRoute, NavigationEnd, Router, RouterModule } from '@angular/router';
import { InputTextareaModule } from 'primeng/inputtextarea';
import { ChipModule } from 'primeng/chip';
import { DividerModule } from 'primeng/divider';
import { RippleModule } from 'primeng/ripple';
import { faEnvelope } from '@fortawesome/free-regular-svg-icons';
import { faRightFromBracket } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { UserService } from '../../services/user.service';
@Component({
selector: 'menu-account',
standalone: true,
imports: [CommonModule, StyleClassModule, ButtonModule, DividerModule, RouterModule, RippleModule, FontAwesomeModule ],
templateUrl: './menu-account.component.html',
styleUrl: './menu-account.component.scss'
})
export class MenuAccountComponent {
activeLink: string;
faEnvelope=faEnvelope;
faRightFromBracket=faRightFromBracket;
constructor(private router: Router,public userService:UserService) {
// Abonniere Router-Events, um den aktiven Link zu ermitteln
this.router.events.subscribe(event => {
if (event instanceof NavigationEnd) {
this.activeLink = event.url;
}
});
}
}

View File

@@ -0,0 +1,6 @@
.wrapper {
width: 1491px;
max-width: 100%;
height: 100%;
margin: auto;
}

View File

@@ -0,0 +1,85 @@
<div class="container">
<div class="wrapper">
<div class="py-3 px-6 flex flex-column align-items-center justify-content-between relative">
<a routerLink="/home"><img src="../../../assets/images/header-logo.png" alt="Image" height="50" ></a>
<div class="px-4 py-8 md:px-6 lg:px-8 bg-no-repeat bg-cover" >
<div class="flex flex-wrap">
<div class="w-full lg:w-6 lg:pr-8">
<div class="text-900 font-bold text-6xl text-blue-900 mb-4">Pricing</div>
<div class="text-700 text-xl text-blue-600 line-height-3 mb-4 lg:mb-0">Lorem ipsum dolor sit, amet consectetur adipisicing elit. Velitnumquam eligendi quos.</div>
</div>
<div class="w-full md:w-6 lg:w-3">
<ul class="list-none p-0 m-0">
<li class="flex align-items-center my-4 bg-white-alpha-40 shadow-2 py-1 px-2 border-round-lg w-max">
<i class="pi pi-check text-green-500 mr-3"></i>
<span>Arcu vitae elementum</span>
</li>
<li class="flex align-items-center my-4 bg-white-alpha-40 shadow-2 py-1 px-2 border-round-lg w-max">
<i class="pi pi-check text-green-500 mr-3"></i>
<span>Dui faucibus in ornare</span>
</li>
<li class="flex align-items-center my-4 bg-white-alpha-40 shadow-2 py-1 px-2 border-round-lg w-max">
<i class="pi pi-check text-green-500 mr-3"></i>
<span>Morbi tincidunt augue</span>
</li>
</ul>
</div>
<div class="w-full md:w-6 lg:w-3 md:pl-5">
<ul class="list-none p-0 m-0">
<li class="flex align-items-center my-4 bg-white-alpha-40 shadow-2 py-1 px-2 border-round-lg w-max">
<i class="pi pi-check text-green-500 mr-3"></i>
<span>Duis ultricies lacus sed</span>
</li>
<li class="flex align-items-center my-4 bg-white-alpha-40 shadow-2 py-1 px-2 border-round-lg w-max">
<i class="pi pi-check text-green-500 mr-3"></i>
<span>Imperdiet proin</span>
</li>
<li class="flex align-items-center my-4 bg-white-alpha-40 shadow-2 py-1 px-2 border-round-lg w-max">
<i class="pi pi-check text-green-500 mr-3"></i>
<span>Nisi scelerisque</span>
</li>
</ul>
</div>
</div>
<div class="flex flex-wrap mt-5 -mx-3">
<div class="w-full lg:w-4 p-3">
<div class="shadow-2 p-3 h-full bg-primary" style="border-radius: 6px">
<div class="font-medium text-xl mb-5">Free Forever</div>
<div class="font-bold text-5xl mb-5">Free</div>
<button (click)="register()" type="button" pRipple class="font-medium appearance-none border-none p-2 surface-0 text-primary hover:surface-100 p-component lg:w-full border-rounded cursor-pointer transition-colors transition-duration-150" style="border-radius: 6px">
<span>Create Account</span>
</button>
<p class="text-sm line-height-3 mb-0 mt-5">Lorem ipsum dolor sit, amet consectetur adipisicing elit.</p>
</div>
</div>
<div class="w-full lg:w-4 p-3">
<div class="shadow-2 p-3 h-full surface-card" style="border-radius: 6px">
<div class="font-medium text-xl mb-5 text-900 ">Monthly</div>
<div class="flex align-items-center mb-5">
<span class="text-900 font-bold text-5xl">$29</span>
<span class="font-medium text-500 ml-2">per month</span>
</div>
<button (click)="register()" pButton pRipple label="Proceed Monthly" icon="pi pi-arrow-right" iconPos="right" class="lg:w-full font-medium p-2" style="border-radius: 6px"></button>
<p class="text-sm line-height-3 mb-0 mt-5">Nec ultrices dui sapien eget. Amet nulla facilisi morbi tempus.</p>
</div>
</div>
<div class="w-full lg:w-4 p-3">
<div class="shadow-2 p-3 h-full flex flex-column surface-card" style="border-radius: 6px">
<div class="flex flex-row justify-content-between mb-5 align-items-center">
<div class="text-900 text-xl font-medium">Yearly</div>
<span class="bg-orange-100 500 text-orange-500 font-semibold px-2 py-1 border-round">🎉 Save 20%</span>
</div>
<div class="flex align-items-center mb-5">
<span class="text-900 font-bold text-5xl">$275</span>
<span class="font-medium text-500 ml-2">per year</span>
</div>
<button (click)="register()" pButton pRipple label="Proceed Yearly" icon="pi pi-arrow-right" iconPos="right" class="lg:w-full font-medium p-2" style="border-radius: 6px"></button>
<p class="text-sm line-height-3 mb-0 mt-5">Placerat in egestas erat imperdiet sed euismod nisi porta.</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,11 @@
:host {
height: 100%
}
.container {
background-image: url(../../../assets/images/index-bg.jpg), url(../../../assets/images/pricing-4.svg);
//background-image: url(../../../assets/images/corpusChristiSkyline.jpg);
background-size: cover;
background-position: center;
height: 100vh;
}

View File

@@ -0,0 +1,17 @@
import { Component } from '@angular/core';
import { SharedModule } from '../../shared/shared/shared.module';
import { UserService } from '../../services/user.service';
@Component({
selector: 'app-pricing',
standalone: true,
imports: [SharedModule],
templateUrl: './pricing.component.html',
styleUrl: './pricing.component.scss'
})
export class PricingComponent {
constructor(private userService:UserService){}
register(){
this.userService.register(`${window.location.origin}/account`);
}
}

View File

@@ -0,0 +1,108 @@
<div class="surface-ground px-4 py-8 md:px-6 lg:px-8">
<div class="p-fluid flex flex-column lg:flex-row">
<menu-account></menu-account>
<p-toast></p-toast>
<div class="surface-card p-5 shadow-2 border-round flex-auto">
<div class="text-900 font-semibold text-lg mt-3">Account Details</div>
<p-divider></p-divider>
<div class="flex gap-5 flex-column-reverse md:flex-row">
<div class="flex-auto p-fluid">
<div class="mb-4">
<label for="email" class="block font-medium text-900 mb-2">Username</label>
<input id="email" type="text" [disabled]="true" pInputText [(ngModel)]="user.username">
<p class="font-italic text-sm line-height-1">Usernames cannot be changed.</p>
</div>
<div class="mb-4">
<label for="state" class="block font-medium text-900 mb-2">First Name</label>
<input id="state" type="text" pInputText [(ngModel)]="user.firstname">
</div>
<div class="mb-4">
<label for="state" class="block font-medium text-900 mb-2">Last Name</label>
<input id="state" type="text" pInputText [(ngModel)]="user.lastname">
</div>
<div class="mb-4">
<label for="state" class="block font-medium text-900 mb-2">E-mail (required)</label>
<input id="state" type="text" pInputText [(ngModel)]="user.email">
</div>
<div class="mb-4">
<label for="state" class="block font-medium text-900 mb-2">New Password</label>
<p class="font-italic text-sm line-height-1">If you would like to change the password type a new one. Otherwise leave this blank.</p>
<input id="state" type="text" pInputText>
<p class="font-italic text-sm line-height-1">Password repetition</p>
<input id="state" type="text" pInputText>
</div>
<div>
<button pButton pRipple label="Update Profile" class="w-auto" (click)="updateProfile(user)"></button>
</div>
</div>
<div class="flex flex-column align-items-center flex-or">
<span class="font-medium text-900 mb-2">Profile Picture</span>
<img [src]="imageUrl" (error)="setImageToFallback($event)" class="rounded-image"/>
<!-- <p-image src="http://localhost:3000/public/user.png" alt="Image" width="10rem" [preview]="true"></p-image> -->
<!-- <button pButton type="button" icon="pi pi-pencil" class="p-button-rounded -mt-4"></button> -->
<p-fileUpload mode="basic" chooseLabel="Upload" name="file" [url]="uploadUrl" accept="image/*" [maxFileSize]="maxFileSize" (onUpload)="onUpload($event)" [auto]="true" styleClass="p-button-outlined p-button-plain p-button-rounded mt-4"></p-fileUpload>
</div>
</div>
<div class="text-900 font-semibold text-lg mt-3">Membership Level</div>
<p-divider></p-divider>
<p-table [value]="userSubscriptions" [tableStyle]="{ 'min-width': '50rem' }" dataKey="id">
<ng-template pTemplate="header">
<tr>
<th style="width: 5rem"></th>
<th>ID</th>
<th>Level</th>
<th>Start Date</th>
<th>Date Modified</th>
<th>End Date</th>
<th>Status</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-subscription let-expanded="expanded">
<tr>
<td>
<button type="button" pButton pRipple [pRowToggler]="subscription" class="p-button-text p-button-rounded p-button-plain" [icon]="expanded ? 'pi pi-chevron-down' : 'pi pi-chevron-right'"></button>
</td>
<td>{{ subscription.id }}</td>
<td>{{ subscription.level }}</td>
<td>{{ subscription.start | date }}</td>
<td>{{ subscription.modified | date }}</td>
<td>{{ subscription.end | date }}</td>
<td>{{ subscription.status }}</td>
</tr>
</ng-template>
<ng-template pTemplate="rowexpansion" let-subscription>
<tr>
<td colspan="7">
<div class="p-3">
<p-table [value]="subscription.invoices" dataKey="id">
<ng-template pTemplate="header">
<tr>
<th style="width: 5rem"></th>
<th>ID</th>
<th>Date</th>
<th>Price</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-invoice>
<tr>
<td>
<button pButton pRipple icon="pi pi-print" class="p-button-rounded p-button-success mr-2" (click)="printInvoice(invoice)"></button>
</td>
<td>{{ invoice.id }}</td>
<td>{{ invoice.date | date}}</td>
<td>{{ invoice.price | currency}}</td>
<td></td>
<td></td>
</tr>
</ng-template>
</p-table>
</div>
</td>
</tr>
</ng-template>
</p-table>
</div>
</div>
</div>

View File

@@ -0,0 +1,9 @@
.rounded-image {
border-radius: 6px;
width: 120px;
height: 30px;
border: 1px solid #6b7280;
padding: 1px 1px;
object-fit: contain;
}

View File

@@ -0,0 +1,64 @@
import { Component } from '@angular/core';
import { ButtonModule } from 'primeng/button';
import { CheckboxModule } from 'primeng/checkbox';
import { InputTextModule } from 'primeng/inputtext';
import { StyleClassModule } from 'primeng/styleclass';
import { BusinessListing, Invoice, KeyValue, Subscription, User } from '../../../models/main.model';
import { SelectOptionsService } from '../../../services/select-options.service';
import { DropdownModule } from 'primeng/dropdown';
import { FormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { ToggleButtonModule } from 'primeng/togglebutton';
import { TagModule } from 'primeng/tag';
import { ActivatedRoute } from '@angular/router';
import { InputTextareaModule } from 'primeng/inputtextarea';
import { ChipModule } from 'primeng/chip';
import { MenuAccountComponent } from '../../menu-account/menu-account.component';
import { DividerModule } from 'primeng/divider';
import { TableModule } from 'primeng/table';
import { HttpClient } from '@angular/common/http';
import { UserService } from '../../../services/user.service';
import { SharedModule } from '../../../shared/shared/shared.module';
import { SubscriptionsService } from '../../../services/subscriptions.service';
import { lastValueFrom } from 'rxjs';
import { MessageService } from 'primeng/api';
import { environment } from '../../../../environments/environment';
import { FileUploadModule } from 'primeng/fileupload';
@Component({
selector: 'app-account',
standalone: true,
// imports: [CommonModule, StyleClassModule, MenuAccountComponent, DividerModule,ButtonModule, TableModule, InputTextModule, DropdownModule, FormsModule, ChipModule,InputTextareaModule ],
imports: [SharedModule,FileUploadModule],
providers:[MessageService],
templateUrl: './account.component.html',
styleUrl: './account.component.scss'
})
export class AccountComponent {
user:User;
subscriptions:Array<Subscription>;
userSubscriptions:Array<Subscription>=[];
uploadUrl:string;
maxFileSize=1000000;
imageUrl:string;
constructor(public userService: UserService, private subscriptionService: SubscriptionsService,private messageService: MessageService) {
this.user=this.userService.getUser()
}
async ngOnInit(){
this.imageUrl = `${environment.apiBaseUrl}/profile_${this.user.id}`
this.userSubscriptions=await lastValueFrom(this.subscriptionService.getAllSubscriptions());
this.uploadUrl = `${environment.apiBaseUrl}/bizmatch/account/uploadPhoto/${this.user.id}`;
}
printInvoice(invoice:Invoice){}
updateProfile(user:User){
this.messageService.add({ severity: 'warn', summary: 'Information', detail: 'This function is not yet available, please send an email to info@bizmatch.net for changes to your customer data', life: 15000 });
}
onUpload(event:any){
const uniqueSuffix = '?_ts=' + new Date().getTime();
this.imageUrl = `${environment.apiBaseUrl}/profile_${this.user.id}${uniqueSuffix}` //`http://IhrServer:Port/${newImagePath}${uniqueSuffix}`;
}
setImageToFallback(event: Event) {
(event.target as HTMLImageElement).src = `/assets/images/placeholder.png`; // Pfad zum Platzhalterbild
}
}

View File

@@ -0,0 +1,156 @@
<div class="surface-ground px-4 py-8 md:px-6 lg:px-8">
<div class="p-fluid flex flex-column lg:flex-row">
<menu-account></menu-account>
<p-toast></p-toast>
<div class="surface-card p-5 shadow-2 border-round flex-auto">
<div class="text-900 font-semibold text-lg mt-3">{{mode==='create'?'New':'Edit'}} Listing</div>
<p-divider></p-divider>
<div class="flex gap-5 flex-column-reverse md:flex-row">
<div class="flex-auto p-fluid">
<div class="mb-4">
<label for="listingCategory" class="block font-medium text-900 mb-2">Listing category</label>
<p-dropdown id="listingCategory" [options]="selectOptions?.listingCategories" [(ngModel)]="listing.listingsCategory" optionLabel="name"
optionValue="value" placeholder="Listing category" [disabled]="mode==='edit'"
[style]="{ width: '100%'}"></p-dropdown>
</div>
<div class="mb-4">
<label for="email" class="block font-medium text-900 mb-2">Title of Listing</label>
<input id="email" type="text" pInputText [(ngModel)]="listing.title">
</div>
@if (listing.listingsCategory==='business' || listing.listingsCategory==='professionals_brokers'){
<div>
<div class="mb-4">
<label for="summary" class="block font-medium text-900 mb-2">Summary (Brief description)</label>
<textarea id="summary" type="text" pInputTextarea rows="5" [autoResize]="true" [ngModel]="listing.summary | arrayToString:'\n\n'" (ngModelChange)="updateSummary($event)"></textarea>
</div>
</div>
}
<div>
<div class="mb-4">
<label for="description" class="block font-medium text-900 mb-2">Description</label>
<textarea id="description" type="text" pInputTextarea rows="5" [autoResize]="true" [(ngModel)]="listing.description"></textarea>
</div>
</div>
@if (listing.listingsCategory==='business'){
<div class="mb-4">
<label for="listingCategory" class="block font-medium text-900 mb-2">Type of business</label>
<p-dropdown id="listingCategory" [options]="selectOptions?.typesOfBusiness" [(ngModel)]="listing.type" optionLabel="name"
optionValue="value" [showClear]="true" placeholder="Type of business"
[style]="{ width: '100%'}"></p-dropdown>
</div>
}
<div class="mb-4">
<label for="listingCategory" class="block font-medium text-900 mb-2">Location</label>
<p-dropdown id="listingCategory" [options]="selectOptions?.locations" [(ngModel)]="listing.location" optionLabel="name"
optionValue="value" [showClear]="true" placeholder="Location"
[style]="{ width: '100%'}"></p-dropdown>
</div>
@if (listing.listingsCategory==='professionals_brokers'){
<div>
<div class="mb-4">
<label for="address" class="block font-medium text-900 mb-2">Address</label>
<input id="address" type="text" pInputText [(ngModel)]="listing.address">
</div>
</div>
}
@if (listing.listingsCategory==='professionals_brokers' || listing.listingsCategory==='investment'){
<div>
<div class="mb-4">
<label for="email" class="block font-medium text-900 mb-2">Email</label>
<input id="address" type="text" pInputText [(ngModel)]="listing.email">
</div>
</div>
<div>
<div class="mb-4">
<label for="website" class="block font-medium text-900 mb-2">Website</label>
<input id="address" type="text" pInputText [(ngModel)]="listing.website">
</div>
</div>
}
@if (listing.listingsCategory==='professionals_brokers'){
<div class="mb-4">
<label for="category" class="block font-medium text-900 mb-2">Category</label>
<p-dropdown id="category" [options]="selectOptions?.categories" [(ngModel)]="listing.category" optionLabel="name"
optionValue="value" [showClear]="true" placeholder="Category"
[style]="{ width: '100%'}"></p-dropdown>
</div>
}
@if (listing.listingsCategory==='investment'){
<div>
<div class="mb-4">
<label for="phoneNumber" class="block font-medium text-900 mb-2">Phone Number</label>
<input id="phoneNumber" type="text" pInputText [(ngModel)]="listing.phoneNumber">
</div>
</div>
}
</div>
</div>
<p-divider></p-divider>
<div class="flex gap-5 flex-column-reverse md:flex-row">
<div class="flex-auto p-fluid">
@if (listing.listingsCategory==='business'){
<div class="grid">
<div class="mb-4 col-12 md:col-6">
<label for="price" class="block font-medium text-900 mb-2">Price</label>
<!-- <input id="price" type="text" pInputText [(ngModel)]="listing.price"> -->
<p-inputNumber mode="currency" currency="USD" inputId="price" type="text" [(ngModel)]="listing.price"></p-inputNumber>
</div>
<div class="mb-4 col-12 md:col-6 flex align-items-end justify-content-center">
<p-checkbox [binary]="true" [(ngModel)]="listing.realEstateIncluded"></p-checkbox>
<span class="ml-2 text-900">Real Estate Included</span>
</div>
</div>
<div class="grid">
<div class="mb-4 col-12 md:col-6">
<label for="salesRevenue" class="block font-medium text-900 mb-2">Sales Revenue</label>
<!-- <input id="salesRevenue" type="text" pInputText [(ngModel)]="listing.salesRevenue"> -->
<p-inputNumber mode="currency" currency="USD" inputId="salesRevenue" type="text" [(ngModel)]="listing.salesRevenue"></p-inputNumber>
</div>
<div class="mb-4 col-12 md:col-6">
<label for="cashFlow" class="block font-medium text-900 mb-2">Cash Flow</label>
<!-- <input id="cashFlow" type="text" pInputText [(ngModel)]="listing.cashFlow"> -->
<p-inputNumber mode="currency" currency="USD" inputId="cashFlow" type="text" [(ngModel)]="listing.cashFlow"></p-inputNumber>
</div>
</div>
<div class="grid">
<div class="mb-4 col-12 md:col-6">
<label for="netProfit" class="block font-medium text-900 mb-2">Net Profit</label>
<!-- <input id="netProfit" type="text" pInputText [(ngModel)]="listing.netProfit"> -->
<p-inputNumber mode="currency" currency="USD" inputId="netProfit" type="text" [(ngModel)]="listing.netProfit"></p-inputNumber>
</div>
<div class="mb-4 col-12 md:col-6">
<label for="employees" class="block font-medium text-900 mb-2">Employees</label>
<!-- <input id="employees" type="text" pInputText [(ngModel)]="listing.employees"> -->
<p-inputNumber mode="employees" mode="decimal" inputId="employees" type="text" [(ngModel)]="listing.employees"></p-inputNumber>
</div>
</div>
<div class="mb-4">
<label for="inventory" class="block font-medium text-900 mb-2">Inventory</label>
<textarea id="inventory" type="text" pInputTextarea rows="5" [autoResize]="true" [(ngModel)]="listing.inventory"></textarea>
</div>
<div class="mb-4">
<label for="reasonForSale" class="block font-medium text-900 mb-2">Reason for Sale</label>
<textarea id="reasonForSale" type="text" pInputTextarea rows="5" [autoResize]="true" [(ngModel)]="listing.reasonForSale"></textarea>
</div>
<div class="mb-4">
<label for="brokerLicensing" class="block font-medium text-900 mb-2">Broker Licensing</label>
<input id="brokerLicensing" type="text" pInputText [(ngModel)]="listing.brokerLicencing">
</div>
<div class="mb-4">
<label for="internalListing" class="block font-medium text-900 mb-2">Internal Listing (Will not be shown on the listing, for your records only.)</label>
<input id="internalListing" type="text" pInputText [(ngModel)]="listing.internals">
</div>
}
<div>
@if (mode==='create'){
<button pButton pRipple label="Post Listing" class="w-auto" (click)="create()"></button>
} @else {
<button pButton pRipple label="Update Listing" class="w-auto" (click)="update(listing.id)"></button>
}
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,77 @@
import { Component } from '@angular/core';
import { ButtonModule } from 'primeng/button';
import { CheckboxModule } from 'primeng/checkbox';
import { InputTextModule } from 'primeng/inputtext';
import { StyleClassModule } from 'primeng/styleclass';
import { BusinessListing, InvestmentsListing, Invoice, KeyValue, ProfessionalsBrokersListing, User } from '../../../models/main.model';
import { SelectOptionsService } from '../../../services/select-options.service';
import { DropdownModule } from 'primeng/dropdown';
import { FormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { ToggleButtonModule } from 'primeng/togglebutton';
import { TagModule } from 'primeng/tag';
import data from '../../../../assets/data/user.json';
import dataListings from '../../../../assets/data/listings.json';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { InputTextareaModule } from 'primeng/inputtextarea';
import { ChipModule } from 'primeng/chip';
import { MenuAccountComponent } from '../../menu-account/menu-account.component';
import { DividerModule } from 'primeng/divider';
import { TableModule } from 'primeng/table';
import { createGenericObject } from '../../../utils/utils';
import { ListingsService } from '../../../services/listings.service';
import { lastValueFrom } from 'rxjs';
import { InputNumberModule } from 'primeng/inputnumber';
import { ArrayToStringPipe } from '../../../pipes/array-to-string.pipe';
import { UserService } from '../../../services/user.service';
import { SharedModule } from '../../../shared/shared/shared.module';
import { MessageService } from 'primeng/api';
@Component({
selector: 'create-listing',
standalone: true,
imports: [SharedModule,ArrayToStringPipe],
providers:[MessageService],
templateUrl: './edit-listing.component.html',
styleUrl: './edit-listing.component.scss'
})
export class EditListingComponent {
listingCategory:'Business'|'Professionals/Brokers Directory'|'Investment Property';
category:string;
location:string;
mode:'edit'|'create';
separator:'\n\n'
listing:BusinessListing|ProfessionalsBrokersListing|InvestmentsListing = createGenericObject<BusinessListing>();
private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;
user:User;
constructor(public selectOptions:SelectOptionsService,private router: Router,private activatedRoute: ActivatedRoute,private listingsService:ListingsService,public userService: UserService,private messageService: MessageService){
this.user=this.userService.getUser();
// Abonniere Router-Events, um den aktiven Link zu ermitteln
this.router.events.subscribe(event => {
if (event instanceof NavigationEnd) {
this.mode = event.url==='/createListing'?'create':'edit';
}
});
}
async ngOnInit(){
if (this.mode==='edit'){
this.listing=await lastValueFrom(this.listingsService.getListingById(this.id));
} else {
this.listing=createGenericObject<BusinessListing>();
this.listing.userId=this.user.id
this.listing.listingsCategory='business';
}
}
updateSummary(value: string): void {
const lines = value.split('\n');
(<BusinessListing>this.listing).summary = lines.filter(l=>l.trim().length>0);
}
async update(id:string){
await this.listingsService.update(this.listing,this.listing.id);
this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Listing has been updated', life: 3000 });
}
async create(){
await this.listingsService.create(this.listing);
this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Listing has been created', life: 3000 });
}
}

View File

@@ -0,0 +1,35 @@
<div class="surface-ground px-4 py-8 md:px-6 lg:px-8 h-full">
<div class="p-fluid flex flex-column lg:flex-row">
<menu-account></menu-account>
<div class="surface-card p-5 shadow-2 border-round flex-auto">
<div class="text-900 font-semibold text-lg mt-3">Contact Us</div>
<p-divider></p-divider>
<div class="flex gap-5 flex-column-reverse md:flex-row">
<div class="flex-auto p-fluid">
<div class="grid">
<div class="mb-4 col-12 md:col-6">
<label for="name" class="block font-medium text-900 mb-2">Your name</label>
<input id="name" type="text" pInputText>
</div>
<div class="mb-4 col-12 md:col-6">
<label for="email" class="block font-medium text-900 mb-2">Your Email</label>
<input id="email" type="text" pInputText>
</div>
</div>
<div class="mb-4">
<label for="phone" class="block font-medium text-900 mb-2">Your Phone</label>
<input id="phone" type="text" pInputText>
</div>
<div class="mb-4">
<label for="help" class="block font-medium text-900 mb-2">How can we help you ?</label>
<textarea id="help" type="text" pInputTextarea rows="5" [autoResize]="true"></textarea>
</div>
<div>
<button pButton pRipple label="Submit" class="w-auto"></button>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,29 @@
import { Component } from '@angular/core';
import { ButtonModule } from 'primeng/button';
import { CheckboxModule } from 'primeng/checkbox';
import { InputTextModule } from 'primeng/inputtext';
import { StyleClassModule } from 'primeng/styleclass';
import { BusinessListing, Invoice, KeyValue, User } from '../../../models/main.model';
import { SelectOptionsService } from '../../../services/select-options.service';
import { DropdownModule } from 'primeng/dropdown';
import { FormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { ToggleButtonModule } from 'primeng/togglebutton';
import { TagModule } from 'primeng/tag';
import data from '../../../../assets/data/user.json';
import { ActivatedRoute } from '@angular/router';
import { InputTextareaModule } from 'primeng/inputtextarea';
import { ChipModule } from 'primeng/chip';
import { MenuAccountComponent } from '../../menu-account/menu-account.component';
import { DividerModule } from 'primeng/divider';
import { TableModule } from 'primeng/table';
@Component({
selector: 'app-email-us',
standalone: true,
imports: [CommonModule, StyleClassModule, MenuAccountComponent, DividerModule,ButtonModule, CheckboxModule, InputTextModule, DropdownModule, FormsModule, ChipModule,InputTextareaModule],
templateUrl: './email-us.component.html',
styleUrl: './email-us.component.scss'
})
export class EmailUsComponent {
}

View File

@@ -0,0 +1,30 @@
<div class="surface-ground px-4 py-8 md:px-6 lg:px-8 h-full">
<div class="p-fluid flex flex-column lg:flex-row">
<menu-account></menu-account>
<div class="surface-card p-5 shadow-2 border-round flex-auto">
<div class="text-900 font-semibold text-lg mt-3">My Favorites</div>
<p-divider></p-divider>
<p-table [value]="favorites" [tableStyle]="{ 'min-width': '50rem' }" dataKey="id" [paginator]="true" [rows]="10" [rowsPerPageOptions]="[10, 20, 50]" [showCurrentPageReport]="true" currentPageReportTemplate="Showing {first} to {last} of {totalRecords} entries">
<ng-template pTemplate="header">
<tr>
<th class="wide-column">Title</th>
<th>Category</th>
<th>Located in</th>
<th></th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-listing>
<tr>
<td class="wide-column line-height-3">{{ listing.title }}</td>
<td>{{ selectOptions.getListingsCategory(listing.listingsCategory) }}</td>
<td>{{ selectOptions.getLocation(listing.location) }}</td>
<td>
<button pButton pRipple icon="pi pi-eye" class="p-button-rounded p-button-success mr-2" [routerLink]="['/details',listing.id]"></button>
</td>
</tr>
</ng-template>
</p-table>
</div>
</div>
</div>

View File

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

View File

@@ -0,0 +1,29 @@
import { Component } from '@angular/core';
import { MenuAccountComponent } from '../../menu-account/menu-account.component';
import dataListings from '../../../../assets/data/listings.json';
import { BusinessListing, User } from '../../../models/main.model';
import { SharedModule } from '../../../shared/shared/shared.module';
import { UserService } from '../../../services/user.service';
import { lastValueFrom } from 'rxjs';
import { ListingsService } from '../../../services/listings.service';
import { SelectOptionsService } from '../../../services/select-options.service';
@Component({
selector: 'app-favorites',
standalone: true,
imports: [MenuAccountComponent, SharedModule],
templateUrl: './favorites.component.html',
styleUrl: './favorites.component.scss'
})
export class FavoritesComponent {
user: User;
listings: Array<BusinessListing> //= dataListings as unknown as Array<BusinessListing>;
favorites: Array<BusinessListing>
constructor(public userService: UserService, private listingsService:ListingsService, public selectOptions:SelectOptionsService){
this.user=this.userService.getUser();
}
async ngOnInit(){
this.listings=await lastValueFrom(this.listingsService.getAllListings());
this.favorites=this.listings.filter(l=>l.favoritesForUser?.includes(this.user.id));
}
}

View File

@@ -0,0 +1,33 @@
<div class="surface-ground px-4 py-8 md:px-6 lg:px-8 h-full">
<div class="p-fluid flex flex-column lg:flex-row">
<menu-account></menu-account>
<p-toast></p-toast>
<p-confirmPopup></p-confirmPopup>
<div class="surface-card p-5 shadow-2 border-round flex-auto">
<div class="text-900 font-semibold text-lg mt-3">My Listings</div>
<p-divider></p-divider>
<p-table [value]="myListings" [tableStyle]="{ 'min-width': '50rem' }" dataKey="id" [paginator]="true" [rows]="10" [rowsPerPageOptions]="[10, 20, 50]" [showCurrentPageReport]="true" currentPageReportTemplate="Showing {first} to {last} of {totalRecords} entries">
<ng-template pTemplate="header">
<tr>
<th class="wide-column">Title</th>
<th>Category</th>
<th>Located in</th>
<th></th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-listing>
<tr>
<td class="wide-column line-height-3">{{ listing.title }}</td>
<td>{{ selectOptions.getListingsCategory(listing.listingsCategory) }}</td>
<td>{{ selectOptions.getLocation(listing.location) }}</td>
<td>
<button pButton pRipple icon="pi pi-pencil" class="p-button-rounded p-button-success mr-2" [routerLink]="['/editListing',listing.id]"></button>
<button pButton pRipple icon="pi pi-trash" class="p-button-rounded p-button-warning" (click)="confirm($event,listing)"></button>
</td>
</tr>
</ng-template>
</p-table>
</div>
</div>
</div>

View File

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

View File

@@ -0,0 +1,49 @@
import { ChangeDetectorRef, Component } from '@angular/core';
import { MenuAccountComponent } from '../../menu-account/menu-account.component';
import dataListings from '../../../../assets/data/listings.json';
import { BusinessListing, ListingType, User } from '../../../models/main.model';
import { SharedModule } from '../../../shared/shared/shared.module';
import { UserService } from '../../../services/user.service';
import { ListingsService } from '../../../services/listings.service';
import { lastValueFrom } from 'rxjs';
import { SelectOptionsService } from '../../../services/select-options.service';
import { ConfirmationService, MessageService } from 'primeng/api';
@Component({
selector: 'app-my-listing',
standalone: true,
imports: [MenuAccountComponent, SharedModule],
providers:[ConfirmationService,MessageService],
templateUrl: './my-listing.component.html',
styleUrl: './my-listing.component.scss'
})
export class MyListingComponent {
user: User;
listings: Array<BusinessListing> //dataListings as unknown as Array<BusinessListing>;
myListings: Array<BusinessListing>
constructor(public userService: UserService,private listingsService:ListingsService, private cdRef:ChangeDetectorRef,public selectOptions:SelectOptionsService,private confirmationService: ConfirmationService,private messageService: MessageService){
this.user=this.userService.getUser();
}
async ngOnInit(){
this.listings=await lastValueFrom(this.listingsService.getAllListings());
this.myListings=this.listings.filter(l=>l.userId===this.user.id);
}
async deleteListing(listing:ListingType){
await this.listingsService.deleteListing(listing.id);
this.listings=await lastValueFrom(this.listingsService.getAllListings());
this.myListings=this.listings.filter(l=>l.userId===this.user.id);
}
confirm(event: Event,listing:ListingType) {
this.confirmationService.confirm({
target: event.target as EventTarget,
message: 'Are you sure you want to delet this listing?',
icon: 'pi pi-exclamation-triangle',
accept: () => {
this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Listing has been deleted', life: 3000 });
this.deleteListing(listing);
}
});
}
}

View File

@@ -0,0 +1,13 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'arrayToString',
standalone: true
})
export class ArrayToStringPipe implements PipeTransform {
transform(value: string|string[], separator: string = '\n'): string {
return Array.isArray(value)?value.join(separator):value;
}
}

View File

@@ -0,0 +1,561 @@
/**
* @license
* Copyright Mauricio Gemelli Vigolo and contributors.
*
* Use of this source code is governed by a MIT-style license that can be
* found in the LICENSE file at https://github.com/mauriciovigolo/keycloak-angular/blob/main/LICENSE.md
*/
import { Injectable } from '@angular/core';
import { HttpHeaders, HttpRequest } from '@angular/common/http';
import { Subject, from } from 'rxjs';
import { map } from 'rxjs/operators';
import Keycloak from 'keycloak-js';
import { ExcludedUrl, ExcludedUrlRegex, KeycloakOptions } from '../models/keycloak-options';
import { KeycloakEvent, KeycloakEventType } from '../models/keycloak-event';
/**
* Service to expose existent methods from the Keycloak JS adapter, adding new
* functionalities to improve the use of keycloak in Angular v > 4.3 applications.
*
* This class should be injected in the application bootstrap, so the same instance will be used
* along the web application.
*/
@Injectable()
export class KeycloakService {
/**
* Keycloak-js instance.
*/
private _instance: Keycloak.KeycloakInstance;
/**
* User profile as KeycloakProfile interface.
*/
private _userProfile: Keycloak.KeycloakProfile;
/**
* Flag to indicate if the bearer will not be added to the authorization header.
*/
private _enableBearerInterceptor: boolean;
/**
* When the implicit flow is choosen there must exist a silentRefresh, as there is
* no refresh token.
*/
private _silentRefresh: boolean;
/**
* Indicates that the user profile should be loaded at the keycloak initialization,
* just after the login.
*/
private _loadUserProfileAtStartUp: boolean;
/**
* The bearer prefix that will be appended to the Authorization Header.
*/
private _bearerPrefix: string;
/**
* Value that will be used as the Authorization Http Header name.
*/
private _authorizationHeaderName: string;
/**
* @deprecated
* The excluded urls patterns that must skip the KeycloakBearerInterceptor.
*/
private _excludedUrls: ExcludedUrlRegex[];
/**
* Observer for the keycloak events
*/
private _keycloakEvents$: Subject<KeycloakEvent> =
new Subject<KeycloakEvent>();
/**
* The amount of required time remaining before expiry of the token before the token will be refreshed.
*/
private _updateMinValidity: number;
/**
* Returns true if the request should have the token added to the headers by the KeycloakBearerInterceptor.
*/
shouldAddToken: (request: HttpRequest<unknown>) => boolean;
/**
* Returns true if the request being made should potentially update the token.
*/
shouldUpdateToken: (request: HttpRequest<unknown>) => boolean;
/**
* Binds the keycloak-js events to the keycloakEvents Subject
* which is a good way to monitor for changes, if needed.
*
* The keycloakEvents returns the keycloak-js event type and any
* argument if the source function provides any.
*/
private bindsKeycloakEvents(): void {
this._instance.onAuthError = (errorData) => {
this._keycloakEvents$.next({
args: errorData,
type: KeycloakEventType.OnAuthError
});
};
this._instance.onAuthLogout = () => {
this._keycloakEvents$.next({ type: KeycloakEventType.OnAuthLogout });
};
this._instance.onAuthRefreshSuccess = () => {
this._keycloakEvents$.next({
type: KeycloakEventType.OnAuthRefreshSuccess
});
};
this._instance.onAuthRefreshError = () => {
this._keycloakEvents$.next({
type: KeycloakEventType.OnAuthRefreshError
});
};
this._instance.onAuthSuccess = () => {
this._keycloakEvents$.next({ type: KeycloakEventType.OnAuthSuccess });
};
this._instance.onTokenExpired = () => {
this._keycloakEvents$.next({
type: KeycloakEventType.OnTokenExpired
});
};
this._instance.onActionUpdate = (state) => {
this._keycloakEvents$.next({
args: state,
type: KeycloakEventType.OnActionUpdate
});
};
this._instance.onReady = (authenticated) => {
this._keycloakEvents$.next({
args: authenticated,
type: KeycloakEventType.OnReady
});
};
}
/**
* Loads all bearerExcludedUrl content in a uniform type: ExcludedUrl,
* so it becomes easier to handle.
*
* @param bearerExcludedUrls array of strings or ExcludedUrl that includes
* the url and HttpMethod.
*/
private loadExcludedUrls(
bearerExcludedUrls: (string | ExcludedUrl)[]
): ExcludedUrlRegex[] {
const excludedUrls: ExcludedUrlRegex[] = [];
for (const item of bearerExcludedUrls) {
let excludedUrl: ExcludedUrlRegex;
if (typeof item === 'string') {
excludedUrl = { urlPattern: new RegExp(item, 'i'), httpMethods: [] };
} else {
excludedUrl = {
urlPattern: new RegExp(item.url, 'i'),
httpMethods: item.httpMethods
};
}
excludedUrls.push(excludedUrl);
}
return excludedUrls;
}
/**
* Handles the class values initialization.
*
* @param options
*/
private initServiceValues({
enableBearerInterceptor = true,
loadUserProfileAtStartUp = false,
bearerExcludedUrls = [],
authorizationHeaderName = 'Authorization',
bearerPrefix = 'Bearer',
initOptions,
updateMinValidity = 20,
shouldAddToken = () => true,
shouldUpdateToken = () => true
}: KeycloakOptions): void {
this._enableBearerInterceptor = enableBearerInterceptor;
this._loadUserProfileAtStartUp = loadUserProfileAtStartUp;
this._authorizationHeaderName = authorizationHeaderName;
this._bearerPrefix = bearerPrefix.trim().concat(' ');
this._excludedUrls = this.loadExcludedUrls(bearerExcludedUrls);
this._silentRefresh = initOptions ? initOptions.flow === 'implicit' : false;
this._updateMinValidity = updateMinValidity;
this.shouldAddToken = shouldAddToken;
this.shouldUpdateToken = shouldUpdateToken;
}
/**
* Keycloak initialization. It should be called to initialize the adapter.
* Options is an object with 2 main parameters: config and initOptions. The first one
* will be used to create the Keycloak instance. The second one are options to initialize the
* keycloak instance.
*
* @param options
* Config: may be a string representing the keycloak URI or an object with the
* following content:
* - url: Keycloak json URL
* - realm: realm name
* - clientId: client id
*
* initOptions:
* Options to initialize the Keycloak adapter, matches the options as provided by Keycloak itself.
*
* enableBearerInterceptor:
* Flag to indicate if the bearer will added to the authorization header.
*
* loadUserProfileInStartUp:
* Indicates that the user profile should be loaded at the keycloak initialization,
* just after the login.
*
* bearerExcludedUrls:
* String Array to exclude the urls that should not have the Authorization Header automatically
* added.
*
* authorizationHeaderName:
* This value will be used as the Authorization Http Header name.
*
* bearerPrefix:
* This value will be included in the Authorization Http Header param.
*
* tokenUpdateExcludedHeaders:
* Array of Http Header key/value maps that should not trigger the token to be updated.
*
* updateMinValidity:
* This value determines if the token will be refreshed based on its expiration time.
*
* @returns
* A Promise with a boolean indicating if the initialization was successful.
*/
public async init(options: KeycloakOptions = {}) {
this.initServiceValues(options);
const { config, initOptions } = options;
this._instance = new Keycloak(config);
this.bindsKeycloakEvents();
const authenticated = await this._instance.init(initOptions);
if (authenticated && this._loadUserProfileAtStartUp) {
await this.loadUserProfile();
}
return authenticated;
}
/**
* Redirects to login form on (options is an optional object with redirectUri and/or
* prompt fields).
*
* @param options
* Object, where:
* - redirectUri: Specifies the uri to redirect to after login.
* - prompt:By default the login screen is displayed if the user is not logged-in to Keycloak.
* To only authenticate to the application if the user is already logged-in and not display the
* login page if the user is not logged-in, set this option to none. To always require
* re-authentication and ignore SSO, set this option to login .
* - maxAge: Used just if user is already authenticated. Specifies maximum time since the
* authentication of user happened. If user is already authenticated for longer time than
* maxAge, the SSO is ignored and he will need to re-authenticate again.
* - loginHint: Used to pre-fill the username/email field on the login form.
* - action: If value is 'register' then user is redirected to registration page, otherwise to
* login page.
* - locale: Specifies the desired locale for the UI.
* @returns
* A void Promise if the login is successful and after the user profile loading.
*/
public async login(options: Keycloak.KeycloakLoginOptions = {}) {
await this._instance.login(options);
if (this._loadUserProfileAtStartUp) {
await this.loadUserProfile();
}
}
/**
* Redirects to logout.
*
* @param redirectUri
* Specifies the uri to redirect to after logout.
* @returns
* A void Promise if the logout was successful, cleaning also the userProfile.
*/
public async logout(redirectUri?: string) {
const options = {
redirectUri
};
await this._instance.logout(options);
this._userProfile = undefined;
}
/**
* Redirects to registration form. Shortcut for login with option
* action = 'register'. Options are same as for the login method but 'action' is set to
* 'register'.
*
* @param options
* login options
* @returns
* A void Promise if the register flow was successful.
*/
public async register(
options: Keycloak.KeycloakLoginOptions = { action: 'register' }
) {
await this._instance.register(options);
}
/**
* Check if the user has access to the specified role. It will look for roles in
* realm and the given resource, but will not check if the user is logged in for better performance.
*
* @param role
* role name
* @param resource
* resource name. If not specified, `clientId` is used
* @returns
* A boolean meaning if the user has the specified Role.
*/
isUserInRole(role: string, resource?: string): boolean {
let hasRole: boolean;
hasRole = this._instance.hasResourceRole(role, resource);
if (!hasRole) {
hasRole = this._instance.hasRealmRole(role);
}
return hasRole;
}
/**
* Return the roles of the logged user. The realmRoles parameter, with default value
* true, will return the resource roles and realm roles associated with the logged user. If set to false
* it will only return the resource roles. The resource parameter, if specified, will return only resource roles
* associated with the given resource.
*
* @param realmRoles
* Set to false to exclude realm roles (only client roles)
* @param resource
* resource name If not specified, returns roles from all resources
* @returns
* Array of Roles associated with the logged user.
*/
getUserRoles(realmRoles: boolean = true, resource?: string): string[] {
let roles: string[] = [];
if (this._instance.resourceAccess) {
Object.keys(this._instance.resourceAccess).forEach((key) => {
if (resource && resource !== key) {
return;
}
const resourceAccess = this._instance.resourceAccess[key];
const clientRoles = resourceAccess['roles'] || [];
roles = roles.concat(clientRoles);
});
}
if (realmRoles && this._instance.realmAccess) {
const realmRoles = this._instance.realmAccess['roles'] || [];
roles.push(...realmRoles);
}
return roles;
}
/**
* Check if user is logged in.
*
* @returns
* A boolean that indicates if the user is logged in.
*/
isLoggedIn(): boolean {
if (!this._instance) {
return false;
}
return this._instance.authenticated;
}
/**
* Returns true if the token has less than minValidity seconds left before
* it expires.
*
* @param minValidity
* Seconds left. (minValidity) is optional. Default value is 0.
* @returns
* Boolean indicating if the token is expired.
*/
isTokenExpired(minValidity: number = 0): boolean {
return this._instance.isTokenExpired(minValidity);
}
/**
* If the token expires within _updateMinValidity seconds the token is refreshed. If the
* session status iframe is enabled, the session status is also checked.
* Returns a promise telling if the token was refreshed or not. If the session is not active
* anymore, the promise is rejected.
*
* @param minValidity
* Seconds left. (minValidity is optional, if not specified updateMinValidity - default 20 is used)
* @returns
* Promise with a boolean indicating if the token was succesfully updated.
*/
public async updateToken(minValidity = this._updateMinValidity) {
// TODO: this is a workaround until the silent refresh (issue #43)
// is not implemented, avoiding the redirect loop.
if (this._silentRefresh) {
if (this.isTokenExpired()) {
throw new Error(
'Failed to refresh the token, or the session is expired'
);
}
return true;
}
if (!this._instance) {
throw new Error('Keycloak Angular library is not initialized.');
}
try {
return await this._instance.updateToken(minValidity);
} catch (error) {
return false;
}
}
/**
* Loads the user profile.
* Returns promise to set functions to be invoked if the profile was loaded
* successfully, or if the profile could not be loaded.
*
* @param forceReload
* If true will force the loadUserProfile even if its already loaded.
* @returns
* A promise with the KeycloakProfile data loaded.
*/
public async loadUserProfile(forceReload = false) {
if (this._userProfile && !forceReload) {
return this._userProfile;
}
if (!this._instance.authenticated) {
throw new Error(
'The user profile was not loaded as the user is not logged in.'
);
}
return (this._userProfile = await this._instance.loadUserProfile());
}
/**
* Returns the authenticated token, calling updateToken to get a refreshed one if necessary.
*/
public async getToken() {
return this._instance.token;
}
/**
* Returns the logged username.
*
* @returns
* The logged username.
*/
public getUsername() {
if (!this._userProfile) {
throw new Error('User not logged in or user profile was not loaded.');
}
return this._userProfile.username;
}
/**
* Clear authentication state, including tokens. This can be useful if application
* has detected the session was expired, for example if updating token fails.
* Invoking this results in onAuthLogout callback listener being invoked.
*/
clearToken(): void {
this._instance.clearToken();
}
/**
* Adds a valid token in header. The key & value format is:
* Authorization Bearer <token>.
* If the headers param is undefined it will create the Angular headers object.
*
* @param headers
* Updated header with Authorization and Keycloak token.
* @returns
* An observable with with the HTTP Authorization header and the current token.
*/
public addTokenToHeader(headers: HttpHeaders = new HttpHeaders()) {
return from(this.getToken()).pipe(
map((token) =>
token
? headers.set(
this._authorizationHeaderName,
this._bearerPrefix + token
)
: headers
)
);
}
/**
* Returns the original Keycloak instance, if you need any customization that
* this Angular service does not support yet. Use with caution.
*
* @returns
* The KeycloakInstance from keycloak-js.
*/
getKeycloakInstance(): Keycloak.KeycloakInstance {
return this._instance;
}
/**
* @deprecated
* Returns the excluded URLs that should not be considered by
* the http interceptor which automatically adds the authorization header in the Http Request.
*
* @returns
* The excluded urls that must not be intercepted by the KeycloakBearerInterceptor.
*/
get excludedUrls(): ExcludedUrlRegex[] {
return this._excludedUrls;
}
/**
* Flag to indicate if the bearer will be added to the authorization header.
*
* @returns
* Returns if the bearer interceptor was set to be disabled.
*/
get enableBearerInterceptor(): boolean {
return this._enableBearerInterceptor;
}
/**
* Keycloak subject to monitor the events triggered by keycloak-js.
* The following events as available (as described at keycloak docs -
* https://www.keycloak.org/docs/latest/securing_apps/index.html#callback-events):
* - OnAuthError
* - OnAuthLogout
* - OnAuthRefreshError
* - OnAuthRefreshSuccess
* - OnAuthSuccess
* - OnReady
* - OnTokenExpire
* In each occurrence of any of these, this subject will return the event type,
* described at {@link KeycloakEventType} enum and the function args from the keycloak-js
* if provided any.
*
* @returns
* A subject with the {@link KeycloakEvent} which describes the event type and attaches the
* function args.
*/
get keycloakEvents$(): Subject<KeycloakEvent> {
return this._keycloakEvents$;
}
}

View File

@@ -0,0 +1,33 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, lastValueFrom } from 'rxjs';
import { BusinessListing, ListingCriteria } from '../models/main.model';
import { environment } from '../../environments/environment';
@Injectable({
providedIn: 'root'
})
export class ListingsService {
private apiBaseUrl = environment.apiBaseUrl;
constructor(private http: HttpClient) { }
getAllListings():Observable<BusinessListing[]>{
return this.http.get<BusinessListing[]>(`${this.apiBaseUrl}/bizmatch/listings`);
}
async getListings(criteria:ListingCriteria):Promise<BusinessListing[]>{
const result = await lastValueFrom(this.http.post<BusinessListing[]>(`${this.apiBaseUrl}/bizmatch/listings/search`,criteria));
return result;
}
getListingById(id:string):Observable<BusinessListing>{
return this.http.get<BusinessListing>(`${this.apiBaseUrl}/bizmatch/listings/${id}`);
}
async update(listing:any,id:string){
await lastValueFrom(this.http.put<BusinessListing>(`${this.apiBaseUrl}/bizmatch/listings/${id}`,listing));
}
async create(listing:any){
await lastValueFrom(this.http.post<BusinessListing>(`${this.apiBaseUrl}/bizmatch/listings`,listing));
}
async deleteListing(id:string){
await lastValueFrom(this.http.delete<BusinessListing>(`${this.apiBaseUrl}/bizmatch/listings/${id}`));
}
}

View File

@@ -0,0 +1,28 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, debounceTime, distinctUntilChanged, map, shareReplay } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class LoadingService {
public loading$ = new BehaviorSubject<string[]>([]);
public isLoading$ = this.loading$.asObservable().pipe(
map((loading) => loading.length > 0),
debounceTime(200),
distinctUntilChanged(),
shareReplay(1)
);
public startLoading(type: string): void {
if (!this.loading$.value.includes(type)) {
this.loading$.next(this.loading$.value.concat(type));
}
}
public stopLoading(type: string): void {
if (this.loading$.value.includes(type)) {
this.loading$.next(this.loading$.value.filter((t) => t !== type));
}
}
}

View File

@@ -0,0 +1,75 @@
import { Injectable } from '@angular/core';
import { KeyValue, KeyValueStyle } from '../models/main.model';
import { HttpClient } from '@angular/common/http';
import { InitEditableRow } from 'primeng/table';
import { lastValueFrom } from 'rxjs';
import { environment } from '../../environments/environment';
@Injectable({
providedIn: 'root',
})
export class SelectOptionsService {
private apiBaseUrl = environment.apiBaseUrl;
constructor(private http: HttpClient) {}
async init() {
const allSelectOptions = await lastValueFrom(
this.http.get<any>(`${this.apiBaseUrl}/bizmatch/select-options`)
);
this.typesOfBusiness = allSelectOptions.typesOfBusiness;
this.prices = allSelectOptions.prices;
this.listingCategories = allSelectOptions.listingCategories;
this.categories = allSelectOptions.categories;
this.locations = allSelectOptions.locations;
}
public typesOfBusiness: Array<KeyValueStyle>;
public prices: Array<KeyValue>;
public listingCategories: Array<KeyValue>;
public categories: Array<KeyValueStyle>;
public locations: Array<any>;
getLocation(value:string):string{
return this.locations.find(l=>l.value===value)?.name
}
getBusiness(value:string):string{
return this.typesOfBusiness.find(t=>t.value===value)?.name
}
getListingsCategory(value:string):string{
return this.listingCategories.find(l=>l.value===value)?.name
}
getCategory(value:string):string{
return this.categories.find(c=>c.value===value)?.name
}
getIcon(value:string):string{
return this.categories.find(c=>c.value===value)?.icon
}
getTextColor(value:string):string{
return this.categories.find(c=>c.value===value)?.textColorClass
}
getBgColor(value:string):string{
return this.categories.find(c=>c.value===value)?.bgColorClass
}
getIconAndTextColor(value:string):string{
const category = this.categories.find(c=>c.value===value)
return `${category?.icon} ${category?.textColorClass}`
}
getIconType(value:string):string{
return this.typesOfBusiness.find(c=>c.value===value)?.icon
}
getTextColorType(value:string):string{
return this.typesOfBusiness.find(c=>c.value===value)?.textColorClass
}
getBgColorType(value:string):string{
return this.typesOfBusiness.find(c=>c.value===value)?.bgColorClass
}
getIconAndTextColorType(value:string):string{
const category = this.typesOfBusiness.find(c=>c.value===value)
return `${category?.icon} ${category?.textColorClass}`
}
}

View File

@@ -0,0 +1,17 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { Subscription } from '../models/main.model';
import { environment } from '../../environments/environment';
import { HttpClient } from '@angular/common/http';
@Injectable({
providedIn: 'root'
})
export class SubscriptionsService {
private apiBaseUrl = environment.apiBaseUrl;
constructor(private http: HttpClient) { }
getAllSubscriptions():Observable<Subscription[]>{
return this.http.get<Subscription[]>(`${this.apiBaseUrl}/bizmatch/subscriptions`);
}
}

View File

@@ -0,0 +1,94 @@
import { Injectable, Signal, WritableSignal, computed, effect, signal } from '@angular/core';
import { Component } from '@angular/core';
import { jwtDecode } from 'jwt-decode';
import { Observable, distinctUntilChanged, filter, from, map } from 'rxjs';
import { CommonModule } from '@angular/common';
import { JwtToken, User } from '../models/main.model';
import { KeycloakService } from './keycloak.service';
@Injectable({
providedIn: 'root'
})
export class UserService {
private user$ = new Observable<User>();
private user:User
public $isLoggedIn : Signal<boolean>;
constructor(public keycloak:KeycloakService){
this.user$ = from(this.keycloak.getToken()).pipe(
filter(t => !!t),
distinctUntilChanged(),
map(t => this.map2User(t)),
// tap(u => {
// logger.info('Logged in user:', u);
// this.analyticsService.identify(u);
// }),
);
this.$isLoggedIn = signal(false)
this.$isLoggedIn = computed(() => {
return keycloak.isLoggedIn()
})
effect(async () => {
if (this.$isLoggedIn()){
this.updateTokenDetails()
} else {
this.user=null;
}
})
}
private async refreshToken(): Promise<void> {
try {
await this.keycloak.updateToken(10); // Versuche, den Token zu erneuern
await this.updateTokenDetails(); // Aktualisiere den Token und seine Details
} catch (error) {
console.error('Fehler beim Token-Refresh', error);
}
}
private async updateTokenDetails(): Promise<void> {
const token = await this.keycloak.getToken();
this.user = this.map2User(token);
}
getUserName(){
return this.user?.username
}
private map2User(jwt:string):User{
const token = jwtDecode<JwtToken>(jwt);
return {
id:token.user_id,
username:token.preferred_username,
firstname:token.given_name,
lastname:token.family_name,
email:token.email
}
}
isLoggedIn():boolean{
return this.$isLoggedIn();
}
getUser():User{
return this.user;
}
getUserObservable():Observable<User>{
return this.user$;
}
logout(){
this.keycloak.logout(window.location.origin + '/home');
}
async login(url:string){
await this.keycloak.login({
redirectUri: url
});
}
getUserRoles(){
return this.keycloak.getUserRoles(true);
}
hasAdminRole(){
return this.keycloak.getUserRoles(true).includes('ADMIN');
}
register(url:string){
this.keycloak.register({redirectUri:url});
}
}

View File

@@ -0,0 +1,33 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { ButtonModule } from 'primeng/button';
import { CheckboxModule } from 'primeng/checkbox';
import { InputTextModule } from 'primeng/inputtext';
import { StyleClassModule } from 'primeng/styleclass';
import { DropdownModule } from 'primeng/dropdown';
import { FormsModule } from '@angular/forms';
import { ToggleButtonModule } from 'primeng/togglebutton';
import { TagModule } from 'primeng/tag';
import { ActivatedRoute, RouterModule } from '@angular/router';
import { InputTextareaModule } from 'primeng/inputtextarea';
import { ChipModule } from 'primeng/chip';
import { DividerModule } from 'primeng/divider';
import { TableModule } from 'primeng/table';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { MenuAccountComponent } from '../../pages/menu-account/menu-account.component';
import { InputNumberModule } from 'primeng/inputnumber';
import { ConfirmDialogModule } from 'primeng/confirmdialog';
import { ConfirmPopupModule } from 'primeng/confirmpopup';
import { ToastModule } from 'primeng/toast';
@NgModule({
declarations: [],
imports: [
CommonModule, StyleClassModule, DividerModule,ButtonModule, TableModule, InputTextModule, DropdownModule, FormsModule, ChipModule,InputTextareaModule, RouterModule,FontAwesomeModule,MenuAccountComponent,InputNumberModule,ConfirmDialogModule,ConfirmPopupModule, ToastModule, CheckboxModule
],
exports:[
CommonModule, StyleClassModule, DividerModule,ButtonModule, TableModule, InputTextModule, DropdownModule, FormsModule, ChipModule,InputTextareaModule,RouterModule,FontAwesomeModule,MenuAccountComponent,InputNumberModule,ConfirmDialogModule,ConfirmPopupModule, ToastModule, CheckboxModule
]
})
export class SharedModule { }

View File

@@ -0,0 +1,38 @@
import { INFO, ConsoleFormattedStream, createLogger as _createLogger, stdSerializers } from "browser-bunyan";
import { ListingCriteria } from "../models/main.model";
export function createGenericObject<T>(): T {
// Ein leeres Objekt vom Typ T erstellen
const ergebnis: Partial<T> = {};
// Für ein reales Interface funktioniert diese direkte Iteration nicht,
// da Interfaces zur Compile-Zeit entfernt werden. Stattdessen könnten Sie
// ein Dummy-Objekt oder spezifische Typtransformationen verwenden.
// Hier nur als Pseudocode dargestellt, um die Idee zu vermitteln:
for (const key in ergebnis) {
ergebnis[key] = null; // oder undefined, je nach Bedarf
}
return ergebnis as T;
}
export function createLogger(name:string, level: number = INFO, options:any = {}){
return _createLogger({
name,
streams:[{level, stream: new ConsoleFormattedStream()}],
serializers:stdSerializers,
src:true,
...options,
})
}
export const getSessionStorageHandler = function(path,value,previous,applyData){
sessionStorage.setItem('criteria',JSON.stringify(this));
}
export function getCriteriaStateObject(){
const initialState = createGenericObject<ListingCriteria>();
initialState.listingsCategory='business';
const storedState = sessionStorage.getItem('criteria');
return storedState ? JSON.parse(storedState) : initialState;
}