First version AI Search

This commit is contained in:
2024-07-15 19:37:35 +02:00
parent b7b34dacab
commit af982d19d8
21 changed files with 702 additions and 105 deletions

View File

@@ -1,7 +1,6 @@
import { CommonModule } from '@angular/common';
import { Component, HostListener } from '@angular/core';
import { ActivatedRoute, NavigationEnd, Router, RouterOutlet } from '@angular/router';
import { initFlowbite } from 'flowbite';
import { KeycloakService } from 'keycloak-angular';
import onChange from 'on-change';
@@ -42,7 +41,11 @@ export class AppComponent {
ngOnInit() {}
@HostListener('window:keydown', ['$event'])
handleKeyboardEvent(event: KeyboardEvent) {
initFlowbite();
// this.router.events.subscribe(event => {
// if (event instanceof NavigationEnd) {
// initFlowbite();
// }
// });
if (event.shiftKey && event.ctrlKey && event.key === 'V') {
this.showVersionDialog();
}

View File

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

View File

@@ -0,0 +1,129 @@
import { AfterViewInit, Component, ElementRef, HostBinding, Input, OnDestroy, ViewChild } from '@angular/core';
import { createPopper, Instance as PopperInstance } from '@popperjs/core';
@Component({
selector: 'app-dropdown',
template: `
<div #targetEl [class.hidden]="!isVisible" class="z-10">
<ng-content></ng-content>
</div>
`,
standalone: true,
})
export class DropdownComponent implements AfterViewInit, OnDestroy {
@ViewChild('targetEl') targetEl!: ElementRef<HTMLElement>;
@Input() triggerEl!: HTMLElement;
@Input() placement: any = 'bottom';
@Input() triggerType: 'click' | 'hover' = 'click';
@Input() offsetSkidding: number = 0;
@Input() offsetDistance: number = 10;
@Input() delay: number = 300;
@Input() ignoreClickOutsideClass: string | false = false;
@HostBinding('class.hidden') isHidden: boolean = true;
private popperInstance: PopperInstance | null = null;
isVisible: boolean = false;
private clickOutsideListener: any;
private hoverShowListener: any;
private hoverHideListener: any;
ngAfterViewInit() {
if (!this.triggerEl) {
console.error('Trigger element is not provided to the dropdown component.');
return;
}
this.initializePopper();
this.setupEventListeners();
}
ngOnDestroy() {
this.destroyPopper();
this.removeEventListeners();
}
private initializePopper() {
this.popperInstance = createPopper(this.triggerEl, this.targetEl.nativeElement, {
placement: this.placement,
modifiers: [
{
name: 'offset',
options: {
offset: [this.offsetSkidding, this.offsetDistance],
},
},
],
});
}
private setupEventListeners() {
if (this.triggerType === 'click') {
this.triggerEl.addEventListener('click', () => this.toggle());
} else if (this.triggerType === 'hover') {
this.hoverShowListener = () => this.show();
this.hoverHideListener = () => this.hide();
this.triggerEl.addEventListener('mouseenter', this.hoverShowListener);
this.triggerEl.addEventListener('mouseleave', this.hoverHideListener);
this.targetEl.nativeElement.addEventListener('mouseenter', this.hoverShowListener);
this.targetEl.nativeElement.addEventListener('mouseleave', this.hoverHideListener);
}
this.clickOutsideListener = (event: MouseEvent) => this.handleClickOutside(event);
document.addEventListener('click', this.clickOutsideListener);
}
private removeEventListeners() {
if (this.triggerType === 'click') {
this.triggerEl.removeEventListener('click', () => this.toggle());
} else if (this.triggerType === 'hover') {
this.triggerEl.removeEventListener('mouseenter', this.hoverShowListener);
this.triggerEl.removeEventListener('mouseleave', this.hoverHideListener);
this.targetEl.nativeElement.removeEventListener('mouseenter', this.hoverShowListener);
this.targetEl.nativeElement.removeEventListener('mouseleave', this.hoverHideListener);
}
document.removeEventListener('click', this.clickOutsideListener);
}
toggle() {
this.isVisible ? this.hide() : this.show();
}
show() {
this.isVisible = true;
this.isHidden = false;
this.targetEl.nativeElement.classList.remove('hidden');
this.popperInstance?.update();
}
hide() {
this.isVisible = false;
this.isHidden = true;
this.targetEl.nativeElement.classList.add('hidden');
}
private handleClickOutside(event: MouseEvent) {
if (!this.isVisible) return;
const clickedElement = event.target as HTMLElement;
if (this.ignoreClickOutsideClass) {
const ignoredElements = document.querySelectorAll(`.${this.ignoreClickOutsideClass}`);
const arr = Array.from(ignoredElements);
for (const el of arr) {
if (el.contains(clickedElement)) return;
}
}
if (!this.targetEl.nativeElement.contains(clickedElement) && !this.triggerEl.contains(clickedElement)) {
this.hide();
}
}
private destroyPopper() {
if (this.popperInstance) {
this.popperInstance.destroy();
this.popperInstance = null;
}
}
}

View File

@@ -1,7 +1,7 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { NavigationEnd, Router, RouterModule } from '@angular/router';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { initFlowbite } from 'flowbite';
@Component({
@@ -15,7 +15,12 @@ export class FooterComponent {
privacyVisible = false;
termsVisible = false;
currentYear: number = new Date().getFullYear();
constructor(private router: Router) {}
ngOnInit() {
initFlowbite();
this.router.events.subscribe(event => {
if (event instanceof NavigationEnd) {
initFlowbite();
}
});
}
}

View File

@@ -1,15 +1,114 @@
<!-- <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>
<p-menubar [model]="loginItems"></p-menubar>
@if(user){
<div>Welcome, {{ user.firstName }}</div>
}
<ng-template #empty> </ng-template>
<!-- <nav class="bg-white border-gray-200 dark:bg-gray-900">
<div class="max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-4">
<a routerLink="/home" class="flex items-center space-x-3 rtl:space-x-reverse">
<img src="assets/images/header-logo.png" class="h-8" alt="Flowbite Logo" />
</a>
<div class="flex items-center md:order-2 space-x-3 md:space-x-0 rtl:space-x-reverse">
<button
type="button"
class="flex text-sm bg-gray-200 rounded-full md:me-0 focus:ring-4 focus:ring-gray-300 dark:focus:ring-gray-600"
id="user-menu-button"
aria-expanded="false"
[attr.data-dropdown-toggle]="user ? 'user-login' : 'user-unknown'"
data-dropdown-placement="bottom"
>
<span class="sr-only">Open user menu</span>
@if(user){
<img class="w-8 h-8 rounded-full object-cover" src="{{ profileUrl }}" alt="user photo" />
} @else {
<i class="flex justify-center items-center text-stone-50 w-8 h-8 rounded-full fa-solid fa-bars"></i>
}
</button>
@if(user){
<div class="z-50 hidden my-4 text-base list-none bg-white divide-y divide-gray-100 rounded-lg shadow dark:bg-gray-700 dark:divide-gray-600" id="user-login">
<div class="px-4 py-3">
<span class="block text-sm text-gray-900 dark:text-white">Welcome, {{ user.firstname }} </span>
<span class="block text-sm text-gray-500 truncate dark:text-gray-400">{{ user.email }}</span>
</div>
<ul class="py-2" aria-labelledby="user-menu-button">
<li>
<a routerLink="/account" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Account</a>
</li>
<li>
<a routerLink="/createBusinessListing" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white"
>Create Listing</a
>
</li>
<li>
<a routerLink="/myListings" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">My Listings</a>
</li>
<li>
<a routerLink="/emailUs" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">EMail Us</a>
</li>
<li>
<a routerLink="/logout" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Logout</a>
</li>
</ul>
</div>
} @else {
<div class="z-50 hidden my-4 text-base list-none bg-white divide-y divide-gray-100 rounded-lg shadow dark:bg-gray-700 dark:divide-gray-600" id="user-unknown">
<ul class="py-2" aria-labelledby="user-menu-button">
<li>
<a (click)="login()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Log In</a>
</li>
<li>
<a (click)="register()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Register</a>
</li>
</ul>
</div>
}
<button
data-collapse-toggle="navbar-user"
type="button"
class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm text-gray-500 rounded-lg md:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600"
aria-controls="navbar-user"
aria-expanded="false"
>
<span class="sr-only">Open main menu</span>
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 17 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 1h15M1 7h15M1 13h15" />
</svg>
</button>
</div>
<div class="items-center justify-between hidden w-full md:flex md:w-auto md:order-1" id="navbar-user">
<ul
class="flex flex-col font-medium p-4 md:p-0 mt-4 border border-gray-100 rounded-lg bg-gray-50 md:space-x-8 rtl:space-x-reverse md:flex-row md:mt-0 md:border-0 md:bg-white dark:bg-gray-800 md:dark:bg-gray-900 dark:border-gray-700"
>
<li>
<a
routerLinkActive="active-link"
routerLink="/businessListings"
[ngClass]="{ 'bg-blue-700 text-white md:text-blue-700 md:bg-transparent md:dark:text-blue-500': isActive('/businessListings') }"
class="block py-2 px-3 rounded hover:bg-gray-100 md:hover:bg-transparent md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
aria-current="page"
(click)="closeMenus()"
>Businesses</a
>
</li>
<li>
<a
routerLinkActive="active-link"
routerLink="/commercialPropertyListings"
[ngClass]="{ 'bg-blue-700 text-white md:text-blue-700 md:bg-transparent md:dark:text-blue-500': isActive('/commercialPropertyListings') }"
class="block py-2 px-3 rounded hover:bg-gray-100 md:hover:bg-transparent md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
(click)="closeMenus()"
>Properties</a
>
</li>
<li>
<a
routerLinkActive="active-link"
routerLink="/brokerListings"
[ngClass]="{ 'bg-blue-700 text-white md:text-blue-700 md:bg-transparent md:dark:text-blue-500': isActive('/brokerListings') }"
class="block py-2 px-3 rounded hover:bg-gray-100 md:hover:bg-transparent md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
(click)="closeMenus()"
>Professionals</a
>
</li>
</ul>
</div>
</div>
</div> -->
</nav> -->
<nav class="bg-white border-gray-200 dark:bg-gray-900">
<div class="max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-4">
@@ -17,6 +116,16 @@
<img src="assets/images/header-logo.png" class="h-8" alt="Flowbite Logo" />
</a>
<div class="flex items-center md:order-2 space-x-3 md:space-x-0 rtl:space-x-reverse">
<!-- Filter button -->
<button
type="button"
#triggerButton
id="filterDropdownButton"
class="max-sm:hidden px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 md:me-2"
>
<i class="fas fa-filter mr-2"></i>Filter (1)
</button>
<button
type="button"
class="flex text-sm bg-gray-200 rounded-full md:me-0 focus:ring-4 focus:ring-gray-300 dark:focus:ring-gray-600"
@@ -122,4 +231,170 @@
</ul>
</div>
</div>
<!-- Mobile filter button -->
<!-- <div class="md:hidden flex justify-center pb-4">
<button
data-dropdown-toggle="filterDropdown"
type="button"
id="filterDropdownMobileButton"
class="w-full mx-4 px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
>
<i class="fas fa-filter mr-2"></i>Filter (1)
</button>
</div> -->
</nav>
<!-- ############################### -->
<!-- Filter Dropdown -->
<!-- ############################### -->
<app-dropdown [triggerEl]="triggerButton" [triggerType]="'click'">
<div id="filterDropdown" class="z-10 w-80 p-3 bg-slate-200 rounded-lg shadow-lg dark:bg-gray-700">
<h3 class="mb-3 text-sm font-medium text-gray-900 dark:text-white">Filter</h3>
<!-- Price Range -->
<div class="mb-4">
<div class="flex items-center space-x-4">
<input
type="text"
[ngModel]="prompt"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
value="300"
/>
</div>
<!-- <label for="price-range" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Price Range</label>
<div class="flex items-center space-x-4">
<input
type="number"
id="price-from"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="From"
value="300"
/>
<input
type="number"
id="price-to"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="To"
value="3500"
/>
</div> -->
</div>
<!-- Sales -->
<!-- <div class="mb-4">
<label for="sales-range" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Sales</label>
<div class="flex items-center space-x-4">
<input
type="number"
id="sales-from"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="From"
value="1"
/>
<input
type="number"
id="sales-to"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="To"
value="100"
/>
</div>
</div> -->
<!-- Category -->
<!-- <div class="mb-4">
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Category</label>
<div class="flex flex-wrap gap-2">
<button
class="px-3 py-1 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-blue-500 dark:focus:text-white"
>
Gaming
</button>
<button
class="px-3 py-1 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-blue-500 dark:focus:text-white"
>
Electronics
</button>
<button
class="px-3 py-1 text-sm font-medium text-white bg-blue-700 border border-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
>
Phone
</button>
<button
class="px-3 py-1 text-sm font-medium text-white bg-blue-700 border border-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
>
TV/Monitor
</button>
<button
class="px-3 py-1 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-blue-500 dark:focus:text-white"
>
Laptop
</button>
<button
class="px-3 py-1 text-sm font-medium text-white bg-blue-700 border border-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
>
Watch
</button>
</div>
</div> -->
<!-- State -->
<!-- <div class="mb-4">
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">State</label>
<ul class="w-48 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600 dark:text-white">
<li class="w-full border-b border-gray-200 rounded-t-lg dark:border-gray-600">
<div class="flex items-center ps-3">
<input
id="state-all"
type="radio"
value="all"
name="state"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-700 dark:focus:ring-offset-gray-700 focus:ring-2 dark:bg-gray-600 dark:border-gray-500"
checked
/>
<label for="state-all" class="w-full py-3 ms-2 text-sm font-medium text-gray-900 dark:text-gray-300">All</label>
</div>
</li>
<li class="w-full border-b border-gray-200 rounded-t-lg dark:border-gray-600">
<div class="flex items-center ps-3">
<input
id="state-new"
type="radio"
value="new"
name="state"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-700 dark:focus:ring-offset-gray-700 focus:ring-2 dark:bg-gray-600 dark:border-gray-500"
/>
<label for="state-new" class="w-full py-3 ms-2 text-sm font-medium text-gray-900 dark:text-gray-300">New</label>
</div>
</li>
<li class="w-full border-b border-gray-200 rounded-t-lg dark:border-gray-600">
<div class="flex items-center ps-3">
<input
id="state-refurbished"
type="radio"
value="refurbished"
name="state"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-700 dark:focus:ring-offset-gray-700 focus:ring-2 dark:bg-gray-600 dark:border-gray-500"
/>
<label for="state-refurbished" class="w-full py-3 ms-2 text-sm font-medium text-gray-900 dark:text-gray-300">Refurbished</label>
</div>
</li>
</ul>
</div> -->
<!-- Action Buttons -->
<div class="flex justify-between">
<button
type="button"
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
>
Show 32 Results
</button>
<button
type="button"
class="py-2.5 px-5 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
>
Reset
</button>
</div>
</div>
</app-dropdown>

View File

@@ -1,20 +1,23 @@
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { Router, RouterModule } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { NavigationEnd, Router, RouterModule } from '@angular/router';
import { faUserGear } from '@fortawesome/free-solid-svg-icons';
import { Collapse, Dropdown, initFlowbite } from 'flowbite';
import { KeycloakService } from 'keycloak-angular';
import { Observable } from 'rxjs';
import { Observable, Subject, takeUntil } from 'rxjs';
import { User } from '../../../../../bizmatch-server/src/models/db.model';
import { emailToDirName, KeycloakUser } from '../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../environments/environment';
import { SharedService } from '../../services/shared.service';
import { UserService } from '../../services/user.service';
import { map2User } from '../../utils/utils';
import { DropdownComponent } from '../dropdown/dropdown.component';
@Component({
selector: 'header',
standalone: true,
imports: [CommonModule, RouterModule],
imports: [CommonModule, RouterModule, DropdownComponent, FormsModule],
templateUrl: './header.component.html',
styleUrl: './header.component.scss',
})
@@ -27,23 +30,50 @@ export class HeaderComponent {
faUserGear = faUserGear;
profileUrl: string;
env = environment;
constructor(public keycloakService: KeycloakService, private router: Router, private userService: UserService, private sharedService: SharedService) {}
private filterDropdown: Dropdown | null = null;
isMobile: boolean = false;
private destroy$ = new Subject<void>();
prompt: string;
constructor(public keycloakService: KeycloakService, private router: Router, private userService: UserService, private sharedService: SharedService, private breakpointObserver: BreakpointObserver) {}
async ngOnInit() {
const token = await this.keycloakService.getToken();
this.keycloakUser = map2User(token);
this.user = await this.userService.getByMail(this.keycloakUser.email);
this.profileUrl = this.user.hasProfile ? `${this.env.imageBaseUrl}/pictures/profile/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}` : `/assets/images/placeholder.png`;
setTimeout(() => {
initFlowbite();
if (this.keycloakUser) {
this.user = await this.userService.getByMail(this.keycloakUser?.email);
this.profileUrl = this.user.hasProfile ? `${this.env.imageBaseUrl}/pictures/profile/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}` : `/assets/images/placeholder.png`;
}
// setTimeout(() => {
// initFlowbite();
// });
this.router.events.subscribe(event => {
if (event instanceof NavigationEnd) {
initFlowbite();
}
});
this.breakpointObserver
.observe([Breakpoints.Handset])
.pipe(takeUntil(this.destroy$))
.subscribe(result => {
this.isMobile = result.matches;
const targetEl = document.getElementById('filterDropdown');
const triggerEl = this.isMobile ? document.getElementById('filterDropdownMobileButton') : document.getElementById('filterDropdownButton');
if (targetEl && triggerEl) {
this.filterDropdown = new Dropdown(targetEl, triggerEl);
}
});
this.sharedService.currentProfilePhoto.subscribe(photoUrl => {
if (photoUrl) {
this.profileUrl = photoUrl;
}
});
}
toggleFilterDropdown() {
if (this.filterDropdown) {
this.filterDropdown.toggle();
}
}
ngAfterViewInit() {}
navigateWithState(dest: string, state: any) {
@@ -82,4 +112,9 @@ export class HeaderComponent {
this.closeDropdown();
this.closeMobileMenu();
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}

View File

@@ -77,7 +77,7 @@
</ul>
</div>
<div class="flex items-center border border-gray-300 rounded-full p-2">
<input type="text" placeholder="AI Search" class="flex-grow px-4 py-2 outline-none rounded-full text-sm md:text-base" />
<input type="text" [(ngModel)]="prompt" placeholder="AI Search" class="flex-grow px-4 py-2 outline-none rounded-full text-sm md:text-base" />
<button class="bg-blue-600 text-white p-2 rounded-full" (click)="search()">
<svg class="h-5 w-5 md:h-6 md:w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-4.35-4.35M10.5 18.5A7.5 7.5 0 1018 10.5 7.5 7.5 0 0010.5 18.5z"></path>

View File

@@ -2,7 +2,6 @@ import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { initFlowbite } from 'flowbite';
import { KeycloakService } from 'keycloak-angular';
import onChange from 'on-change';
import { KeycloakUser, ListingCriteria } from '../../../../../bizmatch-server/src/models/main.model';
@@ -25,7 +24,7 @@ export class HomeComponent {
states = [];
isMenuOpen = false;
user: KeycloakUser;
prompt: string;
public constructor(private router: Router, private activatedRoute: ActivatedRoute, public selectOptions: SelectOptionsService, public keycloakService: KeycloakService, private listingsService: ListingsService) {
this.criteria = onChange(getCriteriaStateObject(), getSessionStorageHandler);
resetCriteria(this.criteria);
@@ -33,9 +32,11 @@ export class HomeComponent {
async ngOnInit() {
const token = await this.keycloakService.getToken();
this.user = map2User(token);
setTimeout(() => {
initFlowbite();
});
// this.router.events.subscribe(event => {
// if (event instanceof NavigationEnd) {
// initFlowbite();
// }
// });
}
async changeTab(tabname: 'business' | 'commercialProperty' | 'broker') {
this.activeTabAction = tabname;
@@ -48,6 +49,7 @@ export class HomeComponent {
}
search() {
const data = { keep: true };
this.criteria.prompt = this.prompt;
this.router.navigate([`${this.activeTabAction}Listings`]);
}

View File

@@ -85,11 +85,12 @@
</div> -->
<!-- business-listing.component.html -->
<!-- <div class="w-full bg-slate-100"> -->
<div class="container mx-auto p-4">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
@for (listing of listings; track listing.id) {
<div class="bg-white rounded-lg shadow-md overflow-hidden">
<div class="p-4 flex flex-col h-full relative">
<div class="p-4 flex flex-col h-full relative z-[0]">
<div class="flex items-center mb-2">
<i [class]="selectOptions.getIconAndTextColorType(listing.type)" class="mr-2"></i>
<span [class]="selectOptions.getTextColorType(listing.type)" class="font-semibold">{{ selectOptions.getBusiness(listing.type) }}</span>
@@ -111,4 +112,17 @@
}
</div>
</div>
<!-- <div class="container mx-auto px-4 py-8">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@for (listing of listings; track listing.id) {
<div class="bg-white rounded-lg shadow-md overflow-hidden">
<div class="p-4">
<h3 class="text-lg font-semibold mb-2">{{ listing.title }}</h3>
</div>
</div>
}
</div>
</div> -->
<!-- </div> -->

View File

@@ -70,9 +70,10 @@ export class BusinessListingsComponent {
this.search();
}
async search() {
const listingReponse = await this.listingsService.getListings(this.criteria, 'business');
this.listings = listingReponse.data;
this.totalRecords = listingReponse.total;
this.listings = await this.listingsService.getListingsByPrompt(this.criteria);
// const listingReponse = await this.listingsService.getListings(this.criteria, 'business');
// this.listings = listingReponse.data;
// this.totalRecords = listingReponse.total;
// this.cdRef.markForCheck();
// this.cdRef.detectChanges();
}

View File

@@ -13,7 +13,11 @@ export class ListingsService {
constructor(private http: HttpClient) {}
async getListings(criteria: ListingCriteria, listingsCategory: 'business' | 'professionals_brokers' | 'commercialProperty'): Promise<ResponseBusinessListingArray | ResponseCommercialPropertyListingArray> {
const result = await lastValueFrom(this.http.post<ResponseBusinessListingArray | ResponseCommercialPropertyListingArray>(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/search`, criteria));
const result = await lastValueFrom(this.http.post<ResponseBusinessListingArray | ResponseCommercialPropertyListingArray>(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/find`, criteria));
return result;
}
async getListingsByPrompt(criteria: ListingCriteria): Promise<BusinessListing[]> {
const result = await lastValueFrom(this.http.post<BusinessListing[]>(`${this.apiBaseUrl}/bizmatch/listings/business/search`, criteria));
return result;
}
getListingById(id: string, listingsCategory?: 'business' | 'commercialProperty'): Observable<ListingType> {

View File

@@ -33,13 +33,4 @@ export class UserService {
async getAllStates(): Promise<any> {
return await lastValueFrom(this.http.get<StatesResult[]>(`${this.apiBaseUrl}/bizmatch/user/states/all`));
}
// async getId(email: string): Promise<string> {
// if (sessionStorage.getItem('USERID')) {
// return sessionStorage.getItem('USERID');
// } else {
// const user = await this.getByMail(email);
// sessionStorage.setItem('USERID', user.id);
// return user.id;
// }
// }
}

View File

@@ -96,6 +96,7 @@ export function createDefaultListingCriteria(): ListingCriteria {
title: '',
category: 'broker',
name: '',
prompt: '',
};
}
export function createLogger(name: string, level: number = INFO, options: any = {}) {
@@ -179,3 +180,58 @@ export function getDialogWidth(dimensions): string {
}
return dialogWidth;
}
import { initFlowbite } from 'flowbite';
import { Subject, concatMap, delay, of } from 'rxjs';
const flowbiteQueue = new Subject<() => void>();
flowbiteQueue.pipe(concatMap(item => of(item).pipe(delay(100)))).subscribe(x => {
x();
});
export function Flowbite() {
return function (target: any) {
const originalOnInit = target.prototype.ngOnInit;
target.prototype.ngOnInit = function () {
if (originalOnInit) {
originalOnInit.apply(this);
}
initFlowbiteFix();
};
};
}
export function initFlowbiteFix() {
flowbiteQueue.next(() => {
const elements = Array.from(document.querySelectorAll('*'));
const flowbiteElements: Element[] = [];
const initializedElements = Array.from(document.querySelectorAll('[flowbite-initialized]'));
for (const element of elements) {
const attributes = Array.from(element.attributes);
for (const attribute of attributes) {
if (attribute.name.startsWith('data-') && !initializedElements.includes(element)) {
flowbiteElements.push(element);
break;
}
}
}
for (const element of flowbiteElements) {
element.setAttribute('flowbite-initialized', '');
}
initFlowbite();
for (const element of flowbiteElements) {
const attributes: { name: string; value: string }[] = Array.from(element.attributes);
const dataAttributes = attributes.filter(attribute => attribute.name.startsWith('data-'));
for (const attribute of dataAttributes) {
element.setAttribute(attribute.name.replace('data-', 'fb-'), attribute.value);
element.removeAttribute(attribute.name);
}
}
});
}