First version AI Search
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<p>dropdown works!</p>
|
||||
129
bizmatch/src/app/components/dropdown/dropdown.component.ts
Normal file
129
bizmatch/src/app/components/dropdown/dropdown.component.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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`]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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> -->
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user